mapshaper 0.6.76 → 0.6.78

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.
@@ -1475,6 +1475,7 @@
1475
1475
 
1476
1476
  async function saveSnapshot(gui) {
1477
1477
  var obj = await captureSnapshot(gui);
1478
+ if (!obj) return;
1478
1479
  // storing an unpacked object is usually a bit faster (~20%)
1479
1480
  // note: we don't know the size of unpacked snapshot objects
1480
1481
  // obj = internal.pack(obj);
@@ -1554,12 +1555,13 @@
1554
1555
  }
1555
1556
 
1556
1557
  async function captureSnapshot(gui) {
1557
- var datasets = gui.model.getDatasets();
1558
- var lyr = gui.model.getActiveLayer().layer;
1558
+ var lyr = gui.model.getActiveLayer()?.layer;
1559
+ if (!lyr) return null; // no data -- no snapshot
1560
+ lyr.active = true;
1559
1561
  // compact: true applies compression to vector coordinates, for ~30% reduction
1560
1562
  // in file size in a typical polygon or polyline file, but longer processing time
1561
1563
  var opts = {compact: false};
1562
- lyr.active = true;
1564
+ var datasets = gui.model.getDatasets();
1563
1565
  // console.time('msx');
1564
1566
  var obj = await internal.exportDatasetsToPack(datasets, opts);
1565
1567
  // console.timeEnd('msx')
@@ -1736,12 +1738,12 @@
1736
1738
  var btn = el.findChild('.btn').on('click', function() {
1737
1739
  var nameStr = name.node().value.trim();
1738
1740
  var type = el.findChild('input:checked').node().value;
1739
- addLayer(gui, nameStr, type);
1741
+ addEmptyLayer(gui, nameStr, type);
1740
1742
  popup.close();
1741
1743
  });
1742
1744
  }
1743
1745
 
1744
- function addLayer(gui, name, type) {
1746
+ function addEmptyLayer(gui, name, type) {
1745
1747
  var targ = gui.model.getActiveLayer();
1746
1748
  var crsInfo = targ && internal.getDatasetCrsInfo(targ.dataset);
1747
1749
  var dataset = {
@@ -1752,7 +1754,7 @@
1752
1754
  }],
1753
1755
  info: {}
1754
1756
  };
1755
- if (type == 'polygon' || type == 'point') {
1757
+ if (type == 'polygon' || type == 'polyline') {
1756
1758
  dataset.arcs = new internal.ArcCollection();
1757
1759
  }
1758
1760
  if (crsInfo) {
@@ -1864,7 +1866,6 @@
1864
1866
  var initialImport = true;
1865
1867
  var importCount = 0;
1866
1868
  var importTotal = 0;
1867
- var overQuickView = false;
1868
1869
  var useQuickView = false;
1869
1870
  var queuedFiles = [];
1870
1871
  var manifestFiles = opts.files || [];
@@ -1874,50 +1875,48 @@
1874
1875
  catalog = new CatalogControl(gui, opts.catalog, downloadFiles);
1875
1876
  }
1876
1877
 
1877
- new SimpleButton('#import-buttons .submit-btn').on('click', importQueuedFiles);
1878
- new SimpleButton('#import-buttons .cancel-btn').on('click', gui.clearMode);
1879
- new DropControl(gui, 'body', receiveFiles);
1880
- new FileChooser('#file-selection-btn', receiveFiles);
1881
- new FileChooser('#import-buttons .add-btn', receiveFiles);
1878
+ var submitBtn = new SimpleButton('#import-options .submit-btn').on('click', importQueuedFiles);
1879
+ new SimpleButton('#import-options .cancel-btn').on('click', gui.clearMode);
1880
+ new DropControl(gui, 'body', receiveFilesWithOption);
1881
+ new FileChooser('#import-options .add-btn', receiveFilesWithOption);
1882
1882
  new FileChooser('#add-file-btn', receiveFiles);
1883
1883
  new SimpleButton('#add-empty-btn').on('click', function() {
1884
1884
  gui.clearMode(); // close import dialog
1885
1885
  openAddLayerPopup(gui);
1886
1886
  });
1887
- initDropArea('#import-quick-drop', true);
1888
- initDropArea('#import-drop');
1887
+ // initDropArea('#import-quick-drop', true);
1888
+ // initDropArea('#import-drop');
1889
1889
  gui.keyboard.onMenuSubmit(El('#import-options'), importQueuedFiles);
1890
1890
 
1891
1891
  gui.addMode('import', turnOn, turnOff);
1892
1892
  gui.enterMode('import');
1893
1893
 
1894
- gui.on('mode', function(e) {
1895
- // re-open import opts if leaving alert or console modes and nothing has been imported yet
1896
- if (!e.name && model.isEmpty()) {
1897
- gui.enterMode('import');
1898
- }
1899
- });
1900
-
1901
- function initDropArea(el, isQuick) {
1902
- var area = El(el)
1903
- .on('dragleave', onout)
1904
- .on('dragover', function() {overQuickView = !!isQuick; onover();})
1905
- .on('mouseover', onover)
1906
- .on('mouseout', onout);
1907
-
1908
- function onover() {
1909
- area.addClass('dragover');
1894
+ function turnOn() {
1895
+ if (manifestFiles.length > 0) {
1896
+ downloadFiles(manifestFiles, true);
1897
+ manifestFiles = [];
1898
+ } else if (model.isEmpty()) {
1899
+ showImportMenu();
1910
1900
  }
1901
+ }
1911
1902
 
1912
- function onout() {
1913
- overQuickView = false;
1914
- area.removeClass('dragover');
1903
+ function turnOff() {
1904
+ var target;
1905
+ if (catalog) catalog.reset(); // re-enable clickable catalog
1906
+ if (importCount > 0) {
1907
+ onImportComplete();
1908
+ importTotal += importCount;
1909
+ importCount = 0;
1915
1910
  }
1911
+ gui.clearProgressMessage();
1912
+ initialImport = false; // unset 'quick view' mode, if on
1913
+ clearQueuedFiles();
1914
+ hideImportMenu();
1916
1915
  }
1917
1916
 
1918
1917
  async function importQueuedFiles() {
1919
- gui.container.removeClass('queued-files');
1920
- gui.container.removeClass('splash-screen');
1918
+ // gui.container.removeClass('queued-files');
1919
+ hideImportMenu();
1921
1920
  var files = queuedFiles;
1922
1921
  try {
1923
1922
  if (files.length > 0) {
@@ -1934,27 +1933,7 @@
1934
1933
  }
1935
1934
  }
1936
1935
 
1937
- function turnOn() {
1938
- if (manifestFiles.length > 0) {
1939
- downloadFiles(manifestFiles, true);
1940
- manifestFiles = [];
1941
- } else if (model.isEmpty()) {
1942
- gui.container.addClass('splash-screen');
1943
- }
1944
- }
1945
1936
 
1946
- function turnOff() {
1947
- var target;
1948
- if (catalog) catalog.reset(); // re-enable clickable catalog
1949
- if (importCount > 0) {
1950
- onImportComplete();
1951
- importTotal += importCount;
1952
- importCount = 0;
1953
- }
1954
- gui.clearProgressMessage();
1955
- initialImport = false; // unset 'quick view' mode, if on
1956
- clearQueuedFiles();
1957
- }
1958
1937
 
1959
1938
  function onImportComplete() {
1960
1939
  // display last layer of last imported dataset
@@ -1987,6 +1966,7 @@
1987
1966
  }, []);
1988
1967
  }
1989
1968
 
1969
+
1990
1970
  function showQueuedFiles() {
1991
1971
  var list = gui.container.findChild('.dropped-file-list').empty();
1992
1972
  queuedFiles.forEach(function(f) {
@@ -2006,13 +1986,20 @@
2006
1986
  }
2007
1987
  });
2008
1988
  });
1989
+ submitBtn.classed('disabled', queuedFiles.length === 0);
1990
+ }
1991
+
1992
+ function receiveFilesWithOption(files) {
1993
+ var quickView = !El('.advanced-import-options').node().checked;
1994
+ receiveFiles(files, quickView);
2009
1995
  }
2010
1996
 
