mapshaper 0.6.70 → 0.6.72

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.
@@ -2532,48 +2532,12 @@
2532
2532
  };
2533
2533
  }
2534
2534
 
2535
- function flattenArcs(lyr) {
2536
- lyr.source.dataset.arcs.flatten();
2537
- if (isProjectedLayer(lyr)) {
2538
- lyr.arcs.flatten();
2539
- }
2540
- }
2541
-
2542
- function setZ(lyr, z) {
2543
- lyr.source.dataset.arcs.setRetainedInterval(z);
2544
- if (isProjectedLayer(lyr)) {
2545
- lyr.arcs.setRetainedInterval(z);
2546
- }
2547
- }
2548
-
2549
- function updateZ(lyr) {
2550
- if (isProjectedLayer(lyr) && !lyr.source.dataset.arcs.isFlat()) {
2551
- lyr.arcs.setThresholds(lyr.source.dataset.arcs.getVertexData().zz);
2552
- }
2553
- }
2554
-
2555
- function insertVertex$1(lyr, id, dataPoint) {
2556
- internal.insertVertex(lyr.source.dataset.arcs, id, dataPoint);
2557
- if (isProjectedLayer(lyr)) {
2558
- internal.insertVertex(lyr.arcs, id, lyr.projectPoint(dataPoint[0], dataPoint[1]));
2559
- }
2560
- }
2561
-
2562
- function deleteVertex$1(lyr, id) {
2563
- internal.deleteVertex(lyr.arcs, id);
2564
- if (isProjectedLayer(lyr)) {
2565
- internal.deleteVertex(lyr.source.dataset.arcs, id);
2566
- }
2567
- }
2568
-
2535
+ // Convert a point from display CRS coordinates to data coordinates.
2536
+ // These are only different when using dynamic reprojection (basemap view).
2569
2537
  function translateDisplayPoint(lyr, p) {
2570
2538
  return isProjectedLayer(lyr) ? lyr.invertPoint(p[0], p[1]) : p;
2571
2539
  }
2572
2540
 
2573
- function getPointCoords(lyr, fid) {
2574
- return internal.cloneShape(lyr.source.layer.shapes[fid]);
2575
- }
2576
-
2577
2541
  // bbox: display coords
2578
2542
  // intended to work with rectangular projections like Mercator
2579
2543
  function getBBoxCoords(lyr, bbox) {
@@ -2586,58 +2550,519 @@
2586
2550
  return bounds.toArray();
2587
2551
  }
2588
2552
 
2589
- function getVertexCoords(lyr, id) {
2590
- return lyr.source.dataset.arcs.getVertex2(id);
2553
+ function isProjectedLayer(lyr) {
2554
+ // TODO: could do some validation on the layer's contents
2555
+ return !!(lyr.source && lyr.invertPoint);
2591
2556
  }
2592
2557
 
2593
- function setVertexCoords(lyr, ids, dataPoint) {
2594
- internal.snapVerticesToPoint(ids, dataPoint, lyr.source.dataset.arcs, true);
2595
- if (isProjectedLayer(lyr)) {
2596
- var p = lyr.projectPoint(dataPoint[0], dataPoint[1]);
2597
- internal.snapVerticesToPoint(ids, p, lyr.arcs, true);
2598
- }
2558
+ var darkStroke = "#334",
2559
+ lightStroke = "#b7d9ea",
2560
+ violet = "#cc6acc",
2561
+ violetFill = "rgba(249, 120, 249, 0.20)",
2562
+ gold = "#efc100",
2563
+ black = "black",
2564
+ grey = "#888",
2565
+ selectionFill = "rgba(237, 214, 0, 0.12)",
2566
+ hoverFill = "rgba(255, 120, 255, 0.12)",
2567
+ activeStyle = { // outline style for the active layer
2568
+ type: 'outline',
2569
+ strokeColors: [lightStroke, darkStroke],
2570
+ strokeWidth: 0.8,
2571
+ dotColor: "#223",
2572
+ dotSize: 1
2573
+ },
2574
+ activeStyleDarkMode = {
2575
+ type: 'outline',
2576
+ strokeColors: [lightStroke, 'white'],
2577
+ strokeWidth: 0.9,
2578
+ dotColor: 'white',
2579
+ dotSize: 1
2580
+ },
2581
+ activeStyleForLabels = {
2582
+ dotColor: "rgba(250, 0, 250, 0.45)", // violet dot with transparency
2583
+ dotSize: 1
2584
+ },
2585
+ referenceStyle = { // outline style for reference layers
2586
+ type: 'outline',
2587
+ strokeColors: [null, '#78c110'], // upped saturation from #86c927
2588
+ strokeWidth: 0.85,
2589
+ dotColor: "#73ba20",
2590
+ dotSize: 1
2591
+ },
2592
+ intersectionStyle = {
2593
+ dotColor: "#F24400",
2594
+ dotSize: 1
2595
+ },
2596
+ hoverStyles = {
2597
+ polygon: {
2598
+ fillColor: hoverFill,
2599
+ strokeColor: black,
2600
+ strokeWidth: 1.2
2601
+ }, point: {
2602
+ dotColor: violet, // black,
2603
+ dotSize: 2.5
2604
+ }, polyline: {
2605
+ strokeColor: black,
2606
+ strokeWidth: 2.5,
2607
+ }
2608
+ },
2609
+ unselectedHoverStyles = {
2610
+ polygon: {
2611
+ fillColor: 'rgba(0,0,0,0)',
2612
+ strokeColor: black,
2613
+ strokeWidth: 1.2
2614
+ }, point: {
2615
+ dotColor: black, // grey,
2616
+ dotSize: 2
2617
+ }, polyline: {
2618
+ strokeColor: black, // grey,
2619
+ strokeWidth: 2.5
2620
+ }
2621
+ },
2622
+ selectionStyles = {
2623
+ polygon: {
2624
+ fillColor: hoverFill,
2625
+ strokeColor: black,
2626
+ strokeWidth: 1.2
2627
+ }, point: {
2628
+ dotColor: violet, // black,
2629
+ dotSize: 1.5
2630
+ }, polyline: {
2631
+ strokeColor: violet, // black,
2632
+ strokeWidth: 2.5
2633
+ }
2634
+ },
2635
+ // not used
2636
+ selectionHoverStyles = {
2637
+ polygon: {
2638
+ fillColor: selectionFill,
2639
+ strokeColor: black,
2640
+ strokeWidth: 1.2
2641
+ }, point: {
2642
+ dotColor: black,
2643
+ dotSize: 1.5
2644
+ }, polyline: {
2645
+ strokeColor: black,
2646
+ strokeWidth: 2
2647
+ }
2648
+ },
2649
+ pinnedStyles = {
2650
+ polygon: {
2651
+ fillColor: violetFill,
2652
+ strokeColor: violet,
2653
+ strokeWidth: 1.8
2654
+ }, point: {
2655
+ dotColor: violet,
2656
+ dotSize: 3
2657
+ }, polyline: {
2658
+ strokeColor: violet, // black, // violet,
2659
+ strokeWidth: 3
2660
+ }
2661
+ };
2662
+
2663
+ function getIntersectionStyle(lyr, opts) {
2664
+ return getDefaultStyle(lyr, intersectionStyle);
2599
2665
  }
2600
2666
 
2601
- // coords: [x, y] point in data CRS (not display CRS)
2602
- function setPointCoords(lyr, fid, coords) {
2603
- lyr.source.layer.shapes[fid] = coords;
2604
- if (isProjectedLayer(lyr)) {
2605
- lyr.layer.shapes[fid] = projectPointCoords(coords, lyr.projectPoint);
2667
+ // Style for unselected layers with visibility turned on
2668
+ // (styled layers have)
2669
+ function getReferenceLayerStyle(lyr, opts) {
2670
+ var style;
2671
+ if (layerHasCanvasDisplayStyle(lyr) && !opts.outlineMode) {
2672
+ style = getCanvasDisplayStyle(lyr);
2673
+ } else if (internal.layerHasLabels(lyr) && !opts.outlineMode) {
2674
+ style = {dotSize: 0}; // no reference dots if labels are visible
2675
+ } else {
2676
+ style = getDefaultStyle(lyr, referenceStyle);
2606
2677
  }
2678
+ return style;
2607
2679
  }
2608
2680
 
2609
- function updateVertexCoords(lyr, ids) {
2610
- if (!isProjectedLayer(lyr)) return;
2611
- var p = lyr.arcs.getVertex2(ids[0]);
2612
- internal.snapVerticesToPoint(ids, lyr.invertPoint(p[0], p[1]), lyr.source.dataset.arcs, true);
2681
+ function getActiveLayerStyle(lyr, opts) {
2682
+ var style;
2683
+ if (layerHasCanvasDisplayStyle(lyr) && !opts.outlineMode) {
2684
+ style = getCanvasDisplayStyle(lyr);
2685
+ } else if (internal.layerHasLabels(lyr) && !opts.outlineMode) {
2686
+ style = getDefaultStyle(lyr, activeStyleForLabels);
2687
+ } else if (opts.darkMode) {
2688
+ style = getDefaultStyle(lyr, activeStyleDarkMode);
2689
+ } else {
2690
+ style = getDefaultStyle(lyr, activeStyle);
2691
+ }
2692
+ return style;
2613
2693
  }
2614
2694
 
2615
- function setRectangleCoords(lyr, ids, coords) {
2616
- ids.forEach(function(id, i) {
2617
- var p = coords[i];
2618
- internal.snapVerticesToPoint([id], p, lyr.source.dataset.arcs, true);
2619
- if (isProjectedLayer(lyr)) {
2620
- internal.snapVerticesToPoint([id], lyr.projectPoint(p[0], p[1]), lyr.arcs, true);
2695
+ // Returns a display style for the overlay layer.
2696
+ // The overlay layer renders several kinds of feature, each of which is displayed
2697
+ // with a different style.
2698
+ //
2699
+ // * hover shapes
2700
+ // * selected shapes
2701
+ // * pinned shapes
2702
+ //
2703
+ function getOverlayStyle(baseLyr, o, opts) {
2704
+ if (opts.interactionMode == 'vertices') {
2705
+ return getVertexStyle(baseLyr, o);
2706
+ }
2707
+ if (opts.interactionMode == 'edit-lines') {
2708
+ return getLineEditingStyle(baseLyr, o);
2709
+ }
2710
+ var geomType = baseLyr.geometry_type;
2711
+ var topId = o.id; // pinned id (if pinned) or hover id
2712
+ var topIdx = -1;
2713
+ var styler = function(style, i) {
2714
+ var defaultStyle = i === topIdx ? topStyle : outlineStyle;
2715
+ if (baseStyle.styler) {
2716
+ // TODO: render default stroke widths without scaling
2717
+ // (will need to pass symbol scale to the styler function)
2718
+ style.strokeWidth = defaultStyle.strokeWidth;
2719
+ baseStyle.styler(style, i); // get styled stroke width (if set)
2720
+ style.strokeColor = defaultStyle.strokeColor;
2721
+ style.fillColor = defaultStyle.fillColor;
2722
+ } else {
2723
+ Object.assign(style, defaultStyle);
2621
2724
  }
2725
+ };
2726
+ var baseStyle = getActiveLayerStyle(baseLyr, opts);
2727
+ var outlineStyle = getDefaultStyle(baseLyr, selectionStyles[geomType]);
2728
+ var topStyle;
2729
+ var ids = o.ids.filter(function(i) {
2730
+ return i != o.id; // move selected id to the end
2622
2731
  });
2732
+ if (o.id > -1) { // pinned or hover style
2733
+ topStyle = getSelectedFeatureStyle(baseLyr, o, opts);
2734
+ topIdx = ids.length;
2735
+ ids.push(o.id); // put the pinned/hover feature last in the render order
2736
+ }
2737
+ var style = {
2738
+ baseStyle: baseStyle,
2739
+ styler,
2740
+ ids,
2741
+ overlay: true
2742
+ };
2743
+
2744
+ if (layerHasCanvasDisplayStyle(baseLyr) && !opts.outlineMode) {
2745
+ if (geomType == 'point') {
2746
+ style.styler = getOverlayPointStyler(getCanvasDisplayStyle(baseLyr).styler, styler);
2747
+ }
2748
+ style.type = 'styled';
2749
+ }
2750
+ return ids.length > 0 ? style : null;
2623
2751
  }
2624
2752
 
2625
- // lyr: display layer
2626
- // export function updateRectangleCoords(lyr, ids, coords) {
2627
- // if (!isProjectedLayer(lyr)) return;
2628
- // ids.forEach(function(id, i) {
2629
- // var p = coords[i];
2630
- // internal.snapVerticesToPoint([id], lyr.invertPoint(p[0], p[1]), lyr.source.dataset.arcs, true);
2631
- // });
2632
- // }
2633
2753
 
2634
- function isProjectedLayer(lyr) {
2635
- // TODO: could do some validation on the layer's contents
2636
- return !!(lyr.source && lyr.invertPoint);
2754
+ function getDefaultStyle(lyr, baseStyle) {
2755
+ var style = Object.assign({}, baseStyle);
2756
+ // reduce the dot size of large point layers
2757
+ if (lyr.geometry_type == 'point' && style.dotSize > 0) {
2758
+ style.dotSize *= getDotScale$1(lyr);
2759
+ }
2760
+ return style;
2637
2761
  }
2638
2762
 
2639
- // Update source data coordinates by projecting display coordinates
2640
- function updatePointCoords(lyr, fid) {
2763
+ function getDotScale$1(lyr) {
2764
+ var topTier = 10000;
2765
+ var n = countPoints(lyr.shapes, topTier); // short-circuit point counting above top threshold
2766
+ var k = n < 200 && 4 || n < 2500 && 3 || n < topTier && 2 || 1;
2767
+ return k;
2768
+ }
2769
+
2770
+ function countPoints(shapes, max) {
2771
+ var count = 0;
2772
+ var i, n, shp;
2773
+ max = max || Infinity;
2774
+ for (i=0, n=shapes.length; i<n && count<max; i++) {
2775
+ shp = shapes[i];
2776
+ count += shp ? shp.length : 0;
2777
+ }
2778
+ return count;
2779
+ }
2780
+
2781
+
2782
+ // style for vertex edit mode
2783
+ function getVertexStyle(lyr, o) {
2784
+ return {
2785
+ ids: o.ids,
2786
+ overlay: true,
2787
+ strokeColor: black,
2788
+ strokeWidth: 1.5,
2789
+ vertices: true,
2790
+ vertex_overlay_color: violet,
2791
+ vertex_overlay: o.hit_coordinates || null,
2792
+ selected_points: o.selected_points || null,
2793
+ fillColor: null
2794
+ };
2795
+ }
2796
+
2797
+ // style for vertex edit mode
2798
+ function getLineEditingStyle(lyr, o) {
2799
+ return {
2800
+ ids: o.ids,
2801
+ overlay: true,
2802
+ strokeColor: 'black',
2803
+ strokeWidth: 1.2,
2804
+ vertices: true,
2805
+ vertex_overlay_color: violet,
2806
+ vertex_overlay: o.hit_coordinates || null,
2807
+ selected_points: o.selected_points || null,
2808
+ fillColor: null
2809
+ };
2810
+ }
2811
+
2812
+
2813
+ function getSelectedFeatureStyle(lyr, o, opts) {
2814
+ var isPinned = o.pinned;
2815
+ var inSelection = o.ids.indexOf(o.id) > -1;
2816
+ var geomType = lyr.geometry_type;
2817
+ var style;
2818
+ if (isPinned && opts.interactionMode == 'rectangles') {
2819
+ // kludge for rectangle editing mode
2820
+ style = selectionStyles[geomType];
2821
+ } else if (isPinned) {
2822
+ // a feature is pinned
2823
+ style = pinnedStyles[geomType];
2824
+ } else if (inSelection) {
2825
+ // normal hover, or hover id is in the selection set
2826
+ style = hoverStyles[geomType];
2827
+ } else {
2828
+ // features are selected, but hover id is not in the selection set
2829
+ style = unselectedHoverStyles[geomType];
2830
+ }
2831
+ return getDefaultStyle(lyr, style);
2832
+ }
2833
+
2834
+ // Modify style to use scaled circle instead of dot symbol
2835
+ function getOverlayPointStyler(baseStyler, overlayStyler) {
2836
+ return function(obj, i) {
2837
+ var dotColor;
2838
+ var id = obj.ids ? obj.ids[i] : -1;
2839
+ obj.strokeWidth = 0; // kludge to support setting minimum stroke width
2840
+ baseStyler(obj, id);
2841
+ if (overlayStyler) {
2842
+ overlayStyler(obj, i);
2843
+ }
2844
+ dotColor = obj.dotColor;
2845
+ if (obj.radius && dotColor) {
2846
+ obj.radius += 0.4;
2847
+ // delete obj.fillColor; // only show outline
2848
+ obj.fillColor = dotColor; // comment out to only highlight stroke
2849
+ obj.strokeColor = dotColor;
2850
+ obj.strokeWidth = Math.max(obj.strokeWidth + 0.8, 1.5);
2851
+ obj.opacity = 1;
2852
+ }
2853
+ };
2854
+ }
2855
+
2856
+ function getCanvasDisplayStyle(lyr) {
2857
+ var styleIndex = {
2858
+ opacity: 'opacity',
2859
+ r: 'radius',
2860
+ 'fill': 'fillColor',
2861
+ 'fill-pattern': 'fillPattern',
2862
+ 'fill-effect': 'fillEffect',
2863
+ 'fill-opacity': 'fillOpacity',
2864
+ 'stroke': 'strokeColor',
2865
+ 'stroke-width': 'strokeWidth',
2866
+ 'stroke-dasharray': 'lineDash',
2867
+ 'stroke-opacity': 'strokeOpacity',
2868
+ 'stroke-linecap': 'lineCap',
2869
+ 'stroke-linejoin': 'lineJoin',
2870
+ 'stroke-miterlimit': 'miterLimit'
2871
+ },
2872
+ // array of field names of relevant svg display properties
2873
+ fields = getCanvasStyleFields(lyr).filter(function(f) {return f in styleIndex;}),
2874
+ records = lyr.data.getRecords();
2875
+ var styler = function(style, i) {
2876
+ var rec = records[i];
2877
+ var fname, val;
2878
+ for (var j=0; j<fields.length; j++) {
2879
+ fname = fields[j];
2880
+ val = rec && rec[fname];
2881
+ if (val == 'none') {
2882
+ val = 'transparent'; // canvas equivalent of CSS 'none'
2883
+ }
2884
+ // convert svg property name to mapshaper style equivalent
2885
+ style[styleIndex[fname]] = val;
2886
+ }
2887
+
2888
+ if (style.strokeWidth && !style.strokeColor) {
2889
+ style.strokeColor = 'black';
2890
+ }
2891
+ if (!('strokeWidth' in style) && style.strokeColor) {
2892
+ style.strokeWidth = 1;
2893
+ }
2894
+ if (style.radius > 0 && !style.strokeWidth && !style.fillColor && lyr.geometry_type == 'point') {
2895
+ style.fillColor = 'black';
2896
+ }
2897
+ };
2898
+ return {styler: styler, type: 'styled'};
2899
+ }
2900
+
2901
+ // check if layer should be displayed with styles
2902
+ function layerHasCanvasDisplayStyle(lyr) {
2903
+ var fields = getCanvasStyleFields(lyr);
2904
+ if (lyr.geometry_type == 'point') {
2905
+ return fields.indexOf('r') > -1; // require 'r' field for point symbols
2906
+ }
2907
+ return utils$1.difference(fields, ['opacity', 'class']).length > 0;
2908
+ }
2909
+
2910
+
2911
+ function getCanvasStyleFields(lyr) {
2912
+ var fields = lyr.data ? lyr.data.getFields() : [];
2913
+ return internal.findPropertiesBySymbolGeom(fields, lyr.geometry_type);
2914
+ }
2915
+
2916
+ function flattenArcs(lyr) {
2917
+ lyr.source.dataset.arcs.flatten();
2918
+ if (isProjectedLayer(lyr)) {
2919
+ lyr.arcs.flatten();
2920
+ }
2921
+ }
2922
+
2923
+ function setZ(lyr, z) {
2924
+ lyr.source.dataset.arcs.setRetainedInterval(z);
2925
+ if (isProjectedLayer(lyr)) {
2926
+ lyr.arcs.setRetainedInterval(z);
2927
+ }
2928
+ }
2929
+
2930
+ function updateZ(lyr) {
2931
+ if (isProjectedLayer(lyr) && !lyr.source.dataset.arcs.isFlat()) {
2932
+ lyr.arcs.setThresholds(lyr.source.dataset.arcs.getVertexData().zz);
2933
+ }
2934
+ }
2935
+
2936
+ function appendNewDataRecord(layer) {
2937
+ if (!layer.data) return null;
2938
+ var fields = layer.data.getFields();
2939
+ var d = getEmptyDataRecord(layer.data);
2940
+ // TODO: handle SVG symbol layer
2941
+ if (internal.layerHasLabels(layer)) {
2942
+ d['label-text'] = 'TBD'; // without text, new labels will be invisible
2943
+ } else if (layer.geometry_type == 'point' && fields.includes('r')) {
2944
+ d.r = 3; // show a black circle if layer is styled
2945
+ } else if (true || layer.geometry_type == 'polyline' && fields.includes('stroke')) {
2946
+ d.stroke = 'black';
2947
+ } else if (layer.geometry_type == 'polygon' && fields.includes('stroke')) {
2948
+ d.stroke = 'black';
2949
+ }
2950
+ // TODO: better styling
2951
+ layer.data.getRecords().push(d);
2952
+ return d;
2953
+ }
2954
+
2955
+ function getEmptyDataRecord(table) {
2956
+ return table.getFields().reduce(function(memo, name) {
2957
+ memo[name] = null;
2958
+ return memo;
2959
+ }, {});
2960
+ }
2961
+
2962
+ function deleteLastPath(lyr) {
2963
+ var arcId = lyr.arcs.size() - 1;
2964
+ if (lyr.layer.data) {
2965
+ lyr.layer.data.getRecords().pop();
2966
+ }
2967
+ var shp = lyr.layer.shapes.pop();
2968
+ internal.deleteLastArc(lyr.arcs);
2969
+ if (isProjectedLayer(lyr)) {
2970
+ internal.deleteLastArc(lyr.source.dataset.arcs);
2971
+ }
2972
+ }
2973
+
2974
+ // p1, p2: two points in source data CRS coords.
2975
+ function appendNewPath(lyr, p1, p2) {
2976
+ var arcId = lyr.arcs.size();
2977
+ internal.appendEmptyArc(lyr.arcs);
2978
+ lyr.layer.shapes.push([[arcId]]);
2979
+ if (isProjectedLayer(lyr)) {
2980
+ // lyr.source.layer.shapes.push([[arcId]]);
2981
+ internal.appendEmptyArc(lyr.source.dataset.arcs);
2982
+ }
2983
+ appendVertex$1(lyr, p1);
2984
+ appendVertex$1(lyr, p2);
2985
+ appendNewDataRecord(lyr.layer);
2986
+ }
2987
+
2988
+ // p: point in source data CRS coords.
2989
+ function insertVertex$1(lyr, id, p) {
2990
+ internal.insertVertex(lyr.source.dataset.arcs, id, p);
2991
+ if (isProjectedLayer(lyr)) {
2992
+ internal.insertVertex(lyr.arcs, id, lyr.projectPoint(p[0], p[1]));
2993
+ }
2994
+ }
2995
+
2996
+ function appendVertex$1(lyr, p) {
2997
+ var n = lyr.source.dataset.arcs.getPointCount();
2998
+ insertVertex$1(lyr, n, p);
2999
+ }
3000
+
3001
+ // TODO: make sure we're not also removing an entire arc
3002
+ function deleteLastVertex(lyr) {
3003
+ deleteVertex$1(lyr, lyr.arcs.getPointCount() - 1);
3004
+ }
3005
+
3006
+ function deleteVertex$1(lyr, id) {
3007
+ internal.deleteVertex(lyr.arcs, id);
3008
+ if (isProjectedLayer(lyr)) {
3009
+ internal.deleteVertex(lyr.source.dataset.arcs, id);
3010
+ }
3011
+ }
3012
+
3013
+ function getLastArcCoords(target) {
3014
+ var arcId = target.source.dataset.arcs.size() - 1;
3015
+ return internal.getUnfilteredArcCoords(arcId, target.source.dataset.arcs);
3016
+ }
3017
+
3018
+ function getLastArcLength(target) {
3019
+ var arcId = target.source.dataset.arcs.size() - 1;
3020
+ return internal.getUnfilteredArcLength(arcId, target.source.dataset.arcs);
3021
+ }
3022
+
3023
+ function getPointCoords(lyr, fid) {
3024
+ return internal.cloneShape(lyr.source.layer.shapes[fid]);
3025
+ }
3026
+
3027
+ function getVertexCoords(lyr, id) {
3028
+ return lyr.source.dataset.arcs.getVertex2(id);
3029
+ }
3030
+
3031
+ // set data coords (not display coords) of one or more vertices.
3032
+ function setVertexCoords(lyr, ids, dataPoint) {
3033
+ internal.snapVerticesToPoint(ids, dataPoint, lyr.source.dataset.arcs, true);
3034
+ if (isProjectedLayer(lyr)) {
3035
+ var p = lyr.projectPoint(dataPoint[0], dataPoint[1]);
3036
+ internal.snapVerticesToPoint(ids, p, lyr.arcs, true);
3037
+ }
3038
+ }
3039
+
3040
+ // coords: [x, y] point in data CRS (not display CRS)
3041
+ function setPointCoords(lyr, fid, coords) {
3042
+ lyr.source.layer.shapes[fid] = coords;
3043
+ if (isProjectedLayer(lyr)) {
3044
+ lyr.layer.shapes[fid] = projectPointCoords(coords, lyr.projectPoint);
3045
+ }
3046
+ }
3047
+
3048
+ function updateVertexCoords(lyr, ids) {
3049
+ if (!isProjectedLayer(lyr)) return;
3050
+ var p = lyr.arcs.getVertex2(ids[0]);
3051
+ internal.snapVerticesToPoint(ids, lyr.invertPoint(p[0], p[1]), lyr.source.dataset.arcs, true);
3052
+ }
3053
+
3054
+ function setRectangleCoords(lyr, ids, coords) {
3055
+ ids.forEach(function(id, i) {
3056
+ var p = coords[i];
3057
+ internal.snapVerticesToPoint([id], p, lyr.source.dataset.arcs, true);
3058
+ if (isProjectedLayer(lyr)) {
3059
+ internal.snapVerticesToPoint([id], lyr.projectPoint(p[0], p[1]), lyr.arcs, true);
3060
+ }
3061
+ });
3062
+ }
3063
+
3064
+ // Update source data coordinates by projecting display coordinates
3065
+ function updatePointCoords(lyr, fid) {
2641
3066
  if (!isProjectedLayer(lyr)) return;
2642
3067
  var displayShp = lyr.layer.shapes[fid];
2643
3068
  lyr.source.layer.shapes[fid] = projectPointCoords(displayShp, lyr.invertPoint);
@@ -4619,6 +5044,14 @@
4619
5044
 
4620
5045
  var copyRecord = internal.copyRecord;
4621
5046
 
5047
+ function isUndoEvt(e) {
5048
+ return (e.ctrlKey || e.metaKey) && !e.shiftKey && e.key == 'z';
5049
+ }
5050
+
5051
+ function isRedoEvt(e) {
5052
+ return (e.ctrlKey || e.metaKey) && (e.shiftKey && e.key == 'z' || !e.shiftKey && e.key == 'y');
5053
+ }
5054
+
4622
5055
  function Undo(gui) {
4623
5056
  var history, offset, stashedUndo;
4624
5057
  reset();
@@ -4634,14 +5067,6 @@
4634
5067
  offset = 0;
4635
5068
  }
4636
5069
 
4637
- function isUndoEvt(e) {
4638
- return (e.ctrlKey || e.metaKey) && !e.shiftKey && e.key == 'z';
4639
- }
4640
-
4641
- function isRedoEvt(e) {
4642
- return (e.ctrlKey || e.metaKey) && (e.shiftKey && e.key == 'z' || !e.shiftKey && e.key == 'y');
4643
- }
4644
-
4645
5070
  function makeMultiDataSetter(ids) {
4646
5071
  if (ids.length == 1) return makeDataSetter(ids[0]);
4647
5072
  var target = gui.model.getActiveLayer();
@@ -4728,7 +5153,7 @@
4728
5153
 
4729
5154
  gui.on('vertex_dragend', function(e) {
4730
5155
  var target = e.data.target;
4731
- var startPoint = e.points[0]; // in data coords
5156
+ var startPoint = e.point; // in data coords
4732
5157
  var endPoint = getVertexCoords(target, e.ids[0]);
4733
5158
  var undo = function() {
4734
5159
  if (e.insertion) {
@@ -4758,7 +5183,27 @@
4758
5183
  addHistoryState(undo, redo);
4759
5184
  });
4760
5185
 
4761
- this.clear = function() {
5186
+ gui.on('path_add', function(e) {
5187
+ var redo = function() {
5188
+ gui.dispatchEvent('redo_path_add', {p1: e.p1, p2: e.p2});
5189
+ };
5190
+ var undo = function() {
5191
+ gui.dispatchEvent('undo_path_add');
5192
+ };
5193
+ addHistoryState(undo, redo);
5194
+ });
5195
+
5196
+ gui.on('path_extend', function(e) {
5197
+ var redo = function() {
5198
+ gui.dispatchEvent('redo_path_extend', {p: e.p});
5199
+ };
5200
+ var undo = function() {
5201
+ gui.dispatchEvent('undo_path_extend');
5202
+ };
5203
+ addHistoryState(undo, redo);
5204
+ });
5205
+
5206
+ this.clear = function() {
4762
5207
  reset();
4763
5208
  };
4764
5209
 
@@ -4773,21 +5218,23 @@
4773
5218
  this.undo = function() {
4774
5219
  // firing even if history is empty
4775
5220
  // (because this event may trigger a new history state)
4776
- gui.dispatchEvent('undo_redo_pre');
5221
+ gui.dispatchEvent('undo_redo_pre', {type: 'undo'});
4777
5222
  var item = getHistoryItem();
4778
5223
  if (item) {
4779
5224
  offset++;
4780
5225
  item.undo();
5226
+ gui.dispatchEvent('undo_redo_post', {type: 'undo'});
4781
5227
  gui.dispatchEvent('map-needs-refresh');
4782
5228
  }
4783
5229
  };
4784
5230
 
4785
5231
  this.redo = function() {
4786
- gui.dispatchEvent('undo_redo_pre');
5232
+ gui.dispatchEvent('undo_redo_pre', {type: 'redo'});
4787
5233
  if (offset <= 0) return;
4788
5234
  offset--;
4789
5235
  var item = getHistoryItem();
4790
5236
  item.redo();
5237
+ gui.dispatchEvent('undo_redo_post', {type: 'redo'});
4791
5238
  gui.dispatchEvent('map-needs-refresh');
4792
5239
  };
4793
5240
 
@@ -4927,12 +5374,13 @@
4927
5374
  document.addEventListener('keyup', function(e) {
4928
5375
  if (!GUI.isActiveInstance(gui)) return;
4929
5376
  if (e.keyCode == 16) shiftDown = false;
5377
+ self.dispatchEvent('keyup', getEventData(e));
4930
5378
  });
4931
5379
 
4932
5380
  document.addEventListener('keydown', function(e) {
4933
5381
  if (!GUI.isActiveInstance(gui)) return;
4934
5382
  if (e.keyCode == 16) shiftDown = true;
4935
- self.dispatchEvent('keydown', {originalEvent: e});
5383
+ self.dispatchEvent('keydown', getEventData(e));
4936
5384
  });
4937
5385
 
4938
5386
  this.shiftIsPressed = function() { return shiftDown; };
@@ -4947,33 +5395,37 @@
4947
5395
  };
4948
5396
  }
4949
5397
 
5398
+ var names = {
5399
+ 8: 'delete',
5400
+ 9: 'tab',
5401
+ 13: 'enter',
5402
+ 16: 'shift',
5403
+ 27: 'esc',
5404
+ 32: 'space',
5405
+ 37: 'left',
5406
+ 38: 'up',
5407
+ 39: 'right',
5408
+ 40: 'down'
5409
+ };
5410
+
5411
+ function getEventData(originalEvent) {
5412
+ var keyCode = originalEvent.keyCode;
5413
+ var keyName = names[keyCode] || '';
5414
+ return {originalEvent, keyCode, keyName};
5415
+ }
5416
+
4950
5417
  utils$1.inherit(KeyboardEvents, EventDispatcher);
4951
5418
 
4952
5419
  function InteractionMode(gui) {
4953
5420
 
4954
- // TODO: finish this list
4955
- // var modes = [{
4956
- // name: 'info',
4957
- // label: 'inspect features',
4958
- // selection: true,
4959
- // popup: true,
4960
- // types: ['standard', 'polygons', 'lines', 'labels', 'points']
4961
- // }, {
4962
- // name: 'selection',
4963
- // label: 'select features',
4964
- // selection: true,
4965
- // popup: true,
4966
- // types: ['standard', 'polygons', 'lines', 'table', 'labels']
4967
- // }]
4968
-
4969
5421
  var menus = {
4970
- standard: ['info', 'selection', 'data', 'box'],
4971
- polygons: ['info', 'selection', 'data', 'box', 'vertices'],
4972
- rectangles: ['info', 'selection', 'data', 'box', 'rectangles', 'vertices'],
4973
- lines: ['info', 'selection', 'data', 'box', 'vertices' /*, 'draw-lines'*/],
4974
- table: ['info', 'selection', 'data'],
4975
- labels: ['info', 'selection', 'data', 'box', 'labels', 'location', 'add-points'],
4976
- points: ['info', 'selection', 'data', 'box', 'location', 'add-points']
5422
+ standard: ['info', 'selection', 'box'],
5423
+ polygons: ['info', 'selection', 'box', 'vertices'],
5424
+ rectangles: ['info', 'selection', 'box', 'rectangles', 'vertices'],
5425
+ lines: ['info', 'selection', 'box' , 'edit-lines'],
5426
+ table: ['info', 'selection'],
5427
+ labels: ['info', 'selection', 'box', 'labels', 'location', 'add-points'],
5428
+ points: ['info', 'selection', 'box', 'location', 'add-points']
4977
5429
  };
4978
5430
 
4979
5431
  var prompts = {
@@ -4993,6 +5445,7 @@
4993
5445
  selection: 'select features',
4994
5446
  'add-points': 'add points',
4995
5447
  'draw-lines': 'draw lines',
5448
+ 'edit-lines': 'draw/modify lines',
4996
5449
  rectangles: 'drag-to-resize',
4997
5450
  off: 'turn off'
4998
5451
  };
@@ -5047,8 +5500,8 @@
5047
5500
  setMode('off');
5048
5501
  };
5049
5502
 
5050
- this.modeUsesSelection = function(mode) {
5051
- return ['info', 'selection', 'data', 'labels', 'location', 'vertices', 'rectangles'].includes(mode);
5503
+ this.modeUsesHitDetection = function(mode) {
5504
+ return ['info', 'selection', 'data', 'labels', 'location', 'vertices', 'rectangles', 'edit-lines'].includes(mode);
5052
5505
  };
5053
5506
 
5054
5507
  this.modeUsesPopup = function(mode) {
@@ -6587,6 +7040,21 @@
6587
7040
  return [xmin, ymin, xmax, ymax];
6588
7041
  }
6589
7042
 
7043
+ function getUnfilteredArcLength(arcId, arcs) {
7044
+ var data = arcs.getVertexData();
7045
+ return data.nn[arcId];
7046
+ }
7047
+
7048
+ function getUnfilteredArcCoords(arcId, arcs) {
7049
+ var data = arcs.getVertexData();
7050
+ var coords = [];
7051
+ var start = data.ii[arcId];
7052
+ var n = data.nn[arcId];
7053
+ for (var i=0; i<n; i++) {
7054
+ coords.push([data.xx[start + i], data.yy[start + i]]);
7055
+ }
7056
+ return coords;
7057
+ }
6590
7058
 
6591
7059
  function findArcIdFromVertexId(i, ii) {
6592
7060
  // binary search
@@ -6604,6 +7072,22 @@
6604
7072
  return lower; // assumes dataset is not empty
6605
7073
  }
6606
7074
 
7075
+ function deleteLastArc(arcs) {
7076
+ var data = arcs.getVertexData();
7077
+ var arcId = arcs.size() - 1;
7078
+ var arcLen = data.nn[arcId];
7079
+ var n = data.xx.length;
7080
+ var z = arcs.getRetainedInterval();
7081
+ var xx2 = new Float64Array(data.xx.buffer, 0, n-arcLen);
7082
+ var yy2 = new Float64Array(data.yy.buffer, 0, n-arcLen);
7083
+ var nn2 = new Int32Array(data.nn.buffer, 0, arcs.size() - 1);
7084
+ var zz2 = arcs.isFlat() ?
7085
+ null :
7086
+ new Float64Array(data.zz.buffer, 0, n-arcLen);
7087
+ arcs.updateVertexData(nn2, xx2, yy2, zz2);
7088
+ arcs.setRetainedInterval(z);
7089
+ }
7090
+
6607
7091
  function deleteVertex(arcs, i) {
6608
7092
  var data = arcs.getVertexData();
6609
7093
  var nn = data.nn;
@@ -6634,12 +7118,25 @@
6634
7118
  arcs.setRetainedInterval(z);
6635
7119
  }
6636
7120
 
7121
+ function appendEmptyArc(arcs) {
7122
+ var data = arcs.getVertexData();
7123
+ var nn = utils.extendBuffer(data.nn, data.nn.length + 1, data.nn.length);
7124
+ arcs.updateVertexData(nn, data.xx, data.yy, data.zz);
7125
+ }
7126
+
7127
+ // adds vertex to last arc
7128
+ // (used when adding lines in the GUI)
7129
+ // p: [x, y] point in display coordinates
7130
+ function appendVertex(arcs, p) {
7131
+ var i = arcs.getPointCount(); // one past the last idx
7132
+ insertVertex(arcs, i, p);
7133
+ }
7134
+
6637
7135
  function insertVertex(arcs, i, p) {
6638
7136
  var data = arcs.getVertexData();
6639
7137
  var nn = data.nn;
6640
7138
  var n = data.xx.length;
6641
7139
  var count = 0;
6642
- var found = false;
6643
7140
  var xx2, yy2, zz2;
6644
7141
  // avoid re-allocating memory on each insertion
6645
7142
  if (data.xx.buffer.byteLength >= data.xx.length * 8 + 8) {
@@ -6652,13 +7149,21 @@
6652
7149
  if (!arcs.isFlat()) {
6653
7150
  zz2 = new Float64Array(new ArrayBuffer((n + 1) * 8), 0, n+1);
6654
7151
  }
6655
- for (var j=0; j<nn.length; j++) {
6656
- count += nn[j];
6657
- if (count >= i && !found) { // TODO: confirm this
6658
- nn[j] = nn[j] + 1;
6659
- found = true;
7152
+ if (i < 0 || i > n) {
7153
+ error('Out-of-range vertex insertion index:', i);
7154
+ } else if (i == n) {
7155
+ // appending vertex to last arc
7156
+ nn[nn.length - 1]++;
7157
+ } else {
7158
+ for (var j=0; j<nn.length; j++) {
7159
+ count += nn[j];
7160
+ if (count >= i) { // TODO: confirm this
7161
+ nn[j] = nn[j] + 1;
7162
+ break;
7163
+ }
6660
7164
  }
6661
7165
  }
7166
+
6662
7167
  utils.copyElements(data.xx, 0, xx2, 0, i);
6663
7168
  utils.copyElements(data.yy, 0, yy2, 0, i);
6664
7169
  utils.copyElements(data.xx, i, xx2, i+1, n-i);
@@ -6728,14 +7233,16 @@
6728
7233
  };
6729
7234
  }
6730
7235
 
6731
- function getShapeHitTest(displayLayer, ext, interactionMode) {
7236
+ // featureFilter: optional test function, accepts feature id
7237
+ //
7238
+ function getShapeHitTest(displayLayer, ext, interactionMode, featureFilter) {
6732
7239
  var geoType = displayLayer.layer.geometry_type;
6733
7240
  var test;
6734
7241
  if (geoType == 'point' && displayLayer.style.type == 'styled') {
6735
7242
  test = getGraduatedCircleTest(getRadiusFunction(displayLayer.style));
6736
7243
  } else if (geoType == 'point') {
6737
7244
  test = pointTest;
6738
- } else if (interactionMode == 'vertices') {
7245
+ } else if (interactionMode == 'vertices' || interactionMode == 'edit-lines') {
6739
7246
  test = vertexTest;
6740
7247
  } else if (geoType == 'polyline') {
6741
7248
  test = polylineTest;
@@ -6800,7 +7307,8 @@
6800
7307
  }
6801
7308
 
6802
7309
  function vertexTest(x, y) {
6803
- var maxDist = getZoomAdjustedHitBuffer(25, 2),
7310
+ var bufferPix = 15; // 25;
7311
+ var maxDist = getZoomAdjustedHitBuffer(bufferPix, 2),
6804
7312
  cands = findHitCandidates(x, y, maxDist);
6805
7313
  sortByDistance(x, y, cands, displayLayer.arcs);
6806
7314
  cands = pickNearestCandidates(cands, 0, maxDist);
@@ -6916,6 +7424,9 @@
6916
7424
  bbox = [];
6917
7425
  displayLayer.layer.shapes.forEach(function(shp, shpId) {
6918
7426
  var cand;
7427
+ if (featureFilter && !featureFilter(shpId)) {
7428
+ return;
7429
+ }
6919
7430
  for (var i = 0, n = shp && shp.length; i < n; i++) {
6920
7431
  arcs.getSimpleShapeBounds2(shp[i], bbox);
6921
7432
  if (x + dist < bbox[0] || x - dist > bbox[2] ||
@@ -7041,12 +7552,12 @@
7041
7552
 
7042
7553
  }
7043
7554
 
7044
- function getPointerHitTest(mapLayer, ext, interactionMode) {
7555
+ function getPointerHitTest(mapLayer, ext, interactionMode, featureFilter) {
7045
7556
  var shapeTest, targetLayer;
7046
7557
  if (!mapLayer || !internal.layerHasGeometry(mapLayer.layer)) {
7047
7558
  return function() {return {ids: []};};
7048
7559
  }
7049
- shapeTest = getShapeHitTest(mapLayer, ext, interactionMode);
7560
+ shapeTest = getShapeHitTest(mapLayer, ext, interactionMode, featureFilter);
7050
7561
 
7051
7562
  // e: pointer event
7052
7563
  return function(e) {
@@ -7071,6 +7582,7 @@
7071
7582
  var storedData = noHitData(); // may include additional data from SVG symbol hit (e.g. hit node)
7072
7583
  var selectionIds = [];
7073
7584
  var transientIds = []; // e.g. hit ids while dragging a box
7585
+ var drawingId = -1; // kludge to allow hit detection and drawing (different feature ids)
7074
7586
  var active = false;
7075
7587
  var interactionMode;
7076
7588
  var targetLayer;
@@ -7120,8 +7632,9 @@
7120
7632
  updateHitTest();
7121
7633
  };
7122
7634
 
7123
- function updateHitTest() {
7124
- hitTest = getPointerHitTest(targetLayer, ext, interactionMode);
7635
+ function updateHitTest(featureFilter) {
7636
+ if (!hoverable()) return;
7637
+ hitTest = getPointerHitTest(targetLayer, ext, interactionMode, featureFilter);
7125
7638
  }
7126
7639
 
7127
7640
  function turnOn(mode) {
@@ -7136,9 +7649,14 @@
7136
7649
  active = false;
7137
7650
  hitTest = null;
7138
7651
  pinnedOn = false;
7652
+ drawingId = -1;
7139
7653
  }
7140
7654
  }
7141
7655
 
7656
+ function hoverable() {
7657
+ return true;
7658
+ }
7659
+
7142
7660
  function selectable() {
7143
7661
  return interactionMode == 'selection';
7144
7662
  }
@@ -7148,16 +7666,20 @@
7148
7666
  }
7149
7667
 
7150
7668
  function draggable() {
7151
- return interactionMode == 'vertices' || interactionMode == 'location' || interactionMode == 'labels';
7669
+ return interactionMode == 'vertices' || interactionMode == 'location' ||
7670
+ interactionMode == 'labels' || interactionMode == 'edit-lines';
7152
7671
  }
7153
7672
 
7154
7673
  function clickable() {
7155
7674
  // click used to pin popup and select features
7156
7675
  return interactionMode == 'data' || interactionMode == 'info' ||
7676
+ // interactionMode == 'edit-lines';
7157
7677
  interactionMode == 'selection' || interactionMode == 'rectangles';
7158
7678
  }
7159
7679
 
7160
- self.getHitId = function() {return storedData.id;};
7680
+ self.getHitId = function() {
7681
+ return hitTest ? storedData.id : -1;
7682
+ };
7161
7683
 
7162
7684
  // Get a reference to the active layer, so listeners to hit events can interact
7163
7685
  // with data and shapes
@@ -7187,6 +7709,25 @@
7187
7709
  }
7188
7710
  };
7189
7711
 
7712
+ // manually set the selected feature id(s)
7713
+ // used when hit detection is turned off, e.g. 'edit-lines' mode
7714
+ self.setDrawingId = function(id) {
7715
+ if (id == drawingId) return;
7716
+ drawingId = id >= 0 ? id : -1;
7717
+ updateHitTest(function(shpId) {
7718
+ return shpId != id;
7719
+ });
7720
+ self.triggerChangeEvent();
7721
+ };
7722
+
7723
+ self.triggerChangeEvent = function() {
7724
+ triggerHitEvent('change');
7725
+ };
7726
+
7727
+ self.clearDrawingId = function() {
7728
+ self.setDrawingId(-1);
7729
+ };
7730
+
7190
7731
  self.setHoverVertex = function(p, type) {
7191
7732
  var p2 = storedData.hit_coordinates;
7192
7733
  if (!active || !p) return;
@@ -7195,7 +7736,7 @@
7195
7736
  triggerHitEvent('change');
7196
7737
  };
7197
7738
 
7198
- self.clearVertexOverlay = function() {
7739
+ self.clearHoverVertex = function() {
7199
7740
  if (!storedData.hit_coordinates) return;
7200
7741
  delete storedData.hit_coordinates;
7201
7742
  triggerHitEvent('change');
@@ -7250,7 +7791,7 @@
7250
7791
  gui.on('interaction_mode_change', function(e) {
7251
7792
  self.clearSelection();
7252
7793
  // if (e.mode == 'off' || e.mode == 'box') {
7253
- if (gui.interaction.modeUsesSelection(e.mode)) {
7794
+ if (gui.interaction.modeUsesHitDetection(e.mode)) {
7254
7795
  turnOn(e.mode);
7255
7796
  } else {
7256
7797
  turnOff();
@@ -7270,6 +7811,7 @@
7270
7811
  mouse.on('drag', handlePointerEvent, null, priority);
7271
7812
  mouse.on('dragend', handlePointerEvent, null, priority);
7272
7813
 
7814
+
7273
7815
  mouse.on('click', function(e) {
7274
7816
  if (!hitTest || !active) return;
7275
7817
  e.stopPropagation();
@@ -7383,6 +7925,16 @@
7383
7925
 
7384
7926
  // check if an event is used in the current interaction mode
7385
7927
  function eventIsEnabled(type) {
7928
+ if (!active) return false;
7929
+ if (interactionMode == 'edit-lines' && (type == 'hover' || type == 'dblclick')) {
7930
+ return true; // special case -- using hover for line drawing animation
7931
+ }
7932
+
7933
+ // ignore pointer events when no features are being hit
7934
+ // (don't block pan and other navigation when events aren't being used for editing)
7935
+ var hitId = self.getHitId();
7936
+ if (hitId == -1) return false;
7937
+
7386
7938
  if (type == 'click' && !clickable()) {
7387
7939
  return false;
7388
7940
  }
@@ -7397,22 +7949,31 @@
7397
7949
  }
7398
7950
 
7399
7951
  function handlePointerEvent(e) {
7400
- if (!hitTest || !active) return;
7401
- if (self.getHitId() == -1) return; // ignore pointer events when no features are being hit
7402
- // don't block pan and other navigation in modes when they are not being used
7403
7952
  if (eventIsEnabled(e.type)) {
7404
7953
  e.stopPropagation(); // block navigation
7405
7954
  triggerHitEvent(e.type, e.data);
7406
7955
  }
7407
7956
  }
7408
7957
 
7409
- // d: event data (may be a pointer event object, an ordinary object or null)
7410
- function triggerHitEvent(type, d) {
7958
+ // evt: event data (may be a pointer event object, an ordinary object or null)
7959
+ function triggerHitEvent(type, evt) {
7960
+ var eventData = {
7961
+ mode: interactionMode,
7962
+ overMap: evt ? isOverMap(evt) : null
7963
+ };
7411
7964
  // Merge stored hit data into the event data
7412
- var eventData = utils$1.extend({mode: interactionMode}, d || {}, storedData);
7965
+ utils$1.extend(eventData, evt || {}, storedData);
7413
7966
  if (transientIds.length) {
7967
+ // add transient ids to any other hit ids
7414
7968
  eventData.ids = utils$1.uniq(transientIds.concat(eventData.ids || []));
7415
7969
  }
7970
+ // when drawing, we want the overlay layer to show the path being currently
7971
+ // drawn.
7972
+ if (drawingId >= 0) {
7973
+ // eventData.ids = [drawingId];
7974
+ // eventData.id = drawingId;
7975
+ eventData.ids = utils$1.uniq(eventData.ids.concat([drawingId]));
7976
+ }
7416
7977
  if (pinnedOn) {
7417
7978
  eventData.pinned = true;
7418
7979
  }
@@ -8557,7 +9118,7 @@
8557
9118
  var prevLink = El('span').addClass('popup-nav-arrow colored-text').appendTo(nav).text('◀');
8558
9119
  var navInfo = El('span').addClass('popup-nav-info').appendTo(nav);
8559
9120
  var nextLink = El('span').addClass('popup-nav-arrow colored-text').appendTo(nav).text('▶');
8560
- var refresh = null;
9121
+ var refresh;
8561
9122
 
8562
9123
  el.addClass('rollover'); // used as a sentinel for the hover function
8563
9124
 
@@ -8567,16 +9128,15 @@
8567
9128
  if (refresh) refresh();
8568
9129
  });
8569
9130
 
8570
- self.show = function(id, ids, lyr, pinned) {
8571
- var singleEdit = pinned && gui.interaction.getMode() == 'data';
9131
+ self.show = function(id, ids, lyr, pinned, edit) {
9132
+ var singleEdit = edit || pinned && gui.interaction.getMode() == 'data';
8572
9133
  var multiEdit = pinned && gui.interaction.getMode() == 'selection';
8573
9134
  var maxHeight = parent.node().clientHeight - 36;
8574
- var recIds = multiEdit ? ids : [id];
8575
9135
 
8576
9136
  // stash a function for refreshing the current popup when data changes
8577
9137
  // while the popup is being displayed (e.g. while dragging a label)
8578
9138
  refresh = function() {
8579
- render(content, recIds, lyr, singleEdit || multiEdit);
9139
+ render(id, ids, lyr, pinned, singleEdit || multiEdit);
8580
9140
  };
8581
9141
  refresh();
8582
9142
  if (multiEdit) {
@@ -8623,7 +9183,9 @@
8623
9183
  tab.show();
8624
9184
  }
8625
9185
 
8626
- function render(el, recIds, lyr, editable) {
9186
+ function render(id, ids, lyr, pinned, editable) {
9187
+ var recIds = id >= 0 ? [id] : ids;
9188
+ var el = content;
8627
9189
  var table = lyr.data; // table can be null (e.g. if layer has no attribute data)
8628
9190
  var tableEl = table ? renderTable(recIds, table, editable) : null;
8629
9191
  el.empty(); // clean up if panel is already open
@@ -8647,13 +9209,18 @@
8647
9209
  table && table.getFields().length > 0 ? 'feature': 'layer'));
8648
9210
  }
8649
9211
 
9212
+ var footer = El('div').appendTo(el);
8650
9213
  if (editable) {
8651
9214
  // render "add field" button
8652
- var line = El('div').appendTo(el);
8653
- El('span').addClass('add-field-btn').appendTo(line).on('click', async function(e) {
9215
+ El('span').addClass('add-field-btn').appendTo(footer).on('click', async function(e) {
8654
9216
  // show "add field" dialog
8655
9217
  openAddFieldPopup(gui, recIds, lyr);
8656
9218
  }).text('+ add field');
9219
+ } else if (pinned) {
9220
+ // render "Click to edit" button
9221
+ El('span').addClass('edit-data-btn').appendTo(footer).on('click', async function(e) {
9222
+ self.show(id, ids, lyr, true, true);
9223
+ }).text('▸ click to edit');
8657
9224
  }
8658
9225
  }
8659
9226
 
@@ -9108,19 +9675,27 @@
9108
9675
  }
9109
9676
 
9110
9677
  // pointer thresholds for hovering near a vertex or segment midpoint
9111
- var HOVER_THRESHOLD = 8;
9112
- var MIDPOINT_THRESHOLD = 11;
9678
+ var HOVER_THRESHOLD$1 = 8;
9679
+ var MIDPOINT_THRESHOLD$1 = 11;
9113
9680
 
9114
- function initVertexDragging(gui, ext, hit) {
9115
- var insertionPoint;
9116
- var dragInfo;
9681
+ function initLineEditing(gui, ext, hit) {
9682
+ var insertionPoint; // not used in this mode
9683
+ var dragVertexInfo;
9684
+ var hoverVertexInfo;
9685
+ var prevClickEvent;
9686
+ var prevHoverEvent;
9687
+ var drawingId = -1; // feature id of path being drawn
9117
9688
 
9118
9689
  function active() {
9119
- return gui.interaction.getMode() == 'vertices';
9690
+ return gui.interaction.getMode() == 'edit-lines';
9120
9691
  }
9121
9692
 
9122
9693
  function dragging() {
9123
- return active() && !!dragInfo;
9694
+ return active() && !!dragVertexInfo;
9695
+ }
9696
+
9697
+ function drawing() {
9698
+ return drawingId > -1;
9124
9699
  }
9125
9700
 
9126
9701
  function setHoverVertex(id) {
@@ -9129,42 +9704,69 @@
9129
9704
  }
9130
9705
 
9131
9706
  function clearHoverVertex() {
9132
- hit.clearVertexOverlay();
9707
+ hit.clearHoverVertex();
9708
+ hoverVertexInfo = null;
9133
9709
  }
9134
9710
 
9135
- // return data on the nearest vertex (or identical vertices) to the pointer
9136
- // (if within a distance threshold)
9137
- //
9138
- function findDraggableVertices(e) {
9711
+ gui.on('interaction_mode_change', function(e) {
9712
+ gui.container.findChild('.map-layers').classed('edit-lines', e.mode == 'edit-lines');
9713
+ if (e.mode == 'edit-lines') {
9714
+ turnOn();
9715
+ } else {
9716
+ turnOff();
9717
+ }
9718
+ }, null, 10); // higher priority than hit control, so turnOff() has correct hit target
9719
+
9720
+ gui.on('redo_path_add', function(e) {
9139
9721
  var target = hit.getHitTarget();
9140
- var shp = target.layer.shapes[e.id];
9141
- var p = ext.translatePixelCoords(e.x, e.y);
9142
- var nearestIds = internal.findNearestVertices(p, shp, target.arcs);
9143
- var p2 = target.arcs.getVertex2(nearestIds[0]);
9144
- var dist = geom.distance2D(p[0], p[1], p2[0], p2[1]);
9145
- var pixelDist = dist / ext.getPixelSize();
9146
- if (pixelDist > HOVER_THRESHOLD) {
9147
- return null;
9722
+ clearDrawingInfo();
9723
+ appendNewPath(target, e.p1, e.p2);
9724
+ deleteLastVertex(target); // second vertex is a placeholder
9725
+ gui.undo.redo(); // add next vertex in the path
9726
+ gui.model.updated({arc_count: true});
9727
+ });
9728
+
9729
+ gui.on('undo_path_add', function(e) {
9730
+ deleteLastPath(hit.getHitTarget());
9731
+ clearDrawingInfo();
9732
+ });
9733
+
9734
+ gui.on('redo_path_extend', function(e) {
9735
+ var target = hit.getHitTarget();
9736
+ if (drawing() && prevHoverEvent) {
9737
+ updatePathEndpoint(e.p);
9738
+ appendVertex$1(target, pixToDataCoords(prevHoverEvent.x, prevHoverEvent.y));
9739
+ } else {
9740
+ appendVertex$1(target, e.p);
9148
9741
  }
9149
- var points = nearestIds.map(function(i) {
9150
- return getVertexCoords(target, i); // data coordinates
9151
- });
9152
- return {
9153
- target: target,
9154
- ids: nearestIds,
9155
- points: points
9156
- };
9157
- }
9742
+ });
9158
9743
 
9159
- function findVertexInsertionPoint(e) {
9744
+ gui.on('undo_path_extend', function(e) {
9160
9745
  var target = hit.getHitTarget();
9161
- //// vertex insertion not supported with simplification
9162
- // if (!target.arcs.isFlat()) return null;
9163
- var p = ext.translatePixelCoords(e.x, e.y);
9164
- var midpoint = findNearestMidpoint(p, e.id, target);
9165
- if (!midpoint ||
9166
- midpoint.distance / ext.getPixelSize() > MIDPOINT_THRESHOLD) return null;
9167
- return midpoint;
9746
+ if (drawing() && prevHoverEvent) {
9747
+ deleteLastVertex(target);
9748
+ updatePathEndpoint(pixToDataCoords(prevHoverEvent.x, prevHoverEvent.y));
9749
+ } else {
9750
+ deleteLastVertex(target);
9751
+ }
9752
+ if (getLastArcLength(target) < 2) {
9753
+ gui.undo.undo(); // remove the path
9754
+ }
9755
+ });
9756
+
9757
+ function turnOn() {}
9758
+
9759
+ function turnOff() {
9760
+ finishPath();
9761
+ clearDrawingInfo();
9762
+ insertionPoint = null;
9763
+ }
9764
+
9765
+ function clearDrawingInfo() {
9766
+ hit.clearDrawingId();
9767
+ drawingId = -1;
9768
+ dragVertexInfo = hoverVertexInfo = null;
9769
+ prevClickEvent = prevHoverEvent = null;
9168
9770
  }
9169
9771
 
9170
9772
  hit.on('dragstart', function(e) {
@@ -9172,30 +9774,30 @@
9172
9774
  if (insertionPoint) {
9173
9775
  var target = hit.getHitTarget();
9174
9776
  insertVertex$1(target, insertionPoint.i, insertionPoint.point);
9175
- dragInfo = {
9777
+ dragVertexInfo = {
9176
9778
  target: target,
9177
9779
  insertion: true,
9178
- ids: [insertionPoint.i],
9179
- points: [insertionPoint.point]
9780
+ point: insertionPoint.point,
9781
+ ids: [insertionPoint.i]
9180
9782
  };
9181
9783
  insertionPoint = null;
9182
- } else {
9183
- dragInfo = findDraggableVertices(e);
9784
+ } else if (!drawing()) {
9785
+ dragVertexInfo = findDraggableVertices(e);
9184
9786
  }
9185
- if (dragInfo) {
9186
- setHoverVertex(dragInfo.ids[0]);
9787
+ if (dragVertexInfo) {
9788
+ setHoverVertex(dragVertexInfo.ids[0]);
9187
9789
  }
9188
9790
  });
9189
9791
 
9190
9792
  hit.on('drag', function(e) {
9191
- if (!dragging()) return;
9793
+ if (!dragging() || drawing()) return;
9192
9794
  var target = hit.getHitTarget();
9193
9795
  var p = ext.translatePixelCoords(e.x, e.y);
9194
9796
  if (gui.keyboard.shiftIsPressed()) {
9195
- internal.snapPointToArcEndpoint(p, dragInfo.ids, target.arcs);
9797
+ internal.snapPointToArcEndpoint(p, dragVertexInfo.ids, target.arcs);
9196
9798
  }
9197
- internal.snapVerticesToPoint(dragInfo.ids, p, target.arcs);
9198
- setHoverVertex(dragInfo.ids[0]);
9799
+ internal.snapVerticesToPoint(dragVertexInfo.ids, p, target.arcs);
9800
+ setHoverVertex(dragVertexInfo.ids[0]);
9199
9801
  // redrawing the whole map updates the data layer as well as the overlay layer
9200
9802
  // gui.dispatchEvent('map-needs-refresh');
9201
9803
  });
@@ -9205,18 +9807,112 @@
9205
9807
  // kludge to get dataset to recalculate internal bounding boxes
9206
9808
  hit.getHitTarget().arcs.transformPoints(function() {});
9207
9809
  clearHoverVertex();
9208
- updateVertexCoords(dragInfo.target, dragInfo.ids);
9209
- gui.dispatchEvent('vertex_dragend', dragInfo);
9810
+ updateVertexCoords(dragVertexInfo.target, dragVertexInfo.ids);
9811
+ gui.dispatchEvent('vertex_dragend', dragVertexInfo);
9210
9812
  gui.dispatchEvent('map-needs-refresh');
9211
- dragInfo = null;
9813
+ dragVertexInfo = null;
9212
9814
  });
9213
9815
 
9816
+ // shift + double-click deletes a vertex (when not drawing)
9817
+ // double-click finishes a path (when drawing)
9214
9818
  hit.on('dblclick', function(e) {
9215
9819
  if (!active()) return;
9216
- var info = findDraggableVertices(e); // same selection criteria as for dragging
9820
+ if (drawing()) {
9821
+ // double click finishes a path
9822
+ // before: dblclick is preceded by two clicks, need another vertex delete
9823
+ // now: second click is suppressed
9824
+ // deleteLastVertex(hit.getHitTarget());
9825
+ finishPath();
9826
+ e.stopPropagation(); // prevent dblclick zoom
9827
+ return;
9828
+ }
9829
+ });
9830
+
9831
+ // hover event highlights the nearest point in close proximity to the pointer
9832
+ // ... or the closest segment midpoint (for adding a new vertex)
9833
+ hit.on('hover', function(e) {
9834
+ if (!active() || dragging()) return;
9835
+ if (drawing() && !e.overMap) {
9836
+ finishPath();
9837
+ return;
9838
+ }
9839
+ if (drawing()) {
9840
+ if (gui.keyboard.shiftIsPressed()) {
9841
+ alignPointerPosition(e, prevClickEvent);
9842
+ }
9843
+ updatePathEndpoint(pixToDataCoords(e.x, e.y));
9844
+
9845
+ // highlight nearby snappable vertex (the closest vertex on a nearby line,
9846
+ // or the first vertex of the current drawing path if not near a line)
9847
+ hoverVertexInfo = e.id >= 0 && findDraggableVertices(e) || findPathStartInfo(e);
9848
+ if (hoverVertexInfo) {
9849
+ // hovering near a vertex: highlight the vertex
9850
+ setHoverVertex(hoverVertexInfo.ids[0]);
9851
+ } else {
9852
+ clearHoverVertex();
9853
+ }
9854
+ prevHoverEvent = e;
9855
+ return;
9856
+ }
9857
+ if (e.id >= 0 === false) {
9858
+ // pointer is not near a path
9859
+ return;
9860
+ }
9861
+ hoverVertexInfo = findDraggableVertices(e);
9862
+ insertionPoint = hoverVertexInfo ? null : findMidpointInsertionPoint(e);
9863
+ if (hoverVertexInfo) {
9864
+ // hovering near a vertex: highlight the vertex
9865
+ setHoverVertex(hoverVertexInfo.ids[0]);
9866
+ } else if (insertionPoint) {
9867
+ // hovering near a segment midpoint: highlight the midpoint
9868
+ hit.setHoverVertex(insertionPoint.displayPoint);
9869
+ } else {
9870
+ // pointer is not over a vertex: clear any hover effect
9871
+ clearHoverVertex();
9872
+ }
9873
+ }, null, 100);
9874
+
9875
+ // click starts or extends a new path
9876
+ hit.on('click', function(e) {
9877
+ if (!active()) return;
9878
+ if (detectDoubleClick(e)) return; // ignore second click of a dblclick
9879
+ var p = pixToDataCoords(e.x, e.y);
9880
+ if (drawing() && hoverVertexInfo) {
9881
+ // finish the path if a vertex is highlighted
9882
+ p = hoverVertexInfo.point;
9883
+ extendPath(p);
9884
+ finishPath();
9885
+ } else if (drawing()) {
9886
+ extendPath(p);
9887
+ } else if (gui.keyboard.shiftIsPressed()) {
9888
+ deleteActiveVertex(e);
9889
+ } else {
9890
+ startPath(p);
9891
+ }
9892
+ prevClickEvent = e;
9893
+ });
9894
+
9895
+ // esc key finishes a path
9896
+ gui.keyboard.on('keydown', function(e) {
9897
+ if (active() && e.keyName == 'esc') {
9898
+ finishPath();
9899
+ }
9900
+ });
9901
+
9902
+ function detectDoubleClick(evt) {
9903
+ if (!prevClickEvent) return false;
9904
+ var elapsed = evt.time - prevClickEvent.time;
9905
+ var dx = Math.abs(evt.x - prevClickEvent.x);
9906
+ var dy = Math.abs(evt.y - prevClickEvent.y);
9907
+ var dbl = elapsed < 500 && dx <= 2 && dy <= 2;
9908
+ return dbl;
9909
+ }
9910
+
9911
+ function deleteActiveVertex(e) {
9912
+ var info = findDraggableVertices(e);
9217
9913
  if (!info) return;
9218
- var target = hit.getHitTarget();
9219
9914
  var vId = info.ids[0];
9915
+ var target = hit.getHitTarget();
9220
9916
  if (internal.vertexIsArcStart(vId, target.arcs) ||
9221
9917
  internal.vertexIsArcEnd(vId, target.arcs)) {
9222
9918
  // TODO: support removing arc endpoints
@@ -9229,415 +9925,178 @@
9229
9925
  deleteVertex$1(target, vId);
9230
9926
  clearHoverVertex();
9231
9927
  gui.dispatchEvent('map-needs-refresh');
9232
- });
9928
+ }
9233
9929
 
9234
- // highlight hit vertex in path edit mode
9235
- hit.on('hover', function(e) {
9236
- insertionPoint = null;
9237
- if (!active() || dragging()) return; // no hover effect while dragging
9238
- var info = findDraggableVertices(e);
9239
- if (info) {
9240
- // hovering near a vertex: highlight the vertex
9241
- setHoverVertex(info.ids[0]);
9242
- return;
9243
- }
9244
- // if hovering near a segment midpoint: show the midpoint and save midpoint info
9245
- insertionPoint = findVertexInsertionPoint(e);
9246
- if (insertionPoint) {
9247
- hit.setHoverVertex(insertionPoint.displayPoint);
9930
+ function pixToDataCoords(x, y) {
9931
+ var target = hit.getHitTarget();
9932
+ return translateDisplayPoint(target, ext.translatePixelCoords(x, y));
9933
+ }
9934
+
9935
+ // Change the x, y pixel location of thisEvt so that the segment extending
9936
+ // from prevEvt is aligned to one of 8 angles.
9937
+ function alignPointerPosition(thisEvt, prevEvt) {
9938
+ if (!prevEvt) return;
9939
+ var x0 = prevEvt.x;
9940
+ var y0 = prevEvt.y;
9941
+ var dist = geom.distance2D(thisEvt.x, thisEvt.y, x0, y0);
9942
+ var dist2 = dist / Math.sqrt(2);
9943
+ if (dist < 1) return;
9944
+ var minDist = Infinity;
9945
+ var cands = [
9946
+ {x: x0, y: y0 + dist},
9947
+ {x: x0, y: y0 - dist},
9948
+ {x: x0 + dist, y: y0},
9949
+ {x: x0 - dist, y: y0},
9950
+ {x: x0 + dist2, y: y0 + dist2},
9951
+ {x: x0 + dist2, y: y0 - dist2},
9952
+ {x: x0 - dist2, y: y0 + dist2},
9953
+ {x: x0 - dist2, y: y0 - dist2}
9954
+ ];
9955
+ var snapped = cands.reduce(function(memo, cand) {
9956
+ var dist = geom.distance2D(thisEvt.x, thisEvt.y, cand.x, cand.y);
9957
+ if (dist < minDist) {
9958
+ minDist = dist;
9959
+ return cand;
9960
+ }
9961
+ return memo;
9962
+ }, null);
9963
+ thisEvt.x = snapped.x;
9964
+ thisEvt.y = snapped.y;
9965
+ }
9966
+
9967
+ function finishPath() {
9968
+ if (!drawing()) return;
9969
+ var target = hit.getHitTarget();
9970
+ if (getLastArcLength(target) <= 2) { // includes hover point
9971
+ deleteLastPath(target);
9248
9972
  } else {
9249
- // pointer is not over a vertex: clear any hover effect
9250
- clearHoverVertex();
9973
+ deleteLastVertex(target);
9251
9974
  }
9252
- }, null, 100);
9253
- }
9975
+ clearDrawingInfo();
9976
+ gui.model.updated({arc_count: true});
9977
+ }
9254
9978
 
9979
+ function startPath(p) {
9980
+ var target = hit.getHitTarget();
9981
+ var p1 = hoverVertexInfo ? getVertexCoords(target, hoverVertexInfo.ids[0]) : p;
9982
+ var p2 = p;
9983
+ appendNewPath(target, p1, p2);
9984
+ gui.dispatchEvent('path_add', {target, p1, p2});
9985
+ drawingId = target.layer.shapes.length - 1;
9986
+ hit.setDrawingId(drawingId);
9987
+ }
9255
9988
 
9256
- // Given a location @p (e.g. corresponding to the mouse pointer location),
9257
- // find the midpoint of two vertices on @shp suitable for inserting a new vertex
9258
- function findNearestMidpoint(p, fid, target) {
9259
- var arcs = target.arcs;
9260
- var shp = target.layer.shapes[fid];
9261
- var minDist = Infinity, v;
9262
- internal.forEachSegmentInShape(shp, arcs, function(i, j, xx, yy) {
9263
- var x1 = xx[i],
9264
- y1 = yy[i],
9265
- x2 = xx[j],
9266
- y2 = yy[j],
9267
- cx = (x1 + x2) / 2,
9268
- cy = (y1 + y2) / 2,
9269
- midpoint = [cx, cy],
9270
- dist = geom.distance2D(cx, cy, p[0], p[1]);
9271
- if (dist < minDist) {
9272
- minDist = dist;
9273
- v = {
9274
- i: (i < j ? i : j) + 1, // insertion point
9275
- segment: [i, j],
9276
- segmentLen: geom.distance2D(x1, y1, x2, y2),
9277
- displayPoint: midpoint,
9278
- point: translateDisplayPoint(target, midpoint),
9279
- distance: dist
9280
- };
9989
+ function extendPath(p) {
9990
+ var target = hit.getHitTarget();
9991
+ var len = getLastArcLength(target);
9992
+ if (false && len == 2) {
9993
+ var pathCoords = getLastArcCoords(target);
9994
+ gui.dispatchEvent('path_add', {target, p1: pathCoords[0], p2: pathCoords[1]});
9995
+ } else if (len >= 2) {
9996
+ gui.dispatchEvent('path_extend', {target, p});
9281
9997
  }
9282
- });
9283
- return v || null;
9284
- }
9285
-
9286
- function initInteractiveEditing(gui, ext, hit) {
9287
- initLabelDragging(gui, ext, hit);
9288
- initPointDragging(gui, ext, hit);
9289
- initVertexDragging(gui, ext, hit);
9998
+ appendVertex$1(target, p);
9999
+ hit.triggerChangeEvent();
10000
+ }
9290
10001
 
9291
- // function isClickEvent(up, down) {
9292
- // var elapsed = Math.abs(down.timeStamp - up.timeStamp);
9293
- // var dx = up.screenX - down.screenX;
9294
- // var dy = up.screenY - down.screenY;
9295
- // var dist = Math.sqrt(dx * dx + dy * dy);
9296
- // return dist <= 4 && elapsed < 300;
9297
- // }
9298
- }
10002
+ // p: [x, y] source data coordinates
10003
+ function updatePathEndpoint(p) {
10004
+ var target = hit.getHitTarget();
10005
+ var i = target.arcs.getPointCount() - 1;
10006
+ if (hoverVertexInfo) {
10007
+ p = getVertexCoords(target, hoverVertexInfo.ids[0]); // snap to selected point
10008
+ }
10009
+ setVertexCoords(target, [i], p);
10010
+ hit.triggerChangeEvent();
10011
+ }
9299
10012
 
9300
- var darkStroke = "#334",
9301
- lightStroke = "#b7d9ea",
9302
- violet = "#cc6acc",
9303
- violetFill = "rgba(249, 120, 249, 0.20)",
9304
- gold = "#efc100",
9305
- black = "black",
9306
- grey = "#888",
9307
- selectionFill = "rgba(237, 214, 0, 0.12)",
9308
- hoverFill = "rgba(255, 120, 255, 0.12)",
9309
- activeStyle = { // outline style for the active layer
9310
- type: 'outline',
9311
- strokeColors: [lightStroke, darkStroke],
9312
- strokeWidth: 0.8,
9313
- dotColor: "#223",
9314
- dotSize: 1
9315
- },
9316
- activeStyleDarkMode = {
9317
- type: 'outline',
9318
- strokeColors: [lightStroke, 'white'],
9319
- strokeWidth: 0.9,
9320
- dotColor: 'white',
9321
- dotSize: 1
9322
- },
9323
- activeStyleForLabels = {
9324
- dotColor: "rgba(250, 0, 250, 0.45)", // violet dot with transparency
9325
- dotSize: 1
9326
- },
9327
- referenceStyle = { // outline style for reference layers
9328
- type: 'outline',
9329
- strokeColors: [null, '#78c110'], // upped saturation from #86c927
9330
- strokeWidth: 0.85,
9331
- dotColor: "#73ba20",
9332
- dotSize: 1
9333
- },
9334
- intersectionStyle = {
9335
- dotColor: "#F24400",
9336
- dotSize: 1
9337
- },
9338
- hoverStyles = {
9339
- polygon: {
9340
- fillColor: hoverFill,
9341
- strokeColor: black,
9342
- strokeWidth: 1.2
9343
- }, point: {
9344
- dotColor: violet, // black,
9345
- dotSize: 2.5
9346
- }, polyline: {
9347
- strokeColor: black,
9348
- strokeWidth: 2.5,
9349
- }
9350
- },
9351
- unselectedHoverStyles = {
9352
- polygon: {
9353
- fillColor: 'rgba(0,0,0,0)',
9354
- strokeColor: black,
9355
- strokeWidth: 1.2
9356
- }, point: {
9357
- dotColor: black, // grey,
9358
- dotSize: 2
9359
- }, polyline: {
9360
- strokeColor: black, // grey,
9361
- strokeWidth: 2.5
9362
- }
9363
- },
9364
- selectionStyles = {
9365
- polygon: {
9366
- fillColor: hoverFill,
9367
- strokeColor: black,
9368
- strokeWidth: 1.2
9369
- }, point: {
9370
- dotColor: violet, // black,
9371
- dotSize: 1.5
9372
- }, polyline: {
9373
- strokeColor: violet, // black,
9374
- strokeWidth: 2.5
9375
- }
9376
- },
9377
- // not used
9378
- selectionHoverStyles = {
9379
- polygon: {
9380
- fillColor: selectionFill,
9381
- strokeColor: black,
9382
- strokeWidth: 1.2
9383
- }, point: {
9384
- dotColor: black,
9385
- dotSize: 1.5
9386
- }, polyline: {
9387
- strokeColor: black,
9388
- strokeWidth: 2
9389
- }
9390
- },
9391
- pinnedStyles = {
9392
- polygon: {
9393
- fillColor: violetFill,
9394
- strokeColor: violet,
9395
- strokeWidth: 1.8
9396
- }, point: {
9397
- dotColor: violet,
9398
- dotSize: 3
9399
- }, polyline: {
9400
- strokeColor: violet, // black, // violet,
9401
- strokeWidth: 3
9402
- }
9403
- };
9404
-
9405
- function getIntersectionStyle(lyr, opts) {
9406
- return getDefaultStyle(lyr, intersectionStyle);
9407
- }
9408
-
9409
- // Style for unselected layers with visibility turned on
9410
- // (styled layers have)
9411
- function getReferenceLayerStyle(lyr, opts) {
9412
- var style;
9413
- if (layerHasCanvasDisplayStyle(lyr) && !opts.outlineMode) {
9414
- style = getCanvasDisplayStyle(lyr);
9415
- } else if (internal.layerHasLabels(lyr) && !opts.outlineMode) {
9416
- style = {dotSize: 0}; // no reference dots if labels are visible
9417
- } else {
9418
- style = getDefaultStyle(lyr, referenceStyle);
9419
- }
9420
- return style;
9421
- }
9422
-
9423
- function getActiveLayerStyle(lyr, opts) {
9424
- var style;
9425
- if (layerHasCanvasDisplayStyle(lyr) && !opts.outlineMode) {
9426
- style = getCanvasDisplayStyle(lyr);
9427
- } else if (internal.layerHasLabels(lyr) && !opts.outlineMode) {
9428
- style = getDefaultStyle(lyr, activeStyleForLabels);
9429
- } else if (opts.darkMode) {
9430
- style = getDefaultStyle(lyr, activeStyleDarkMode);
9431
- } else {
9432
- style = getDefaultStyle(lyr, activeStyle);
9433
- }
9434
- return style;
9435
- }
9436
-
9437
- // Returns a display style for the overlay layer.
9438
- // The overlay layer renders several kinds of feature, each of which is displayed
9439
- // with a different style.
9440
- //
9441
- // * hover shapes
9442
- // * selected shapes
9443
- // * pinned shapes
9444
- //
9445
- function getOverlayStyle(baseLyr, o, opts) {
9446
- if (opts.interactionMode == 'vertices') {
9447
- return getVertexStyle(baseLyr, o);
9448
- }
9449
- var geomType = baseLyr.geometry_type;
9450
- var topId = o.id; // pinned id (if pinned) or hover id
9451
- var topIdx = -1;
9452
- var styler = function(style, i) {
9453
- var defaultStyle = i === topIdx ? topStyle : outlineStyle;
9454
- if (baseStyle.styler) {
9455
- // TODO: render default stroke widths without scaling
9456
- // (will need to pass symbol scale to the styler function)
9457
- style.strokeWidth = defaultStyle.strokeWidth;
9458
- baseStyle.styler(style, i); // get styled stroke width (if set)
9459
- style.strokeColor = defaultStyle.strokeColor;
9460
- style.fillColor = defaultStyle.fillColor;
9461
- } else {
9462
- Object.assign(style, defaultStyle);
9463
- }
9464
- };
9465
- var baseStyle = getActiveLayerStyle(baseLyr, opts);
9466
- var outlineStyle = getDefaultStyle(baseLyr, selectionStyles[geomType]);
9467
- var topStyle;
9468
- var ids = o.ids.filter(function(i) {
9469
- return i != o.id; // move selected id to the end
9470
- });
9471
- if (o.id > -1) { // pinned or hover style
9472
- topStyle = getSelectedFeatureStyle(baseLyr, o, opts);
9473
- topIdx = ids.length;
9474
- ids.push(o.id); // put the pinned/hover feature last in the render order
9475
- }
9476
- var style = {
9477
- baseStyle: baseStyle,
9478
- styler,
9479
- ids,
9480
- overlay: true
9481
- };
9482
-
9483
- if (layerHasCanvasDisplayStyle(baseLyr) && !opts.outlineMode) {
9484
- if (geomType == 'point') {
9485
- style.styler = getOverlayPointStyler(getCanvasDisplayStyle(baseLyr).styler, styler);
9486
- }
9487
- style.type = 'styled';
9488
- }
9489
- return ids.length > 0 ? style : null;
9490
- }
9491
-
9492
-
9493
- function getDefaultStyle(lyr, baseStyle) {
9494
- var style = Object.assign({}, baseStyle);
9495
- // reduce the dot size of large point layers
9496
- if (lyr.geometry_type == 'point' && style.dotSize > 0) {
9497
- style.dotSize *= getDotScale$1(lyr);
9498
- }
9499
- return style;
9500
- }
9501
-
9502
- function getDotScale$1(lyr) {
9503
- var topTier = 10000;
9504
- var n = countPoints(lyr.shapes, topTier); // short-circuit point counting above top threshold
9505
- var k = n < 200 && 4 || n < 2500 && 3 || n < topTier && 2 || 1;
9506
- return k;
9507
- }
9508
-
9509
- function countPoints(shapes, max) {
9510
- var count = 0;
9511
- var i, n, shp;
9512
- max = max || Infinity;
9513
- for (i=0, n=shapes.length; i<n && count<max; i++) {
9514
- shp = shapes[i];
9515
- count += shp ? shp.length : 0;
9516
- }
9517
- return count;
9518
- }
9519
-
9520
-
9521
- // style for vertex edit mode
9522
- function getVertexStyle(lyr, o) {
9523
- return {
9524
- ids: o.ids,
9525
- overlay: true,
9526
- strokeColor: black,
9527
- strokeWidth: 1.5,
9528
- vertices: true,
9529
- vertex_overlay_color: violet,
9530
- vertex_overlay: o.hit_coordinates || null,
9531
- selected_points: o.selected_points || null,
9532
- fillColor: null
9533
- };
9534
- }
9535
-
9536
-
9537
- function getSelectedFeatureStyle(lyr, o, opts) {
9538
- var isPinned = o.pinned;
9539
- var inSelection = o.ids.indexOf(o.id) > -1;
9540
- var geomType = lyr.geometry_type;
9541
- var style;
9542
- if (isPinned && opts.interactionMode == 'rectangles') {
9543
- // kludge for rectangle editing mode
9544
- style = selectionStyles[geomType];
9545
- } else if (isPinned) {
9546
- // a feature is pinned
9547
- style = pinnedStyles[geomType];
9548
- } else if (inSelection) {
9549
- // normal hover, or hover id is in the selection set
9550
- style = hoverStyles[geomType];
9551
- } else {
9552
- // features are selected, but hover id is not in the selection set
9553
- style = unselectedHoverStyles[geomType];
9554
- }
9555
- return getDefaultStyle(lyr, style);
9556
- }
9557
-
9558
- // Modify style to use scaled circle instead of dot symbol
9559
- function getOverlayPointStyler(baseStyler, overlayStyler) {
9560
- return function(obj, i) {
9561
- var dotColor;
9562
- var id = obj.ids ? obj.ids[i] : -1;
9563
- obj.strokeWidth = 0; // kludge to support setting minimum stroke width
9564
- baseStyler(obj, id);
9565
- if (overlayStyler) {
9566
- overlayStyler(obj, i);
9567
- }
9568
- dotColor = obj.dotColor;
9569
- if (obj.radius && dotColor) {
9570
- obj.radius += 0.4;
9571
- // delete obj.fillColor; // only show outline
9572
- obj.fillColor = dotColor; // comment out to only highlight stroke
9573
- obj.strokeColor = dotColor;
9574
- obj.strokeWidth = Math.max(obj.strokeWidth + 0.8, 1.5);
9575
- obj.opacity = 1;
9576
- }
9577
- };
9578
- }
9579
-
9580
- function getCanvasDisplayStyle(lyr) {
9581
- var styleIndex = {
9582
- opacity: 'opacity',
9583
- r: 'radius',
9584
- 'fill': 'fillColor',
9585
- 'fill-pattern': 'fillPattern',
9586
- 'fill-effect': 'fillEffect',
9587
- 'fill-opacity': 'fillOpacity',
9588
- 'stroke': 'strokeColor',
9589
- 'stroke-width': 'strokeWidth',
9590
- 'stroke-dasharray': 'lineDash',
9591
- 'stroke-opacity': 'strokeOpacity',
9592
- 'stroke-linecap': 'lineCap',
9593
- 'stroke-linejoin': 'lineJoin',
9594
- 'stroke-miterlimit': 'miterLimit'
9595
- },
9596
- // array of field names of relevant svg display properties
9597
- fields = getCanvasStyleFields(lyr).filter(function(f) {return f in styleIndex;}),
9598
- records = lyr.data.getRecords();
9599
- var styler = function(style, i) {
9600
- var rec = records[i];
9601
- var fname, val;
9602
- for (var j=0; j<fields.length; j++) {
9603
- fname = fields[j];
9604
- val = rec && rec[fname];
9605
- if (val == 'none') {
9606
- val = 'transparent'; // canvas equivalent of CSS 'none'
9607
- }
9608
- // convert svg property name to mapshaper style equivalent
9609
- style[styleIndex[fname]] = val;
9610
- }
9611
-
9612
- if (style.strokeWidth && !style.strokeColor) {
9613
- style.strokeColor = 'black';
9614
- }
9615
- if (!('strokeWidth' in style) && style.strokeColor) {
9616
- style.strokeWidth = 1;
10013
+ function findPathStartInfo(e) {
10014
+ var target = hit.getHitTarget();
10015
+ var arcId = target.arcs.size() - 1;
10016
+ var data = target.arcs.getVertexData();
10017
+ var id = data.ii[arcId];
10018
+ var x = data.xx[id];
10019
+ var y = data.yy[id];
10020
+ var p = ext.translatePixelCoords(e.x, e.y);
10021
+ var dist = geom.distance2D(p[0], p[1], x, y);
10022
+ var pathLen = data.nn[arcId];
10023
+ var pixelDist = dist / ext.getPixelSize();
10024
+ if (pixelDist > HOVER_THRESHOLD$1 || pathLen < 4) {
10025
+ return null;
9617
10026
  }
9618
- if (style.radius > 0 && !style.strokeWidth && !style.fillColor && lyr.geometry_type == 'point') {
9619
- style.fillColor = 'black';
10027
+ var point = translateDisplayPoint(target, [x, y]);
10028
+ return {
10029
+ target, ids: [id], extendable: false, point
10030
+ };
10031
+ }
10032
+
10033
+ // return data on the nearest vertex (or identical vertices) to the pointer
10034
+ // (if within a distance threshold)
10035
+ //
10036
+ function findDraggableVertices(e) {
10037
+ var target = hit.getHitTarget();
10038
+ var shp = target.layer.shapes[e.id];
10039
+ var p = ext.translatePixelCoords(e.x, e.y);
10040
+ var ids = internal.findNearestVertices(p, shp, target.arcs);
10041
+ var p2 = target.arcs.getVertex2(ids[0]);
10042
+ var dist = geom.distance2D(p[0], p[1], p2[0], p2[1]);
10043
+ var pixelDist = dist / ext.getPixelSize();
10044
+ if (pixelDist > HOVER_THRESHOLD$1) {
10045
+ return null;
9620
10046
  }
9621
- };
9622
- return {styler: styler, type: 'styled'};
9623
- }
10047
+ var point = getVertexCoords(target, ids[0]); // data coordinates
10048
+ // find out if the vertex is the endpoint of a single path
10049
+ // (which could be extended by a newly drawn path)
10050
+ var extendable = ids.length == 1 &&
10051
+ internal.vertexIsArcEndpoint(ids[0], target.arcs);
10052
+ return {target, ids, extendable, point};
10053
+ }
9624
10054
 
9625
- // check if layer should be displayed with styles
9626
- function layerHasCanvasDisplayStyle(lyr) {
9627
- var fields = getCanvasStyleFields(lyr);
9628
- if (lyr.geometry_type == 'point') {
9629
- return fields.indexOf('r') > -1; // require 'r' field for point symbols
10055
+ function findMidpointInsertionPoint(e) {
10056
+ var target = hit.getHitTarget();
10057
+ //// vertex insertion not supported with simplification
10058
+ // if (!target.arcs.isFlat()) return null;
10059
+ var p = ext.translatePixelCoords(e.x, e.y);
10060
+ var midpoint = findNearestMidpoint$1(p, e.id, target);
10061
+ if (!midpoint ||
10062
+ midpoint.distance / ext.getPixelSize() > MIDPOINT_THRESHOLD$1) return null;
10063
+ return midpoint;
9630
10064
  }
9631
- return utils$1.difference(fields, ['opacity', 'class']).length > 0;
9632
10065
  }
9633
10066
 
9634
10067
 
9635
- function getCanvasStyleFields(lyr) {
9636
- var fields = lyr.data ? lyr.data.getFields() : [];
9637
- return internal.findPropertiesBySymbolGeom(fields, lyr.geometry_type);
10068
+ // Given a location @p (e.g. corresponding to the mouse pointer location),
10069
+ // find the midpoint of two vertices on @shp suitable for inserting a new vertex
10070
+ function findNearestMidpoint$1(p, fid, target) {
10071
+ var arcs = target.arcs;
10072
+ var shp = target.layer.shapes[fid];
10073
+ var minDist = Infinity, v;
10074
+ internal.forEachSegmentInShape(shp, arcs, function(i, j, xx, yy) {
10075
+ var x1 = xx[i],
10076
+ y1 = yy[i],
10077
+ x2 = xx[j],
10078
+ y2 = yy[j],
10079
+ cx = (x1 + x2) / 2,
10080
+ cy = (y1 + y2) / 2,
10081
+ midpoint = [cx, cy],
10082
+ dist = geom.distance2D(cx, cy, p[0], p[1]);
10083
+ if (dist < minDist) {
10084
+ minDist = dist;
10085
+ v = {
10086
+ i: (i < j ? i : j) + 1, // insertion point
10087
+ segment: [i, j],
10088
+ segmentLen: geom.distance2D(x1, y1, x2, y2),
10089
+ displayPoint: midpoint,
10090
+ point: translateDisplayPoint(target, midpoint),
10091
+ distance: dist
10092
+ };
10093
+ }
10094
+ });
10095
+ return v || null;
9638
10096
  }
9639
10097
 
9640
- function initPointDrawing(gui, ext, mouse, hit) {
10098
+ function initPointDrawing(gui, ext, hit) {
10099
+ var mouse = gui.map.getMouse();
9641
10100
 
9642
10101
  gui.on('interaction_mode_change', function(e) {
9643
10102
  gui.container.findChild('.map-layers').classed('add-points', e.mode === 'add-points');
@@ -9659,114 +10118,201 @@
9659
10118
  var target = hit.getHitTarget();
9660
10119
  var lyr = target.layer;
9661
10120
  var fid = lyr.shapes.length;
9662
- var d = lyr.data ? getEmptyDataRecord(lyr.data) : null;
10121
+ var d = appendNewDataRecord(lyr);
9663
10122
  if (d) {
9664
10123
  // this seems to work even for projected layers -- the data tables
9665
10124
  // of projected and original data seem to be shared.
9666
10125
  lyr.data.getRecords()[fid] = d;
9667
- // TODO: handle SVG symbol layer
9668
- if (internal.layerHasLabels(lyr)) {
9669
- d['label-text'] = 'TBD'; // without text, new labels will be invisible
9670
- } else if (layerHasCanvasDisplayStyle(lyr)) {
9671
- d.r = 3; // show a black circle if layer is styled
9672
- }
9673
10126
  }
9674
10127
  lyr.shapes[fid] = [p];
9675
10128
  updatePointCoords(target, fid);
9676
10129
  }
9677
10130
 
9678
- function getEmptyDataRecord(table) {
9679
- return table.getFields().reduce(function(memo, name) {
9680
- memo[name] = null;
9681
- return memo;
9682
- }, {});
9683
- }
10131
+
9684
10132
  }
9685
10133
 
9686
- function initLineDrawing(gui, ext, mouse, hit) {
9687
- var _on = false;
9688
- var _coords;
9689
- var _lastClick;
10134
+ // pointer thresholds for hovering near a vertex or segment midpoint
10135
+ var HOVER_THRESHOLD = 8;
10136
+ var MIDPOINT_THRESHOLD = 11;
9690
10137
 
9691
- gui.on('interaction_mode_change', function(e) {
9692
- _on = e.mode === 'draw-lines';
9693
- gui.container.findChild('.map-layers').classed('draw-lines', _on);
9694
- });
10138
+ function initVertexDragging(gui, ext, hit) {
10139
+ var insertionPoint;
10140
+ var dragInfo;
9695
10141
 
9696
10142
  function active() {
9697
- return _on;
10143
+ return gui.interaction.getMode() == 'vertices';
9698
10144
  }
9699
10145
 
9700
- function extending() {
9701
- return active() && !!_coords;
10146
+ function dragging() {
10147
+ return active() && !!dragInfo;
9702
10148
  }
9703
10149
 
9704
- function startPath(e) {
9705
-
10150
+ function setHoverVertex(id) {
10151
+ var target = hit.getHitTarget();
10152
+ hit.setHoverVertex(target.arcs.getVertex2(id));
9706
10153
  }
9707
10154
 
9708
- function extendPath(e) {
10155
+ function clearHoverVertex() {
10156
+ hit.clearHoverVertex();
10157
+ }
9709
10158
 
10159
+ // return data on the nearest vertex (or identical vertices) to the pointer
10160
+ // (if within a distance threshold)
10161
+ //
10162
+ function findDraggableVertices(e) {
10163
+ var target = hit.getHitTarget();
10164
+ var shp = target.layer.shapes[e.id];
10165
+ var p = ext.translatePixelCoords(e.x, e.y);
10166
+ var nearestIds = internal.findNearestVertices(p, shp, target.arcs);
10167
+ var p2 = target.arcs.getVertex2(nearestIds[0]);
10168
+ var dist = geom.distance2D(p[0], p[1], p2[0], p2[1]);
10169
+ var pixelDist = dist / ext.getPixelSize();
10170
+ if (pixelDist > HOVER_THRESHOLD) {
10171
+ return null;
10172
+ }
10173
+ var points = nearestIds.map(function(i) {
10174
+ return getVertexCoords(target, i); // data coordinates
10175
+ });
10176
+ return {
10177
+ target: target,
10178
+ ids: nearestIds,
10179
+ points: points
10180
+ };
9710
10181
  }
9711
10182
 
9712
- function finishPath() {
9713
- _coords = null;
10183
+ function findVertexInsertionPoint(e) {
10184
+ var target = hit.getHitTarget();
10185
+ //// vertex insertion not supported with simplification
10186
+ // if (!target.arcs.isFlat()) return null;
10187
+ var p = ext.translatePixelCoords(e.x, e.y);
10188
+ var midpoint = findNearestMidpoint(p, e.id, target);
10189
+ if (!midpoint ||
10190
+ midpoint.distance / ext.getPixelSize() > MIDPOINT_THRESHOLD) return null;
10191
+ return midpoint;
9714
10192
  }
9715
10193
 
9716
- gui.keyboard.on('keydown', function(evt) {
10194
+ hit.on('dragstart', function(e) {
9717
10195
  if (!active()) return;
9718
- if (evt.keyCode == 27) { // esc
9719
- finishPath();
10196
+ if (insertionPoint) {
10197
+ var target = hit.getHitTarget();
10198
+ insertVertex$1(target, insertionPoint.i, insertionPoint.point);
10199
+ dragInfo = {
10200
+ target: target,
10201
+ insertion: true,
10202
+ ids: [insertionPoint.i],
10203
+ points: [insertionPoint.point]
10204
+ };
10205
+ insertionPoint = null;
10206
+ } else {
10207
+ dragInfo = findDraggableVertices(e);
10208
+ }
10209
+ if (dragInfo) {
10210
+ setHoverVertex(dragInfo.ids[0]);
9720
10211
  }
9721
10212
  });
9722
10213
 
9723
- mouse.on('click', function(e) {
9724
- if (!active()) return;
9725
- // console.log('[click]', e)
9726
- // addPoint(e.x, e.y);
10214
+ hit.on('drag', function(e) {
10215
+ if (!dragging()) return;
10216
+ var target = hit.getHitTarget();
10217
+ var p = ext.translatePixelCoords(e.x, e.y);
10218
+ if (gui.keyboard.shiftIsPressed()) {
10219
+ internal.snapPointToArcEndpoint(p, dragInfo.ids, target.arcs);
10220
+ }
10221
+ internal.snapVerticesToPoint(dragInfo.ids, p, target.arcs);
10222
+ setHoverVertex(dragInfo.ids[0]);
10223
+ // redrawing the whole map updates the data layer as well as the overlay layer
9727
10224
  // gui.dispatchEvent('map-needs-refresh');
9728
- _lastClick = e;
9729
10225
  });
9730
10226
 
9731
- // note: second click event is fired before this
9732
- mouse.on('dblclick', function(e) {
9733
- if (!active()) return;
9734
- // block navigation
9735
- e.stopPropagation();
9736
- finishPath();
9737
-
9738
- }, null, 3); // hit detection is priority 2
10227
+ hit.on('dragend', function(e) {
10228
+ if (!dragging()) return;
10229
+ // kludge to get dataset to recalculate internal bounding boxes
10230
+ hit.getHitTarget().arcs.transformPoints(function() {});
10231
+ clearHoverVertex();
10232
+ updateVertexCoords(dragInfo.target, dragInfo.ids);
10233
+ gui.dispatchEvent('vertex_dragend', dragInfo);
10234
+ gui.dispatchEvent('map-needs-refresh');
10235
+ dragInfo = null;
10236
+ });
9739
10237
 
9740
- mouse.on('hover', function(e) {
10238
+ hit.on('dblclick', function(e) {
9741
10239
  if (!active()) return;
10240
+ var info = findDraggableVertices(e); // same selection criteria as for dragging
10241
+ if (!info) return;
10242
+ var target = hit.getHitTarget();
10243
+ var vId = info.ids[0];
10244
+ if (internal.vertexIsArcStart(vId, target.arcs) ||
10245
+ internal.vertexIsArcEnd(vId, target.arcs)) {
10246
+ // TODO: support removing arc endpoints
10247
+ return;
10248
+ }
10249
+ gui.dispatchEvent('vertex_delete', {
10250
+ target: target,
10251
+ vertex_id: vId
10252
+ });
10253
+ deleteVertex$1(target, vId);
10254
+ clearHoverVertex();
10255
+ gui.dispatchEvent('map-needs-refresh');
9742
10256
  });
9743
10257
 
9744
- // x, y: pixel coordinates
9745
- function addPoint(x, y) {
9746
- var p = ext.translatePixelCoords(x, y);
9747
- var target = hit.getHitTarget();
9748
- var lyr = target.layer;
9749
- var fid = lyr.shapes.length;
9750
- if (lyr.data) {
9751
- // this seems to work even for projected layers -- the data tables
9752
- // of projected and original data seem to be shared.
9753
- lyr.data.getRecords()[fid] = getEmptyDataRecord(lyr.data);
10258
+ // highlight hit vertex in path edit mode
10259
+ hit.on('hover', function(e) {
10260
+ insertionPoint = null;
10261
+ if (!active() || dragging()) return; // no hover effect while dragging
10262
+ var info = findDraggableVertices(e);
10263
+ if (info) {
10264
+ // hovering near a vertex: highlight the vertex
10265
+ setHoverVertex(info.ids[0]);
10266
+ return;
9754
10267
  }
9755
- lyr.shapes[fid] = [p];
9756
- updatePointCoords(target, fid);
9757
- }
10268
+ // if hovering near a segment midpoint: show the midpoint and save midpoint info
10269
+ insertionPoint = findVertexInsertionPoint(e);
10270
+ if (insertionPoint) {
10271
+ hit.setHoverVertex(insertionPoint.displayPoint);
10272
+ } else {
10273
+ // pointer is not over a vertex: clear any hover effect
10274
+ clearHoverVertex();
10275
+ }
10276
+ }, null, 100);
10277
+ }
9758
10278
 
9759
- function getEmptyDataRecord(table) {
9760
- return table.getFields().reduce(function(memo, name) {
9761
- memo[name] = null;
9762
- return memo;
9763
- }, {});
9764
- }
10279
+
10280
+ // Given a location @p (e.g. corresponding to the mouse pointer location),
10281
+ // find the midpoint of two vertices on @shp suitable for inserting a new vertex
10282
+ function findNearestMidpoint(p, fid, target) {
10283
+ var arcs = target.arcs;
10284
+ var shp = target.layer.shapes[fid];
10285
+ var minDist = Infinity, v;
10286
+ internal.forEachSegmentInShape(shp, arcs, function(i, j, xx, yy) {
10287
+ var x1 = xx[i],
10288
+ y1 = yy[i],
10289
+ x2 = xx[j],
10290
+ y2 = yy[j],
10291
+ cx = (x1 + x2) / 2,
10292
+ cy = (y1 + y2) / 2,
10293
+ midpoint = [cx, cy],
10294
+ dist = geom.distance2D(cx, cy, p[0], p[1]);
10295
+ if (dist < minDist) {
10296
+ minDist = dist;
10297
+ v = {
10298
+ i: (i < j ? i : j) + 1, // insertion point
10299
+ segment: [i, j],
10300
+ segmentLen: geom.distance2D(x1, y1, x2, y2),
10301
+ displayPoint: midpoint,
10302
+ point: translateDisplayPoint(target, midpoint),
10303
+ distance: dist
10304
+ };
10305
+ }
10306
+ });
10307
+ return v || null;
9765
10308
  }
9766
10309
 
9767
- function initDrawing(gui, ext, mouse, hit) {
9768
- initPointDrawing(gui, ext, mouse, hit);
9769
- initLineDrawing(gui, ext, mouse, hit);
10310
+ function initInteractiveEditing(gui, ext, hit) {
10311
+ initLabelDragging(gui, ext, hit);
10312
+ initPointDragging(gui, ext, hit);
10313
+ initPointDrawing(gui, ext, hit);
10314
+ initLineEditing(gui, ext, hit);
10315
+ initVertexDragging(gui, ext, hit);
9770
10316
  }
9771
10317
 
9772
10318
  function MapExtent(_position) {
@@ -10317,7 +10863,7 @@
10317
10863
  _ctx.beginPath();
10318
10864
  _ctx.fillStyle = style.vertex_overlay_color || 'black';
10319
10865
  p = style.vertex_overlay;
10320
- drawCircle(p[0] * t.mx + t.bx, p[1] * t.my + t.by, radius * 1.6, _ctx);
10866
+ drawCircle(p[0] * t.mx + t.bx, p[1] * t.my + t.by, radius * 2, _ctx);
10321
10867
  _ctx.fill();
10322
10868
  _ctx.closePath();
10323
10869
  }
@@ -10964,6 +11510,10 @@
10964
11510
  runCommand(cmd);
10965
11511
  });
10966
11512
 
11513
+ new SimpleButton(popup.findChild('.frame-btn')).on('click', function() {
11514
+ openAddFramePopup(gui, box.getDataCoords());
11515
+ });
11516
+
10967
11517
  gui.addMode('box_tool', turnOn, turnOff);
10968
11518
 
10969
11519
  gui.on('interaction_mode_change', function(e) {
@@ -11036,6 +11586,30 @@
11036
11586
  hideCoords();
11037
11587
  }
11038
11588
 
11589
+ function openAddFramePopup(gui, bbox) {
11590
+ var popup = showPopupAlert('', 'Add a map frame');
11591
+ var el = popup.container();
11592
+ el.addClass('option-menu');
11593
+ var html = `<p>Enter a width in px, cm or inches to create a frame layer
11594
+ for setting the size of the map for symbol scaling in the
11595
+ GUI and setting the size and crop of SVG output.</p><div><input type="text" class="frame-width text-input" placeholder="examples: 600px 5in"></div>
11596
+ <div tabindex="0" class="btn dialog-btn">Create</div></span>`;
11597
+ el.html(html);
11598
+ var input = el.findChild('.frame-width');
11599
+ input.node().focus();
11600
+ var btn = el.findChild('.btn').on('click', function() {
11601
+ var widthStr = input.node().value.trim();
11602
+ if (parseFloat(widthStr) > 0 === false) {
11603
+ // invalid input
11604
+ input.node().value = '';
11605
+ return;
11606
+ }
11607
+ var cmd = `-rectangle + name=frame bbox='${bbox.join(',')}' width='${widthStr}'`;
11608
+ runCommand(cmd);
11609
+ popup.close();
11610
+ });
11611
+ }
11612
+
11039
11613
  return self;
11040
11614
  }
11041
11615
 
@@ -11143,6 +11717,7 @@
11143
11717
  return filteredArcs;
11144
11718
  }
11145
11719
 
11720
+ // TODO: better job of detecting arc change... e.g. revision number
11146
11721
  unfilteredArcs.getScaledArcs = function(ext) {
11147
11722
  if (filteredArcs) {
11148
11723
  // match simplification of unfiltered arcs
@@ -11646,7 +12221,6 @@
11646
12221
  model.on('update', onUpdate);
11647
12222
 
11648
12223
 
11649
-
11650
12224
  // Update display of segment intersections
11651
12225
  this.setIntersectionLayer = function(lyr, dataset) {
11652
12226
  if (lyr == _intersectionLyr) return; // no change
@@ -11692,6 +12266,14 @@
11692
12266
  this.isActiveLayer = isActiveLayer;
11693
12267
  this.isVisibleLayer = isVisibleLayer;
11694
12268
  this.getActiveLayer = function() { return _activeLyr; };
12269
+ // this.getViewData = function() {
12270
+ // return {
12271
+ // isPreview: isPreviewView(),
12272
+ // isTable: isTableView(),
12273
+ // isEmpty: !_activeLyr,
12274
+ // dynamicCRS: _dynamicCRS || null
12275
+ // };
12276
+ // };
11695
12277
 
11696
12278
  // called by layer menu after layer visibility is updated
11697
12279
  this.redraw = function() {
@@ -11808,7 +12390,6 @@
11808
12390
  new BoxTool(gui, _ext, _nav),
11809
12391
  new RectangleControl(gui, _hit),
11810
12392
  initInteractiveEditing(gui, _ext, _hit);
11811
- initDrawing(gui, _ext, _mouse, _hit);
11812
12393
  _hit.on('change', updateOverlayLayer);
11813
12394
  }
11814
12395
 
@@ -12014,7 +12595,7 @@
12014
12595
  // 'hover' highlight has changed -- only refresh overlay
12015
12596
  // (default) anything could have changed
12016
12597
  function drawLayers2(action) {
12017
- // sometimes styles need to be regenerated with 'hover' action (when)
12598
+ // sometimes styles need to be regenerated with 'hover' action (when?)
12018
12599
  var layersMayHaveChanged = action != 'nav'; // !action;
12019
12600
  var fullBounds;
12020
12601
  var contentLayers = getDrawableContentLayers();