2011
- async function receiveFiles(files) {
1997
+ async function receiveFiles(files, quickView) {
2012
1998
  var names = getFileNames(files);
2013
1999
  var expanded = [];
2014
2000
  if (files.length === 0) return;
2015
- useQuickView = importTotal === 0 && (opts.quick_view || overQuickView);
2001
+ useQuickView = importTotal === 0 && (opts.quick_view ||
2002
+ quickView);
2016
2003
  try {
2017
2004
  expanded = await expandFiles(files);
2018
2005
  } catch(e) {
@@ -2030,12 +2017,23 @@
2030
2017
  if (useQuickView) {
2031
2018
  await importQueuedFiles();
2032
2019
  } else {
2033
- gui.container.addClass('queued-files');
2034
- El('#path-import-options').classed('hidden', !filesMayContainPaths(queuedFiles));
2035
- showQueuedFiles();
2020
+ showImportMenu();
2036
2021
  }
2037
2022
  }
2038
2023
 
2024
+ function showImportMenu() {
2025
+ // gui.container.addClass('queued-files');
2026
+ El('#import-options').show();
2027
+ gui.container.classed('queued-files', queuedFiles.length > 0);
2028
+ El('#path-import-options').classed('hidden', !filesMayContainPaths(queuedFiles));
2029
+ showQueuedFiles();
2030
+ }
2031
+
2032
+ function hideImportMenu() {
2033
+ // gui.container.removeClass('queued-files');
2034
+ El('#import-options').hide();
2035
+ }
2036
+
2039
2037
  function getFileNames(files) {
2040
2038
  return Array.from(files).map(function(f) {return f.name;});
2041
2039
  }
@@ -2531,7 +2529,11 @@
2531
2529
  // prevent GUI message popup on error
2532
2530
  internal.setLoggingForCLI();
2533
2531
  try {
2534
- crs = internal.getDatasetCRS(dataset);
2532
+ if (!dataset || internal.datasetIsEmpty(dataset)) {
2533
+ crs = internal.parseCrsString('wgs84');
2534
+ } else {
2535
+ crs = internal.getDatasetCRS(dataset);
2536
+ }
2535
2537
  } catch(e) {
2536
2538
  err = e.message;
2537
2539
  }
@@ -2714,7 +2716,8 @@
2714
2716
  if (opts.interactionMode == 'vertices') {
2715
2717
  return getVertexStyle(baseLyr, o);
2716
2718
  }
2717
- if (opts.interactionMode == 'drawing') {
2719
+ if (opts.interactionMode == 'edit_lines' ||
2720
+ opts.interactionMode == 'edit_polygons') {
2718
2721
  return getLineEditingStyle(o);
2719
2722
  }
2720
2723
  var geomType = baseLyr.geometry_type;
@@ -2923,235 +2926,708 @@
2923
2926
  return internal.findPropertiesBySymbolGeom(fields, lyr.geometry_type);
2924
2927
  }
2925
2928
 
2926
- function flattenArcs(lyr) {
2927
- lyr.gui.source.dataset.arcs.flatten();
2928
- if (isProjectedLayer(lyr)) {
2929
- lyr.gui.displayArcs.flatten();
2930
- }
2931
- }
2929
+ // Create low-detail versions of large arc collections for faster rendering
2930
+ // at zoomed-out scales.
2931
+ function enhanceArcCollectionForDisplay(unfilteredArcs) {
2932
+ var size = unfilteredArcs.getPointCount(),
2933
+ filteredArcs, filteredSegLen;
2932
2934
 
2933
- function setZ(lyr, z) {
2934
- lyr.gui.source.dataset.arcs.setRetainedInterval(z);
2935
- if (isProjectedLayer(lyr)) {
2936
- lyr.gui.displayArcs.setRetainedInterval(z);
2935
+ // Only generate low-detail arcs for larger datasets
2936
+ if (size > 5e5) {
2937
+ update();
2937
2938
  }
2938
- }
2939
2939
 
2940
- function updateZ(lyr) {
2941
- if (isProjectedLayer(lyr) && !lyr.gui.source.dataset.arcs.isFlat()) {
2942
- lyr.gui.displayArcs.setThresholds(lyr.gui.source.dataset.arcs.getVertexData().zz);
2940
+ function update() {
2941
+ if (unfilteredArcs.getVertexData().zz) {
2942
+ // Use precalculated simplification data for vertex filtering, if available
2943
+ filteredArcs = initFilteredArcs(unfilteredArcs);
2944
+ filteredSegLen = internal.getAvgSegment(filteredArcs);
2945
+ } else {
2946
+ // Use fast simplification as a fallback
2947
+ filteredSegLen = internal.getAvgSegment(unfilteredArcs) * 4;
2948
+ filteredArcs = internal.simplifyArcsFast(unfilteredArcs, filteredSegLen);
2949
+ }
2943
2950
  }
2944
- }
2945
2951
 
2946
- function appendNewDataRecord(layer) {
2947
- if (!layer.data) return null;
2948
- var fields = layer.data.getFields();
2949
- var d = getEmptyDataRecord(layer.data);
2950
- // TODO: handle SVG symbol layer
2951
- if (internal.layerHasLabels(layer)) {
2952
- d['label-text'] = 'TBD'; // without text, new labels will be invisible
2953
- } else if (layer.geometry_type == 'point' && fields.includes('r')) {
2954
- d.r = 3; // show a black circle if layer is styled
2955
- }
2956
- if (layer.geometry_type == 'polyline' || layer.geometry_type == 'polygon') {
2957
- if (fields.includes('stroke')) d.stroke = 'black';
2958
- if (fields.includes('stroke-width')) d['stroke-width'] = 1;
2952
+ function initFilteredArcs(arcs) {
2953
+ var filterPct = 0.08;
2954
+ var nth = Math.ceil(arcs.getPointCount() / 5e5);
2955
+ var currInterval = arcs.getRetainedInterval();
2956
+ var filterZ = arcs.getThresholdByPct(filterPct, nth);
2957
+ var filteredArcs = arcs.setRetainedInterval(filterZ).getFilteredCopy();
2958
+ arcs.setRetainedInterval(currInterval); // reset current simplification
2959
+ return filteredArcs;
2959
2960
  }
2960
- if (layer.geometry_type == 'polygon') {
2961
- if (fields.includes('fill')) {
2962
- d.fill = 'rgba(0,0,0,0.10)'; // 'rgba(249,120,249,0.20)';
2961
+
2962
+ // TODO: better job of detecting arc change... e.g. revision number
2963
+ unfilteredArcs.getScaledArcs = function(ext) {
2964
+ // check for changes in the number of arcs (probably due to editing)
2965
+ if (filteredArcs && filteredArcs.size() != unfilteredArcs.size()) {
2966
+ // arc count has changed... probably due to editing
2967
+ update();
2968
+ if (filteredArcs.size() != unfilteredArcs.size()) {
2969
+ throw Error('Internal error');
2970
+ }
2963
2971
  }
2964
- }
2965
- // TODO: better styling
2966
- layer.data.getRecords().push(d);
2967
- return d;
2972
+ if (filteredArcs) {
2973
+ // match simplification of unfiltered arcs
2974
+ filteredArcs.setRetainedInterval(unfilteredArcs.getRetainedInterval());
2975
+ }
2976
+ // switch to filtered version of arcs at small scales
2977
+ var unitsPerPixel = 1/ext.getTransform().mx,
2978
+ useFiltering = filteredArcs && unitsPerPixel > filteredSegLen * 1.5;
2979
+ return useFiltering ? filteredArcs : unfilteredArcs;
2980
+ };
2968
2981
  }
2969
2982
 
2970
- function getEmptyDataRecord(table) {
2971
- return table.getFields().reduce(function(memo, name) {
2972
- memo[name] = null;
2973
- return memo;
2974
- }, {});
2975
- }
2983
+ function getDisplayLayerForTable(tableArg) {
2984
+ var table = tableArg || new internal.DataTable(0),
2985
+ n = table.size(),
2986
+ cellWidth = 12,
2987
+ cellHeight = 5,
2988
+ gutter = 6,
2989
+ arcs = [],
2990
+ shapes = [],
2991
+ aspectRatio = 1.1,
2992
+ x, y, col, row, blockSize;
2976
2993
 
2977
- function deleteLastPath(lyr) {
2978
- var arcId = lyr.gui.displayArcs.size() - 1;
2979
- if (lyr.data) {
2980
- lyr.data.getRecords().pop();
2994
+ if (n > 10000) {
2995
+ arcs = null;
2996
+ gutter = 0;
2997
+ cellWidth = 4;
2998
+ cellHeight = 4;
2999
+ aspectRatio = 1.45;
3000
+ } else if (n > 5000) {
3001
+ cellWidth = 5;
3002
+ gutter = 3;
3003
+ aspectRatio = 1.45;
3004
+ } else if (n > 1000) {
3005
+ gutter = 3;
3006
+ cellWidth = 8;
3007
+ aspectRatio = 1.3;
2981
3008
  }
2982
- var shp = lyr.shapes.pop();
2983
- internal.deleteLastArc(lyr.gui.displayArcs);
2984
- if (isProjectedLayer(lyr)) {
2985
- internal.deleteLastArc(lyr.gui.source.dataset.arcs);
3009
+
3010
+ if (n < 25) {
3011
+ blockSize = n;
3012
+ } else {
3013
+ blockSize = Math.sqrt(n * (cellWidth + gutter) / cellHeight / aspectRatio) | 0;
2986
3014
  }
2987
- }
2988
3015
 
2989
- // p1, p2: two points in source data CRS coords.
2990
- function appendNewPath(lyr, p1, p2) {
2991
- var arcId = lyr.gui.displayArcs.size();
2992
- internal.appendEmptyArc(lyr.gui.displayArcs);
2993
- lyr.shapes.push([[arcId]]);
2994
- if (isProjectedLayer(lyr)) {
2995
- internal.appendEmptyArc(lyr.gui.source.dataset.arcs);
3016
+ for (var i=0; i<n; i++) {
3017
+ row = i % blockSize;
3018
+ col = Math.floor(i / blockSize);
3019
+ x = col * (cellWidth + gutter);
3020
+ y = cellHeight * (blockSize - row);
3021
+ if (arcs) {
3022
+ arcs.push(getArc(x, y, cellWidth, cellHeight));
3023
+ shapes.push([[i]]);
3024
+ } else {
3025
+ shapes.push([[x, y]]);
3026
+ }
2996
3027
  }
2997
- appendVertex$1(lyr, p1);
2998
- appendVertex$1(lyr, p2);
2999
- appendNewDataRecord(lyr);
3000
- }
3001
3028
 
3002
- // p: point in source data CRS coords.
3003
- function insertVertex$1(lyr, id, p) {
3004
- internal.insertVertex(lyr.gui.source.dataset.arcs, id, p);
3005
- if (isProjectedLayer(lyr)) {
3006
- internal.insertVertex(lyr.gui.displayArcs, id, lyr.gui.projectPoint(p[0], p[1]));
3029
+ function getArc(x, y, w, h) {
3030
+ return [[x, y], [x + w, y], [x + w, y - h], [x, y - h], [x, y]];
3007
3031
  }
3008
- }
3009
3032
 
3010
- function appendVertex$1(lyr, p) {
3011
- var n = lyr.gui.source.dataset.arcs.getPointCount();
3012
- insertVertex$1(lyr, n, p);
3033
+ return {
3034
+ layer: {
3035
+ geometry_type: arcs ? 'polygon' : 'point',
3036
+ shapes: shapes,
3037
+ data: table
3038
+ },
3039
+ arcs: arcs ? new internal.ArcCollection(arcs) : null
3040
+ };
3013
3041
  }
3014
3042
 
3015
- // TODO: make sure we're not also removing an entire arc
3016
- function deleteLastVertex(lyr) {
3017
- deleteVertex$1(lyr, lyr.gui.displayArcs.getPointCount() - 1);
3018
- }
3043
+ var R = 6378137;
3044
+ var D2R = Math.PI / 180;
3045
+ var R2D = 180 / Math.PI;
3019
3046
 
3020
- function deleteVertex$1(lyr, id) {
3021
- internal.deleteVertex(lyr.gui.displayArcs, id);
3022
- if (isProjectedLayer(lyr)) {
3023
- internal.deleteVertex(lyr.gui.source.dataset.arcs, id);
3047
+ // Assumes projections are available
3048
+
3049
+ function needReprojectionForDisplay(sourceCRS, displayCRS) {
3050
+ if (!sourceCRS || !displayCRS) {
3051
+ return false;
3052
+ }
3053
+ if (internal.crsAreEqual(sourceCRS, displayCRS)) {
3054
+ return false;
3024
3055
  }
3056
+ return true;
3025
3057
  }
3026
3058
 
3027
- function getLastArcCoords(target) {
3028
- var arcId = target.gui.source.dataset.arcs.size() - 1;
3029
- return internal.getUnfilteredArcCoords(arcId, target.gui.source.dataset.arcs);
3059
+ // bbox in wgs84 coords
3060
+ // dest: CRS, which may also be wgs84
3061
+ function projectLatLonBBox(bbox, dest) {
3062
+ if (!dest || internal.isWGS84(dest)) {
3063
+ return bbox.concat();
3064
+ }
3065
+ var proj = internal.getProjTransform2(internal.parseCrsString('wgs84'), dest);
3066
+ var bbox2 = proj(bbox[0], bbox[1]).concat(proj(bbox[2], bbox[3]));
3067
+ return bbox2;
3030
3068
  }
3031
3069
 
3032
- function getLastVertexCoords(target) {
3033
- var arcs = target.gui.source.dataset.arcs;
3034
- return internal.getVertexCoords(arcs.getPointCount() - 1, arcs);
3035
- }
3070
+ function projectArcsForDisplay(arcs, src, dest) {
3071
+ var copy = arcs.getCopy(); // need to flatten first?
3072
+ var destIsWebMerc = internal.isWebMercator(dest);
3073
+ if (destIsWebMerc && internal.isWebMercator(src)) {
3074
+ return copy;
3075
+ }
3036
3076
 
3037
- function getLastArcLength(target) {
3038
- var arcId = target.gui.source.dataset.arcs.size() - 1;
3039
- return internal.getUnfilteredArcLength(arcId, target.gui.source.dataset.arcs);
3077
+ var wgs84 = internal.parseCrsString('wgs84');
3078
+ var toWGS84 = internal.getProjTransform2(src, wgs84);
3079
+ var fromWGS84 = internal.getProjTransform2(wgs84, dest);
3080
+
3081
+ try {
3082
+ // first try projectArcs() -- it's fast and preserves arc ids
3083
+ // (so vertex editing doesn't break)
3084
+ if (!internal.isWGS84(src)) {
3085
+ // use wgs84 as a pivot CRS, so we can handle polar coordinates
3086
+ // that can't be projected to Mercator
3087
+ internal.projectArcs(copy, toWGS84);
3088
+ }
3089
+ if (destIsWebMerc) {
3090
+ // handle polar points by clamping them to they will project
3091
+ // (downside: may cause unexpected behavior when editing vertices interactively)
3092
+ clampY(copy);
3093
+ }
3094
+ internal.projectArcs(copy, fromWGS84);
3095
+ } catch(e) {
3096
+ console.error(e);
3097
+ // use the more robust projectArcs2 if projectArcs throws an error
3098
+ // downside: projectArcs2 discards Z values and changes arc indexing,
3099
+ // which will break vertex editing.
3100
+ var reproject = internal.getProjTransform2(src, dest);
3101
+ copy = arcs.getCopy();
3102
+ internal.projectArcs2(copy, reproject);
3103
+ }
3104
+ return copy;
3040
3105
  }
3041
3106
 
3042
- function getPointCoords(lyr, fid) {
3043
- return internal.cloneShape(lyr.shapes[fid]);
3107
+ function clampY(arcs) {
3108
+ var max = 89.9,
3109
+ min = -89.9,
3110
+ bbox = arcs.getBounds().toArray();
3111
+ if (bbox[1] >= min && bbox[3] <= max) return;
3112
+ arcs.transformPoints(function(x, y) {
3113
+ if (y > max) return [x, max];
3114
+ if (y < min) return [x, min];
3115
+ });
3044
3116
  }
3045
3117
 
3046
- function getVertexCoords(lyr, id) {
3047
- return lyr.gui.source.dataset.arcs.getVertex2(id);
3118
+ function projectPointsForDisplay(lyr, src, dest) {
3119
+ var copy = utils$1.extend({}, lyr);
3120
+ var proj = internal.getProjTransform2(src, dest);
3121
+ copy.shapes = internal.cloneShapes(lyr.shapes);
3122
+ internal.projectPointLayer(copy, proj);
3123
+ return copy;
3048
3124
  }
3049
3125
 
3050
- // set data coords (not display coords) of one or more vertices.
3051
- function setVertexCoords(lyr, ids, dataPoint) {
3052
- internal.snapVerticesToPoint(ids, dataPoint, lyr.gui.source.dataset.arcs);
3053
- if (isProjectedLayer(lyr)) {
3054
- var p = lyr.gui.projectPoint(dataPoint[0], dataPoint[1]);
3055
- internal.snapVerticesToPoint(ids, p, lyr.gui.displayArcs);
3056
- }
3126
+
3127
+ function toWebMercator(lng, lat) {
3128
+ var k = Math.cos(lat * D2R);
3129
+ var x = R * lng * D2R;
3130
+ var y = R * Math.log(Math.tan(Math.PI * 0.25 + lat * D2R * 0.5));
3131
+ return [x, y];
3057
3132
  }
3058
3133
 
3059
- // coords: [x, y] point in data CRS (not display CRS)
3060
- function setPointCoords(lyr, fid, coords) {
3061
- lyr.shapes[fid] = coords;
3062
- if (isProjectedLayer(lyr)) {
3063
- lyr.shapes[fid] = projectPointCoords(coords, lyr.gui.projectPoint);
3064
- }
3134
+ function fromWebMercator(x, y) {
3135
+ var lon = x / R * R2D;
3136
+ var lat = R2D * (Math.PI * 0.5 - 2 * Math.atan(Math.exp(-y / R)));
3137
+ return [lon, lat];
3065
3138
  }
3066
3139
 
3067
- function updateVertexCoords(lyr, ids) {
3068
- if (!isProjectedLayer(lyr)) return;
3069
- var p = lyr.gui.displayArcs.getVertex2(ids[0]);
3070
- internal.snapVerticesToPoint(ids, lyr.gui.invertPoint(p[0], p[1]), lyr.gui.source.dataset.arcs);
3140
+ function scaleToZoom(metersPerPix) {
3141
+ return Math.log(40075017 / 512 / metersPerPix) / Math.log(2);
3071
3142
  }
3072
3143
 
3073
- function setRectangleCoords(lyr, ids, coords) {
3074
- ids.forEach(function(id, i) {
3075
- var p = coords[i];
3076
- internal.snapVerticesToPoint([id], p, lyr.gui.source.dataset.arcs);
3077
- if (isProjectedLayer(lyr)) {
3078
- internal.snapVerticesToPoint([id], lyr.gui.projectPoint(p[0], p[1]), lyr.gui.displayArcs);
3079
- }
3080
- });
3144
+ function getMapboxBounds() {
3145
+ var ymax = toWebMercator(0, 84)[1];
3146
+ var ymin = toWebMercator(0, -84)[1];
3147
+ return [-Infinity, ymin, Infinity, ymax];
3081
3148
  }
3082
3149
 
3083
- // Update source data coordinates by projecting display coordinates
3084
- function updatePointCoords(lyr, fid) {
3085
- if (!isProjectedLayer(lyr)) return;
3086
- var displayShp = lyr.shapes[fid];
3087
- lyr.shapes[fid] = projectPointCoords(displayShp, lyr.gui.invertPoint);
3150
+
3151
+ // Update map extent and trigger redraw, after a new display CRS has been applied
3152
+ function projectMapExtent(ext, src, dest, newBounds) {
3153
+ var oldBounds = ext.getBounds();
3154
+ var oldScale = ext.scale();
3155
+ var newCP, proj, strictBounds;
3156
+
3157
+ if (dest && internal.isWebMercator(dest)) {
3158
+ // clampToMapboxBounds(newBounds);
3159
+ strictBounds = getMapboxBounds();
3160
+ }
3161
+
3162
+ // if source or destination CRS is unknown, show full extent
3163
+ // if map is at full extent, show full extent
3164
+ // TODO: handle case that scale is 1 and map is panned away from center
3165
+ if (ext.scale() == 1 || !dest) {
3166
+ ext.setFullBounds(newBounds, strictBounds);
3167
+ ext.reset();
3168
+ } else {
3169
+ // if map is zoomed, stay centered on the same geographic location, at the same relative scale
3170
+ proj = internal.getProjTransform2(src, dest);
3171
+ newCP = proj(oldBounds.centerX(), oldBounds.centerY());
3172
+ ext.setFullBounds(newBounds, strictBounds);
3173
+ if (!newCP) {
3174
+ // projection of center point failed; use center of bounds
3175
+ // (also consider just resetting the view using ext.home())
3176
+ newCP = [newBounds.centerX(), newBounds.centerY()];
3177
+ }
3178
+ ext.recenter(newCP[0], newCP[1], oldScale);
3179
+ }
3180
+ // trigger full redraw, in case some SVG symbols are unprojectable
3181
+ ext.dispatchEvent('change', {redraw: true});
3088
3182
  }
3089
3183
 
3090
- function projectPointCoords(src, proj) {
3091
- var dest = [], p;
3092
- for (var i=0; i<src.length; i++) {
3093
- p = proj(src[i][0], src[i][1]);
3094
- if (p) dest.push(p);
3184
+ // Called from console; for testing dynamic crs
3185
+ function setDisplayProjection(gui, cmd) {
3186
+ var arg = cmd.replace(/^projd[ ]*/, '');
3187
+ if (arg) {
3188
+ gui.map.setDisplayCRS(internal.parseCrsString(arg));
3189
+ } else {
3190
+ gui.map.setDisplayCRS(null);
3095
3191
  }
3096
- return dest.length ? dest : null;
3097
3192
  }
3098
3193
 
3099
- /*
3100
- How changes in the simplify control should affect other components
3194
+ function filterLayerByIds(lyr, ids) {
3195
+ var shapes;
3196
+ if (lyr.shapes) {
3197
+ shapes = ids.map(function(id) {
3198
+ return lyr.shapes[id];
3199
+ });
3200
+ return utils$1.defaults({shapes: shapes, data: null}, lyr);
3201
+ }
3202
+ return lyr;
3203
+ }
3101
3204
 
3102
- data calculated, 100% simplification
3103
- -> [map] filtered arcs update
3205
+ function formatLayerNameForDisplay(name) {
3206
+ return name || '[unnamed]';
3207
+ }
3104
3208
 
3105
- data calculated, <100% simplification
3106
- -> [map] filtered arcs update, redraw; [repair] intersection update
3209
+ function cleanLayerName(raw) {
3210
+ return raw.replace(/[\n\t/\\]/g, '')
3211
+ .replace(/^[.\s]+/, '').replace(/[.\s]+$/, '');
3212
+ }
3107
3213
 
3108
- change via text field
3109
- -> [map] redraw; [repair] intersection update
3214
+ function updateLayerStackOrder(layers) {
3215
+ // 1. assign ascending ids to unassigned layers above the range of other layers
3216
+ layers.forEach(function(o, i) {
3217
+ if (!o.layer.menu_order) o.layer.menu_order = 1e6 + i;
3218
+ });
3219
+ // 2. sort in ascending order
3220
+ layers.sort(function(a, b) {
3221
+ return a.layer.menu_order - b.layer.menu_order;
3222
+ });
3223
+ // 3. assign consecutve ids
3224
+ layers.forEach(function(o, i) {
3225
+ o.layer.menu_order = i + 1;
3226
+ });
3227
+ return layers;
3228
+ }
3110
3229
 
3111
- slider drag start
3112
- -> [repair] hide display
3230
+ function sortLayersForMenuDisplay(layers) {
3231
+ layers = updateLayerStackOrder(layers);
3232
+ return layers.reverse();
3233
+ }
3113
3234
 
3114
- slider drag
3115
- -> [map] redraw
3235
+ // lyr: a map layer with gui property
3236
+ // displayCRS: CRS to use for display, or null (which clears any current display CRS)
3237
+ function projectLayerForDisplay(lyr, displayCRS) {
3238
+ var crsInfo = getDatasetCrsInfo(lyr.gui.source.dataset);
3239
+ var sourceCRS = crsInfo.crs || null; // let enhanceLayerForDisplay() handle null case
3240
+ if (!lyr.gui.geographic) {
3241
+ return;
3242
+ }
3243
+ if (lyr.gui.dynamic_crs && internal.crsAreEqual(sourceCRS, lyr.gui.dynamic_crs)) {
3244
+ return;
3245
+ }
3246
+ var gui = lyr.gui;
3247
+ enhanceLayerForDisplay(lyr, lyr.gui.source.dataset, {crs: displayCRS});
3248
+ utils$1.defaults(lyr.gui, gui); // re-apply any properties that were lost (e.g. svg_id)
3249
+ if (lyr.gui.style?.ids) {
3250
+ // re-apply layer filter
3251
+ lyr.gui.displayLayer = filterLayerByIds(lyr.gui.displayLayer, lyr.gui.style.ids);
3252
+ }
3253
+ }
3116
3254
 
3117
- slider drag end
3118
- -> [repair] intersection update
3119
3255
 
3120
- */
3256
+ // Supplement a layer with information needed for rendering
3257
+ function enhanceLayerForDisplay(layer, dataset, opts) {
3258
+ var gui = {
3259
+ empty: internal.getFeatureCount(layer) === 0,
3260
+ geographic: false,
3261
+ displayArcs: null,
3262
+ displayLayer: null,
3263
+ source: {dataset},
3264
+ bounds: null,
3265
+ style: null,
3266
+ dynamic_crs: null,
3267
+ invertPoint: null,
3268
+ projectPoint: null
3269
+ };
3121
3270
 
3122
- var SimplifyControl = function(gui) {
3123
- var model = gui.model;
3124
- var control = {};
3125
- var _value = 1;
3126
- var el = gui.container.findChild('.simplify-control-wrapper');
3127
- var menu = gui.container.findChild('.simplify-options');
3128
- var slider, text, fromPct;
3271
+ var displayCRS = opts.crs || null;
3272
+ // display arcs may have been generated when another layer in the dataset
3273
+ // was converted for display... re-use if available
3274
+ var displayArcs = dataset.gui?.displayArcs;
3275
+ var unprojectable = false;
3276
+ var sourceCRS;
3277
+ var emptyArcs;
3129
3278
 
3130
- // init settings menu
3131
- new SimpleButton(menu.findChild('.submit-btn').addClass('default-btn')).on('click', onSubmit);
3132
- new SimpleButton(menu.findChild('.close2-btn')).on('click', function() {
3133
- if (el.visible()) {
3134
- // cancel just hides menu if slider is visible
3135
- menu.hide();
3279
+ if (displayCRS && layer.geometry_type) {
3280
+ var crsInfo = getDatasetCrsInfo(dataset);
3281
+ if (crsInfo.error) {
3282
+ // unprojectable dataset -- return empty layer
3283
+ gui.unprojectable = true;
3136
3284
  } else {
3137
- gui.clearMode();
3285
+ sourceCRS = crsInfo.crs;
3138
3286
  }
3139
- });
3140
- new SimpleButton(el.findChild('.simplify-settings-btn')).on('click', function() {
3141
- if (menu.visible()) {
3142
- menu.hide();
3287
+ }
3288
+
3289
+ // Assume that dataset.displayArcs is in the display CRS
3290
+ // (it must be deleted upstream if reprojection is needed)
3291
+ // if (!obj.empty && dataset.arcs && !displayArcs) {
3292
+ if (dataset.arcs && !displayArcs && !gui.unprojectable) {
3293
+ // project arcs, if needed
3294
+ if (needReprojectionForDisplay(sourceCRS, displayCRS)) {
3295
+ displayArcs = projectArcsForDisplay(dataset.arcs, sourceCRS, displayCRS);
3143
3296
  } else {
3144
- showMenu();
3297
+ // Use original arcs for display if there is no dynamic reprojection
3298
+ displayArcs = dataset.arcs;
3299
+ }
3300
+
3301
+ enhanceArcCollectionForDisplay(displayArcs);
3302
+ dataset.gui = {displayArcs}; // stash these in the dataset for other layers to use
3303
+ }
3304
+
3305
+ if (internal.layerHasFurniture(layer)) {
3306
+ // TODO: consider how to render furniture in GUI
3307
+ // treating furniture layers (other than frame) as tabular for now,
3308
+ // so there is something to show if they are selected
3309
+ }
3310
+
3311
+ if (gui.unprojectable) {
3312
+ gui.displayLayer = {shapes: []}; // TODO: improve
3313
+ } else if (layer.geometry_type) {
3314
+ gui.geographic = true;
3315
+ gui.displayLayer = layer;
3316
+ gui.displayArcs = displayArcs;
3317
+ } else {
3318
+ var table = getDisplayLayerForTable(layer.data);
3319
+ gui.tabular = true;
3320
+ gui.displayLayer = table.layer;
3321
+ gui.displayArcs = table.arcs;
3322
+ }
3323
+
3324
+ // dynamic reprojection (arcs were already reprojected above)
3325
+ if (gui.geographic && needReprojectionForDisplay(sourceCRS, displayCRS)) {
3326
+ gui.dynamic_crs = displayCRS;
3327
+ gui.invertPoint = internal.getProjTransform2(displayCRS, sourceCRS);
3328
+ gui.projectPoint = internal.getProjTransform2(sourceCRS, displayCRS);
3329
+ if (internal.layerHasPoints(layer)) {
3330
+ gui.displayLayer = projectPointsForDisplay(layer, sourceCRS, displayCRS);
3331
+ } else if (internal.layerHasPaths(layer)) {
3332
+ emptyArcs = findEmptyArcs(displayArcs);
3333
+ if (emptyArcs.length > 0) {
3334
+ // Don't try to draw paths containing coordinates that failed to project
3335
+ gui.displayLayer = internal.filterPathLayerByArcIds(gui.displayLayer, emptyArcs);
3336
+ }
3337
+ }
3338
+ }
3339
+
3340
+ gui.bounds = getDisplayBounds(gui.displayLayer, gui.displayArcs);
3341
+ layer.gui = gui;
3342
+ }
3343
+
3344
+ function getDisplayBounds(lyr, arcs) {
3345
+ var bounds = internal.getLayerBounds(lyr, arcs) || new Bounds();
3346
+ if (lyr.geometry_type == 'point' && arcs && bounds.hasBounds() && bounds.area() > 0 === false) {
3347
+ // if a point layer has no extent (e.g. contains only a single point),
3348
+ // then merge with arc bounds, to place the point in context.
3349
+ bounds = bounds.mergeBounds(arcs.getBounds());
3350
+ }
3351
+ return bounds;
3352
+ }
3353
+
3354
+ // Returns an array of ids of empty arcs (arcs can be set to empty if errors occur while projecting them)
3355
+ function findEmptyArcs(arcs) {
3356
+ var nn = arcs.getVertexData().nn;
3357
+ var ids = [];
3358
+ for (var i=0, n=nn.length; i<n; i++) {
3359
+ if (nn[i] === 0) {
3360
+ ids.push(i);
3361
+ }
3362
+ }
3363
+ return ids;
3364
+ }
3365
+
3366
+ function flattenArcs(lyr) {
3367
+ lyr.gui.source.dataset.arcs.flatten();
3368
+ if (isProjectedLayer(lyr)) {
3369
+ lyr.gui.displayArcs.flatten();
3370
+ }
3371
+ }
3372
+
3373
+ function setZ(lyr, z) {
3374
+ lyr.gui.source.dataset.arcs.setRetainedInterval(z);
3375
+ if (isProjectedLayer(lyr)) {
3376
+ lyr.gui.displayArcs.setRetainedInterval(z);
3377
+ }
3378
+ }
3379
+
3380
+ function updateZ(lyr) {
3381
+ if (isProjectedLayer(lyr) && !lyr.gui.source.dataset.arcs.isFlat()) {
3382
+ lyr.gui.displayArcs.setThresholds(lyr.gui.source.dataset.arcs.getVertexData().zz);
3383
+ }
3384
+ }
3385
+
3386
+ function appendNewDataRecord(layer) {
3387
+ if (!layer.data) return null;
3388
+ var fields = layer.data.getFields();
3389
+ var d = getEmptyDataRecord(layer.data);
3390
+ // TODO: handle SVG symbol layer
3391
+ if (internal.layerHasLabels(layer)) {
3392
+ d['label-text'] = 'TBD'; // without text, new labels will be invisible
3393
+ } else if (layer.geometry_type == 'point' && fields.includes('r')) {
3394
+ d.r = 3; // show a black circle if layer is styled
3395
+ }
3396
+ if (layer.geometry_type == 'polyline' || layer.geometry_type == 'polygon') {
3397
+ if (fields.includes('stroke')) d.stroke = 'black';
3398
+ if (fields.includes('stroke-width')) d['stroke-width'] = 1;
3399
+ }
3400
+ if (layer.geometry_type == 'polygon') {
3401
+ if (fields.includes('fill')) {
3402
+ d.fill = 'rgba(0,0,0,0.10)'; // 'rgba(249,120,249,0.20)';
3403
+ }
3404
+ }
3405
+ // TODO: better styling
3406
+ layer.data.getRecords().push(d);
3407
+ return d;
3408
+ }
3409
+
3410
+ function getEmptyDataRecord(table) {
3411
+ return table.getFields().reduce(function(memo, name) {
3412
+ memo[name] = null;
3413
+ return memo;
3414
+ }, {});
3415
+ }
3416
+
3417
+ // p1, p2: two points in source data CRS coords.
3418
+ function appendNewPath(lyr, p1, p2) {
3419
+ var arcId = lyr.gui.displayArcs.size();
3420
+ internal.appendEmptyArc(lyr.gui.displayArcs);
3421
+ lyr.shapes.push([[arcId]]);
3422
+ if (isProjectedLayer(lyr)) {
3423
+ internal.appendEmptyArc(lyr.gui.source.dataset.arcs);
3424
+ }
3425
+ appendVertex$1(lyr, p1);
3426
+ appendVertex$1(lyr, p2);
3427
+ appendNewDataRecord(lyr);
3428
+ }
3429
+
3430
+ function deleteLastPath(lyr) {
3431
+ var arcId = lyr.gui.displayArcs.size() - 1;
3432
+ if (lyr.data) {
3433
+ lyr.data.getRecords().pop();
3434
+ }
3435
+ var shp = lyr.shapes.pop();
3436
+ internal.deleteLastArc(lyr.gui.displayArcs);
3437
+ if (isProjectedLayer(lyr)) {
3438
+ internal.deleteLastArc(lyr.gui.source.dataset.arcs);
3439
+ }
3440
+ }
3441
+
3442
+ // p: one point in source data coords
3443
+ function appendNewPoint(lyr, p) {
3444
+ lyr.shapes.push([p]);
3445
+ if (lyr.data) {
3446
+ appendNewDataRecord(lyr);
3447
+ }
3448
+ // this reprojects all the points... TODO: improve
3449
+ if (isProjectedLayer(lyr)) {
3450
+ projectLayerForDisplay(lyr, lyr.gui.dynamic_crs);
3451
+ }
3452
+ }
3453
+
3454
+ function deleteLastPoint(lyr) {
3455
+ if (lyr.data) {
3456
+ lyr.data.getRecords().pop();
3457
+ }
3458
+ lyr.shapes.pop();
3459
+ if (isProjectedLayer(lyr)) {
3460
+ lyr.gui.displayLayer.shapes.pop();
3461
+ }
3462
+ }
3463
+
3464
+ // p: point in source data CRS coords.
3465
+ function insertVertex$1(lyr, id, p) {
3466
+ internal.insertVertex(lyr.gui.source.dataset.arcs, id, p);
3467
+ if (isProjectedLayer(lyr)) {
3468
+ internal.insertVertex(lyr.gui.displayArcs, id, lyr.gui.projectPoint(p[0], p[1]));
3469
+ }
3470
+ }
3471
+
3472
+ function appendVertex$1(lyr, p) {
3473
+ var n = lyr.gui.source.dataset.arcs.getPointCount();
3474
+ insertVertex$1(lyr, n, p);
3475
+ }
3476
+
3477
+ // TODO: make sure we're not also removing an entire arc
3478
+ function deleteLastVertex(lyr) {
3479
+ deleteVertex$1(lyr, lyr.gui.displayArcs.getPointCount() - 1);
3480
+ }
3481
+
3482
+
3483
+ function deleteVertex$1(lyr, id) {
3484
+ internal.deleteVertex(lyr.gui.displayArcs, id);
3485
+ if (isProjectedLayer(lyr)) {
3486
+ internal.deleteVertex(lyr.gui.source.dataset.arcs, id);
3487
+ }
3488
+ }
3489
+
3490
+ function getLastArcCoords(target) {
3491
+ var arcId = target.gui.source.dataset.arcs.size() - 1;
3492
+ return internal.getUnfilteredArcCoords(arcId, target.gui.source.dataset.arcs);
3493
+ }
3494
+
3495
+ function getLastVertexCoords(target) {
3496
+ var arcs = target.gui.source.dataset.arcs;
3497
+ return internal.getVertexCoords(arcs.getPointCount() - 1, arcs);
3498
+ }
3499
+
3500
+ function getLastArcLength(target) {
3501
+ var arcId = target.gui.source.dataset.arcs.size() - 1;
3502
+ return internal.getUnfilteredArcLength(arcId, target.gui.source.dataset.arcs);
3503
+ }
3504
+
3505
+ function getVertexCoords(lyr, id) {
3506
+ return lyr.gui.source.dataset.arcs.getVertex2(id);
3507
+ }
3508
+
3509
+ // set data coords (not display coords) of one or more vertices.
3510
+ function setVertexCoords(lyr, ids, dataPoint) {
3511
+ internal.snapVerticesToPoint(ids, dataPoint, lyr.gui.source.dataset.arcs);
3512
+ if (isProjectedLayer(lyr)) {
3513
+ var p = lyr.gui.projectPoint(dataPoint[0], dataPoint[1]);
3514
+ internal.snapVerticesToPoint(ids, p, lyr.gui.displayArcs);
3515
+ }
3516
+ }
3517
+
3518
+ function updateVertexCoords(lyr, ids) {
3519
+ if (!isProjectedLayer(lyr)) return;
3520
+ var p = lyr.gui.displayArcs.getVertex2(ids[0]);
3521
+ internal.snapVerticesToPoint(ids, lyr.gui.invertPoint(p[0], p[1]), lyr.gui.source.dataset.arcs);
3522
+ }
3523
+
3524
+ function setRectangleCoords(lyr, ids, coords) {
3525
+ ids.forEach(function(id, i) {
3526
+ var p = coords[i];
3527
+ internal.snapVerticesToPoint([id], p, lyr.gui.source.dataset.arcs);
3528
+ if (isProjectedLayer(lyr)) {
3529
+ internal.snapVerticesToPoint([id], lyr.gui.projectPoint(p[0], p[1]), lyr.gui.displayArcs);
3530
+ }
3531
+ });
3532
+ }
3533
+
3534
+ // Update source data coordinates by projecting display coordinates
3535
+ function updatePointCoords(lyr, fid) {
3536
+ if (!isProjectedLayer(lyr)) return;
3537
+ var displayShp = lyr.gui.displayLayer.shapes[fid];
3538
+ lyr.shapes[fid] = projectPointCoords(displayShp, lyr.gui.invertPoint);
3539
+ }
3540
+
3541
+ // coords: [[x, y]] point in data CRS (not display CRS)
3542
+ function setPointCoords(lyr, fid, coords) {
3543
+ lyr.shapes[fid] = coords;
3544
+ if (isProjectedLayer(lyr)) {
3545
+ lyr.gui.displayLayer.shapes[fid] = projectPointCoords(coords, lyr.gui.projectPoint);
3546
+ }
3547
+ }
3548
+
3549
+ // return an [[x, y]] point in data CRS
3550
+ function getPointCoords(lyr, fid) {
3551
+ var coords = lyr.geometry_type == 'point' && lyr.shapes[fid];
3552
+ if (!coords || coords.length != 1) {
3553
+ return null;
3554
+ }
3555
+ return internal.cloneShape(coords);
3556
+ }
3557
+
3558
+ // export function getPointCoords(lyr, fid) {
3559
+ // return internal.cloneShape(lyr.shapes[fid]);
3560
+ // }
3561
+
3562
+ function projectPointCoords(src, proj) {
3563
+ var dest = [], p;
3564
+ for (var i=0; i<src.length; i++) {
3565
+ p = proj(src[i][0], src[i][1]);
3566
+ if (p) dest.push(p);
3567
+ }
3568
+ return dest.length ? dest : null;
3569
+ }
3570
+
3571
+ /*
3572
+ How changes in the simplify control should affect other components
3573
+
3574
+ data calculated, 100% simplification
3575
+ -> [map] filtered arcs update
3576
+
3577
+ data calculated, <100% simplification
3578
+ -> [map] filtered arcs update, redraw; [repair] intersection update
3579
+
3580
+ change via text field
3581
+ -> [map] redraw; [repair] intersection update
3582
+
3583
+ slider drag start
3584
+ -> [repair] hide display
3585
+
3586
+ slider drag
3587
+ -> [map] redraw
3588
+
3589
+ slider drag end
3590
+ -> [repair] intersection update
3591
+
3592
+ */
3593
+
3594
+ var SimplifyControl = function(gui) {
3595
+ var model = gui.model;
3596
+ var control = {};
3597
+ var _value = 1;
3598
+ var el = gui.container.findChild('.simplify-control-wrapper');
3599
+ var menu = gui.container.findChild('.simplify-options');
3600
+ var slider, text, fromPct;
3601
+ var menuBtn = gui.container.findChild('.simplify-btn').addClass('disabled');
3602
+
3603
+ // init settings menu
3604
+ new SimpleButton(menu.findChild('.submit-btn').addClass('default-btn')).on('click', onSubmit);
3605
+ new SimpleButton(menu.findChild('.close2-btn')).on('click', function() {
3606
+ if (el.visible()) {
3607
+ // cancel just hides menu if slider is visible
3608
+ menu.hide();
3609
+ } else {
3610
+ gui.clearMode();
3611
+ }
3612
+ });
3613
+ new SimpleButton(el.findChild('.simplify-settings-btn')).on('click', function() {
3614
+ if (menu.visible()) {
3615
+ menu.hide();
3616
+ } else {
3617
+ showMenu();
3145
3618
  }
3146
3619
  });
3147
3620
  gui.keyboard.onMenuSubmit(menu, onSubmit);
3148
3621
 
3149
3622
  // init simplify button and mode
3150
- gui.addMode('simplify', turnOn, turnOff, gui.container.findChild('.simplify-btn'));
3151
- model.on('select', function() {
3623
+ gui.addMode('simplify', turnOn, turnOff, menuBtn);
3624
+
3625
+ model.on('update', function() {
3626
+ menuBtn.classed('disabled', !model.getActiveLayer());
3152
3627
  if (gui.getMode() == 'simplify') gui.clearMode();
3153
3628
  });
3154
3629
 
3630
+
3155
3631
  // exit simplify mode when user clicks off the visible part of the menu
3156
3632
  menu.on('click', GUI.handleDirectEvent(gui.clearMode));
3157
3633
 
@@ -3331,144 +3807,6 @@
3331
3807
  }
3332
3808
  };
3333
3809
 
3334
- var R = 6378137;
3335
- var D2R = Math.PI / 180;
3336
- var R2D = 180 / Math.PI;
3337
-
3338
- // Assumes projections are available
3339
-
3340
- function needReprojectionForDisplay(sourceCRS, displayCRS) {
3341
- if (!sourceCRS || !displayCRS) {
3342
- return false;
3343
- }
3344
- if (internal.crsAreEqual(sourceCRS, displayCRS)) {
3345
- return false;
3346
- }
3347
- return true;
3348
- }
3349
-
3350
- function projectArcsForDisplay(arcs, src, dest) {
3351
- var copy = arcs.getCopy(); // need to flatten first?
3352
- var destIsWebMerc = internal.isWebMercator(dest);
3353
- if (destIsWebMerc && internal.isWebMercator(src)) {
3354
- return copy;
3355
- }
3356
-
3357
- var wgs84 = internal.parseCrsString('wgs84');
3358
- var toWGS84 = internal.getProjTransform2(src, wgs84);
3359
- var fromWGS84 = internal.getProjTransform2(wgs84, dest);
3360
-
3361
- try {
3362
- // first try projectArcs() -- it's fast and preserves arc ids
3363
- // (so vertex editing doesn't break)
3364
- if (!internal.isWGS84(src)) {
3365
- // use wgs84 as a pivot CRS, so we can handle polar coordinates
3366
- // that can't be projected to Mercator
3367
- internal.projectArcs(copy, toWGS84);
3368
- }
3369
- if (destIsWebMerc) {
3370
- // handle polar points by clamping them to they will project
3371
- // (downside: may cause unexpected behavior when editing vertices interactively)
3372
- clampY(copy);
3373
- }
3374
- internal.projectArcs(copy, fromWGS84);
3375
- } catch(e) {
3376
- console.error(e);
3377
- // use the more robust projectArcs2 if projectArcs throws an error
3378
- // downside: projectArcs2 discards Z values and changes arc indexing,
3379
- // which will break vertex editing.
3380
- var reproject = internal.getProjTransform2(src, dest);
3381
- copy = arcs.getCopy();
3382
- internal.projectArcs2(copy, reproject);
3383
- }
3384
- return copy;
3385
- }
3386
-
3387
- function clampY(arcs) {
3388
- var max = 89.9,
3389
- min = -89.9,
3390
- bbox = arcs.getBounds().toArray();
3391
- if (bbox[1] >= min && bbox[3] <= max) return;
3392
- arcs.transformPoints(function(x, y) {
3393
- if (y > max) return [x, max];
3394
- if (y < min) return [x, min];
3395
- });
3396
- }
3397
-
3398
- function projectPointsForDisplay(lyr, src, dest) {
3399
- var copy = utils$1.extend({}, lyr);
3400
- var proj = internal.getProjTransform2(src, dest);
3401
- copy.shapes = internal.cloneShapes(lyr.shapes);
3402
- internal.projectPointLayer(copy, proj);
3403
- return copy;
3404
- }
3405
-
3406
-
3407
- function toWebMercator(lng, lat) {
3408
- var k = Math.cos(lat * D2R);
3409
- var x = R * lng * D2R;
3410
- var y = R * Math.log(Math.tan(Math.PI * 0.25 + lat * D2R * 0.5));
3411
- return [x, y];
3412
- }
3413
-
3414
- function fromWebMercator(x, y) {
3415
- var lon = x / R * R2D;
3416
- var lat = R2D * (Math.PI * 0.5 - 2 * Math.atan(Math.exp(-y / R)));
3417
- return [lon, lat];
3418
- }
3419
-
3420
- function scaleToZoom(metersPerPix) {
3421
- return Math.log(40075017 / 512 / metersPerPix) / Math.log(2);
3422
- }
3423
-
3424
- function getMapboxBounds() {
3425
- var ymax = toWebMercator(0, 84)[1];
3426
- var ymin = toWebMercator(0, -84)[1];
3427
- return [-Infinity, ymin, Infinity, ymax];
3428
- }
3429
-
3430
-
3431
- // Update map extent and trigger redraw, after a new display CRS has been applied
3432
- function projectMapExtent(ext, src, dest, newBounds) {
3433
- var oldBounds = ext.getBounds();
3434
- var oldScale = ext.scale();
3435
- var newCP, proj, strictBounds;
3436
-
3437
- if (dest && internal.isWebMercator(dest)) {
3438
- // clampToMapboxBounds(newBounds);
3439
- strictBounds = getMapboxBounds();
3440
- }
3441
-
3442
- // if source or destination CRS is unknown, show full extent
3443
- // if map is at full extent, show full extent
3444
- // TODO: handle case that scale is 1 and map is panned away from center
3445
- if (ext.scale() == 1 || !dest) {
3446
- ext.setFullBounds(newBounds, strictBounds);
3447
- ext.home(); // sets full extent and triggers redraw
3448
- } else {
3449
- // if map is zoomed, stay centered on the same geographic location, at the same relative scale
3450
- proj = internal.getProjTransform2(src, dest);
3451
- newCP = proj(oldBounds.centerX(), oldBounds.centerY());
3452
- ext.setFullBounds(newBounds, strictBounds);
3453
- if (!newCP) {
3454
- // projection of center point failed; use center of bounds
3455
- // (also consider just resetting the view using ext.home())
3456
- newCP = [newBounds.centerX(), newBounds.centerY()];
3457
- }
3458
- ext.recenter(newCP[0], newCP[1], oldScale);
3459
- }
3460
- }
3461
-
3462
- // Called from console; for testing dynamic crs
3463
- function setDisplayProjection(gui, cmd) {
3464
- var arg = cmd.replace(/^projd[ ]*/, '');
3465
- if (arg) {
3466
- gui.map.setDisplayCRS(internal.parseCrsString(arg));
3467
- } else {
3468
- gui.map.setDisplayCRS(null);
3469
- }
3470
- }
3471
-
3472
3810
  function Console(gui) {
3473
3811
  var model = gui.model;
3474
3812
  var CURSOR = '$ ';
@@ -3537,7 +3875,8 @@
3537
3875
  }
3538
3876
 
3539
3877
  function turnOn() {
3540
- if (!_isOpen && !model.isEmpty()) {
3878
+ // if (!_isOpen && !model.isEmpty()) {
3879
+ if (!_isOpen) {
3541
3880
  btn.addClass('active');
3542
3881
  _isOpen = true;
3543
3882
  // use console for messages while open
@@ -3876,8 +4215,8 @@
3876
4215
 
3877
4216
  function applyParsedCommands(commands, done) {
3878
4217
  var active = model.getActiveLayer(),
3879
- prevArcs = active.dataset.arcs,
3880
- prevTable = active.layer.data,
4218
+ prevArcs = active?.dataset.arcs,
4219
+ prevTable = active?.layer.data,
3881
4220
  prevTableSize = prevTable ? prevTable.size() : 0,
3882
4221
  prevArcCount = prevArcs ? prevArcs.size() : 0,
3883
4222
  job = new internal.Job(model);
@@ -3886,9 +4225,9 @@
3886
4225
  internal.runParsedCommands(commands, job, function(err) {
3887
4226
  var flags = getCommandFlags(commands),
3888
4227
  active2 = model.getActiveLayer(),
3889
- postArcs = active2.dataset.arcs,
4228
+ postArcs = active2?.dataset.arcs,
3890
4229
  postArcCount = postArcs ? postArcs.size() : 0,
3891
- postTable = active2.layer.data,
4230
+ postTable = active2?.layer.data,
3892
4231
  postTableSize = postTable ? postTable.size() : 0,
3893
4232
  sameTable = prevTable == postTable && prevTableSize == postTableSize,
3894
4233
  sameArcs = prevArcs == postArcs && postArcCount == prevArcCount;
@@ -3901,7 +4240,7 @@
3901
4240
  if (sameTable) {
3902
4241
  flags.same_table = true;
3903
4242
  }
3904
- if (active.layer != active2.layer) {
4243
+ if (active && active?.layer == active2?.layer) {
3905
4244
  // this can get set after some commands that don't set a new target
3906
4245
  // (e.g. -dissolve)
3907
4246
  flags.select = true;
@@ -3916,10 +4255,10 @@
3916
4255
  function onError(err) {
3917
4256
  if (utils$1.isString(err)) {
3918
4257
  consoleStop(err);
3919
- } else if (err.name == 'UserError') {
4258
+ } else if (err.name == 'UserError' ) {
3920
4259
  // stop() has already been called, don't need to log
4260
+ console.error(err.stack);
3921
4261
  } else if (err.name) {
3922
- // log stack trace to browser console
3923
4262
  console.error(err.stack);
3924
4263
  // log to console window
3925
4264
  consoleWarning(err.message);
@@ -4102,6 +4441,7 @@
4102
4441
  return;
4103
4442
  }
4104
4443
  var e = model.getActiveLayer();
4444
+ if (!e) return;
4105
4445
  if (internal.layerHasPaths(e.layer)) {
4106
4446
  el.show();
4107
4447
  checkBtn.show();
@@ -4180,47 +4520,6 @@
4180
4520
 
4181
4521
  utils$1.inherit(RepairControl, EventDispatcher);
4182
4522
 
4183
- function filterLayerByIds(lyr, ids) {
4184
- var shapes;
4185
- if (lyr.shapes) {
4186
- shapes = ids.map(function(id) {
4187
- return lyr.shapes[id];
4188
- });
4189
- return utils$1.defaults({shapes: shapes, data: null}, lyr);
4190
- }
4191
- return lyr;
4192
- }
4193
-
4194
- function formatLayerNameForDisplay(name) {
4195
- return name || '[unnamed]';
4196
- }
4197
-
4198
- function cleanLayerName(raw) {
4199
- return raw.replace(/[\n\t/\\]/g, '')
4200
- .replace(/^[.\s]+/, '').replace(/[.\s]+$/, '');
4201
- }
4202
-
4203
- function updateLayerStackOrder(layers) {
4204
- // 1. assign ascending ids to unassigned layers above the range of other layers
4205
- layers.forEach(function(o, i) {
4206
- if (!o.layer.menu_order) o.layer.menu_order = 1e6 + i;
4207
- });
4208
- // 2. sort in ascending order
4209
- layers.sort(function(a, b) {
4210
- return a.layer.menu_order - b.layer.menu_order;
4211
- });
4212
- // 3. assign consecutve ids
4213
- layers.forEach(function(o, i) {
4214
- o.layer.menu_order = i + 1;
4215
- });
4216
- return layers;
4217
- }
4218
-
4219
- function sortLayersForMenuDisplay(layers) {
4220
- layers = updateLayerStackOrder(layers);
4221
- return layers.reverse();
4222
- }
4223
-
4224
4523
  async function saveFileContentToClipboard(content) {
4225
4524
  var str = utils$1.isString(content) ? content : content.toString();
4226
4525
  await navigator.clipboard.writeText(str);
@@ -4233,7 +4532,7 @@
4233
4532
  var menu = gui.container.findChild('.export-options').on('click', GUI.handleDirectEvent(gui.clearMode));
4234
4533
  var layersArr = [];
4235
4534
  var toggleBtn = null; // checkbox <input> for toggling layer selection
4236
- var exportBtn = gui.container.findChild('.export-btn');
4535
+ var exportBtn = gui.container.findChild('.export-btn').addClass('disabled');
4237
4536
  var ofileName = gui.container.findChild('#ofile-name');
4238
4537
  new SimpleButton(menu.findChild('.close2-btn')).on('click', gui.clearMode);
4239
4538
 
@@ -4248,6 +4547,10 @@
4248
4547
  return;
4249
4548
  }
4250
4549
 
4550
+ model.on('update', function() {
4551
+ exportBtn.classed('disabled', !model.getActiveLayer());
4552
+ });
4553
+
4251
4554
  new SimpleButton(menu.findChild('#export-btn').addClass('default-btn')).on('click', onExportClick);
4252
4555
  gui.addMode('export', turnOn, turnOff, exportBtn);
4253
4556
  gui.keyboard.onMenuSubmit(menu, onExportClick);
@@ -4578,6 +4881,14 @@
4578
4881
  var layerOrderSlug;
4579
4882
 
4580
4883
  gui.addMode('layer_menu', turnOn, turnOff, btn.findChild('.header-btn'));
4884
+
4885
+ // kludge to show menu button after initial import dialog is dismissed
4886
+ gui.on('mode', function(e) {
4887
+ if (!e.name) {
4888
+ updateMenuBtn();
4889
+ }
4890
+ });
4891
+
4581
4892
  model.on('update', function(e) {
4582
4893
  updateMenuBtn();
4583
4894
  if (isOpen) render();
@@ -4668,8 +4979,9 @@
4668
4979
  }
4669
4980
 
4670
4981
  function updateMenuBtn() {
4671
- var lyrName = model.getActiveLayer().layer.name || '';
4672
- var menuTitle = lyrName || '[unnamed layer]';
4982
+ var lyr = model.getActiveLayer()?.layer;
4983
+ var lyrName = lyr?.name || '';
4984
+ var menuTitle = lyrName || lyr && '[unnamed layer]' || '[no data]';
4673
4985
  var pageTitle = lyrName || 'mapshaper';
4674
4986
  btn.classed('active', 'true').findChild('.layer-name').html(menuTitle + " &nbsp;&#9660;");
4675
4987
  window.document.title = pageTitle;
@@ -4681,6 +4993,8 @@
4681
4993
  }
4682
4994
 
4683
4995
  function renderSourceFileList() {
4996
+ el.findChild('.no-layer-note').classed('hidden', model.getActiveLayer());
4997
+ el.findChild('.source-file-section').classed('hidden', !model.getActiveLayer());
4684
4998
  var list = el.findChild('.file-list');
4685
4999
  var files = [];
4686
5000
  list.empty();
@@ -5198,6 +5512,16 @@
5198
5512
  addHistoryState(undo, redo);
5199
5513
  });
5200
5514
 
5515
+ gui.on('point_add', function(e) {
5516
+ var redo = function() {
5517
+ appendNewPoint(e.data.target, e.p);
5518
+ };
5519
+ var undo = function() {
5520
+ deleteLastPoint(e.data.target);
5521
+ };
5522
+ addHistoryState(undo, redo);
5523
+ });
5524
+
5201
5525
  gui.on('path_add', function(e) {
5202
5526
  var redo = function() {
5203
5527
  gui.dispatchEvent('redo_path_add', {p1: e.p1, p2: e.p2});
@@ -5266,6 +5590,7 @@
5266
5590
  var _hidden = true;
5267
5591
  gui.on('active', updateVisibility);
5268
5592
  gui.on('inactive', updateVisibility);
5593
+ gui.model.on('update', updateVisibility);
5269
5594
 
5270
5595
  // @iconRef: selector for an (svg) button icon
5271
5596
  this.addButton = function(iconRef) {
@@ -5294,7 +5619,10 @@
5294
5619
  };
5295
5620
 
5296
5621
  function updateVisibility() {
5297
- if (GUI.isActiveInstance(gui) && !_hidden) {
5622
+ var noData = !gui.model.getActiveLayer();
5623
+ if (GUI.isActiveInstance(gui) && !_hidden && !noData) {
5624
+ buttons.show();
5625
+ } else if (noData && gui.getMode() != 'import') {
5298
5626
  buttons.show();
5299
5627
  } else {
5300
5628
  buttons.hide();
@@ -5315,6 +5643,7 @@
5315
5643
  });
5316
5644
 
5317
5645
  btn.on('click', function() {
5646
+ if (btn.hasClass('disabled')) return;
5318
5647
  modes.enterMode(active ? null : name);
5319
5648
  });
5320
5649
  }
@@ -5389,14 +5718,16 @@
5389
5718
  var ctrlDown = false;
5390
5719
  document.addEventListener('keyup', function(e) {
5391
5720
  if (!GUI.isActiveInstance(gui)) return;
5392
- if (e.keyCode == 16) shiftDown = false;
5721
+ // this can fail to fire if keyup occurs over a context menu
5722
+ // if (e.keyCode == 16) shiftDown = false;
5723
+ shiftDown = e.shiftKey;
5393
5724
  if (e.keyCode == 17) ctrlDown = false;
5394
5725
  self.dispatchEvent('keyup', getEventData(e));
5395
5726
  });
5396
5727
 
5397
5728
  document.addEventListener('keydown', function(e) {
5398
5729
  if (!GUI.isActiveInstance(gui)) return;
5399
- if (e.keyCode == 16) shiftDown = true;
5730
+ shiftDown = e.shiftKey;
5400
5731
  if (e.keyCode == 17) ctrlDown = true;
5401
5732
  self.dispatchEvent('keydown', getEventData(e));
5402
5733
  });
@@ -5440,12 +5771,13 @@
5440
5771
 
5441
5772
  var menus = {
5442
5773
  standard: ['info', 'selection', 'box'],
5443
- polygons: ['info', 'selection', 'box', 'drawing'],
5774
+ empty: ['edit_points', 'edit_lines', 'edit_polygons'],
5775
+ polygons: ['info', 'selection', 'box', 'edit_polygons'],
5444
5776
  rectangles: ['info', 'selection', 'box', 'rectangles'],
5445
- lines: ['info', 'selection', 'box' , 'drawing'],
5777
+ lines: ['info', 'selection', 'box' , 'edit_lines'],
5446
5778
  table: ['info', 'selection'],
5447
- labels: ['info', 'selection', 'box', 'labels', 'location', 'add-points'],
5448
- points: ['info', 'selection', 'box', 'location', 'add-points']
5779
+ labels: ['info', 'selection', 'box', 'labels', 'edit_points'],
5780
+ points: ['info', 'selection', 'box', 'edit_points'] // , 'add-points'
5449
5781
  };
5450
5782
 
5451
5783
  var prompts = {
@@ -5460,11 +5792,12 @@
5460
5792
  box: 'shift-drag box tool',
5461
5793
  data: 'edit attributes',
5462
5794
  labels: 'position labels',
5463
- location: 'drag points',
5795
+ edit_points: 'draw/edit points',
5796
+ edit_lines: 'draw/edit lines',
5797
+ edit_polygons: 'draw/edit polygons',
5464
5798
  vertices: 'edit vertices',
5465
5799
  selection: 'selection tool',
5466
5800
  'add-points': 'add points',
5467
- drawing: 'draw/reshape tool',
5468
5801
  rectangles: 'drag-to-resize',
5469
5802
  off: 'turn off'
5470
5803
  };
@@ -5473,6 +5806,7 @@
5473
5806
 
5474
5807
  // state variables
5475
5808
  var _editMode = 'off';
5809
+ var _prevMode;
5476
5810
  var _menuOpen = false;
5477
5811
 
5478
5812
  // Only render edit mode button/menu if this option is present
@@ -5520,11 +5854,11 @@
5520
5854
  };
5521
5855
 
5522
5856
  this.modeUsesHitDetection = function(mode) {
5523
- return ['info', 'selection', 'data', 'labels', 'location', 'vertices', 'rectangles', 'drawing'].includes(mode);
5857
+ return ['info', 'selection', 'data', 'labels', 'edit_points', 'vertices', 'rectangles', 'edit_lines', 'edit_polygons'].includes(mode);
5524
5858
  };
5525
5859
 
5526
5860
  this.modeUsesPopup = function(mode) {
5527
- return ['info', 'selection', 'data', 'box', 'labels', 'location', 'rectangles'].includes(mode);
5861
+ return ['info', 'selection', 'data', 'box', 'labels', 'edit_points', 'rectangles'].includes(mode);
5528
5862
  };
5529
5863
 
5530
5864
  this.getMode = getInteractionMode;
@@ -5551,7 +5885,7 @@
5551
5885
  function getAvailableModes() {
5552
5886
  var o = gui.model.getActiveLayer();
5553
5887
  if (!o || !o.layer) {
5554
- return menus.standard; // TODO: more sensible handling of missing layer
5888
+ return menus.empty; // TODO: more sensible handling of missing layer
5555
5889
  }
5556
5890
  if (!o.layer.geometry_type) {
5557
5891
  return menus.table;
@@ -5634,6 +5968,7 @@
5634
5968
  var changed = mode != _editMode;
5635
5969
  if (changed) {
5636
5970
  menu.classed('active', mode != 'off');
5971
+ _prevMode = _editMode;
5637
5972
  _editMode = mode;
5638
5973
  onModeChange();
5639
5974
  updateArrowButton();
@@ -5644,7 +5979,7 @@
5644
5979
  function onModeChange() {
5645
5980
  var mode = getInteractionMode();
5646
5981
  gui.state.interaction_mode = mode;
5647
- gui.dispatchEvent('interaction_mode_change', {mode: mode});
5982
+ gui.dispatchEvent('interaction_mode_change', {mode: mode, prev_mode: _prevMode});
5648
5983
  }
5649
5984
 
5650
5985
  // Update button highlight and selected menu item highlight (if any)
@@ -7262,11 +7597,11 @@
7262
7597
  test = getGraduatedCircleTest(getRadiusFunction(layer.gui.style));
7263
7598
  } else if (geoType == 'point') {
7264
7599
  test = pointTest;
7265
- } else if (interactionMode == 'drawing' && geoType == 'polygon') {
7600
+ } else if (interactionMode == 'edit_polygons') {
7266
7601
  test = polygonVertexTest;
7267
7602
  } else if (
7268
7603
  interactionMode == 'vertices' ||
7269
- interactionMode == 'drawing') {
7604
+ interactionMode == 'edit_lines') {
7270
7605
  test = vertexTest;
7271
7606
  } else if (geoType == 'polyline') {
7272
7607
  test = polylineTest;
@@ -7366,7 +7701,8 @@
7366
7701
 
7367
7702
  function pointTest(x, y) {
7368
7703
  var bullseyeDist = 2, // hit all points w/in 2 px
7369
- hitThreshold = 25,
7704
+ // use small threshold when adding points
7705
+ hitThreshold = interactionMode == 'edit_points' ? 12 : 25,
7370
7706
  toPx = ext.getTransform().mx,
7371
7707
  hits = [];
7372
7708
 
@@ -7581,7 +7917,9 @@
7581
7917
 
7582
7918
  function getPointerHitTest(mapLayer, ext, interactionMode, featureFilter) {
7583
7919
  var shapeTest, targetLayer;
7584
- if (!mapLayer || !internal.layerHasGeometry(mapLayer.gui?.displayLayer)) {
7920
+ // need hit test on empty layers, in case we are drawing shapes
7921
+ // if (!mapLayer || !internal.layerHasGeometry(mapLayer.gui?.displayLayer)) {
7922
+ if (!mapLayer || !mapLayer.gui?.displayLayer.geometry_type) {
7585
7923
  return function() {return {ids: []};};
7586
7924
  }
7587
7925
  shapeTest = getShapeHitTest(mapLayer, ext, interactionMode, featureFilter);
@@ -7621,9 +7959,12 @@
7621
7959
  var priority = 2;
7622
7960
 
7623
7961
  mouse.on('contextmenu', function(e) {
7624
- if (isOverMap(e)) {
7625
- e.originalEvent.preventDefault();
7962
+ // shift key enables default menu (for development)
7963
+ if (gui.keyboard.shiftIsPressed()) {
7964
+ return;
7626
7965
  }
7966
+ e.originalEvent.preventDefault();
7967
+ if (!targetLayer) return; // TODO: enable menu on empty map
7627
7968
  triggerHitEvent('contextmenu', e);
7628
7969
  }, false);
7629
7970
 
@@ -7667,7 +8008,6 @@
7667
8008
  };
7668
8009
 
7669
8010
  function updateHitTest(featureFilter) {
7670
- if (!hoverable()) return;
7671
8011
  hitTest = getPointerHitTest(targetLayer, ext, interactionMode, featureFilter);
7672
8012
  }
7673
8013
 
@@ -7688,7 +8028,7 @@
7688
8028
  }
7689
8029
 
7690
8030
  function hoverable() {
7691
- return true;
8031
+ return !!interactionMode;
7692
8032
  }
7693
8033
 
7694
8034
  function selectable() {
@@ -7701,20 +8041,29 @@
7701
8041
  }
7702
8042
 
7703
8043
  function draggable() {
7704
- return interactionMode == 'vertices' || interactionMode == 'location' ||
7705
- interactionMode == 'labels' || interactionMode == 'drawing';
8044
+ return interactionMode == 'vertices' || interactionMode == 'edit_points' ||
8045
+ interactionMode == 'labels' || interactionMode == 'edit_lines' ||
8046
+ interactionMode == 'edit_polygons';
7706
8047
  }
7707
8048
 
7708
8049
  function clickable() {
7709
8050
  // click used to pin popup and select features
7710
8051
  return interactionMode == 'data' || interactionMode == 'info' ||
7711
- interactionMode == 'selection' || interactionMode == 'rectangles';
8052
+ interactionMode == 'selection' || interactionMode == 'rectangles' ||
8053
+ interactionMode == 'edit_points';
7712
8054
  }
7713
8055
 
7714
8056
  self.getHitId = function() {
7715
8057
  return hitTest ? storedData.id : -1;
7716
8058
  };
7717
8059
 
8060
+ self.setHitId = function(id) {
8061
+ if (storedData.id == id) return;
8062
+ storedData.id = id;
8063
+ storedData.ids = [id];
8064
+ triggerHitEvent('change');
8065
+ };
8066
+
7718
8067
  // Get a reference to the active layer, so listeners to hit events can interact
7719
8068
  // with data and shapes
7720
8069
  self.getHitTarget = function() {
@@ -7849,9 +8198,9 @@
7849
8198
 
7850
8199
 
7851
8200
  mouse.on('click', function(e) {
8201
+ var pinned = storedData.pinned;
7852
8202
  if (!hitTest || !active) return;
7853
8203
  if (!eventIsEnabled('click')) return;
7854
-
7855
8204
  e.stopPropagation();
7856
8205
 
7857
8206
  // TODO: move pinning to inspection control?
@@ -7859,11 +8208,17 @@
7859
8208
  updateSelectionState(convertClickDataToSelectionData(hitTest(e)));
7860
8209
  }
7861
8210
 
8211
+ if (pinned && interactionMode == 'edit_points') {
8212
+ // kludge: intercept the click event if popup is turning off, so
8213
+ // a new point doesn't get made
8214
+ return;
8215
+ }
7862
8216
  triggerHitEvent('click', e);
7863
8217
  }, null, priority);
7864
8218
 
7865
8219
  // Hits are re-detected on 'hover' (if hit detection is active)
7866
8220
  mouse.on('hover', function(e) {
8221
+ if (gui.contextMenu.isOpen()) return;
7867
8222
  handlePointerEvent(e);
7868
8223
  if (storedData.pinned || !hitTest || !active) return;
7869
8224
  if (e.hover && isOverMap(e)) {
@@ -7979,19 +8334,23 @@
7979
8334
  // check if an event is used in the current interaction mode
7980
8335
  function eventIsEnabled(type) {
7981
8336
  if (!active) return false;
7982
- if (interactionMode == 'drawing' && (type == 'hover' || type == 'dblclick')) {
7983
- return true; // special case -- using hover for line drawing animation
7984
- }
7985
-
7986
8337
  if (type == 'click' && gui.keyboard.ctrlIsPressed()) {
7987
8338
  return false; // don't fire if context menu might open
7988
8339
  }
7989
8340
  if (type == 'click' && gui.contextMenu.isOpen()) {
7990
8341
  return false;
7991
8342
  }
7992
- if (type == 'click' && interactionMode == 'drawing') {
8343
+ if (type == 'click' &&
8344
+ (interactionMode == 'edit_lines' || interactionMode == 'edit_polygons')) {
7993
8345
  return true; // click events are triggered even if no shape is hit
7994
8346
  }
8347
+ if (type == 'click' && interactionMode == 'edit_points') {
8348
+ return true;
8349
+ }
8350
+ if ((interactionMode == 'edit_lines' || interactionMode == 'edit_polygons') &&
8351
+ (type == 'hover' || type == 'dblclick')) {
8352
+ return true; // special case -- using hover for line drawing animation
8353
+ }
7995
8354
 
7996
8355
  // ignore pointer events when no features are being hit
7997
8356
  // (don't block pan and other navigation when events aren't being used for editing)
@@ -8009,7 +8368,7 @@
8009
8368
  }
8010
8369
 
8011
8370
  function possiblyStopPropagation(e) {
8012
- if (interactionMode == 'drawing') {
8371
+ if (interactionMode == 'edit_lines' || interactionMode == 'edit_polygons') {
8013
8372
  // handled conditionally in the control
8014
8373
  return;
8015
8374
  }
@@ -8848,7 +9207,7 @@
8848
9207
  }
8849
9208
 
8850
9209
  gui.on('map_reset', function() {
8851
- ext.home();
9210
+ ext.reset(true);
8852
9211
  });
8853
9212
 
8854
9213
  zoomTween.on('change', function(e) {
@@ -8866,8 +9225,9 @@
8866
9225
 
8867
9226
  mouse.on('dragstart', function(e) {
8868
9227
  if (disabled()) return;
8869
- if (!internal.layerHasGeometry(gui.model.getActiveLayer().layer)) return;
8870
- // zoomDrag = !!e.metaKey || !!e.ctrlKey; // meta is command on mac, windows key on windows
9228
+ // allow drawing rectangles if active layer is empty
9229
+ // var lyr = gui.model.getActiveLayer()?.layer;
9230
+ // if (lyr && !internal.layerHasGeometry(lyr)) return;
8871
9231
  shiftDrag = !!e.shiftKey;
8872
9232
  if (shiftDrag) {
8873
9233
  if (useBoxZoom()) zoomBox.turnOn();
@@ -9205,7 +9565,7 @@
9205
9565
  });
9206
9566
 
9207
9567
  self.show = function(id, ids, lyr, pinned, edit) {
9208
- var singleEdit = edit || pinned && gui.interaction.getMode() == 'data';
9568
+ var singleEdit = edit || pinned && !internal.layerHasAttributeData(lyr);
9209
9569
  var multiEdit = pinned && gui.interaction.getMode() == 'selection';
9210
9570
  var maxHeight = parent.node().clientHeight - 36;
9211
9571
 
@@ -9437,16 +9797,16 @@
9437
9797
  gui.session.dataValueUpdated(e.ids, e.field, e.value);
9438
9798
  // Refresh the display if a style variable has been changed interactively
9439
9799
  if (internal.isSupportedSvgStyleProperty(e.field)) {
9440
- // drawLayers();
9441
9800
  gui.dispatchEvent('map-needs-refresh');
9442
9801
  }
9443
9802
  });
9444
9803
 
9445
9804
  hit.on('contextmenu', function(e) {
9446
9805
  var target = hit.getHitTarget();
9447
- if (!e.overMap || !target) return;
9448
-
9449
- if (e.mode == 'drawing') return; // TODO: make special menu for this mode
9806
+ if (!e.overMap || !target || e.mode == 'edit_lines' ||
9807
+ e.mode == 'edit_polygons') {
9808
+ return;
9809
+ }
9450
9810
  gui.contextMenu.open(e, hit.getHitTarget());
9451
9811
  });
9452
9812
 
@@ -9738,14 +10098,43 @@
9738
10098
  }
9739
10099
  }
9740
10100
 
9741
- function initPointDragging(gui, ext, hit) {
10101
+ function initPointEditing(gui, ext, hit) {
9742
10102
  var symbolInfo;
9743
10103
  function active(e) {
9744
- return e.id > -1 && gui.interaction.getMode() == 'location';
10104
+ return gui.interaction.getMode() == 'edit_points';
9745
10105
  }
9746
10106
 
9747
- hit.on('dragstart', function(e) {
10107
+ function overPoint(e) {
10108
+ return active(e) && e.id > -1;
10109
+ }
10110
+
10111
+ gui.on('interaction_mode_change', function(e) {
10112
+ if (e.mode == 'edit_points' && !gui.model.getActiveLayer()) {
10113
+ addEmptyLayer(gui, undefined, 'point');
10114
+ } else if (e.prev_mode == 'edit_points') {
10115
+ gui.container.findChild('.map-layers').classed('add-points', false);
10116
+ }
10117
+
10118
+ });
10119
+
10120
+ hit.on('click', function(e) {
10121
+ if (overPoint(e) || !active(e)) return;
10122
+ // add point
10123
+ var p = pixToDataCoords(e.x, e.y);
10124
+ var target = hit.getHitTarget();
10125
+ appendNewPoint(target, p);
10126
+ gui.dispatchEvent('point_add', {p, target});
10127
+ gui.dispatchEvent('map-needs-refresh');
10128
+ hit.setHitId(target.shapes.length - 1); // highlight new point
10129
+ });
10130
+
10131
+ hit.on('change', function(e) {
9748
10132
  if (!active(e)) return;
10133
+ gui.container.findChild('.map-layers').classed('add-points', !overPoint(e));
10134
+ });
10135
+
10136
+ hit.on('dragstart', function(e) {
10137
+ if (!overPoint(e)) return;
9749
10138
  var target = hit.getHitTarget();
9750
10139
  symbolInfo = {
9751
10140
  FID: e.id,
@@ -9755,37 +10144,36 @@
9755
10144
  });
9756
10145
 
9757
10146
  hit.on('drag', function(e) {
9758
- if (!active(e)) return;
10147
+ if (!overPoint(e)) return;
9759
10148
  // TODO: support multi points... get id of closest part to the pointer
9760
- var p = getPointCoordsById(e.id, symbolInfo.target);
9761
- if (!p) return;
10149
+ // var p = getPointCoordsById(e.id, symbolInfo.target);
10150
+ var id = symbolInfo.FID;
10151
+ var shp = symbolInfo.target.gui.displayLayer.shapes[id];
10152
+ if (!shp) return;
9762
10153
  var diff = translateDeltaDisplayCoords(e.dx, e.dy, ext);
9763
- p[0] += diff[0];
9764
- p[1] += diff[1];
10154
+ shp[0][0] += diff[0];
10155
+ shp[0][1] += diff[1];
9765
10156
  gui.dispatchEvent('map-needs-refresh');
9766
10157
  });
9767
10158
 
9768
10159
  hit.on('dragend', function(e) {
9769
- if (!active(e) || !symbolInfo ) return;
9770
- updatePointCoords(symbolInfo.target, e.id);
10160
+ if (!overPoint(e) || !symbolInfo ) return;
10161
+ updatePointCoords(symbolInfo.target, symbolInfo.FID);
9771
10162
  symbolInfo.endCoords = getPointCoords(symbolInfo.target, e.id);
9772
10163
  gui.dispatchEvent('symbol_dragend', symbolInfo);
9773
10164
  symbolInfo = null;
9774
10165
  });
9775
10166
 
10167
+ function pixToDataCoords(x, y) {
10168
+ var target = hit.getHitTarget();
10169
+ return translateDisplayPoint(target, ext.translatePixelCoords(x, y));
10170
+ }
10171
+
9776
10172
  function translateDeltaDisplayCoords(dx, dy, ext) {
9777
10173
  var a = ext.translatePixelCoords(0, 0);
9778
10174
  var b = ext.translatePixelCoords(dx, dy);
9779
10175
  return [b[0] - a[0], b[1] - a[1]];
9780
10176
  }
9781
-
9782
- function getPointCoordsById(id, layer) {
9783
- var coords = layer && layer.geometry_type == 'point' && layer.shapes[id];
9784
- if (!coords || coords.length != 1) {
9785
- return null;
9786
- }
9787
- return coords[0];
9788
- }
9789
10177
  }
9790
10178
 
9791
10179
  // pixel distance threshold for hovering near a vertex or segment midpoint
@@ -9826,13 +10214,17 @@
9826
10214
  gui.addMode('drawing_tool', turnOn, turnOff);
9827
10215
 
9828
10216
  gui.on('interaction_mode_change', function(e) {
9829
- gui.container.findChild('.map-layers').classed('drawing', e.mode == 'drawing');
9830
- var prevMode = gui.getMode();
9831
- if (e.mode == 'drawing') {
10217
+ if (e.mode == 'edit_lines' || e.mode == 'edit_polygons') {
10218
+ if (!gui.model.getActiveLayer()) {
10219
+ addEmptyLayer(gui, undefined, e.mode == 'edit_lines' ? 'polyline' : 'polygon');
10220
+ }
9832
10221
  gui.enterMode('drawing_tool');
10222
+ } else if (gui.getMode() == 'drawing_tool') {
10223
+ gui.clearMode();
9833
10224
  } else if (active()) {
9834
10225
  turnOff();
9835
10226
  }
10227
+ updateCursor();
9836
10228
  }, null, 10); // higher priority than hit control, so turnOff() has correct hit target
9837
10229
 
9838
10230
  gui.on('redo_path_add', function(e) {
@@ -9879,6 +10271,7 @@
9879
10271
  });
9880
10272
 
9881
10273
  function turnOn() {
10274
+ if (active()) return;
9882
10275
  var target = hit.getHitTarget();
9883
10276
  initialArcCount = target.gui.displayArcs.size();
9884
10277
  initialShapeCount = target.shapes.length;
@@ -9905,6 +10298,7 @@
9905
10298
 
9906
10299
  function turnOff() {
9907
10300
  var removed = 0;
10301
+ var mode = gui.interaction.getMode();
9908
10302
  finishCurrentPath();
9909
10303
  if (polygonMode()) {
9910
10304
  removed = removeOpenPolygons();
@@ -9913,10 +10307,11 @@
9913
10307
  hideInstructions();
9914
10308
  initialArcCount = -1;
9915
10309
  initialShapeCount = -1;
9916
- if (gui.interaction.getMode() == 'drawing') {
10310
+ if (mode == 'edit_lines' || mode == 'edit_polygons') {
9917
10311
  // mode change was not initiated by interactive menu -- turn off interactivity
9918
10312
  gui.interaction.turnOff();
9919
10313
  }
10314
+ updateCursor();
9920
10315
  if (removed > 0) {
9921
10316
  fullRedraw();
9922
10317
  }
@@ -10084,6 +10479,7 @@
10084
10479
  }
10085
10480
 
10086
10481
  function updateCursor() {
10482
+ gui.container.findChild('.map-layers').classed('drawing', active());
10087
10483
  var useArrow = hoverVertexInfo && !hoverVertexInfo.extendable && !drawing();
10088
10484
  gui.container.findChild('.map-layers').classed('dragging', useArrow);
10089
10485
  }
@@ -10342,45 +10738,9 @@
10342
10738
  }
10343
10739
  }
10344
10740
 
10345
- function initPointDrawing(gui, ext, hit) {
10346
- var mouse = gui.map.getMouse();
10347
-
10348
- gui.on('interaction_mode_change', function(e) {
10349
- gui.container.findChild('.map-layers').classed('add-points', e.mode === 'add-points');
10350
- });
10351
-
10352
- function active() {
10353
- return gui.interaction.getMode() == 'add-points';
10354
- }
10355
-
10356
- mouse.on('click', function(e) {
10357
- if (!active()) return;
10358
- addPoint(e.x, e.y);
10359
- gui.dispatchEvent('map-needs-refresh');
10360
- });
10361
-
10362
- // x, y: pixel coordinates
10363
- function addPoint(x, y) {
10364
- var p = ext.translatePixelCoords(x, y);
10365
- var lyr = hit.getHitTarget();
10366
- var fid = lyr.shapes.length;
10367
- var d = appendNewDataRecord(lyr);
10368
- if (d) {
10369
- // this seems to work even for projected layers -- the data tables
10370
- // of projected and original data seem to be shared.
10371
- lyr.data.getRecords()[fid] = d;
10372
- }
10373
- lyr.shapes[fid] = [p];
10374
- updatePointCoords(lyr, fid);
10375
- }
10376
-
10377
-
10378
- }
10379
-
10380
10741
  function initInteractiveEditing(gui, ext, hit) {
10381
10742
  initLabelDragging(gui, ext, hit);
10382
- initPointDragging(gui, ext, hit);
10383
- initPointDrawing(gui, ext, hit);
10743
+ initPointEditing(gui, ext, hit);
10384
10744
  initLineEditing(gui, ext, hit);
10385
10745
  }
10386
10746
 
@@ -10394,26 +10754,32 @@
10394
10754
 
10395
10755
  _position.on('resize', function(e) {
10396
10756
  if (ready()) {
10397
- triggerChangeEvent({resize: true});
10757
+ // triggerChangeEvent({resize: true});
10758
+ triggerChangeEvent();
10398
10759
  }
10399
10760
  });
10400
10761
 
10401
10762
  function ready() { return !!_fullBounds; }
10402
10763
 
10403
- this.reset = function() {
10764
+ this.reset = function(fire) {
10404
10765
  if (!ready()) return;
10405
- recenter(_fullBounds.centerX(), _fullBounds.centerY(), 1, {reset: true});
10766
+ recenter(_fullBounds.centerX(), _fullBounds.centerY(), 1);
10767
+ if (fire) {
10768
+ triggerChangeEvent();
10769
+ }
10406
10770
  };
10407
10771
 
10408
10772
  this.home = function() {
10409
10773
  if (!ready()) return;
10410
10774
  recenter(_fullBounds.centerX(), _fullBounds.centerY(), 1);
10775
+ triggerChangeEvent();
10411
10776
  };
10412
10777
 
10413
10778
  this.pan = function(xpix, ypix) {
10414
10779
  if (!ready()) return;
10415
10780
  var t = this.getTransform();
10416
10781
  recenter(_cx - xpix / t.mx, _cy - ypix / t.my);
10782
+ triggerChangeEvent();
10417
10783
  };
10418
10784
 
10419
10785
  // Zoom to @w (width of the map viewport in coordinates)
@@ -10436,6 +10802,7 @@
10436
10802
  cx = fx + dx2,
10437
10803
  cy = fy + dy2;
10438
10804
  recenter(cx, cy, scale);
10805
+ triggerChangeEvent();
10439
10806
  };
10440
10807
 
10441
10808
  this.zoomByPct = function(pct, xpct, ypct) {
@@ -10527,11 +10894,10 @@
10527
10894
  return this.getTransform().invert().transform(x, y);
10528
10895
  };
10529
10896
 
10530
- function recenter(cx, cy, scale, data) {
10897
+ function recenter(cx, cy, scale) {
10531
10898
  scale = scale ? limitScale(scale) : _scale;
10532
10899
  if (cx == _cx && cy == _cy && scale == _scale) return;
10533
10900
  navigate(cx, cy, scale);
10534
- triggerChangeEvent(data);
10535
10901
  }
10536
10902
 
10537
10903
  function navigate(cx, cy, scale) {
@@ -10558,9 +10924,8 @@
10558
10924
  _scale = scale;
10559
10925
  }
10560
10926
 
10561
- function triggerChangeEvent(data) {
10562
- data = data || {};
10563
- _self.dispatchEvent('change', data);
10927
+ function triggerChangeEvent() {
10928
+ _self.dispatchEvent('change');
10564
10929
  }
10565
10930
 
10566
10931
  // stop zooming before rounding errors become too obvious
@@ -11453,8 +11818,10 @@
11453
11818
  return el;
11454
11819
  }
11455
11820
 
11456
- function LayerRenderer(gui, container, ext, mouse) {
11821
+ function LayerRenderer(gui, container) {
11457
11822
  var el = El(container),
11823
+ ext = gui.map.getExtent(),
11824
+ mouse = gui.map.getMouse(),
11458
11825
  _mainCanv = new DisplayCanvas().appendTo(el),
11459
11826
  _overlayCanv = new DisplayCanvas().appendTo(el),
11460
11827
  _svg = new SvgDisplayLayer(gui, ext, mouse).appendTo(el),
@@ -11513,493 +11880,248 @@
11513
11880
  });
11514
11881
  };
11515
11882
 
11516
- // kludge: skip rendering base layers if hovering, except on first hover
11517
- // (because highlight shapes may be rendered to the main canvas)
11518
- function skipMainLayerRedraw(action) {
11519
- return action == 'hover' && _overlayCanv.visible();
11520
- }
11521
-
11522
- function drawCanvasLayer(lyr, canv) {
11523
- if (!lyr) return;
11524
- if (lyr.gui.style.type == 'outline') {
11525
- drawOutlineLayerToCanvas(lyr, canv, ext);
11526
- } else {
11527
- drawStyledLayerToCanvas(lyr, canv, ext);
11528
- }
11529
- }
11530
-
11531
- function getSvgLayerType(layer) {
11532
- var type = null;
11533
- if (internal.layerHasSvgSymbols(layer)) {
11534
- type = 'symbol'; // also label + symbol
11535
- } else if (internal.layerHasLabels(layer)) {
11536
- type = 'symbol';
11537
- }
11538
- return type;
11539
- }
11540
- }
11541
-
11542
- // Controls the shift-drag box editing tool
11543
- //
11544
- function BoxTool(gui, ext, nav) {
11545
- var self = new EventDispatcher();
11546
- var box = new HighlightBox(gui, {name: 'box-tool', persistent: true, handles: true, draggable: true});
11547
- var popup = gui.container.findChild('.box-tool-options');
11548
- var coords = popup.findChild('.box-coords');
11549
- var _on = false;
11550
-
11551
- var infoBtn = new SimpleButton(popup.findChild('.info-btn')).on('click', function() {
11552
- if (coords.visible()) hideCoords(); else showCoords();
11553
- });
11554
-
11555
- new SimpleButton(popup.findChild('.cancel-btn')).on('click', function() {
11556
- reset();
11557
- });
11558
-
11559
- new SimpleButton(popup.findChild('.select-btn')).on('click', function() {
11560
- var coords = box.getDataCoords();
11561
- if (!coords) return;
11562
- gui.enterMode('selection_tool');
11563
- gui.interaction.setMode('selection');
11564
- // kludge to pass bbox to the selection tool
11565
- gui.dispatchEvent('selection_bridge', {
11566
- map_data_bbox: coords
11567
- });
11568
- });
11569
-
11570
- new SimpleButton(popup.findChild('.clip-btn')).on('click', function() {
11571
- runCommand('-clip bbox=' + box.getDataCoords().join(','));
11572
- });
11573
-
11574
- new SimpleButton(popup.findChild('.erase-btn')).on('click', function() {
11575
- runCommand('-erase bbox=' + box.getDataCoords().join(','));
11576
- });
11577
-
11578
- new SimpleButton(popup.findChild('.rect-btn')).on('click', function() {
11579
- var cmd = '-rectangle + bbox=' + box.getDataCoords().join(',');
11580
- runCommand(cmd);
11581
- });
11582
-
11583
- new SimpleButton(popup.findChild('.frame-btn')).on('click', function() {
11584
- openAddFramePopup(gui, box.getDataCoords());
11585
- });
11586
-
11587
- gui.addMode('box_tool', turnOn, turnOff);
11588
-
11589
- gui.on('interaction_mode_change', function(e) {
11590
- if (e.mode === 'box') {
11591
- gui.enterMode('box_tool');
11592
- } else if (_on) {
11593
- turnOff();
11594
- }
11595
- });
11596
-
11597
- gui.on('shift_drag_start', function() {
11598
- // box.classed('zooming', inZoomMode());
11599
- hideCoords();
11600
- });
11601
-
11602
- box.on('dragend', function(e) {
11603
- if (_on) popup.show();
11604
- });
11605
-
11606
- box.on('handle_drag', function() {
11607
- if (coords.visible()) {
11608
- showCoords();
11609
- }
11610
- });
11611
-
11612
- function inZoomMode() {
11613
- return !_on && gui.getMode() != 'selection_tool';
11614
- }
11615
-
11616
- function runCommand(cmd) {
11617
- if (gui.console) {
11618
- gui.console.runMapshaperCommands(cmd, function(err) {
11619
- reset();
11620
- gui.clearMode();
11621
- });
11622
- }
11623
- // reset(); // TODO: exit interactive mode
11624
- }
11625
-
11626
- function showCoords() {
11627
- El(infoBtn.node()).addClass('selected-btn');
11628
- coords.text(box.getDataCoords().join(','));
11629
- coords.show();
11630
- GUI.selectElement(coords.node());
11631
- }
11632
-
11633
- function hideCoords() {
11634
- El(infoBtn.node()).removeClass('selected-btn');
11635
- coords.hide();
11636
- }
11637
-
11638
- function turnOn() {
11639
- box.turnOn();
11640
- _on = true;
11641
- }
11642
-
11643
- function turnOff() {
11644
- box.turnOff();
11645
- if (gui.interaction.getMode() == 'box') {
11646
- // mode change was not initiated by interactive menu -- turn off interactivity
11647
- gui.interaction.turnOff();
11648
- }
11649
- _on = false;
11650
- reset();
11651
- }
11652
-
11653
- function reset() {
11654
- box.hide();
11655
- popup.hide();
11656
- hideCoords();
11657
- }
11658
-
11659
- function openAddFramePopup(gui, bbox) {
11660
- var popup = showPopupAlert('', 'Add a map frame');
11661
- var el = popup.container();
11662
- el.addClass('option-menu');
11663
- var html = `<p>Enter a width in px, cm or inches to create a frame layer
11664
- for setting the size of the map for symbol scaling in the
11665
- 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>
11666
- <div tabindex="0" class="btn dialog-btn">Create</div></span>`;
11667
- el.html(html);
11668
- var input = el.findChild('.frame-width');
11669
- input.node().focus();
11670
- var btn = el.findChild('.btn').on('click', function() {
11671
- var widthStr = input.node().value.trim();
11672
- if (parseFloat(widthStr) > 0 === false) {
11673
- // invalid input
11674
- input.node().value = '';
11675
- return;
11676
- }
11677
- var cmd = `-rectangle + name=frame bbox='${bbox.join(',')}' width='${widthStr}'`;
11678
- runCommand(cmd);
11679
- popup.close();
11680
- });
11681
- }
11682
-
11683
- return self;
11684
- }
11685
-
11686
- function RectangleControl(gui, hit) {
11687
- var box = new HighlightBox(gui, {name: 'rectangle-tool', persistent: true, handles: true, classname: 'rectangles', draggable: false});
11688
- var _on = false;
11689
- var dragInfo;
11690
-
11691
- gui.addMode('rectangle_tool', turnOn, turnOff);
11692
-
11693
- gui.on('interaction_mode_change', function(e) {
11694
- if (e.mode === 'rectangles') {
11695
- gui.enterMode('rectangle_tool');
11696
- } else if (_on) {
11697
- turnOff();
11698
- }
11699
- });
11700
-
11701
- hit.on('change', function(e) {
11702
- if (!_on) return;
11703
- // TODO: handle multiple hits (see gui-inspection-control)
11704
- var id = e.id;
11705
- if (e.id > -1 && e.pinned) {
11706
- var target = hit.getHitTarget();
11707
- var path = target.shapes[e.id][0];
11708
- var bbox = target.gui.displayArcs.getSimpleShapeBounds(path).toArray();
11709
- box.setDataCoords(bbox);
11710
- dragInfo = {
11711
- id: e.id,
11712
- target: target,
11713
- ids: [],
11714
- points: []
11715
- };
11716
- var iter = target.gui.displayArcs.getShapeIter(path);
11717
- while (iter.hasNext()) {
11718
- dragInfo.points.push([iter.x, iter.y]);
11719
- dragInfo.ids.push(iter._arc.i);
11720
- }
11721
- gui.container.findChild('.map-layers').classed('dragging', true);
11722
-
11723
- } else if (dragInfo) {
11724
- gui.dispatchEvent('rectangle_dragend', dragInfo); // save undo state
11725
- gui.container.findChild('.map-layers').classed('dragging', false);
11726
- reset();
11727
- } else {
11728
- box.hide();
11729
- }
11730
-
11731
- });
11732
-
11733
- box.on('handle_drag', function(e) {
11734
- if (!_on || !dragInfo) return;
11735
- var coords = internal.bboxToCoords(box.getDataCoords());
11736
- setRectangleCoords(dragInfo.target, dragInfo.ids, coords);
11737
- gui.dispatchEvent('map-needs-refresh');
11738
- });
11739
-
11740
- function turnOn() {
11741
- box.turnOn();
11742
- _on = true;
11883
+ // kludge: skip rendering base layers if hovering, except on first hover
11884
+ // (because highlight shapes may be rendered to the main canvas)
11885
+ function skipMainLayerRedraw(action) {
11886
+ return action == 'hover' && _overlayCanv.visible();
11743
11887
  }
11744
11888
 
11745
- function turnOff() {
11746
- box.turnOff();
11747
- if (gui.interaction.getMode() == 'rectangles') {
11748
- // mode change was not initiated by interactive menu -- turn off interactivity
11749
- gui.interaction.turnOff();
11889
+ function drawCanvasLayer(lyr, canv) {
11890
+ if (!lyr) return;
11891
+ if (lyr.gui.style.type == 'outline') {
11892
+ drawOutlineLayerToCanvas(lyr, canv, ext);
11893
+ } else {
11894
+ drawStyledLayerToCanvas(lyr, canv, ext);
11750
11895
  }
11751
- _on = false;
11752
- reset();
11753
11896
  }
11754
11897
 
11755
- function reset() {
11756
- box.hide();
11757
- dragInfo = null;
11898
+ function getSvgLayerType(layer) {
11899
+ var type = null;
11900
+ if (internal.layerHasSvgSymbols(layer)) {
11901
+ type = 'symbol'; // also label + symbol
11902
+ } else if (internal.layerHasLabels(layer)) {
11903
+ type = 'symbol';
11904
+ }
11905
+ return type;
11758
11906
  }
11759
11907
  }
11760
11908
 
11761
- // Create low-detail versions of large arc collections for faster rendering
11762
- // at zoomed-out scales.
11763
- function enhanceArcCollectionForDisplay(unfilteredArcs) {
11764
- var size = unfilteredArcs.getPointCount(),
11765
- filteredArcs, filteredSegLen;
11909
+ // Controls the shift-drag box editing tool
11910
+ //
11911
+ function BoxTool(gui, ext, nav) {
11912
+ var self = new EventDispatcher();
11913
+ var box = new HighlightBox(gui, {name: 'box-tool', persistent: true, handles: true, draggable: true});
11914
+ var popup = gui.container.findChild('.box-tool-options');
11915
+ var coords = popup.findChild('.box-coords');
11916
+ var _on = false;
11766
11917
 
11767
- // Only generate low-detail arcs for larger datasets
11768
- if (size > 5e5) {
11769
- update();
11770
- }
11918
+ var infoBtn = new SimpleButton(popup.findChild('.info-btn')).on('click', function() {
11919
+ if (coords.visible()) hideCoords(); else showCoords();
11920
+ });
11771
11921
 
11772
- function update() {
11773
- if (unfilteredArcs.getVertexData().zz) {
11774
- // Use precalculated simplification data for vertex filtering, if available
11775
- filteredArcs = initFilteredArcs(unfilteredArcs);
11776
- filteredSegLen = internal.getAvgSegment(filteredArcs);
11777
- } else {
11778
- // Use fast simplification as a fallback
11779
- filteredSegLen = internal.getAvgSegment(unfilteredArcs) * 4;
11780
- filteredArcs = internal.simplifyArcsFast(unfilteredArcs, filteredSegLen);
11781
- }
11782
- }
11922
+ new SimpleButton(popup.findChild('.cancel-btn')).on('click', function() {
11923
+ reset();
11924
+ });
11783
11925
 
11784
- function initFilteredArcs(arcs) {
11785
- var filterPct = 0.08;
11786
- var nth = Math.ceil(arcs.getPointCount() / 5e5);
11787
- var currInterval = arcs.getRetainedInterval();
11788
- var filterZ = arcs.getThresholdByPct(filterPct, nth);
11789
- var filteredArcs = arcs.setRetainedInterval(filterZ).getFilteredCopy();
11790
- arcs.setRetainedInterval(currInterval); // reset current simplification
11791
- return filteredArcs;
11792
- }
11926
+ new SimpleButton(popup.findChild('.select-btn')).on('click', function() {
11927
+ var coords = box.getDataCoords();
11928
+ if (!coords) return;
11929
+ gui.enterMode('selection_tool');
11930
+ gui.interaction.setMode('selection');
11931
+ // kludge to pass bbox to the selection tool
11932
+ gui.dispatchEvent('selection_bridge', {
11933
+ map_data_bbox: coords
11934
+ });
11935
+ });
11793
11936
 
11794
- // TODO: better job of detecting arc change... e.g. revision number
11795
- unfilteredArcs.getScaledArcs = function(ext) {
11796
- // check for changes in the number of arcs (probably due to editing)
11797
- if (filteredArcs && filteredArcs.size() != unfilteredArcs.size()) {
11798
- // arc count has changed... probably due to editing
11799
- update();
11800
- if (filteredArcs.size() != unfilteredArcs.size()) {
11801
- throw Error('Internal error');
11802
- }
11803
- }
11804
- if (filteredArcs) {
11805
- // match simplification of unfiltered arcs
11806
- filteredArcs.setRetainedInterval(unfilteredArcs.getRetainedInterval());
11807
- }
11808
- // switch to filtered version of arcs at small scales
11809
- var unitsPerPixel = 1/ext.getTransform().mx,
11810
- useFiltering = filteredArcs && unitsPerPixel > filteredSegLen * 1.5;
11811
- return useFiltering ? filteredArcs : unfilteredArcs;
11812
- };
11813
- }
11937
+ new SimpleButton(popup.findChild('.clip-btn')).on('click', function() {
11938
+ runCommand('-clip bbox=' + box.getDataCoords().join(','));
11939
+ });
11814
11940
 
11815
- function getDisplayLayerForTable(tableArg) {
11816
- var table = tableArg || new internal.DataTable(0),
11817
- n = table.size(),
11818
- cellWidth = 12,
11819
- cellHeight = 5,
11820
- gutter = 6,
11821
- arcs = [],
11822
- shapes = [],
11823
- aspectRatio = 1.1,
11824
- x, y, col, row, blockSize;
11941
+ new SimpleButton(popup.findChild('.erase-btn')).on('click', function() {
11942
+ runCommand('-erase bbox=' + box.getDataCoords().join(','));
11943
+ });
11825
11944
 
11826
- if (n > 10000) {
11827
- arcs = null;
11828
- gutter = 0;
11829
- cellWidth = 4;
11830
- cellHeight = 4;
11831
- aspectRatio = 1.45;
11832
- } else if (n > 5000) {
11833
- cellWidth = 5;
11834
- gutter = 3;
11835
- aspectRatio = 1.45;
11836
- } else if (n > 1000) {
11837
- gutter = 3;
11838
- cellWidth = 8;
11839
- aspectRatio = 1.3;
11840
- }
11945
+ new SimpleButton(popup.findChild('.rect-btn')).on('click', function() {
11946
+ var cmd = '-rectangle + bbox=' + box.getDataCoords().join(',');
11947
+ runCommand(cmd);
11948
+ });
11841
11949
 
11842
- if (n < 25) {
11843
- blockSize = n;
11844
- } else {
11845
- blockSize = Math.sqrt(n * (cellWidth + gutter) / cellHeight / aspectRatio) | 0;
11846
- }
11950
+ new SimpleButton(popup.findChild('.frame-btn')).on('click', function() {
11951
+ openAddFramePopup(gui, box.getDataCoords());
11952
+ });
11847
11953
 
11848
- for (var i=0; i<n; i++) {
11849
- row = i % blockSize;
11850
- col = Math.floor(i / blockSize);
11851
- x = col * (cellWidth + gutter);
11852
- y = cellHeight * (blockSize - row);
11853
- if (arcs) {
11854
- arcs.push(getArc(x, y, cellWidth, cellHeight));
11855
- shapes.push([[i]]);
11856
- } else {
11857
- shapes.push([[x, y]]);
11954
+ gui.addMode('box_tool', turnOn, turnOff);
11955
+
11956
+ gui.on('interaction_mode_change', function(e) {
11957
+ if (e.mode === 'box') {
11958
+ gui.enterMode('box_tool');
11959
+ } else if (_on) {
11960
+ turnOff();
11858
11961
  }
11859
- }
11962
+ });
11860
11963
 
11861
- function getArc(x, y, w, h) {
11862
- return [[x, y], [x + w, y], [x + w, y - h], [x, y - h], [x, y]];
11863
- }
11964
+ gui.on('shift_drag_start', function() {
11965
+ hideCoords();
11966
+ });
11864
11967
 
11865
- return {
11866
- layer: {
11867
- geometry_type: arcs ? 'polygon' : 'point',
11868
- shapes: shapes,
11869
- data: table
11870
- },
11871
- arcs: arcs ? new internal.ArcCollection(arcs) : null
11872
- };
11873
- }
11968
+ box.on('dragend', function(e) {
11969
+ if (_on) popup.show();
11970
+ });
11874
11971
 
11875
- // lyr: a map layer with gui property
11876
- // displayCRS: CRS to use for display, or null (which clears any current display CRS)
11877
- function projectLayerForDisplay(lyr, displayCRS) {
11878
- var crsInfo = getDatasetCrsInfo(lyr.gui.source.dataset);
11879
- var sourceCRS = crsInfo.crs || null; // let enhanceLayerForDisplay() handle null case
11880
- if (!lyr.gui.geographic) {
11881
- return;
11882
- }
11883
- if (lyr.gui.dynamic_crs && internal.crsAreEqual(sourceCRS, lyr.gui.dynamic_crs)) {
11884
- return;
11885
- }
11886
- enhanceLayerForDisplay(lyr, lyr.gui.source.dataset, {crs: displayCRS});
11887
- if (lyr.gui.style?.ids) {
11888
- // re-apply layer filter
11889
- lyr.gui.displayLayer = filterLayerByIds(lyr.gui.displayLayer, lyr.gui.style.ids);
11972
+ box.on('handle_drag', function() {
11973
+ if (coords.visible()) {
11974
+ showCoords();
11975
+ }
11976
+ });
11977
+
11978
+ function inZoomMode() {
11979
+ return !_on && gui.getMode() != 'selection_tool';
11890
11980
  }
11891
- }
11892
11981
 
11982
+ function runCommand(cmd) {
11983
+ if (gui.console) {
11984
+ gui.console.runMapshaperCommands(cmd, function(err) {
11985
+ reset();
11986
+ gui.clearMode();
11987
+ });
11988
+ }
11989
+ // reset(); // TODO: exit interactive mode
11990
+ }
11893
11991
 
11894
- // Supplement a layer with information needed for rendering
11895
- function enhanceLayerForDisplay(layer, dataset, opts) {
11896
- var gui = {
11897
- empty: internal.getFeatureCount(layer) === 0,
11898
- geographic: false,
11899
- displayArcs: null,
11900
- displayLayer: null,
11901
- source: {dataset},
11902
- bounds: null,
11903
- style: null,
11904
- dynamic_crs: null,
11905
- invertPoint: null,
11906
- projectPoint: null
11907
- };
11992
+ function showCoords() {
11993
+ El(infoBtn.node()).addClass('selected-btn');
11994
+ coords.text(box.getDataCoords().join(','));
11995
+ coords.show();
11996
+ GUI.selectElement(coords.node());
11997
+ }
11908
11998
 
11909
- var displayCRS = opts.crs || null;
11910
- // display arcs may have been generated when another layer in the dataset
11911
- // was converted for display... re-use if available
11912
- var displayArcs = dataset.gui?.displayArcs;
11913
- var unprojectable = false;
11914
- var sourceCRS;
11915
- var emptyArcs;
11999
+ function hideCoords() {
12000
+ El(infoBtn.node()).removeClass('selected-btn');
12001
+ coords.hide();
12002
+ }
11916
12003
 
11917
- if (displayCRS && layer.geometry_type) {
11918
- var crsInfo = getDatasetCrsInfo(dataset);
11919
- if (crsInfo.error) {
11920
- // unprojectable dataset -- return empty layer
11921
- gui.unprojectable = true;
11922
- } else {
11923
- sourceCRS = crsInfo.crs;
11924
- }
12004
+ function turnOn() {
12005
+ box.turnOn();
12006
+ _on = true;
11925
12007
  }
11926
12008
 
11927
- // Assume that dataset.displayArcs is in the display CRS
11928
- // (it must be deleted upstream if reprojection is needed)
11929
- // if (!obj.empty && dataset.arcs && !displayArcs) {
11930
- if (dataset.arcs && !displayArcs && !gui.unprojectable) {
11931
- // project arcs, if needed
11932
- if (needReprojectionForDisplay(sourceCRS, displayCRS)) {
11933
- displayArcs = projectArcsForDisplay(dataset.arcs, sourceCRS, displayCRS);
11934
- } else {
11935
- // Use original arcs for display if there is no dynamic reprojection
11936
- displayArcs = dataset.arcs;
12009
+ function turnOff() {
12010
+ box.turnOff();
12011
+ if (gui.interaction.getMode() == 'box') {
12012
+ // mode change was not initiated by interactive menu -- turn off interactivity
12013
+ gui.interaction.turnOff();
11937
12014
  }
11938
-
11939
- enhanceArcCollectionForDisplay(displayArcs);
11940
- dataset.gui = {displayArcs}; // stash these in the dataset for other layers to use
12015
+ _on = false;
12016
+ reset();
11941
12017
  }
11942
12018
 
11943
- if (internal.layerHasFurniture(layer)) {
11944
- // TODO: consider how to render furniture in GUI
11945
- // treating furniture layers (other than frame) as tabular for now,
11946
- // so there is something to show if they are selected
12019
+ function reset() {
12020
+ box.hide();
12021
+ popup.hide();
12022
+ hideCoords();
11947
12023
  }
11948
12024
 
11949
- if (gui.unprojectable) {
11950
- gui.displayLayer = {shapes: []}; // TODO: improve
11951
- } else if (layer.geometry_type) {
11952
- gui.geographic = true;
11953
- gui.displayLayer = layer;
11954
- gui.displayArcs = displayArcs;
11955
- } else {
11956
- var table = getDisplayLayerForTable(layer.data);
11957
- gui.tabular = true;
11958
- gui.displayLayer = table.layer;
11959
- gui.displayArcs = table.arcs;
12025
+ function openAddFramePopup(gui, bbox) {
12026
+ var popup = showPopupAlert('', 'Add a map frame');
12027
+ var el = popup.container();
12028
+ el.addClass('option-menu');
12029
+ var html = `<p>Enter a width in px, cm or inches to create a frame layer
12030
+ for setting the size of the map for symbol scaling in the
12031
+ 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>
12032
+ <div tabindex="0" class="btn dialog-btn">Create</div></span>`;
12033
+ el.html(html);
12034
+ var input = el.findChild('.frame-width');
12035
+ input.node().focus();
12036
+ var btn = el.findChild('.btn').on('click', function() {
12037
+ var widthStr = input.node().value.trim();
12038
+ if (parseFloat(widthStr) > 0 === false) {
12039
+ // invalid input
12040
+ input.node().value = '';
12041
+ return;
12042
+ }
12043
+ var cmd = `-rectangle + name=frame bbox='${bbox.join(',')}' width='${widthStr}'`;
12044
+ runCommand(cmd);
12045
+ popup.close();
12046
+ });
11960
12047
  }
11961
12048
 
11962
- // dynamic reprojection (arcs were already reprojected above)
11963
- if (gui.geographic && needReprojectionForDisplay(sourceCRS, displayCRS)) {
11964
- gui.dynamic_crs = displayCRS;
11965
- gui.invertPoint = internal.getProjTransform2(displayCRS, sourceCRS);
11966
- gui.projectPoint = internal.getProjTransform2(sourceCRS, displayCRS);
11967
- if (internal.layerHasPoints(layer)) {
11968
- gui.displayLayer = projectPointsForDisplay(layer, sourceCRS, displayCRS);
11969
- } else if (internal.layerHasPaths(layer)) {
11970
- emptyArcs = findEmptyArcs(displayArcs);
11971
- if (emptyArcs.length > 0) {
11972
- // Don't try to draw paths containing coordinates that failed to project
11973
- gui.displayLayer = internal.filterPathLayerByArcIds(gui.displayLayer, emptyArcs);
12049
+ return self;
12050
+ }
12051
+
12052
+ function RectangleControl(gui, hit) {
12053
+ var box = new HighlightBox(gui, {name: 'rectangle-tool', persistent: true, handles: true, classname: 'rectangles', draggable: false});
12054
+ var _on = false;
12055
+ var dragInfo;
12056
+
12057
+ gui.addMode('rectangle_tool', turnOn, turnOff);
12058
+
12059
+ gui.on('interaction_mode_change', function(e) {
12060
+ if (e.mode === 'rectangles') {
12061
+ gui.enterMode('rectangle_tool');
12062
+ } else if (_on) {
12063
+ turnOff();
12064
+ }
12065
+ });
12066
+
12067
+ hit.on('change', function(e) {
12068
+ if (!_on) return;
12069
+ // TODO: handle multiple hits (see gui-inspection-control)
12070
+ var id = e.id;
12071
+ if (e.id > -1 && e.pinned) {
12072
+ var target = hit.getHitTarget();
12073
+ var path = target.shapes[e.id][0];
12074
+ var bbox = target.gui.displayArcs.getSimpleShapeBounds(path).toArray();
12075
+ box.setDataCoords(bbox);
12076
+ dragInfo = {
12077
+ id: e.id,
12078
+ target: target,
12079
+ ids: [],
12080
+ points: []
12081
+ };
12082
+ var iter = target.gui.displayArcs.getShapeIter(path);
12083
+ while (iter.hasNext()) {
12084
+ dragInfo.points.push([iter.x, iter.y]);
12085
+ dragInfo.ids.push(iter._arc.i);
11974
12086
  }
12087
+ gui.container.findChild('.map-layers').classed('dragging', true);
12088
+
12089
+ } else if (dragInfo) {
12090
+ gui.dispatchEvent('rectangle_dragend', dragInfo); // save undo state
12091
+ gui.container.findChild('.map-layers').classed('dragging', false);
12092
+ reset();
12093
+ } else {
12094
+ box.hide();
11975
12095
  }
11976
- }
11977
12096
 
11978
- gui.bounds = getDisplayBounds(gui.displayLayer, gui.displayArcs);
11979
- layer.gui = gui;
11980
- }
12097
+ });
11981
12098
 
12099
+ box.on('handle_drag', function(e) {
12100
+ if (!_on || !dragInfo) return;
12101
+ var coords = internal.bboxToCoords(box.getDataCoords());
12102
+ setRectangleCoords(dragInfo.target, dragInfo.ids, coords);
12103
+ gui.dispatchEvent('map-needs-refresh');
12104
+ });
11982
12105
 
11983
- function getDisplayBounds(lyr, arcs) {
11984
- var bounds = internal.getLayerBounds(lyr, arcs) || new Bounds();
11985
- if (lyr.geometry_type == 'point' && arcs && bounds.hasBounds() && bounds.area() > 0 === false) {
11986
- // if a point layer has no extent (e.g. contains only a single point),
11987
- // then merge with arc bounds, to place the point in context.
11988
- bounds = bounds.mergeBounds(arcs.getBounds());
12106
+ function turnOn() {
12107
+ box.turnOn();
12108
+ _on = true;
11989
12109
  }
11990
- return bounds;
11991
- }
11992
12110
 
11993
- // Returns an array of ids of empty arcs (arcs can be set to empty if errors occur while projecting them)
11994
- function findEmptyArcs(arcs) {
11995
- var nn = arcs.getVertexData().nn;
11996
- var ids = [];
11997
- for (var i=0, n=nn.length; i<n; i++) {
11998
- if (nn[i] === 0) {
11999
- ids.push(i);
12111
+ function turnOff() {
12112
+ box.turnOff();
12113
+ if (gui.interaction.getMode() == 'rectangles') {
12114
+ // mode change was not initiated by interactive menu -- turn off interactivity
12115
+ gui.interaction.turnOff();
12000
12116
  }
12117
+ _on = false;
12118
+ reset();
12119
+ }
12120
+
12121
+ function reset() {
12122
+ box.hide();
12123
+ dragInfo = null;
12001
12124
  }
12002
- return ids;
12003
12125
  }
12004
12126
 
12005
12127
  function loadScript(url, cb) {
@@ -12052,18 +12174,14 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12052
12174
  turnOff();
12053
12175
  });
12054
12176
 
12055
- // hideBtn.on('mousedown', function() {
12056
- // if (activeStyle) {
12057
- // mapEl.css('visibility', 'hidden');
12058
- // hidden = true;
12059
- // }
12060
- // })
12061
12177
  clearBtn.on('click', function() {
12062
12178
  if (activeStyle) {
12063
- updateStyle(null);
12179
+ turnOffBasemap();
12064
12180
  updateButtons();
12181
+ closeMenu();
12065
12182
  }
12066
12183
  });
12184
+
12067
12185
  fadeBtn.on('click', function() {
12068
12186
  if (faded) {
12069
12187
  mapEl.css('opacity', 1);
@@ -12084,28 +12202,42 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12084
12202
  params.styles.forEach(function(style) {
12085
12203
  var btn = El('div').html(`<div class="basemap-style-btn"><img src="${style.icon}"></img></div><div class="basemap-style-label">${style.name}</div>`);
12086
12204
  btn.findChild('.basemap-style-btn').on('click', function() {
12087
- updateStyle(style == activeStyle ? null : style);
12205
+ if (style == activeStyle) {
12206
+ turnOffBasemap();
12207
+ } else {
12208
+ showBasemap(style);
12209
+ }
12088
12210
  updateButtons();
12211
+ closeMenu();
12089
12212
  });
12090
12213
  btn.appendTo(list);
12091
12214
  });
12092
12215
  }
12093
12216
 
12094
- function updateStyle(style) {
12095
- activeStyle = style || null;
12096
- // TODO: consider enabling this
12217
+ // close and turn off mode
12218
+ function closeMenu() {
12219
+ setTimeout(function() {
12220
+ gui.clearMode();
12221
+ }, 200);
12222
+ }
12223
+
12224
+ function turnOffBasemap() {
12225
+ activeStyle = null;
12226
+ gui.map.setDisplayCRS(null);
12227
+ refresh();
12228
+ }
12229
+
12230
+ function showBasemap(style) {
12231
+ activeStyle = style;
12232
+ // TODO: consider enabling dark basemap mode
12097
12233
  // Make sure that the selected layer style gets updated in gui-map.js
12098
12234
  // gui.state.dark_basemap = style && style.dark || false;
12099
- if (!style) {
12100
- gui.map.setDisplayCRS(null);
12101
- refresh();
12102
- } else if (map) {
12235
+ if (map) {
12103
12236
  map.setStyle(style.url);
12104
12237
  refresh();
12105
- } else {
12238
+ } else if (prepareMapView()) {
12106
12239
  initMap();
12107
12240
  }
12108
-
12109
12241
  }
12110
12242
 
12111
12243
  function updateButtons() {
@@ -12115,8 +12247,9 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12115
12247
  }
12116
12248
 
12117
12249
  function turnOn() {
12118
- var activeLyr = gui.model.getActiveLayer();
12119
- var info = getDatasetCrsInfo(activeLyr.dataset);
12250
+ // TODO: show basemap even if there is no data
12251
+ var activeLyr = gui.model.getActiveLayer(); // may be null
12252
+ var info = getDatasetCrsInfo(activeLyr?.dataset); // defaults to wgs84
12120
12253
  var dataCRS = info.crs || null;
12121
12254
  var displayCRS = gui.map.getDisplayCRS();
12122
12255
  var warning;
@@ -12159,9 +12292,9 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12159
12292
 
12160
12293
  function getLonLatBounds() {
12161
12294
  var bbox = ext.getBounds().toArray();
12162
- var tr = fromWebMercator(bbox[2], bbox[3]);
12163
- var bl = fromWebMercator(bbox[0], bbox[1]);
12164
- return bl.concat(tr);
12295
+ var bbox2 = fromWebMercator(bbox[0], bbox[1])
12296
+ .concat(fromWebMercator(bbox[2], bbox[3]));
12297
+ return bbox2;
12165
12298
  }
12166
12299
 
12167
12300
  function initMap() {
@@ -12219,6 +12352,15 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12219
12352
  return true;
12220
12353
  }
12221
12354
 
12355
+ function prepareMapView() {
12356
+ var crs = gui.map.getDisplayCRS();
12357
+ if (!crs) return false;
12358
+ if (!internal.isWebMercator(crs)) {
12359
+ gui.map.setDisplayCRS(internal.parseCrsString('webmercator'));
12360
+ }
12361
+ return true;
12362
+ }
12363
+
12222
12364
  function refresh() {
12223
12365
  var crs = gui.map.getDisplayCRS();
12224
12366
  var off = !crs || !enabled() || !map || loading || !activeStyle;
@@ -12230,9 +12372,7 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12230
12372
  return;
12231
12373
  }
12232
12374
 
12233
- if (!internal.isWebMercator(crs)) {
12234
- gui.map.setDisplayCRS(internal.parseCrsString('webmercator'));
12235
- }
12375
+ prepareMapView();
12236
12376
  var bbox = getLonLatBounds();
12237
12377
  if (!checkBounds(bbox)) {
12238
12378
  // map does not display outside these bounds
@@ -12331,8 +12471,15 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12331
12471
  };
12332
12472
 
12333
12473
  this.getDisplayCRS = function() {
12334
- if (!_activeLyr || !_activeLyr.gui.geographic) return null;
12335
- if (_activeLyr.gui.dynamic_crs) return _activeLyr.gui.dynamic_crs;
12474
+ if (!_activeLyr) {
12475
+ return _dynamicCRS || internal.parseCrsString('wgs84');
12476
+ }
12477
+ if (!_activeLyr.gui.geographic) {
12478
+ return null;
12479
+ }
12480
+ if (_activeLyr.gui.dynamic_crs) {
12481
+ return _activeLyr.gui.dynamic_crs;
12482
+ }
12336
12483
  var info = getDatasetCrsInfo(_activeLyr.gui.source.dataset);
12337
12484
  return info.crs || null;
12338
12485
  };
@@ -12365,7 +12512,7 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12365
12512
  var newCRS = utils$1.isString(crs) ? internal.parseCrsString(crs) : crs;
12366
12513
  // TODO: handle case that old and new CRS are the same
12367
12514
  _dynamicCRS = newCRS;
12368
- if (!_activeLyr) return; // stop here if no layers have been selected
12515
+ // if (!_activeLyr) return; // stop here if no layers have been selected
12369
12516
 
12370
12517
  // clear any stored FilteredArcs objects (so they will be recreated with the desired projection)
12371
12518
  clearAllDisplayArcs();
@@ -12383,6 +12530,34 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12383
12530
  updateFullBounds();
12384
12531
  };
12385
12532
 
12533
+ // Initialization just before displaying the map for the first time
12534
+ this.init = function() {
12535
+ if (_renderer) return;
12536
+ _ext.setFullBounds(calcFullBounds());
12537
+ _ext.resize();
12538
+ _renderer = new LayerRenderer(gui, el);
12539
+ gui.buttons.show();
12540
+
12541
+ if (opts.inspectorControl) {
12542
+ _hit = new HitControl(gui, _ext, _mouse),
12543
+ new InspectionControl2(gui, _hit);
12544
+ new SelectionTool(gui, _ext, _hit),
12545
+ new BoxTool(gui, _ext, _nav),
12546
+ new RectangleControl(gui, _hit),
12547
+ initInteractiveEditing(gui, _ext, _hit);
12548
+ _hit.on('change', updateOverlayLayer);
12549
+ }
12550
+
12551
+ _ext.on('change', function(e) {
12552
+ if (_basemap) _basemap.refresh(); // keep basemap synced up (if enabled)
12553
+ drawLayers(e.redraw ? '' : 'nav');
12554
+ });
12555
+
12556
+ gui.on('resize', function() {
12557
+ position.update(); // kludge to detect new map size after console toggle
12558
+ });
12559
+ };
12560
+
12386
12561
  function getGlobalStyleOptions(opts) {
12387
12562
  var mode = gui.state.interaction_mode;
12388
12563
  return Object.assign({
@@ -12398,7 +12573,7 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12398
12573
  var fullBounds;
12399
12574
  var needReset;
12400
12575
  if (!prevLyr) {
12401
- initMap(); // first call
12576
+ // initMap(); // first call
12402
12577
  }
12403
12578
 
12404
12579
  if (arcsMayHaveChanged(e.flags)) {
@@ -12422,10 +12597,14 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12422
12597
  return;
12423
12598
  }
12424
12599
 
12425
- enhanceLayerForDisplay(e.layer, e.dataset, getDisplayOptions());
12426
- _activeLyr = e.layer;
12427
- _activeLyr.gui.style = getActiveLayerStyle(_activeLyr.gui.displayLayer, getGlobalStyleOptions());
12428
- _activeLyr.active = true;
12600
+ if (e.layer) {
12601
+ enhanceLayerForDisplay(e.layer, e.dataset, getDisplayOptions());
12602
+ _activeLyr = e.layer;
12603
+ _activeLyr.gui.style = getActiveLayerStyle(_activeLyr.gui.displayLayer, getGlobalStyleOptions());
12604
+ _activeLyr.active = true;
12605
+ } else {
12606
+ _activeLyr = null;
12607
+ }
12429
12608
 
12430
12609
  if (popupCanStayOpen(e.flags)) {
12431
12610
  // data may have changed; if popup is open, it needs to be refreshed
@@ -12438,7 +12617,11 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12438
12617
  updateVisibleMapLayers();
12439
12618
  fullBounds = calcFullBounds();
12440
12619
 
12441
- if (!prevLyr || prevLyr.gui.tabular || _activeLyr.gui.tabular) {
12620
+ if (prevLyr?.gui.tabular || _activeLyr?.gui.tabular) {
12621
+ needReset = true;
12622
+ } else if (_activeLyr && internal.layerIsEmpty(_activeLyr)) {
12623
+ needReset = false;
12624
+ } else if (!prevLyr) {
12442
12625
  needReset = true;
12443
12626
  } else {
12444
12627
  needReset = mapNeedsReset(fullBounds, _ext.getFullBounds(), _ext.getBounds(), e.flags);
@@ -12448,37 +12631,12 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12448
12631
 
12449
12632
  if (needReset) {
12450
12633
  _ext.reset();
12634
+ if (_basemap) _basemap.refresh();
12451
12635
  }
12452
12636
  drawLayers();
12453
12637
  map.dispatchEvent('updated');
12454
12638
  }
12455
12639
 
12456
- // Initialization just before displaying the map for the first time
12457
- function initMap() {
12458
- _ext.resize();
12459
- _renderer = new LayerRenderer(gui, el, _ext, _mouse);
12460
- gui.buttons.show();
12461
-
12462
- if (opts.inspectorControl) {
12463
- _hit = new HitControl(gui, _ext, _mouse),
12464
- new InspectionControl2(gui, _hit);
12465
- new SelectionTool(gui, _ext, _hit),
12466
- new BoxTool(gui, _ext, _nav),
12467
- new RectangleControl(gui, _hit),
12468
- initInteractiveEditing(gui, _ext, _hit);
12469
- _hit.on('change', updateOverlayLayer);
12470
- }
12471
-
12472
- _ext.on('change', function(e) {
12473
- if (_basemap) _basemap.refresh(); // keep basemap synced up (if enabled)
12474
- if (e.reset) return; // don't need to redraw map here if extent has been reset
12475
- drawLayers('nav');
12476
- });
12477
-
12478
- gui.on('resize', function() {
12479
- position.update(); // kludge to detect new map size after console toggle
12480
- });
12481
- }
12482
12640
 
12483
12641
  function updateOverlayLayer(e) {
12484
12642
  var style = getOverlayStyle(_activeLyr.gui.displayLayer, e, getGlobalStyleOptions());
@@ -12521,7 +12679,8 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12521
12679
 
12522
12680
  if (!b.hasBounds()) {
12523
12681
  // assign bounds to empty layers, to prevent rendering errors downstream
12524
- b.setBounds(0,0,0,0);
12682
+ // b.setBounds(0,0,0,0);
12683
+ b.setBounds(projectLatLonBBox([11.28,33.43,32.26,46.04], _dynamicCRS));
12525
12684
  }
12526
12685
  return b;
12527
12686
  }
@@ -12546,7 +12705,6 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12546
12705
 
12547
12706
  // Inflate display bounding box by a tiny amount (gives extent to single-point layers and collapsed shapes)
12548
12707
  b.padBounds(1e-4, 1e-4, 1e-4, 1e-4);
12549
-
12550
12708
  return b;
12551
12709
  }
12552
12710
 
@@ -12559,7 +12717,7 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12559
12717
  }
12560
12718
 
12561
12719
  function isTableView() {
12562
- return !!_activeLyr.gui.tabular;
12720
+ return !!_activeLyr?.gui.tabular;
12563
12721
  }
12564
12722
 
12565
12723
  function findFrameLayer() {
@@ -12769,12 +12927,13 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12769
12927
  El('div')
12770
12928
  .appendTo(menu)
12771
12929
  .addClass('contextmenu-item')
12772
- .text(label)
12930
+ .html(label)
12773
12931
  .on('click', func)
12774
12932
  .show();
12775
12933
  }
12776
12934
 
12777
12935
  this.open = function(e, lyr) {
12936
+ if (!lyr || !lyr.gui.geographic) return;
12778
12937
  _open = true;
12779
12938
  _openCount++;
12780
12939
  var rspace = body.clientWidth - e.pageX;
@@ -12782,27 +12941,29 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12782
12941
  menu.empty().show();
12783
12942
  if (rspace > 150) {
12784
12943
  menu.css('left', e.pageX + xoffs + 'px');
12944
+ menu.css('right', null);
12785
12945
  } else {
12786
12946
  menu.css('right', (body.clientWidth - e.pageX + xoffs) + 'px');
12947
+ menu.css('left', null);
12787
12948
  }
12788
12949
  menu.css('top', (e.pageY - 15) + 'px');
12789
12950
 
12790
12951
  // menu contents
12791
- if (e.deleteVertex) {
12792
- addMenuItem('Delete vertex', e.deleteVertex);
12793
- }
12794
12952
  if (e.coordinates) {
12795
12953
  addCopyCoords();
12796
12954
  }
12955
+ if (e.deleteVertex) {
12956
+ addMenuItem('Delete vertex', e.deleteVertex);
12957
+ }
12797
12958
  if (e.ids?.length) {
12798
12959
  addMenuItem('Copy as GeoJSON', copyGeoJSON);
12799
12960
  }
12800
12961
 
12801
12962
  function addCopyCoords() {
12802
12963
  var bbox = internal.getLayerBounds(lyr, lyr.gui.source.dataset.arcs).toArray();
12803
- var p = internal.getRoundedCoords(e.coordinates, internal.getBoundsPrecisionForDisplay(bbox));
12804
- var coordStr = p.join(',');
12805
- addMenuItem(coordStr, function() {
12964
+ var coordStr = internal.getRoundedCoordString(e.coordinates, internal.getBoundsPrecisionForDisplay(bbox));
12965
+ var displayStr = '• &nbsp;' + coordStr.replace(/-/g, '–').replace(',', ', ');
12966
+ addMenuItem(displayStr, function() {
12806
12967
  saveFileContentToClipboard(coordStr);
12807
12968
  });
12808
12969
  }
@@ -12817,7 +12978,7 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12817
12978
  var layer = mapshaper.cmd.filterFeatures(lyr, dataset.arcs, opts);
12818
12979
  // the drawing tool can send open paths with 'polygon' geometry type,
12819
12980
  // should be changed to 'polyline'
12820
- if (layerHasOpenPaths(layer, dataset.arcs)) {
12981
+ if (layer.geometry_type == 'polygon' && layerHasOpenPaths(layer, dataset.arcs)) {
12821
12982
  layer.geometry_type = 'polyline';
12822
12983
  }
12823
12984
  var features = internal.exportLayerAsGeoJSON(layer, dataset, {rfc7946: true, prettify: true}, true, 'string');
@@ -12868,6 +13029,7 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12868
13029
  var clearMsg;
12869
13030
 
12870
13031
  initModeRules(gui);
13032
+ gui.map.init();
12871
13033
 
12872
13034
  gui.showProgressMessage = function(msg) {
12873
13035
  if (!gui.progressMessage) {
@@ -12980,9 +13142,10 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12980
13142
  importOpts = getImportOpts(manifest),
12981
13143
  gui = new GuiInstance('body');
12982
13144
 
12983
- if (manifest.blurb) {
12984
- El('#splash-screen-blurb').text(manifest.blurb);
12985
- }
13145
+ // TODO: re-enable the "blurb"
13146
+ // if (manifest.blurb) {
13147
+ // El('#splash-screen-blurb').text(manifest.blurb);
13148
+ // }
12986
13149
 
12987
13150
  new AlertControl(gui);
12988
13151
  new RepairControl(gui);
@@ -13013,17 +13176,19 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
13013
13176
  });
13014
13177
 
13015
13178
  // Initial display configuration
13016
- gui.model.on('select', function() {
13179
+ gui.on('mode', function(e) {
13017
13180
  if (dataLoaded) return;
13018
13181
  dataLoaded = true;
13182
+ gui.buttons.show();
13019
13183
  El('#mode-buttons').show();
13184
+ El('#splash-buttons').hide();
13185
+ El('body').addClass('map-view');
13020
13186
  if (importOpts.display_all) {
13021
13187
  gui.model.getLayers().forEach(function(o) {
13022
13188
  gui.map.setLayerPinning(o, true);
13023
13189
  });
13024
13190
  }
13025
13191
  gui.console.runInitialCommands(getInitialConsoleCommands());
13026
-
13027
13192
  });
13028
13193
  };
13029
13194