mapshaper 0.6.75 → 0.6.77

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.
@@ -760,10 +760,7 @@
760
760
  //
761
761
  //El.prototype.__domevents = utils.arrayToIndex("click,mousedown,mousemove,mouseup".split(','));
762
762
  El.prototype.__on = El.prototype.on;
763
- El.prototype.on = function(type, func, ctx) {
764
- if (ctx) {
765
- error$1("[El#on()] Third argument no longer supported.");
766
- }
763
+ El.prototype.on = function(type, func) {
767
764
  if (this.constructor == El) {
768
765
  this.el.addEventListener(type, func);
769
766
  } else {
@@ -1369,10 +1366,12 @@
1369
1366
  init();
1370
1367
 
1371
1368
  async function init() {
1372
- var enabled = await isStorageEnabled();
1373
- if (!enabled) return;
1374
-
1375
1369
  btn = gui.buttons.addButton('#ribbon-icon').addClass('menu-btn save-btn');
1370
+ var enabled = await isStorageEnabled();
1371
+ if (!enabled) {
1372
+ btn.remove();
1373
+ return;
1374
+ }
1376
1375
  menu = El('div').addClass('nav-sub-menu save-menu').appendTo(btn.node());
1377
1376
  await initialCleanup();
1378
1377
 
@@ -1444,14 +1443,6 @@
1444
1443
  renderMenu();
1445
1444
  }).text('remove');
1446
1445
  });
1447
-
1448
- // if (snapshots.length >= 1) {
1449
- // addMenuLink({
1450
- // slug: 'clear',
1451
- // label: 'remove all',
1452
- // action: clearData
1453
- // });
1454
- // }
1455
1446
  }
1456
1447
 
1457
1448
  function addMenuLink(item) {
@@ -1484,6 +1475,7 @@
1484
1475
 
1485
1476
  async function saveSnapshot(gui) {
1486
1477
  var obj = await captureSnapshot(gui);
1478
+ if (!obj) return;
1487
1479
  // storing an unpacked object is usually a bit faster (~20%)
1488
1480
  // note: we don't know the size of unpacked snapshot objects
1489
1481
  // obj = internal.pack(obj);
@@ -1563,12 +1555,13 @@
1563
1555
  }
1564
1556
 
1565
1557
  async function captureSnapshot(gui) {
1566
- var datasets = gui.model.getDatasets();
1567
- 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;
1568
1561
  // compact: true applies compression to vector coordinates, for ~30% reduction
1569
1562
  // in file size in a typical polygon or polyline file, but longer processing time
1570
1563
  var opts = {compact: false};
1571
- lyr.active = true;
1564
+ var datasets = gui.model.getDatasets();
1572
1565
  // console.time('msx');
1573
1566
  var obj = await internal.exportDatasetsToPack(datasets, opts);
1574
1567
  // console.timeEnd('msx')
@@ -1761,7 +1754,7 @@
1761
1754
  }],
1762
1755
  info: {}
1763
1756
  };
1764
- if (type == 'polygon' || type == 'point') {
1757
+ if (type == 'polygon' || type == 'polyline') {
1765
1758
  dataset.arcs = new internal.ArcCollection();
1766
1759
  }
1767
1760
  if (crsInfo) {
@@ -1873,7 +1866,6 @@
1873
1866
  var initialImport = true;
1874
1867
  var importCount = 0;
1875
1868
  var importTotal = 0;
1876
- var overQuickView = false;
1877
1869
  var useQuickView = false;
1878
1870
  var queuedFiles = [];
1879
1871
  var manifestFiles = opts.files || [];
@@ -1883,50 +1875,48 @@
1883
1875
  catalog = new CatalogControl(gui, opts.catalog, downloadFiles);
1884
1876
  }
1885
1877
 
1886
- new SimpleButton('#import-buttons .submit-btn').on('click', importQueuedFiles);
1887
- new SimpleButton('#import-buttons .cancel-btn').on('click', gui.clearMode);
1888
- new DropControl(gui, 'body', receiveFiles);
1889
- new FileChooser('#file-selection-btn', receiveFiles);
1890
- 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);
1891
1882
  new FileChooser('#add-file-btn', receiveFiles);
1892
1883
  new SimpleButton('#add-empty-btn').on('click', function() {
1893
1884
  gui.clearMode(); // close import dialog
1894
1885
  openAddLayerPopup(gui);
1895
1886
  });
1896
- initDropArea('#import-quick-drop', true);
1897
- initDropArea('#import-drop');
1887
+ // initDropArea('#import-quick-drop', true);
1888
+ // initDropArea('#import-drop');
1898
1889
  gui.keyboard.onMenuSubmit(El('#import-options'), importQueuedFiles);
1899
1890
 
1900
1891
  gui.addMode('import', turnOn, turnOff);
1901
1892
  gui.enterMode('import');
1902
1893
 
1903
- gui.on('mode', function(e) {
1904
- // re-open import opts if leaving alert or console modes and nothing has been imported yet
1905
- if (!e.name && model.isEmpty()) {
1906
- gui.enterMode('import');
1907
- }
1908
- });
1909
-
1910
- function initDropArea(el, isQuick) {
1911
- var area = El(el)
1912
- .on('dragleave', onout)
1913
- .on('dragover', function() {overQuickView = !!isQuick; onover();})
1914
- .on('mouseover', onover)
1915
- .on('mouseout', onout);
1916
-
1917
- function onover() {
1918
- area.addClass('dragover');
1894
+ function turnOn() {
1895
+ if (manifestFiles.length > 0) {
1896
+ downloadFiles(manifestFiles, true);
1897
+ manifestFiles = [];
1898
+ } else if (model.isEmpty()) {
1899
+ showImportMenu();
1919
1900
  }
1901
+ }
1920
1902
 
1921
- function onout() {
1922
- overQuickView = false;
1923
- 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;
1924
1910
  }
1911
+ gui.clearProgressMessage();
1912
+ initialImport = false; // unset 'quick view' mode, if on
1913
+ clearQueuedFiles();
1914
+ hideImportMenu();
1925
1915
  }
1926
1916
 
1927
1917
  async function importQueuedFiles() {
1928
- gui.container.removeClass('queued-files');
1929
- gui.container.removeClass('splash-screen');
1918
+ // gui.container.removeClass('queued-files');
1919
+ hideImportMenu();
1930
1920
  var files = queuedFiles;
1931
1921
  try {
1932
1922
  if (files.length > 0) {
@@ -1943,27 +1933,7 @@
1943
1933
  }
1944
1934
  }
1945
1935
 
1946
- function turnOn() {
1947
- if (manifestFiles.length > 0) {
1948
- downloadFiles(manifestFiles, true);
1949
- manifestFiles = [];
1950
- } else if (model.isEmpty()) {
1951
- gui.container.addClass('splash-screen');
1952
- }
1953
- }
1954
1936
 
1955
- function turnOff() {
1956
- var target;
1957
- if (catalog) catalog.reset(); // re-enable clickable catalog
1958
- if (importCount > 0) {
1959
- onImportComplete();
1960
- importTotal += importCount;
1961
- importCount = 0;
1962
- }
1963
- gui.clearProgressMessage();
1964
- initialImport = false; // unset 'quick view' mode, if on
1965
- clearQueuedFiles();
1966
- }
1967
1937
 
1968
1938
  function onImportComplete() {
1969
1939
  // display last layer of last imported dataset
@@ -1996,6 +1966,7 @@
1996
1966
  }, []);
1997
1967
  }
1998
1968
 
1969
+
1999
1970
  function showQueuedFiles() {
2000
1971
  var list = gui.container.findChild('.dropped-file-list').empty();
2001
1972
  queuedFiles.forEach(function(f) {
@@ -2015,13 +1986,20 @@
2015
1986
  }
2016
1987
  });
2017
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);
2018
1995
  }
2019
1996
 
2020
- async function receiveFiles(files) {
1997
+ async function receiveFiles(files, quickView) {
2021
1998
  var names = getFileNames(files);
2022
1999
  var expanded = [];
2023
2000
  if (files.length === 0) return;
2024
- useQuickView = importTotal === 0 && (opts.quick_view || overQuickView);
2001
+ useQuickView = importTotal === 0 && (opts.quick_view ||
2002
+ quickView);
2025
2003
  try {
2026
2004
  expanded = await expandFiles(files);
2027
2005
  } catch(e) {
@@ -2039,12 +2017,23 @@
2039
2017
  if (useQuickView) {
2040
2018
  await importQueuedFiles();
2041
2019
  } else {
2042
- gui.container.addClass('queued-files');
2043
- El('#path-import-options').classed('hidden', !filesMayContainPaths(queuedFiles));
2044
- showQueuedFiles();
2020
+ showImportMenu();
2045
2021
  }
2046
2022
  }
2047
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
+
2048
2037
  function getFileNames(files) {
2049
2038
  return Array.from(files).map(function(f) {return f.name;});
2050
2039
  }
@@ -2540,7 +2529,11 @@
2540
2529
  // prevent GUI message popup on error
2541
2530
  internal.setLoggingForCLI();
2542
2531
  try {
2543
- crs = internal.getDatasetCRS(dataset);
2532
+ if (!dataset || internal.datasetIsEmpty(dataset)) {
2533
+ crs = internal.parseCrsString('wgs84');
2534
+ } else {
2535
+ crs = internal.getDatasetCRS(dataset);
2536
+ }
2544
2537
  } catch(e) {
2545
2538
  err = e.message;
2546
2539
  }
@@ -2554,7 +2547,7 @@
2554
2547
  // Convert a point from display CRS coordinates to data coordinates.
2555
2548
  // These are only different when using dynamic reprojection (basemap view).
2556
2549
  function translateDisplayPoint(lyr, p) {
2557
- return isProjectedLayer(lyr) ? lyr.invertPoint(p[0], p[1]) : p;
2550
+ return isProjectedLayer(lyr) ? lyr.gui.invertPoint(p[0], p[1]) : p;
2558
2551
  }
2559
2552
 
2560
2553
  // bbox: display coords
@@ -2571,7 +2564,7 @@
2571
2564
 
2572
2565
  function isProjectedLayer(lyr) {
2573
2566
  // TODO: could do some validation on the layer's contents
2574
- return !!(lyr.source && lyr.invertPoint);
2567
+ return !!lyr.gui.invertPoint;
2575
2568
  }
2576
2569
 
2577
2570
  var darkStroke = "#334",
@@ -2932,160 +2925,619 @@
2932
2925
  return internal.findPropertiesBySymbolGeom(fields, lyr.geometry_type);
2933
2926
  }
2934
2927
 
2935
- function flattenArcs(lyr) {
2936
- lyr.source.dataset.arcs.flatten();
2937
- if (isProjectedLayer(lyr)) {
2938
- lyr.arcs.flatten();
2939
- }
2940
- }
2928
+ // Create low-detail versions of large arc collections for faster rendering
2929
+ // at zoomed-out scales.
2930
+ function enhanceArcCollectionForDisplay(unfilteredArcs) {
2931
+ var size = unfilteredArcs.getPointCount(),
2932
+ filteredArcs, filteredSegLen;
2941
2933
 
2942
- function setZ(lyr, z) {
2943
- lyr.source.dataset.arcs.setRetainedInterval(z);
2944
- if (isProjectedLayer(lyr)) {
2945
- lyr.arcs.setRetainedInterval(z);
2934
+ // Only generate low-detail arcs for larger datasets
2935
+ if (size > 5e5) {
2936
+ update();
2946
2937
  }
2947
- }
2948
2938
 
2949
- function updateZ(lyr) {
2950
- if (isProjectedLayer(lyr) && !lyr.source.dataset.arcs.isFlat()) {
2951
- lyr.arcs.setThresholds(lyr.source.dataset.arcs.getVertexData().zz);
2939
+ function update() {
2940
+ if (unfilteredArcs.getVertexData().zz) {
2941
+ // Use precalculated simplification data for vertex filtering, if available
2942
+ filteredArcs = initFilteredArcs(unfilteredArcs);
2943
+ filteredSegLen = internal.getAvgSegment(filteredArcs);
2944
+ } else {
2945
+ // Use fast simplification as a fallback
2946
+ filteredSegLen = internal.getAvgSegment(unfilteredArcs) * 4;
2947
+ filteredArcs = internal.simplifyArcsFast(unfilteredArcs, filteredSegLen);
2948
+ }
2952
2949
  }
2953
- }
2954
2950
 
2955
- function appendNewDataRecord(layer) {
2956
- if (!layer.data) return null;
2957
- var fields = layer.data.getFields();
2958
- var d = getEmptyDataRecord(layer.data);
2959
- // TODO: handle SVG symbol layer
2960
- if (internal.layerHasLabels(layer)) {
2961
- d['label-text'] = 'TBD'; // without text, new labels will be invisible
2962
- } else if (layer.geometry_type == 'point' && fields.includes('r')) {
2963
- d.r = 3; // show a black circle if layer is styled
2964
- }
2965
- if (layer.geometry_type == 'polyline' || layer.geometry_type == 'polygon') {
2966
- if (fields.includes('stroke')) d.stroke = 'black';
2967
- if (fields.includes('stroke-width')) d['stroke-width'] = 1;
2951
+ function initFilteredArcs(arcs) {
2952
+ var filterPct = 0.08;
2953
+ var nth = Math.ceil(arcs.getPointCount() / 5e5);
2954
+ var currInterval = arcs.getRetainedInterval();
2955
+ var filterZ = arcs.getThresholdByPct(filterPct, nth);
2956
+ var filteredArcs = arcs.setRetainedInterval(filterZ).getFilteredCopy();
2957
+ arcs.setRetainedInterval(currInterval); // reset current simplification
2958
+ return filteredArcs;
2968
2959
  }
2969
- if (layer.geometry_type == 'polygon') {
2970
- if (fields.includes('fill')) {
2971
- d.fill = 'rgba(0,0,0,0.10)'; // 'rgba(249,120,249,0.20)';
2960
+
2961
+ // TODO: better job of detecting arc change... e.g. revision number
2962
+ unfilteredArcs.getScaledArcs = function(ext) {
2963
+ // check for changes in the number of arcs (probably due to editing)
2964
+ if (filteredArcs && filteredArcs.size() != unfilteredArcs.size()) {
2965
+ // arc count has changed... probably due to editing
2966
+ update();
2967
+ if (filteredArcs.size() != unfilteredArcs.size()) {
2968
+ throw Error('Internal error');
2969
+ }
2972
2970
  }
2973
- }
2974
- // TODO: better styling
2975
- layer.data.getRecords().push(d);
2976
- return d;
2971
+ if (filteredArcs) {
2972
+ // match simplification of unfiltered arcs
2973
+ filteredArcs.setRetainedInterval(unfilteredArcs.getRetainedInterval());
2974
+ }
2975
+ // switch to filtered version of arcs at small scales
2976
+ var unitsPerPixel = 1/ext.getTransform().mx,
2977
+ useFiltering = filteredArcs && unitsPerPixel > filteredSegLen * 1.5;
2978
+ return useFiltering ? filteredArcs : unfilteredArcs;
2979
+ };
2977
2980
  }
2978
2981
 
2979
- function getEmptyDataRecord(table) {
2980
- return table.getFields().reduce(function(memo, name) {
2981
- memo[name] = null;
2982
- return memo;
2983
- }, {});
2984
- }
2982
+ function getDisplayLayerForTable(tableArg) {
2983
+ var table = tableArg || new internal.DataTable(0),
2984
+ n = table.size(),
2985
+ cellWidth = 12,
2986
+ cellHeight = 5,
2987
+ gutter = 6,
2988
+ arcs = [],
2989
+ shapes = [],
2990
+ aspectRatio = 1.1,
2991
+ x, y, col, row, blockSize;
2985
2992
 
2986
- function deleteLastPath(lyr) {
2987
- var arcId = lyr.arcs.size() - 1;
2988
- if (lyr.layer.data) {
2989
- lyr.layer.data.getRecords().pop();
2990
- }
2991
- var shp = lyr.layer.shapes.pop();
2992
- internal.deleteLastArc(lyr.arcs);
2993
- if (isProjectedLayer(lyr)) {
2994
- internal.deleteLastArc(lyr.source.dataset.arcs);
2993
+ if (n > 10000) {
2994
+ arcs = null;
2995
+ gutter = 0;
2996
+ cellWidth = 4;
2997
+ cellHeight = 4;
2998
+ aspectRatio = 1.45;
2999
+ } else if (n > 5000) {
3000
+ cellWidth = 5;
3001
+ gutter = 3;
3002
+ aspectRatio = 1.45;
3003
+ } else if (n > 1000) {
3004
+ gutter = 3;
3005
+ cellWidth = 8;
3006
+ aspectRatio = 1.3;
2995
3007
  }
2996
- }
2997
3008
 
2998
- // p1, p2: two points in source data CRS coords.
2999
- function appendNewPath(lyr, p1, p2) {
3000
- var arcId = lyr.arcs.size();
3001
- internal.appendEmptyArc(lyr.arcs);
3002
- lyr.layer.shapes.push([[arcId]]);
3003
- if (isProjectedLayer(lyr)) {
3004
- // lyr.source.layer.shapes.push([[arcId]]);
3005
- internal.appendEmptyArc(lyr.source.dataset.arcs);
3009
+ if (n < 25) {
3010
+ blockSize = n;
3011
+ } else {
3012
+ blockSize = Math.sqrt(n * (cellWidth + gutter) / cellHeight / aspectRatio) | 0;
3006
3013
  }
3007
- appendVertex$1(lyr, p1);
3008
- appendVertex$1(lyr, p2);
3009
- appendNewDataRecord(lyr.layer);
3010
- }
3011
3014
 
3012
- // p: point in source data CRS coords.
3013
- function insertVertex$1(lyr, id, p) {
3014
- internal.insertVertex(lyr.source.dataset.arcs, id, p);
3015
- if (isProjectedLayer(lyr)) {
3016
- internal.insertVertex(lyr.arcs, id, lyr.projectPoint(p[0], p[1]));
3015
+ for (var i=0; i<n; i++) {
3016
+ row = i % blockSize;
3017
+ col = Math.floor(i / blockSize);
3018
+ x = col * (cellWidth + gutter);
3019
+ y = cellHeight * (blockSize - row);
3020
+ if (arcs) {
3021
+ arcs.push(getArc(x, y, cellWidth, cellHeight));
3022
+ shapes.push([[i]]);
3023
+ } else {
3024
+ shapes.push([[x, y]]);
3025
+ }
3017
3026
  }
3018
- }
3019
3027
 
3020
- function appendVertex$1(lyr, p) {
3021
- var n = lyr.source.dataset.arcs.getPointCount();
3022
- insertVertex$1(lyr, n, p);
3023
- }
3024
-
3025
- // TODO: make sure we're not also removing an entire arc
3026
- function deleteLastVertex(lyr) {
3027
- deleteVertex$1(lyr, lyr.arcs.getPointCount() - 1);
3028
- }
3029
-
3030
- function deleteVertex$1(lyr, id) {
3031
- internal.deleteVertex(lyr.arcs, id);
3032
- if (isProjectedLayer(lyr)) {
3033
- internal.deleteVertex(lyr.source.dataset.arcs, id);
3028
+ function getArc(x, y, w, h) {
3029
+ return [[x, y], [x + w, y], [x + w, y - h], [x, y - h], [x, y]];
3034
3030
  }
3035
- }
3036
-
3037
- function getLastArcCoords(target) {
3038
- var arcId = target.source.dataset.arcs.size() - 1;
3039
- return internal.getUnfilteredArcCoords(arcId, target.source.dataset.arcs);
3040
- }
3041
3031
 
3042
- function getLastVertexCoords(target) {
3043
- var arcs = target.source.dataset.arcs;
3044
- return internal.getVertexCoords(arcs.getPointCount() - 1, arcs);
3032
+ return {
3033
+ layer: {
3034
+ geometry_type: arcs ? 'polygon' : 'point',
3035
+ shapes: shapes,
3036
+ data: table
3037
+ },
3038
+ arcs: arcs ? new internal.ArcCollection(arcs) : null
3039
+ };
3045
3040
  }
3046
3041
 
3047
- function getLastArcLength(target) {
3048
- var arcId = target.source.dataset.arcs.size() - 1;
3049
- return internal.getUnfilteredArcLength(arcId, target.source.dataset.arcs);
3050
- }
3042
+ var R = 6378137;
3043
+ var D2R = Math.PI / 180;
3044
+ var R2D = 180 / Math.PI;
3051
3045
 
3052
- function getPointCoords(lyr, fid) {
3053
- return internal.cloneShape(lyr.source.layer.shapes[fid]);
3054
- }
3046
+ // Assumes projections are available
3055
3047
 
3056
- function getVertexCoords(lyr, id) {
3057
- return lyr.source.dataset.arcs.getVertex2(id);
3048
+ function needReprojectionForDisplay(sourceCRS, displayCRS) {
3049
+ if (!sourceCRS || !displayCRS) {
3050
+ return false;
3051
+ }
3052
+ if (internal.crsAreEqual(sourceCRS, displayCRS)) {
3053
+ return false;
3054
+ }
3055
+ return true;
3058
3056
  }
3059
3057
 
3060
- // set data coords (not display coords) of one or more vertices.
3061
- function setVertexCoords(lyr, ids, dataPoint) {
3062
- internal.snapVerticesToPoint(ids, dataPoint, lyr.source.dataset.arcs, true);
3063
- if (isProjectedLayer(lyr)) {
3064
- var p = lyr.projectPoint(dataPoint[0], dataPoint[1]);
3065
- internal.snapVerticesToPoint(ids, p, lyr.arcs, true);
3058
+ // bbox in wgs84 coords
3059
+ // dest: CRS, which may also be wgs84
3060
+ function projectLatLonBBox(bbox, dest) {
3061
+ if (!dest || internal.isWGS84(dest)) {
3062
+ return bbox.concat();
3066
3063
  }
3064
+ var proj = internal.getProjTransform2(internal.parseCrsString('wgs84'), dest);
3065
+ var bbox2 = proj(bbox[0], bbox[1]).concat(proj(bbox[2], bbox[3]));
3066
+ return bbox2;
3067
3067
  }
3068
3068
 
3069
- // coords: [x, y] point in data CRS (not display CRS)
3070
- function setPointCoords(lyr, fid, coords) {
3071
- lyr.source.layer.shapes[fid] = coords;
3072
- if (isProjectedLayer(lyr)) {
3073
- lyr.layer.shapes[fid] = projectPointCoords(coords, lyr.projectPoint);
3069
+ function projectArcsForDisplay(arcs, src, dest) {
3070
+ var copy = arcs.getCopy(); // need to flatten first?
3071
+ var destIsWebMerc = internal.isWebMercator(dest);
3072
+ if (destIsWebMerc && internal.isWebMercator(src)) {
3073
+ return copy;
3074
3074
  }
3075
- }
3076
3075
 
3077
- function updateVertexCoords(lyr, ids) {
3078
- if (!isProjectedLayer(lyr)) return;
3079
- var p = lyr.arcs.getVertex2(ids[0]);
3080
- internal.snapVerticesToPoint(ids, lyr.invertPoint(p[0], p[1]), lyr.source.dataset.arcs, true);
3081
- }
3076
+ var wgs84 = internal.parseCrsString('wgs84');
3077
+ var toWGS84 = internal.getProjTransform2(src, wgs84);
3078
+ var fromWGS84 = internal.getProjTransform2(wgs84, dest);
3082
3079
 
3083
- function setRectangleCoords(lyr, ids, coords) {
3084
- ids.forEach(function(id, i) {
3085
- var p = coords[i];
3086
- internal.snapVerticesToPoint([id], p, lyr.source.dataset.arcs, true);
3080
+ try {
3081
+ // first try projectArcs() -- it's fast and preserves arc ids
3082
+ // (so vertex editing doesn't break)
3083
+ if (!internal.isWGS84(src)) {
3084
+ // use wgs84 as a pivot CRS, so we can handle polar coordinates
3085
+ // that can't be projected to Mercator
3086
+ internal.projectArcs(copy, toWGS84);
3087
+ }
3088
+ if (destIsWebMerc) {
3089
+ // handle polar points by clamping them to they will project
3090
+ // (downside: may cause unexpected behavior when editing vertices interactively)
3091
+ clampY(copy);
3092
+ }
3093
+ internal.projectArcs(copy, fromWGS84);
3094
+ } catch(e) {
3095
+ console.error(e);
3096
+ // use the more robust projectArcs2 if projectArcs throws an error
3097
+ // downside: projectArcs2 discards Z values and changes arc indexing,
3098
+ // which will break vertex editing.
3099
+ var reproject = internal.getProjTransform2(src, dest);
3100
+ copy = arcs.getCopy();
3101
+ internal.projectArcs2(copy, reproject);
3102
+ }
3103
+ return copy;
3104
+ }
3105
+
3106
+ function clampY(arcs) {
3107
+ var max = 89.9,
3108
+ min = -89.9,
3109
+ bbox = arcs.getBounds().toArray();
3110
+ if (bbox[1] >= min && bbox[3] <= max) return;
3111
+ arcs.transformPoints(function(x, y) {
3112
+ if (y > max) return [x, max];
3113
+ if (y < min) return [x, min];
3114
+ });
3115
+ }
3116
+
3117
+ function projectPointsForDisplay(lyr, src, dest) {
3118
+ var copy = utils$1.extend({}, lyr);
3119
+ var proj = internal.getProjTransform2(src, dest);
3120
+ copy.shapes = internal.cloneShapes(lyr.shapes);
3121
+ internal.projectPointLayer(copy, proj);
3122
+ return copy;
3123
+ }
3124
+
3125
+
3126
+ function toWebMercator(lng, lat) {
3127
+ var k = Math.cos(lat * D2R);
3128
+ var x = R * lng * D2R;
3129
+ var y = R * Math.log(Math.tan(Math.PI * 0.25 + lat * D2R * 0.5));
3130
+ return [x, y];
3131
+ }
3132
+
3133
+ function fromWebMercator(x, y) {
3134
+ var lon = x / R * R2D;
3135
+ var lat = R2D * (Math.PI * 0.5 - 2 * Math.atan(Math.exp(-y / R)));
3136
+ return [lon, lat];
3137
+ }
3138
+
3139
+ function scaleToZoom(metersPerPix) {
3140
+ return Math.log(40075017 / 512 / metersPerPix) / Math.log(2);
3141
+ }
3142
+
3143
+ function getMapboxBounds() {
3144
+ var ymax = toWebMercator(0, 84)[1];
3145
+ var ymin = toWebMercator(0, -84)[1];
3146
+ return [-Infinity, ymin, Infinity, ymax];
3147
+ }
3148
+
3149
+
3150
+ // Update map extent and trigger redraw, after a new display CRS has been applied
3151
+ function projectMapExtent(ext, src, dest, newBounds) {
3152
+ var oldBounds = ext.getBounds();
3153
+ var oldScale = ext.scale();
3154
+ var newCP, proj, strictBounds;
3155
+
3156
+ if (dest && internal.isWebMercator(dest)) {
3157
+ // clampToMapboxBounds(newBounds);
3158
+ strictBounds = getMapboxBounds();
3159
+ }
3160
+
3161
+ // if source or destination CRS is unknown, show full extent
3162
+ // if map is at full extent, show full extent
3163
+ // TODO: handle case that scale is 1 and map is panned away from center
3164
+ if (ext.scale() == 1 || !dest) {
3165
+ ext.setFullBounds(newBounds, strictBounds);
3166
+ ext.reset();
3167
+ } else {
3168
+ // if map is zoomed, stay centered on the same geographic location, at the same relative scale
3169
+ proj = internal.getProjTransform2(src, dest);
3170
+ newCP = proj(oldBounds.centerX(), oldBounds.centerY());
3171
+ ext.setFullBounds(newBounds, strictBounds);
3172
+ if (!newCP) {
3173
+ // projection of center point failed; use center of bounds
3174
+ // (also consider just resetting the view using ext.home())
3175
+ newCP = [newBounds.centerX(), newBounds.centerY()];
3176
+ }
3177
+ ext.recenter(newCP[0], newCP[1], oldScale);
3178
+ }
3179
+ // trigger full redraw, in case some SVG symbols are unprojectable
3180
+ ext.dispatchEvent('change', {redraw: true});
3181
+ }
3182
+
3183
+ // Called from console; for testing dynamic crs
3184
+ function setDisplayProjection(gui, cmd) {
3185
+ var arg = cmd.replace(/^projd[ ]*/, '');
3186
+ if (arg) {
3187
+ gui.map.setDisplayCRS(internal.parseCrsString(arg));
3188
+ } else {
3189
+ gui.map.setDisplayCRS(null);
3190
+ }
3191
+ }
3192
+
3193
+ function filterLayerByIds(lyr, ids) {
3194
+ var shapes;
3195
+ if (lyr.shapes) {
3196
+ shapes = ids.map(function(id) {
3197
+ return lyr.shapes[id];
3198
+ });
3199
+ return utils$1.defaults({shapes: shapes, data: null}, lyr);
3200
+ }
3201
+ return lyr;
3202
+ }
3203
+
3204
+ function formatLayerNameForDisplay(name) {
3205
+ return name || '[unnamed]';
3206
+ }
3207
+
3208
+ function cleanLayerName(raw) {
3209
+ return raw.replace(/[\n\t/\\]/g, '')
3210
+ .replace(/^[.\s]+/, '').replace(/[.\s]+$/, '');
3211
+ }
3212
+
3213
+ function updateLayerStackOrder(layers) {
3214
+ // 1. assign ascending ids to unassigned layers above the range of other layers
3215
+ layers.forEach(function(o, i) {
3216
+ if (!o.layer.menu_order) o.layer.menu_order = 1e6 + i;
3217
+ });
3218
+ // 2. sort in ascending order
3219
+ layers.sort(function(a, b) {
3220
+ return a.layer.menu_order - b.layer.menu_order;
3221
+ });
3222
+ // 3. assign consecutve ids
3223
+ layers.forEach(function(o, i) {
3224
+ o.layer.menu_order = i + 1;
3225
+ });
3226
+ return layers;
3227
+ }
3228
+
3229
+ function sortLayersForMenuDisplay(layers) {
3230
+ layers = updateLayerStackOrder(layers);
3231
+ return layers.reverse();
3232
+ }
3233
+
3234
+ // lyr: a map layer with gui property
3235
+ // displayCRS: CRS to use for display, or null (which clears any current display CRS)
3236
+ function projectLayerForDisplay(lyr, displayCRS) {
3237
+ var crsInfo = getDatasetCrsInfo(lyr.gui.source.dataset);
3238
+ var sourceCRS = crsInfo.crs || null; // let enhanceLayerForDisplay() handle null case
3239
+ if (!lyr.gui.geographic) {
3240
+ return;
3241
+ }
3242
+ if (lyr.gui.dynamic_crs && internal.crsAreEqual(sourceCRS, lyr.gui.dynamic_crs)) {
3243
+ return;
3244
+ }
3245
+ var gui = lyr.gui;
3246
+ enhanceLayerForDisplay(lyr, lyr.gui.source.dataset, {crs: displayCRS});
3247
+ utils$1.defaults(lyr.gui, gui); // re-apply any properties that were lost (e.g. svg_id)
3248
+ if (lyr.gui.style?.ids) {
3249
+ // re-apply layer filter
3250
+ lyr.gui.displayLayer = filterLayerByIds(lyr.gui.displayLayer, lyr.gui.style.ids);
3251
+ }
3252
+ }
3253
+
3254
+
3255
+ // Supplement a layer with information needed for rendering
3256
+ function enhanceLayerForDisplay(layer, dataset, opts) {
3257
+ var gui = {
3258
+ empty: internal.getFeatureCount(layer) === 0,
3259
+ geographic: false,
3260
+ displayArcs: null,
3261
+ displayLayer: null,
3262
+ source: {dataset},
3263
+ bounds: null,
3264
+ style: null,
3265
+ dynamic_crs: null,
3266
+ invertPoint: null,
3267
+ projectPoint: null
3268
+ };
3269
+
3270
+ var displayCRS = opts.crs || null;
3271
+ // display arcs may have been generated when another layer in the dataset
3272
+ // was converted for display... re-use if available
3273
+ var displayArcs = dataset.gui?.displayArcs;
3274
+ var unprojectable = false;
3275
+ var sourceCRS;
3276
+ var emptyArcs;
3277
+
3278
+ if (displayCRS && layer.geometry_type) {
3279
+ var crsInfo = getDatasetCrsInfo(dataset);
3280
+ if (crsInfo.error) {
3281
+ // unprojectable dataset -- return empty layer
3282
+ gui.unprojectable = true;
3283
+ } else {
3284
+ sourceCRS = crsInfo.crs;
3285
+ }
3286
+ }
3287
+
3288
+ // Assume that dataset.displayArcs is in the display CRS
3289
+ // (it must be deleted upstream if reprojection is needed)
3290
+ // if (!obj.empty && dataset.arcs && !displayArcs) {
3291
+ if (dataset.arcs && !displayArcs && !gui.unprojectable) {
3292
+ // project arcs, if needed
3293
+ if (needReprojectionForDisplay(sourceCRS, displayCRS)) {
3294
+ displayArcs = projectArcsForDisplay(dataset.arcs, sourceCRS, displayCRS);
3295
+ } else {
3296
+ // Use original arcs for display if there is no dynamic reprojection
3297
+ displayArcs = dataset.arcs;
3298
+ }
3299
+
3300
+ enhanceArcCollectionForDisplay(displayArcs);
3301
+ dataset.gui = {displayArcs}; // stash these in the dataset for other layers to use
3302
+ }
3303
+
3304
+ if (internal.layerHasFurniture(layer)) {
3305
+ // TODO: consider how to render furniture in GUI
3306
+ // treating furniture layers (other than frame) as tabular for now,
3307
+ // so there is something to show if they are selected
3308
+ }
3309
+
3310
+ if (gui.unprojectable) {
3311
+ gui.displayLayer = {shapes: []}; // TODO: improve
3312
+ } else if (layer.geometry_type) {
3313
+ gui.geographic = true;
3314
+ gui.displayLayer = layer;
3315
+ gui.displayArcs = displayArcs;
3316
+ } else {
3317
+ var table = getDisplayLayerForTable(layer.data);
3318
+ gui.tabular = true;
3319
+ gui.displayLayer = table.layer;
3320
+ gui.displayArcs = table.arcs;
3321
+ }
3322
+
3323
+ // dynamic reprojection (arcs were already reprojected above)
3324
+ if (gui.geographic && needReprojectionForDisplay(sourceCRS, displayCRS)) {
3325
+ gui.dynamic_crs = displayCRS;
3326
+ gui.invertPoint = internal.getProjTransform2(displayCRS, sourceCRS);
3327
+ gui.projectPoint = internal.getProjTransform2(sourceCRS, displayCRS);
3328
+ if (internal.layerHasPoints(layer)) {
3329
+ gui.displayLayer = projectPointsForDisplay(layer, sourceCRS, displayCRS);
3330
+ } else if (internal.layerHasPaths(layer)) {
3331
+ emptyArcs = findEmptyArcs(displayArcs);
3332
+ if (emptyArcs.length > 0) {
3333
+ // Don't try to draw paths containing coordinates that failed to project
3334
+ gui.displayLayer = internal.filterPathLayerByArcIds(gui.displayLayer, emptyArcs);
3335
+ }
3336
+ }
3337
+ }
3338
+
3339
+ gui.bounds = getDisplayBounds(gui.displayLayer, gui.displayArcs);
3340
+ layer.gui = gui;
3341
+ }
3342
+
3343
+ function getDisplayBounds(lyr, arcs) {
3344
+ var bounds = internal.getLayerBounds(lyr, arcs) || new Bounds();
3345
+ if (lyr.geometry_type == 'point' && arcs && bounds.hasBounds() && bounds.area() > 0 === false) {
3346
+ // if a point layer has no extent (e.g. contains only a single point),
3347
+ // then merge with arc bounds, to place the point in context.
3348
+ bounds = bounds.mergeBounds(arcs.getBounds());
3349
+ }
3350
+ return bounds;
3351
+ }
3352
+
3353
+ // Returns an array of ids of empty arcs (arcs can be set to empty if errors occur while projecting them)
3354
+ function findEmptyArcs(arcs) {
3355
+ var nn = arcs.getVertexData().nn;
3356
+ var ids = [];
3357
+ for (var i=0, n=nn.length; i<n; i++) {
3358
+ if (nn[i] === 0) {
3359
+ ids.push(i);
3360
+ }
3361
+ }
3362
+ return ids;
3363
+ }
3364
+
3365
+ function flattenArcs(lyr) {
3366
+ lyr.gui.source.dataset.arcs.flatten();
3367
+ if (isProjectedLayer(lyr)) {
3368
+ lyr.gui.displayArcs.flatten();
3369
+ }
3370
+ }
3371
+
3372
+ function setZ(lyr, z) {
3373
+ lyr.gui.source.dataset.arcs.setRetainedInterval(z);
3374
+ if (isProjectedLayer(lyr)) {
3375
+ lyr.gui.displayArcs.setRetainedInterval(z);
3376
+ }
3377
+ }
3378
+
3379
+ function updateZ(lyr) {
3380
+ if (isProjectedLayer(lyr) && !lyr.gui.source.dataset.arcs.isFlat()) {
3381
+ lyr.gui.displayArcs.setThresholds(lyr.gui.source.dataset.arcs.getVertexData().zz);
3382
+ }
3383
+ }
3384
+
3385
+ function appendNewDataRecord(layer) {
3386
+ if (!layer.data) return null;
3387
+ var fields = layer.data.getFields();
3388
+ var d = getEmptyDataRecord(layer.data);
3389
+ // TODO: handle SVG symbol layer
3390
+ if (internal.layerHasLabels(layer)) {
3391
+ d['label-text'] = 'TBD'; // without text, new labels will be invisible
3392
+ } else if (layer.geometry_type == 'point' && fields.includes('r')) {
3393
+ d.r = 3; // show a black circle if layer is styled
3394
+ }
3395
+ if (layer.geometry_type == 'polyline' || layer.geometry_type == 'polygon') {
3396
+ if (fields.includes('stroke')) d.stroke = 'black';
3397
+ if (fields.includes('stroke-width')) d['stroke-width'] = 1;
3398
+ }
3399
+ if (layer.geometry_type == 'polygon') {
3400
+ if (fields.includes('fill')) {
3401
+ d.fill = 'rgba(0,0,0,0.10)'; // 'rgba(249,120,249,0.20)';
3402
+ }
3403
+ }
3404
+ // TODO: better styling
3405
+ layer.data.getRecords().push(d);
3406
+ return d;
3407
+ }
3408
+
3409
+ function getEmptyDataRecord(table) {
3410
+ return table.getFields().reduce(function(memo, name) {
3411
+ memo[name] = null;
3412
+ return memo;
3413
+ }, {});
3414
+ }
3415
+
3416
+ // p1, p2: two points in source data CRS coords.
3417
+ function appendNewPath(lyr, p1, p2) {
3418
+ var arcId = lyr.gui.displayArcs.size();
3419
+ internal.appendEmptyArc(lyr.gui.displayArcs);
3420
+ lyr.shapes.push([[arcId]]);
3421
+ if (isProjectedLayer(lyr)) {
3422
+ internal.appendEmptyArc(lyr.gui.source.dataset.arcs);
3423
+ }
3424
+ appendVertex$1(lyr, p1);
3425
+ appendVertex$1(lyr, p2);
3426
+ appendNewDataRecord(lyr);
3427
+ }
3428
+
3429
+ function deleteLastPath(lyr) {
3430
+ var arcId = lyr.gui.displayArcs.size() - 1;
3431
+ if (lyr.data) {
3432
+ lyr.data.getRecords().pop();
3433
+ }
3434
+ var shp = lyr.shapes.pop();
3435
+ internal.deleteLastArc(lyr.gui.displayArcs);
3436
+ if (isProjectedLayer(lyr)) {
3437
+ internal.deleteLastArc(lyr.gui.source.dataset.arcs);
3438
+ }
3439
+ }
3440
+
3441
+ // p: one point in source data coords
3442
+ function appendNewPoint(lyr, p) {
3443
+ lyr.shapes.push([p]);
3444
+ if (lyr.data) {
3445
+ appendNewDataRecord(lyr);
3446
+ }
3447
+ // this reprojects all the points... TODO: improve
3448
+ if (isProjectedLayer(lyr)) {
3449
+ projectLayerForDisplay(lyr, lyr.gui.dynamic_crs);
3450
+ }
3451
+ }
3452
+
3453
+ function deleteLastPoint(lyr) {
3454
+ if (lyr.data) {
3455
+ lyr.data.getRecords().pop();
3456
+ }
3457
+ lyr.shapes.pop();
3458
+ if (isProjectedLayer(lyr)) {
3459
+ lyr.gui.displayLayer.shapes.pop();
3460
+ }
3461
+ }
3462
+
3463
+ // p: point in source data CRS coords.
3464
+ function insertVertex$1(lyr, id, p) {
3465
+ internal.insertVertex(lyr.gui.source.dataset.arcs, id, p);
3466
+ if (isProjectedLayer(lyr)) {
3467
+ internal.insertVertex(lyr.gui.displayArcs, id, lyr.gui.projectPoint(p[0], p[1]));
3468
+ }
3469
+ }
3470
+
3471
+ function appendVertex$1(lyr, p) {
3472
+ var n = lyr.gui.source.dataset.arcs.getPointCount();
3473
+ insertVertex$1(lyr, n, p);
3474
+ }
3475
+
3476
+ // TODO: make sure we're not also removing an entire arc
3477
+ function deleteLastVertex(lyr) {
3478
+ deleteVertex$1(lyr, lyr.gui.displayArcs.getPointCount() - 1);
3479
+ }
3480
+
3481
+
3482
+ function deleteVertex$1(lyr, id) {
3483
+ internal.deleteVertex(lyr.gui.displayArcs, id);
3484
+ if (isProjectedLayer(lyr)) {
3485
+ internal.deleteVertex(lyr.gui.source.dataset.arcs, id);
3486
+ }
3487
+ }
3488
+
3489
+ function getLastArcCoords(target) {
3490
+ var arcId = target.gui.source.dataset.arcs.size() - 1;
3491
+ return internal.getUnfilteredArcCoords(arcId, target.gui.source.dataset.arcs);
3492
+ }
3493
+
3494
+ function getLastVertexCoords(target) {
3495
+ var arcs = target.gui.source.dataset.arcs;
3496
+ return internal.getVertexCoords(arcs.getPointCount() - 1, arcs);
3497
+ }
3498
+
3499
+ function getLastArcLength(target) {
3500
+ var arcId = target.gui.source.dataset.arcs.size() - 1;
3501
+ return internal.getUnfilteredArcLength(arcId, target.gui.source.dataset.arcs);
3502
+ }
3503
+
3504
+ function getPointCoords(lyr, fid) {
3505
+ return internal.cloneShape(lyr.shapes[fid]);
3506
+ }
3507
+
3508
+ function getVertexCoords(lyr, id) {
3509
+ return lyr.gui.source.dataset.arcs.getVertex2(id);
3510
+ }
3511
+
3512
+ // set data coords (not display coords) of one or more vertices.
3513
+ function setVertexCoords(lyr, ids, dataPoint) {
3514
+ internal.snapVerticesToPoint(ids, dataPoint, lyr.gui.source.dataset.arcs);
3515
+ if (isProjectedLayer(lyr)) {
3516
+ var p = lyr.gui.projectPoint(dataPoint[0], dataPoint[1]);
3517
+ internal.snapVerticesToPoint(ids, p, lyr.gui.displayArcs);
3518
+ }
3519
+ }
3520
+
3521
+ // coords: [x, y] point in data CRS (not display CRS)
3522
+ function setPointCoords(lyr, fid, coords) {
3523
+ lyr.shapes[fid] = coords;
3524
+ if (isProjectedLayer(lyr)) {
3525
+ lyr.shapes[fid] = projectPointCoords(coords, lyr.gui.projectPoint);
3526
+ }
3527
+ }
3528
+
3529
+ function updateVertexCoords(lyr, ids) {
3530
+ if (!isProjectedLayer(lyr)) return;
3531
+ var p = lyr.gui.displayArcs.getVertex2(ids[0]);
3532
+ internal.snapVerticesToPoint(ids, lyr.gui.invertPoint(p[0], p[1]), lyr.gui.source.dataset.arcs);
3533
+ }
3534
+
3535
+ function setRectangleCoords(lyr, ids, coords) {
3536
+ ids.forEach(function(id, i) {
3537
+ var p = coords[i];
3538
+ internal.snapVerticesToPoint([id], p, lyr.gui.source.dataset.arcs);
3087
3539
  if (isProjectedLayer(lyr)) {
3088
- internal.snapVerticesToPoint([id], lyr.projectPoint(p[0], p[1]), lyr.arcs, true);
3540
+ internal.snapVerticesToPoint([id], lyr.gui.projectPoint(p[0], p[1]), lyr.gui.displayArcs);
3089
3541
  }
3090
3542
  });
3091
3543
  }
@@ -3093,8 +3545,8 @@
3093
3545
  // Update source data coordinates by projecting display coordinates
3094
3546
  function updatePointCoords(lyr, fid) {
3095
3547
  if (!isProjectedLayer(lyr)) return;
3096
- var displayShp = lyr.layer.shapes[fid];
3097
- lyr.source.layer.shapes[fid] = projectPointCoords(displayShp, lyr.invertPoint);
3548
+ var displayShp = lyr.shapes[fid];
3549
+ lyr.shapes[fid] = projectPointCoords(displayShp, lyr.gui.invertPoint);
3098
3550
  }
3099
3551
 
3100
3552
  function projectPointCoords(src, proj) {
@@ -3136,6 +3588,7 @@
3136
3588
  var el = gui.container.findChild('.simplify-control-wrapper');
3137
3589
  var menu = gui.container.findChild('.simplify-options');
3138
3590
  var slider, text, fromPct;
3591
+ var menuBtn = gui.container.findChild('.simplify-btn').addClass('disabled');
3139
3592
 
3140
3593
  // init settings menu
3141
3594
  new SimpleButton(menu.findChild('.submit-btn').addClass('default-btn')).on('click', onSubmit);
@@ -3157,11 +3610,14 @@
3157
3610
  gui.keyboard.onMenuSubmit(menu, onSubmit);
3158
3611
 
3159
3612
  // init simplify button and mode
3160
- gui.addMode('simplify', turnOn, turnOff, gui.container.findChild('.simplify-btn'));
3161
- model.on('select', function() {
3613
+ gui.addMode('simplify', turnOn, turnOff, menuBtn);
3614
+
3615
+ model.on('update', function() {
3616
+ menuBtn.classed('disabled', !model.getActiveLayer());
3162
3617
  if (gui.getMode() == 'simplify') gui.clearMode();
3163
3618
  });
3164
3619
 
3620
+
3165
3621
  // exit simplify mode when user clicks off the visible part of the menu
3166
3622
  menu.on('click', GUI.handleDirectEvent(gui.clearMode));
3167
3623
 
@@ -3334,150 +3790,12 @@
3334
3790
  }
3335
3791
  }
3336
3792
 
3337
- function updateSliderDisplay() {
3338
- // TODO: display resolution and vertex count
3339
- // var dataset = model.getActiveLayer().dataset;
3340
- // var interval = dataset.arcs.getRetainedInterval();
3341
- }
3342
- };
3343
-
3344
- var R = 6378137;
3345
- var D2R = Math.PI / 180;
3346
- var R2D = 180 / Math.PI;
3347
-
3348
- // Assumes projections are available
3349
-
3350
- function needReprojectionForDisplay(sourceCRS, displayCRS) {
3351
- if (!sourceCRS || !displayCRS) {
3352
- return false;
3353
- }
3354
- if (internal.crsAreEqual(sourceCRS, displayCRS)) {
3355
- return false;
3356
- }
3357
- return true;
3358
- }
3359
-
3360
- function projectArcsForDisplay(arcs, src, dest) {
3361
- var copy = arcs.getCopy(); // need to flatten first?
3362
- var destIsWebMerc = internal.isWebMercator(dest);
3363
- if (destIsWebMerc && internal.isWebMercator(src)) {
3364
- return copy;
3365
- }
3366
-
3367
- var wgs84 = internal.parseCrsString('wgs84');
3368
- var toWGS84 = internal.getProjTransform2(src, wgs84);
3369
- var fromWGS84 = internal.getProjTransform2(wgs84, dest);
3370
-
3371
- try {
3372
- // first try projectArcs() -- it's fast and preserves arc ids
3373
- // (so vertex editing doesn't break)
3374
- if (!internal.isWGS84(src)) {
3375
- // use wgs84 as a pivot CRS, so we can handle polar coordinates
3376
- // that can't be projected to Mercator
3377
- internal.projectArcs(copy, toWGS84);
3378
- }
3379
- if (destIsWebMerc) {
3380
- // handle polar points by clamping them to they will project
3381
- // (downside: may cause unexpected behavior when editing vertices interactively)
3382
- clampY(copy);
3383
- }
3384
- internal.projectArcs(copy, fromWGS84);
3385
- } catch(e) {
3386
- console.error(e);
3387
- // use the more robust projectArcs2 if projectArcs throws an error
3388
- // downside: projectArcs2 discards Z values and changes arc indexing,
3389
- // which will break vertex editing.
3390
- var reproject = internal.getProjTransform2(src, dest);
3391
- copy = arcs.getCopy();
3392
- internal.projectArcs2(copy, reproject);
3393
- }
3394
- return copy;
3395
- }
3396
-
3397
- function clampY(arcs) {
3398
- var max = 89.9,
3399
- min = -89.9,
3400
- bbox = arcs.getBounds().toArray();
3401
- if (bbox[1] >= min && bbox[3] <= max) return;
3402
- arcs.transformPoints(function(x, y) {
3403
- if (y > max) return [x, max];
3404
- if (y < min) return [x, min];
3405
- });
3406
- }
3407
-
3408
- function projectPointsForDisplay(lyr, src, dest) {
3409
- var copy = utils$1.extend({}, lyr);
3410
- var proj = internal.getProjTransform2(src, dest);
3411
- copy.shapes = internal.cloneShapes(lyr.shapes);
3412
- internal.projectPointLayer(copy, proj);
3413
- return copy;
3414
- }
3415
-
3416
-
3417
- function toWebMercator(lng, lat) {
3418
- var k = Math.cos(lat * D2R);
3419
- var x = R * lng * D2R;
3420
- var y = R * Math.log(Math.tan(Math.PI * 0.25 + lat * D2R * 0.5));
3421
- return [x, y];
3422
- }
3423
-
3424
- function fromWebMercator(x, y) {
3425
- var lon = x / R * R2D;
3426
- var lat = R2D * (Math.PI * 0.5 - 2 * Math.atan(Math.exp(-y / R)));
3427
- return [lon, lat];
3428
- }
3429
-
3430
- function scaleToZoom(metersPerPix) {
3431
- return Math.log(40075017 / 512 / metersPerPix) / Math.log(2);
3432
- }
3433
-
3434
- function getMapboxBounds() {
3435
- var ymax = toWebMercator(0, 84)[1];
3436
- var ymin = toWebMercator(0, -84)[1];
3437
- return [-Infinity, ymin, Infinity, ymax];
3438
- }
3439
-
3440
-
3441
- // Update map extent and trigger redraw, after a new display CRS has been applied
3442
- function projectMapExtent(ext, src, dest, newBounds) {
3443
- var oldBounds = ext.getBounds();
3444
- var oldScale = ext.scale();
3445
- var newCP, proj, strictBounds;
3446
-
3447
- if (dest && internal.isWebMercator(dest)) {
3448
- // clampToMapboxBounds(newBounds);
3449
- strictBounds = getMapboxBounds();
3450
- }
3451
-
3452
- // if source or destination CRS is unknown, show full extent
3453
- // if map is at full extent, show full extent
3454
- // TODO: handle case that scale is 1 and map is panned away from center
3455
- if (ext.scale() == 1 || !dest) {
3456
- ext.setFullBounds(newBounds, strictBounds);
3457
- ext.home(); // sets full extent and triggers redraw
3458
- } else {
3459
- // if map is zoomed, stay centered on the same geographic location, at the same relative scale
3460
- proj = internal.getProjTransform2(src, dest);
3461
- newCP = proj(oldBounds.centerX(), oldBounds.centerY());
3462
- ext.setFullBounds(newBounds, strictBounds);
3463
- if (!newCP) {
3464
- // projection of center point failed; use center of bounds
3465
- // (also consider just resetting the view using ext.home())
3466
- newCP = [newBounds.centerX(), newBounds.centerY()];
3467
- }
3468
- ext.recenter(newCP[0], newCP[1], oldScale);
3469
- }
3470
- }
3471
-
3472
- // Called from console; for testing dynamic crs
3473
- function setDisplayProjection(gui, cmd) {
3474
- var arg = cmd.replace(/^projd[ ]*/, '');
3475
- if (arg) {
3476
- gui.map.setDisplayCRS(internal.parseCrsString(arg));
3477
- } else {
3478
- gui.map.setDisplayCRS(null);
3793
+ function updateSliderDisplay() {
3794
+ // TODO: display resolution and vertex count
3795
+ // var dataset = model.getActiveLayer().dataset;
3796
+ // var interval = dataset.arcs.getRetainedInterval();
3479
3797
  }
3480
- }
3798
+ };
3481
3799
 
3482
3800
  function Console(gui) {
3483
3801
  var model = gui.model;
@@ -3547,7 +3865,8 @@
3547
3865
  }
3548
3866
 
3549
3867
  function turnOn() {
3550
- if (!_isOpen && !model.isEmpty()) {
3868
+ // if (!_isOpen && !model.isEmpty()) {
3869
+ if (!_isOpen) {
3551
3870
  btn.addClass('active');
3552
3871
  _isOpen = true;
3553
3872
  // use console for messages while open
@@ -3886,8 +4205,8 @@
3886
4205
 
3887
4206
  function applyParsedCommands(commands, done) {
3888
4207
  var active = model.getActiveLayer(),
3889
- prevArcs = active.dataset.arcs,
3890
- prevTable = active.layer.data,
4208
+ prevArcs = active?.dataset.arcs,
4209
+ prevTable = active?.layer.data,
3891
4210
  prevTableSize = prevTable ? prevTable.size() : 0,
3892
4211
  prevArcCount = prevArcs ? prevArcs.size() : 0,
3893
4212
  job = new internal.Job(model);
@@ -3896,9 +4215,9 @@
3896
4215
  internal.runParsedCommands(commands, job, function(err) {
3897
4216
  var flags = getCommandFlags(commands),
3898
4217
  active2 = model.getActiveLayer(),
3899
- postArcs = active2.dataset.arcs,
4218
+ postArcs = active2?.dataset.arcs,
3900
4219
  postArcCount = postArcs ? postArcs.size() : 0,
3901
- postTable = active2.layer.data,
4220
+ postTable = active2?.layer.data,
3902
4221
  postTableSize = postTable ? postTable.size() : 0,
3903
4222
  sameTable = prevTable == postTable && prevTableSize == postTableSize,
3904
4223
  sameArcs = prevArcs == postArcs && postArcCount == prevArcCount;
@@ -3911,7 +4230,7 @@
3911
4230
  if (sameTable) {
3912
4231
  flags.same_table = true;
3913
4232
  }
3914
- if (active.layer != active2.layer) {
4233
+ if (active && active?.layer == active2?.layer) {
3915
4234
  // this can get set after some commands that don't set a new target
3916
4235
  // (e.g. -dissolve)
3917
4236
  flags.select = true;
@@ -3926,10 +4245,10 @@
3926
4245
  function onError(err) {
3927
4246
  if (utils$1.isString(err)) {
3928
4247
  consoleStop(err);
3929
- } else if (err.name == 'UserError') {
4248
+ } else if (err.name == 'UserError' ) {
3930
4249
  // stop() has already been called, don't need to log
4250
+ console.error(err.stack);
3931
4251
  } else if (err.name) {
3932
- // log stack trace to browser console
3933
4252
  console.error(err.stack);
3934
4253
  // log to console window
3935
4254
  consoleWarning(err.message);
@@ -4112,6 +4431,7 @@
4112
4431
  return;
4113
4432
  }
4114
4433
  var e = model.getActiveLayer();
4434
+ if (!e) return;
4115
4435
  if (internal.layerHasPaths(e.layer)) {
4116
4436
  el.show();
4117
4437
  checkBtn.show();
@@ -4190,45 +4510,9 @@
4190
4510
 
4191
4511
  utils$1.inherit(RepairControl, EventDispatcher);
4192
4512
 
4193
- function filterLayerByIds(lyr, ids) {
4194
- var shapes;
4195
- if (lyr.shapes) {
4196
- shapes = ids.map(function(id) {
4197
- return lyr.shapes[id];
4198
- });
4199
- return utils$1.defaults({shapes: shapes, data: null}, lyr);
4200
- }
4201
- return lyr;
4202
- }
4203
-
4204
- function formatLayerNameForDisplay(name) {
4205
- return name || '[unnamed]';
4206
- }
4207
-
4208
- function cleanLayerName(raw) {
4209
- return raw.replace(/[\n\t/\\]/g, '')
4210
- .replace(/^[.\s]+/, '').replace(/[.\s]+$/, '');
4211
- }
4212
-
4213
- function updateLayerStackOrder(layers) {
4214
- // 1. assign ascending ids to unassigned layers above the range of other layers
4215
- layers.forEach(function(o, i) {
4216
- if (!o.layer.menu_order) o.layer.menu_order = 1e6 + i;
4217
- });
4218
- // 2. sort in ascending order
4219
- layers.sort(function(a, b) {
4220
- return a.layer.menu_order - b.layer.menu_order;
4221
- });
4222
- // 3. assign consecutve ids
4223
- layers.forEach(function(o, i) {
4224
- o.layer.menu_order = i + 1;
4225
- });
4226
- return layers;
4227
- }
4228
-
4229
- function sortLayersForMenuDisplay(layers) {
4230
- layers = updateLayerStackOrder(layers);
4231
- return layers.reverse();
4513
+ async function saveFileContentToClipboard(content) {
4514
+ var str = utils$1.isString(content) ? content : content.toString();
4515
+ await navigator.clipboard.writeText(str);
4232
4516
  }
4233
4517
 
4234
4518
  // Export buttons and their behavior
@@ -4238,7 +4522,7 @@
4238
4522
  var menu = gui.container.findChild('.export-options').on('click', GUI.handleDirectEvent(gui.clearMode));
4239
4523
  var layersArr = [];
4240
4524
  var toggleBtn = null; // checkbox <input> for toggling layer selection
4241
- var exportBtn = gui.container.findChild('.export-btn');
4525
+ var exportBtn = gui.container.findChild('.export-btn').addClass('disabled');
4242
4526
  var ofileName = gui.container.findChild('#ofile-name');
4243
4527
  new SimpleButton(menu.findChild('.close2-btn')).on('click', gui.clearMode);
4244
4528
 
@@ -4253,6 +4537,10 @@
4253
4537
  return;
4254
4538
  }
4255
4539
 
4540
+ model.on('update', function() {
4541
+ exportBtn.classed('disabled', !model.getActiveLayer());
4542
+ });
4543
+
4256
4544
  new SimpleButton(menu.findChild('#export-btn').addClass('default-btn')).on('click', onExportClick);
4257
4545
  gui.addMode('export', turnOn, turnOff, exportBtn);
4258
4546
  gui.keyboard.onMenuSubmit(menu, onExportClick);
@@ -4374,12 +4662,6 @@
4374
4662
  } else {
4375
4663
  await utils$1.promisify(internal.writeFiles)(files, opts);
4376
4664
  }
4377
-
4378
- }
4379
-
4380
- async function saveFileContentToClipboard(content) {
4381
- var str = utils$1.isString(content) ? content : content.toString();
4382
- await navigator.clipboard.writeText(str);
4383
4665
  }
4384
4666
 
4385
4667
  function initLayerItem(o, i) {
@@ -4589,6 +4871,14 @@
4589
4871
  var layerOrderSlug;
4590
4872
 
4591
4873
  gui.addMode('layer_menu', turnOn, turnOff, btn.findChild('.header-btn'));
4874
+
4875
+ // kludge to show menu button after initial import dialog is dismissed
4876
+ gui.on('mode', function(e) {
4877
+ if (!e.name) {
4878
+ updateMenuBtn();
4879
+ }
4880
+ });
4881
+
4592
4882
  model.on('update', function(e) {
4593
4883
  updateMenuBtn();
4594
4884
  if (isOpen) render();
@@ -4679,8 +4969,9 @@
4679
4969
  }
4680
4970
 
4681
4971
  function updateMenuBtn() {
4682
- var lyrName = model.getActiveLayer().layer.name || '';
4683
- var menuTitle = lyrName || '[unnamed layer]';
4972
+ var lyr = model.getActiveLayer()?.layer;
4973
+ var lyrName = lyr?.name || '';
4974
+ var menuTitle = lyrName || lyr && '[unnamed layer]' || '[no data]';
4684
4975
  var pageTitle = lyrName || 'mapshaper';
4685
4976
  btn.classed('active', 'true').findChild('.layer-name').html(menuTitle + " &nbsp;&#9660;");
4686
4977
  window.document.title = pageTitle;
@@ -4692,6 +4983,8 @@
4692
4983
  }
4693
4984
 
4694
4985
  function renderSourceFileList() {
4986
+ el.findChild('.no-layer-note').classed('hidden', model.getActiveLayer());
4987
+ el.findChild('.source-file-section').classed('hidden', !model.getActiveLayer());
4695
4988
  var list = el.findChild('.file-list');
4696
4989
  var files = [];
4697
4990
  list.empty();
@@ -5209,6 +5502,16 @@
5209
5502
  addHistoryState(undo, redo);
5210
5503
  });
5211
5504
 
5505
+ gui.on('point_add', function(e) {
5506
+ var redo = function() {
5507
+ appendNewPoint(e.data.target, e.p);
5508
+ };
5509
+ var undo = function() {
5510
+ deleteLastPoint(e.data.target);
5511
+ };
5512
+ addHistoryState(undo, redo);
5513
+ });
5514
+
5212
5515
  gui.on('path_add', function(e) {
5213
5516
  var redo = function() {
5214
5517
  gui.dispatchEvent('redo_path_add', {p1: e.p1, p2: e.p2});
@@ -5277,6 +5580,7 @@
5277
5580
  var _hidden = true;
5278
5581
  gui.on('active', updateVisibility);
5279
5582
  gui.on('inactive', updateVisibility);
5583
+ gui.model.on('update', updateVisibility);
5280
5584
 
5281
5585
  // @iconRef: selector for an (svg) button icon
5282
5586
  this.addButton = function(iconRef) {
@@ -5305,7 +5609,7 @@
5305
5609
  };
5306
5610
 
5307
5611
  function updateVisibility() {
5308
- if (GUI.isActiveInstance(gui) && !_hidden) {
5612
+ if (GUI.isActiveInstance(gui) && !_hidden && !!gui.model.getActiveLayer()) {
5309
5613
  buttons.show();
5310
5614
  } else {
5311
5615
  buttons.hide();
@@ -5326,6 +5630,7 @@
5326
5630
  });
5327
5631
 
5328
5632
  btn.on('click', function() {
5633
+ if (btn.hasClass('disabled')) return;
5329
5634
  modes.enterMode(active ? null : name);
5330
5635
  });
5331
5636
  }
@@ -5397,19 +5702,25 @@
5397
5702
  function KeyboardEvents(gui) {
5398
5703
  var self = this;
5399
5704
  var shiftDown = false;
5705
+ var ctrlDown = false;
5400
5706
  document.addEventListener('keyup', function(e) {
5401
5707
  if (!GUI.isActiveInstance(gui)) return;
5402
- if (e.keyCode == 16) shiftDown = false;
5708
+ // this can fail to fire if keyup occurs over a context menu
5709
+ // if (e.keyCode == 16) shiftDown = false;
5710
+ shiftDown = e.shiftKey;
5711
+ if (e.keyCode == 17) ctrlDown = false;
5403
5712
  self.dispatchEvent('keyup', getEventData(e));
5404
5713
  });
5405
5714
 
5406
5715
  document.addEventListener('keydown', function(e) {
5407
5716
  if (!GUI.isActiveInstance(gui)) return;
5408
- if (e.keyCode == 16) shiftDown = true;
5717
+ shiftDown = e.shiftKey;
5718
+ if (e.keyCode == 17) ctrlDown = true;
5409
5719
  self.dispatchEvent('keydown', getEventData(e));
5410
5720
  });
5411
5721
 
5412
5722
  this.shiftIsPressed = function() { return shiftDown; };
5723
+ this.ctrlIsPressed = function() { return ctrlDown; };
5413
5724
 
5414
5725
  this.onMenuSubmit = function(menuEl, cb) {
5415
5726
  gui.on('enter_key', function(e) {
@@ -5426,6 +5737,7 @@
5426
5737
  9: 'tab',
5427
5738
  13: 'enter',
5428
5739
  16: 'shift',
5740
+ 17: 'ctrl',
5429
5741
  27: 'esc',
5430
5742
  32: 'space',
5431
5743
  37: 'left',
@@ -5447,7 +5759,7 @@
5447
5759
  var menus = {
5448
5760
  standard: ['info', 'selection', 'box'],
5449
5761
  polygons: ['info', 'selection', 'box', 'drawing'],
5450
- rectangles: ['info', 'selection', 'box', 'rectangles'], // 'vertices'
5762
+ rectangles: ['info', 'selection', 'box', 'rectangles'],
5451
5763
  lines: ['info', 'selection', 'box' , 'drawing'],
5452
5764
  table: ['info', 'selection'],
5453
5765
  labels: ['info', 'selection', 'box', 'labels', 'location', 'add-points'],
@@ -5468,7 +5780,7 @@
5468
5780
  labels: 'position labels',
5469
5781
  location: 'drag points',
5470
5782
  vertices: 'edit vertices',
5471
- selection: 'select features',
5783
+ selection: 'selection tool',
5472
5784
  'add-points': 'add points',
5473
5785
  drawing: 'draw/reshape tool',
5474
5786
  rectangles: 'drag-to-resize',
@@ -7261,11 +7573,11 @@
7261
7573
 
7262
7574
  // featureFilter: optional test function, accepts feature id
7263
7575
  //
7264
- function getShapeHitTest(displayLayer, ext, interactionMode, featureFilter) {
7265
- var geoType = displayLayer.layer.geometry_type;
7576
+ function getShapeHitTest(layer, ext, interactionMode, featureFilter) {
7577
+ var geoType = layer.gui.displayLayer.geometry_type;
7266
7578
  var test;
7267
- if (geoType == 'point' && displayLayer.style.type == 'styled') {
7268
- test = getGraduatedCircleTest(getRadiusFunction(displayLayer.style));
7579
+ if (geoType == 'point' && layer.gui.style.type == 'styled') {
7580
+ test = getGraduatedCircleTest(getRadiusFunction(layer.gui.style));
7269
7581
  } else if (geoType == 'point') {
7270
7582
  test = pointTest;
7271
7583
  } else if (interactionMode == 'drawing' && geoType == 'polygon') {
@@ -7305,13 +7617,13 @@
7305
7617
  cand, hitId;
7306
7618
  for (var i=0; i<cands.length; i++) {
7307
7619
  cand = cands[i];
7308
- if (geom.testPointInPolygon(x, y, cand.shape, displayLayer.arcs)) {
7620
+ if (geom.testPointInPolygon(x, y, cand.shape, layer.gui.displayArcs)) {
7309
7621
  hits.push(cand);
7310
7622
  }
7311
7623
  }
7312
7624
  if (cands.length > 0 && hits.length === 0) {
7313
7625
  // secondary detection: proximity, if not inside a polygon
7314
- sortByDistance(x, y, cands, displayLayer.arcs);
7626
+ sortByDistance(x, y, cands, layer.gui.displayArcs);
7315
7627
  hits = pickNearestCandidates(cands, 0, maxDist);
7316
7628
  }
7317
7629
  return {
@@ -7336,7 +7648,7 @@
7336
7648
  bufPix = bufArg >= 0 ? bufArg : 0.05, // tiny threshold for hitting almost-identical lines
7337
7649
  bufDist = getZoomAdjustedHitBuffer(bufPix),
7338
7650
  cands = findHitCandidates(x, y, maxDist);
7339
- sortByDistance(x, y, cands, displayLayer.arcs);
7651
+ sortByDistance(x, y, cands, layer.gui.displayArcs);
7340
7652
  cands = pickNearestCandidates(cands, bufDist, maxDist);
7341
7653
  return {
7342
7654
  ids: utils$1.pluck(cands, 'id')
@@ -7377,7 +7689,7 @@
7377
7689
  hits = [];
7378
7690
 
7379
7691
  // inlining forEachPoint() does not not appreciably speed this up
7380
- internal.forEachPoint(displayLayer.layer.shapes, function(p, id) {
7692
+ internal.forEachPoint(layer.gui.displayLayer.shapes, function(p, id) {
7381
7693
  var dist = geom.distance2D(x, y, p[0], p[1]) * toPx;
7382
7694
  if (dist > hitThreshold) return;
7383
7695
  if (dist < hitThreshold && hitThreshold > bullseyeDist) {
@@ -7411,7 +7723,7 @@
7411
7723
  directHit = false,
7412
7724
  hitRadius = 0,
7413
7725
  hitDist;
7414
- internal.forEachPoint(displayLayer.layer.shapes, function(p, id) {
7726
+ internal.forEachPoint(layer.gui.displayLayer.shapes, function(p, id) {
7415
7727
  var distSq = geom.distanceSq(x, y, p[0], p[1]);
7416
7728
  var isHit = false;
7417
7729
  var isOver, isNear, r, d, rpix;
@@ -7451,11 +7763,11 @@
7451
7763
 
7452
7764
  // Returns array of shape ids for shapes that pass a buffered bounding-box test
7453
7765
  function findHitCandidates(x, y, dist) {
7454
- var arcs = displayLayer.arcs,
7766
+ var arcs = layer.gui.displayArcs,
7455
7767
  index = {},
7456
7768
  cands = [],
7457
7769
  bbox = [];
7458
- displayLayer.layer.shapes.forEach(function(shp, shpId) {
7770
+ layer.gui.displayLayer.shapes.forEach(function(shp, shpId) {
7459
7771
  var cand;
7460
7772
  if (featureFilter && !featureFilter(shpId)) {
7461
7773
  return;
@@ -7587,7 +7899,7 @@
7587
7899
 
7588
7900
  function getPointerHitTest(mapLayer, ext, interactionMode, featureFilter) {
7589
7901
  var shapeTest, targetLayer;
7590
- if (!mapLayer || !internal.layerHasGeometry(mapLayer.layer)) {
7902
+ if (!mapLayer || !internal.layerHasGeometry(mapLayer.gui?.displayLayer)) {
7591
7903
  return function() {return {ids: []};};
7592
7904
  }
7593
7905
  shapeTest = getShapeHitTest(mapLayer, ext, interactionMode, featureFilter);
@@ -7626,6 +7938,16 @@
7626
7938
  // pan navigation
7627
7939
  var priority = 2;
7628
7940
 
7941
+ mouse.on('contextmenu', function(e) {
7942
+ // shift key enables default menu (for development)
7943
+ if (gui.keyboard.shiftIsPressed()) {
7944
+ return;
7945
+ }
7946
+ e.originalEvent.preventDefault();
7947
+ if (!targetLayer) return; // TODO: enable menu on empty map
7948
+ triggerHitEvent('contextmenu', e);
7949
+ }, false);
7950
+
7629
7951
  // init keyboard controls for pinned features
7630
7952
  gui.keyboard.on('keydown', function(evt) {
7631
7953
  var e = evt.originalEvent;
@@ -7653,7 +7975,7 @@
7653
7975
  // to help protect against inadvertent deletion, don't delete
7654
7976
  // when console is open or a popup menu is open
7655
7977
  if (!gui.getMode() && !gui.consoleIsOpen()) {
7656
- internal.deleteFeatureById(targetLayer.layer, pinnedId());
7978
+ internal.deleteFeatureById(targetLayer, pinnedId());
7657
7979
  self.clearSelection();
7658
7980
  gui.model.updated({flags: 'filter'}); // signal map to update
7659
7981
  }
@@ -7666,7 +7988,6 @@
7666
7988
  };
7667
7989
 
7668
7990
  function updateHitTest(featureFilter) {
7669
- if (!hoverable()) return;
7670
7991
  hitTest = getPointerHitTest(targetLayer, ext, interactionMode, featureFilter);
7671
7992
  }
7672
7993
 
@@ -7687,7 +8008,7 @@
7687
8008
  }
7688
8009
 
7689
8010
  function hoverable() {
7690
- return true;
8011
+ return !!interactionMode;
7691
8012
  }
7692
8013
 
7693
8014
  function selectable() {
@@ -7696,6 +8017,7 @@
7696
8017
 
7697
8018
  function pinnable() {
7698
8019
  return clickable() && !selectable();
8020
+ // return clickable();
7699
8021
  }
7700
8022
 
7701
8023
  function draggable() {
@@ -7790,7 +8112,7 @@
7790
8112
 
7791
8113
  self.getTargetDataTable = function() {
7792
8114
  var targ = self.getHitTarget();
7793
- return targ && targ.layer.data || null;
8115
+ return targ?.data || null;
7794
8116
  };
7795
8117
 
7796
8118
  // get function for selecting next or prev feature within the current set of
@@ -7803,7 +8125,7 @@
7803
8125
 
7804
8126
  // diff: 1 or -1
7805
8127
  function advanceSelectedFeature(diff) {
7806
- var n = internal.getFeatureCount(targetLayer.layer);
8128
+ var n = internal.getFeatureCount(targetLayer);
7807
8129
  if (n < 2 || pinnedId() == -1) return;
7808
8130
  storedData.id = (pinnedId() + n + diff) % n;
7809
8131
  storedData.ids = [storedData.id];
@@ -7848,17 +8170,21 @@
7848
8170
 
7849
8171
  mouse.on('click', function(e) {
7850
8172
  if (!hitTest || !active) return;
8173
+ if (!eventIsEnabled('click')) return;
8174
+
7851
8175
  e.stopPropagation();
7852
8176
 
7853
8177
  // TODO: move pinning to inspection control?
7854
8178
  if (clickable()) {
7855
8179
  updateSelectionState(convertClickDataToSelectionData(hitTest(e)));
7856
8180
  }
8181
+
7857
8182
  triggerHitEvent('click', e);
7858
8183
  }, null, priority);
7859
8184
 
7860
8185
  // Hits are re-detected on 'hover' (if hit detection is active)
7861
8186
  mouse.on('hover', function(e) {
8187
+ if (gui.contextMenu.isOpen()) return;
7862
8188
  handlePointerEvent(e);
7863
8189
  if (storedData.pinned || !hitTest || !active) return;
7864
8190
  if (e.hover && isOverMap(e)) {
@@ -7872,6 +8198,7 @@
7872
8198
  }
7873
8199
  }, null, priority);
7874
8200
 
8201
+
7875
8202
  function targetIsRollover(target) {
7876
8203
  while (target.parentNode && target != target.parentNode) {
7877
8204
  if (target.className && String(target.className).indexOf('rollover') > -1) {
@@ -7910,6 +8237,19 @@
7910
8237
  return hitData;
7911
8238
  }
7912
8239
 
8240
+ function mergeSelectionModeHoverData(hitData) {
8241
+ if (hitData.ids.length === 0 || selectionIds.includes(hitData.ids[0])) {
8242
+ hitData.ids = selectionIds;
8243
+ hitData.pinned = storedData.pinned;
8244
+ } else {
8245
+ //
8246
+ }
8247
+
8248
+ // kludge to inhibit hover effect while dragging a box
8249
+ if (gui.keydown) hitData.id = -1;
8250
+ return hitData;
8251
+ }
8252
+
7913
8253
  function mergeHoverData(hitData) {
7914
8254
  if (storedData.pinned) {
7915
8255
  hitData.id = storedData.id;
@@ -7960,6 +8300,16 @@
7960
8300
  // check if an event is used in the current interaction mode
7961
8301
  function eventIsEnabled(type) {
7962
8302
  if (!active) return false;
8303
+ if (type == 'click' && gui.keyboard.ctrlIsPressed()) {
8304
+ return false; // don't fire if context menu might open
8305
+ }
8306
+ if (type == 'click' && gui.contextMenu.isOpen()) {
8307
+ return false;
8308
+ }
8309
+ if (type == 'click' && interactionMode == 'drawing') {
8310
+ return true; // click events are triggered even if no shape is hit
8311
+ }
8312
+
7963
8313
  if (interactionMode == 'drawing' && (type == 'hover' || type == 'dblclick')) {
7964
8314
  return true; // special case -- using hover for line drawing animation
7965
8315
  }
@@ -7969,9 +8319,6 @@
7969
8319
  var hitId = self.getHitId();
7970
8320
  if (hitId == -1) return false;
7971
8321
 
7972
- if (type == 'click' && !clickable()) {
7973
- return false;
7974
- }
7975
8322
  if ((type == 'drag' || type == 'dragstart' || type == 'dragend') && !draggable()) {
7976
8323
  return false;
7977
8324
  }
@@ -8000,10 +8347,13 @@
8000
8347
  // evt: event data (may be a pointer event object, an ordinary object or null)
8001
8348
  function triggerHitEvent(type, evt) {
8002
8349
  var eventData = {
8003
- mode: interactionMode,
8004
- overMap: evt ? isOverMap(evt) : null,
8005
- originalEvent: evt ? evt : null
8350
+ mode: interactionMode
8006
8351
  };
8352
+ if (evt) {
8353
+ eventData.coordinates = translateDisplayPoint(targetLayer, ext.translatePixelCoords(evt.x, evt.y));
8354
+ eventData.originalEvent = evt;
8355
+ eventData.overMap = isOverMap(evt);
8356
+ }
8007
8357
  // Merge stored hit data into the event data
8008
8358
  utils$1.defaults(eventData, evt && evt.data || {}, storedData);
8009
8359
  // utils.extend(eventData, storedData);
@@ -8050,7 +8400,7 @@
8050
8400
 
8051
8401
  function enabled() {
8052
8402
  var lyr = gui.map.getActiveLayer();
8053
- return !!(lyr && lyr.layer.geometry_type);
8403
+ return !!(lyr && lyr.gui.displayLayer.geometry_type);
8054
8404
  }
8055
8405
 
8056
8406
  gui.model.on('update', function(e) {
@@ -8095,7 +8445,7 @@
8095
8445
  var p2 = gui.map.translatePixelCoords(ext.width(), 0);
8096
8446
  var bbox = p1.concat(p2);
8097
8447
  var decimals = internal.getBoundsPrecisionForDisplay(bbox);
8098
- var str = internal.getRoundedCoordString(p, decimals);
8448
+ var str = internal.getRoundedCoordString(p, decimals);
8099
8449
  readout.text(str).show();
8100
8450
  }
8101
8451
 
@@ -8376,6 +8726,9 @@
8376
8726
  element.addEventListener('mouseout', onAreaOut);
8377
8727
  element.addEventListener('mousedown', onAreaDown);
8378
8728
  element.addEventListener('dblclick', onAreaDblClick);
8729
+ document.addEventListener('contextmenu', function(e) {
8730
+ _self.dispatchEvent('contextmenu', procMouseEvent(e));
8731
+ });
8379
8732
 
8380
8733
  this.enable = function() {
8381
8734
  if (!_disabled) return;
@@ -8816,7 +9169,7 @@
8816
9169
  }
8817
9170
 
8818
9171
  gui.on('map_reset', function() {
8819
- ext.home();
9172
+ ext.reset(true);
8820
9173
  });
8821
9174
 
8822
9175
  zoomTween.on('change', function(e) {
@@ -8834,8 +9187,9 @@
8834
9187
 
8835
9188
  mouse.on('dragstart', function(e) {
8836
9189
  if (disabled()) return;
8837
- if (!internal.layerHasGeometry(gui.model.getActiveLayer().layer)) return;
8838
- // zoomDrag = !!e.metaKey || !!e.ctrlKey; // meta is command on mac, windows key on windows
9190
+ // allow drawing rectangles if active layer is empty
9191
+ // var lyr = gui.model.getActiveLayer()?.layer;
9192
+ // if (lyr && !internal.layerHasGeometry(lyr)) return;
8839
9193
  shiftDrag = !!e.shiftKey;
8840
9194
  if (shiftDrag) {
8841
9195
  if (useBoxZoom()) zoomBox.turnOn();
@@ -8943,8 +9297,8 @@
8943
9297
  gui.on('interaction_mode_change', function(e) {
8944
9298
  if (e.mode === 'selection') {
8945
9299
  gui.enterMode('selection_tool');
8946
- } else if (gui.getMode() == 'selection_tool') {
8947
- gui.clearMode();
9300
+ } else if (_on) {
9301
+ turnOff();
8948
9302
  }
8949
9303
  });
8950
9304
 
@@ -9410,8 +9764,17 @@
9410
9764
  }
9411
9765
  });
9412
9766
 
9767
+ hit.on('contextmenu', function(e) {
9768
+ var target = hit.getHitTarget();
9769
+ if (!e.overMap || !target) return;
9770
+
9771
+ if (e.mode == 'drawing') return; // TODO: make special menu for this mode
9772
+ gui.contextMenu.open(e, hit.getHitTarget());
9773
+ });
9774
+
9413
9775
  hit.on('change', function(e) {
9414
9776
  if (!inspecting()) return;
9777
+ if (gui.keyboard.ctrlIsPressed()) return;
9415
9778
  var ids;
9416
9779
  if (e.mode == 'selection') {
9417
9780
  ids = e.pinned && e.ids || [];
@@ -9424,8 +9787,8 @@
9424
9787
  // id: Id of a feature in the active layer, or -1
9425
9788
  function inspect(id, ids, pin) {
9426
9789
  var target = hit.getHitTarget();
9427
- if ((id > -1 || ids && ids.length > 0) && inspecting() && target && target.layer) {
9428
- _popup.show(id, ids, target.layer, pin);
9790
+ if ((id > -1 || ids && ids.length > 0) && inspecting() && target && target) {
9791
+ _popup.show(id, ids, target, pin);
9429
9792
  } else {
9430
9793
  _popup.hide();
9431
9794
  }
@@ -9529,15 +9892,20 @@
9529
9892
 
9530
9893
  function initLabelDragging(gui, ext, hit) {
9531
9894
  var downEvt;
9532
- var activeId;
9895
+ var activeId = -1;
9896
+ var prevHitEvt;
9533
9897
  var activeRecord;
9534
9898
 
9535
- function active(e) {
9536
- return e.id > -1 && gui.interaction.getMode() == 'labels';
9899
+ function active() {
9900
+ return gui.interaction.getMode() == 'labels';
9901
+ }
9902
+
9903
+ function labelSelected(e) {
9904
+ return e.id > -1 && active();
9537
9905
  }
9538
9906
 
9539
9907
  hit.on('dragstart', function(e) {
9540
- if (!active(e)) return;
9908
+ if (!labelSelected(e)) return;
9541
9909
  var symNode = getSymbolTarget(e);
9542
9910
  var table = hit.getTargetDataTable();
9543
9911
  if (!symNode || !table) {
@@ -9550,8 +9918,25 @@
9550
9918
  gui.dispatchEvent('label_dragstart', {FID: activeId});
9551
9919
  });
9552
9920
 
9921
+ hit.on('change', function(e) {
9922
+ if (!active()) return;
9923
+ if (prevHitEvt) clearLabelHighlight(prevHitEvt);
9924
+ showLabelHighlight(e);
9925
+ prevHitEvt = e;
9926
+ });
9927
+
9928
+ function clearLabelHighlight(e) {
9929
+ var txt = getTextNode(getSymbolTarget(e));
9930
+ if (txt) txt.classList.remove('active-label');
9931
+ }
9932
+
9933
+ function showLabelHighlight(e) {
9934
+ var txt = getTextNode(getSymbolTarget(e));
9935
+ if (txt) txt.classList.add('active-label');
9936
+ }
9937
+
9553
9938
  hit.on('drag', function(e) {
9554
- if (!active(e) || activeId == -1) return;
9939
+ if (!labelSelected(e) || activeId == -1) return;
9555
9940
  if (e.id != activeId) {
9556
9941
  error$1("Mismatched hit ids:", e.id, activeId);
9557
9942
  }
@@ -9565,7 +9950,7 @@
9565
9950
  // to anchor point, for better placement when eventual display font is
9566
9951
  // different from mapshaper's font.
9567
9952
  // if (!isMultilineLabel(textNode)) {
9568
- autoUpdateTextAnchor(textNode, activeRecord, getDisplayCoordsById(activeId, hit.getHitTarget().layer, ext));
9953
+ autoUpdateTextAnchor(textNode, activeRecord, getDisplayCoordsById(activeId, hit.getHitTarget(), ext));
9569
9954
  // }
9570
9955
  updateNumber(activeRecord, 'dx', internal.roundToDigits(+activeRecord.dx, 3));
9571
9956
  updateNumber(activeRecord, 'dy', internal.roundToDigits(+activeRecord.dy, 3));
@@ -9575,7 +9960,7 @@
9575
9960
  });
9576
9961
 
9577
9962
  hit.on('dragend', function(e) {
9578
- if (!active(e) || activeId == -1) return;
9963
+ if (!labelSelected(e) || activeId == -1) return;
9579
9964
  gui.dispatchEvent('label_dragend', {FID: e.id});
9580
9965
  activeId = -1;
9581
9966
  activeRecord = null;
@@ -9596,19 +9981,25 @@
9596
9981
  }
9597
9982
 
9598
9983
  function getSymbolTarget(e) {
9599
- if (e.id > -1 === false || !e.container) return null;
9600
- return getSymbolNodeById(e.id, e.container);
9984
+ return e.id > -1 ? getSymbolNodeById(e.id) : null;
9601
9985
  }
9602
9986
 
9603
9987
  function getTextNode(symNode) {
9988
+ if (!symNode) return null;
9604
9989
  if (symNode.tagName == 'text') return symNode;
9605
9990
  return symNode.querySelector('text');
9606
9991
  }
9607
9992
 
9608
- function getSymbolNodeById(id, parent) {
9993
+ // function getSymbolNodeById_OLD(id, parent) {
9994
+ // var sel = '[data-id="' + id + '"]';
9995
+ // return parent.querySelector(sel);
9996
+ // }
9997
+
9998
+ function getSymbolNodeById(id) {
9609
9999
  // TODO: optimize selector
9610
10000
  var sel = '[data-id="' + id + '"]';
9611
- return parent.querySelector(sel);
10001
+ var activeLayer = hit.getHitTarget();
10002
+ return activeLayer.gui.svg_container.querySelector(sel);
9612
10003
  }
9613
10004
 
9614
10005
  // function getTextTarget2(e) {
@@ -9655,7 +10046,7 @@
9655
10046
  // fails when symbol includes a dot (<g><circle/><text/></g> structure)
9656
10047
  function updateSymbolNode(node, d, id) {
9657
10048
  var o = internal.svg.renderStyledLabel(d); // TODO: symbol support
9658
- var activeLayer = hit.getHitTarget().layer;
10049
+ var activeLayer = hit.getHitTarget();
9659
10050
  var xy = activeLayer.shapes[id][0];
9660
10051
  var g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
9661
10052
  var node2;
@@ -9688,7 +10079,7 @@
9688
10079
  hit.on('drag', function(e) {
9689
10080
  if (!active(e)) return;
9690
10081
  // TODO: support multi points... get id of closest part to the pointer
9691
- var p = getPointCoordsById(e.id, symbolInfo.target.layer);
10082
+ var p = getPointCoordsById(e.id, symbolInfo.target);
9692
10083
  if (!p) return;
9693
10084
  var diff = translateDeltaDisplayCoords(e.dx, e.dy, ext);
9694
10085
  p[0] += diff[0];
@@ -9719,8 +10110,8 @@
9719
10110
  }
9720
10111
  }
9721
10112
 
9722
- // pointer thresholds for hovering near a vertex or segment midpoint
9723
- var HOVER_THRESHOLD$1 = 10;
10113
+ // pixel distance threshold for hovering near a vertex or segment midpoint
10114
+ var HOVER_THRESHOLD = 10;
9724
10115
 
9725
10116
  function initLineEditing(gui, ext, hit) {
9726
10117
  var hoverVertexInfo;
@@ -9746,7 +10137,7 @@
9746
10137
  }
9747
10138
 
9748
10139
  function polygonMode() {
9749
- return active() && hit.getHitTarget().layer.geometry_type == 'polygon';
10140
+ return active() && hit.getHitTarget().geometry_type == 'polygon';
9750
10141
  }
9751
10142
 
9752
10143
  function clearHoverVertex() {
@@ -9762,7 +10153,7 @@
9762
10153
  if (e.mode == 'drawing') {
9763
10154
  gui.enterMode('drawing_tool');
9764
10155
  } else if (active()) {
9765
- gui.clearMode();
10156
+ turnOff();
9766
10157
  }
9767
10158
  }, null, 10); // higher priority than hit control, so turnOff() has correct hit target
9768
10159
 
@@ -9789,27 +10180,10 @@
9789
10180
  appendVertex$1(target, e.p);
9790
10181
  }
9791
10182
  if (e.shapes) {
9792
- replaceShapes(e.shapes);
10183
+ replaceDrawnShapes(e.shapes);
9793
10184
  }
9794
10185
  });
9795
10186
 
9796
- function replaceShapes(shapes) {
9797
- var target = hit.getHitTarget();
9798
- var records = target.layer.data?.getRecords();
9799
- var prevLen = target.layer.shapes.length;
9800
- var newLen = initialShapeCount + shapes.length;
9801
- var recordCount = records?.length || 0;
9802
- target.layer.shapes = target.layer.shapes.slice(0, initialShapeCount).concat(shapes);
9803
- while (records && records.length > newLen) {
9804
- records.pop();
9805
- }
9806
- while (records && records.length < newLen) {
9807
- appendNewDataRecord(target.layer);
9808
- }
9809
- }
9810
-
9811
-
9812
-
9813
10187
  gui.on('undo_path_extend', function(e) {
9814
10188
  var target = hit.getHitTarget();
9815
10189
  if (drawing() && prevHoverEvent) {
@@ -9822,14 +10196,14 @@
9822
10196
  gui.undo.undo(); // remove the path
9823
10197
  }
9824
10198
  if (e.shapes) {
9825
- replaceShapes(e.shapes);
10199
+ replaceDrawnShapes(e.shapes);
9826
10200
  }
9827
10201
  });
9828
10202
 
9829
10203
  function turnOn() {
9830
10204
  var target = hit.getHitTarget();
9831
- initialArcCount = target.arcs.size();
9832
- initialShapeCount = target.layer.shapes.length;
10205
+ initialArcCount = target.gui.displayArcs.size();
10206
+ initialShapeCount = target.shapes.length;
9833
10207
  if (sessionCount === 0) {
9834
10208
  showInstructions();
9835
10209
  }
@@ -9845,6 +10219,12 @@
9845
10219
  non_blocking: true, max_width: '360px'});
9846
10220
  }
9847
10221
 
10222
+ function hideInstructions() {
10223
+ if (!alert) return;
10224
+ alert.close('fade');
10225
+ alert = null;
10226
+ }
10227
+
9848
10228
  function turnOff() {
9849
10229
  var removed = 0;
9850
10230
  finishCurrentPath();
@@ -9852,10 +10232,7 @@
9852
10232
  removed = removeOpenPolygons();
9853
10233
  }
9854
10234
  clearDrawingInfo();
9855
- if (alert) {
9856
- alert.close();
9857
- alert = null;
9858
- }
10235
+ hideInstructions();
9859
10236
  initialArcCount = -1;
9860
10237
  initialShapeCount = -1;
9861
10238
  if (gui.interaction.getMode() == 'drawing') {
@@ -9870,20 +10247,21 @@
9870
10247
  // returns number of removed shapes
9871
10248
  function removeOpenPolygons() {
9872
10249
  var target = hit.getHitTarget();
9873
- var arcs = target.source.dataset.arcs;
9874
- var n = target.layer.shapes.length;
9875
- // delete open paths (should only occur on single-arc shapes)
10250
+ var arcs = target.gui.source.dataset.arcs;
10251
+ var n = target.shapes.length;
10252
+ // delete open paths
9876
10253
  for (var i=initialShapeCount; i<n; i++) {
9877
- var shp = target.layer.shapes[i];
9878
- if (!geom.pathIsClosed(shp[0], arcs)) {
9879
- target.layer.shapes[i] = null;
10254
+ var shp = target.shapes[i];
10255
+ if (!geom.pathIsClosed(shp[0], arcs)) { // assume open paths have one arc
10256
+ target.shapes[i] = null;
9880
10257
  }
9881
10258
  }
9882
- // removes polygons with wrong winding order and null geometry
9883
- mapshaper.cmd.filterFeatures(target.layer, arcs, {remove_empty: true, quiet: true});
9884
- return n - target.layer.shapes.length;
10259
+ // removes features with wrong winding order or null geometry
10260
+ mapshaper.cmd.filterFeatures(target, arcs, {remove_empty: true, quiet: true});
10261
+ return n - target.shapes.length;
9885
10262
  }
9886
10263
 
10264
+ // updates display arcs and redraws all layers
9887
10265
  function fullRedraw() {
9888
10266
  gui.model.updated({arc_count: true});
9889
10267
  }
@@ -9895,13 +10273,25 @@
9895
10273
  prevClickEvent = prevHoverEvent = null;
9896
10274
  }
9897
10275
 
10276
+ hit.on('contextmenu', function(e) {
10277
+ if (!active() || drawing() || dragging()) return;
10278
+ var target = hit.getHitTarget();
10279
+ var vInfo = hoverVertexInfo;
10280
+ if (hoverVertexInfo?.type == 'vertex' && !vertexIsEndpoint(vInfo, target)) {
10281
+ e.deleteVertex = function() {
10282
+ deleteActiveVertex(e, vInfo);
10283
+ };
10284
+ }
10285
+ // don't allow copying of open paths as geojson in polygon mode
10286
+ gui.contextMenu.open(e, target);
10287
+ });
10288
+
9898
10289
  hit.on('dragstart', function(e) {
9899
10290
  if (!active() || drawing() || !hoverVertexInfo) return;
9900
- if (alert) {
9901
- alert.close('fade');
9902
- }
10291
+ hideInstructions();
9903
10292
  e.originalEvent.stopPropagation();
9904
10293
  _dragging = true;
10294
+ updateCursor();
9905
10295
  if (hoverVertexInfo.type == 'interpolated') {
9906
10296
  insertVertex$1(hit.getHitTarget(), hoverVertexInfo.i, hoverVertexInfo.point);
9907
10297
  hoverVertexInfo.ids = [hoverVertexInfo.i];
@@ -9915,11 +10305,10 @@
9915
10305
  var target = hit.getHitTarget();
9916
10306
  var p = ext.translatePixelCoords(e.x, e.y);
9917
10307
  if (gui.keyboard.shiftIsPressed()) {
9918
- internal.snapPointToArcEndpoint(p, hoverVertexInfo.ids, target.arcs);
10308
+ internal.snapPointToArcEndpoint(p, hoverVertexInfo.ids, target.gui.displayArcs);
9919
10309
  }
9920
- internal.snapVerticesToPoint(hoverVertexInfo.ids, p, target.arcs);
10310
+ internal.snapVerticesToPoint(hoverVertexInfo.ids, p, target.gui.displayArcs);
9921
10311
  hit.setHoverVertex(p, '');
9922
-
9923
10312
  // redrawing the whole map updates the data layer as well as the overlay layer
9924
10313
  // gui.dispatchEvent('map-needs-refresh');
9925
10314
  });
@@ -9927,9 +10316,9 @@
9927
10316
  hit.on('dragend', function(e) {
9928
10317
  if (!dragging()) return;
9929
10318
  _dragging = false;
9930
- // kludge to get dataset to recalculate internal bounding boxes
9931
10319
  var target = hit.getHitTarget();
9932
- target.arcs.transformPoints(function() {});
10320
+ // kludge to get dataset to recalculate internal bounding boxes
10321
+ target.gui.displayArcs.transformPoints(function() {});
9933
10322
  updateVertexCoords(target, hoverVertexInfo.ids);
9934
10323
  gui.dispatchEvent('vertex_dragend', hoverVertexInfo);
9935
10324
  gui.dispatchEvent('map-needs-refresh'); // redraw basemap
@@ -9953,6 +10342,7 @@
9953
10342
  // ... or the closest point along the segment (for adding a new vertex)
9954
10343
  hit.on('hover', function(e) {
9955
10344
  if (!active() || dragging()) return;
10345
+
9956
10346
  if (drawing()) {
9957
10347
  if (!e.overMap) {
9958
10348
  finishCurrentPath();
@@ -9975,6 +10365,7 @@
9975
10365
  } else {
9976
10366
  clearHoverVertex();
9977
10367
  }
10368
+ updateCursor();
9978
10369
  prevHoverEvent = e;
9979
10370
  }, null, 100);
9980
10371
 
@@ -9985,23 +10376,22 @@
9985
10376
  var p = pixToDataCoords(e.x, e.y);
9986
10377
  if (drawing()) {
9987
10378
  extendCurrentPath(hoverVertexInfo?.point || p);
9988
- // extendCurrentPath(p); // just extend to current mouse position (not hover vertex)
9989
10379
  } else if (gui.keyboard.shiftIsPressed()) {
9990
10380
  deleteActiveVertex(e);
9991
10381
  } else {
9992
10382
  startNewPath(p);
9993
- if (alert) {
9994
- alert.close('fade');
9995
- }
10383
+ hideInstructions();
10384
+ updateCursor();
9996
10385
  }
9997
10386
  prevClickEvent = e;
9998
10387
  });
9999
10388
 
10000
- // esc key finishes a path
10389
+ // esc or enter key finishes a path
10001
10390
  gui.keyboard.on('keydown', function(e) {
10002
10391
  if (active() && (e.keyName == 'esc' || e.keyName == 'enter')) {
10003
10392
  e.stopPropagation();
10004
10393
  finishCurrentPath();
10394
+ e.originalEvent.preventDefault(); // block console "enter"
10005
10395
  }
10006
10396
  }, null, 10);
10007
10397
 
@@ -10015,16 +10405,24 @@
10015
10405
  return dbl;
10016
10406
  }
10017
10407
 
10018
- function deleteActiveVertex(e) {
10019
- var info = findDraggableVertices(e);
10020
- if (!info) return;
10408
+ function updateCursor() {
10409
+ var useArrow = hoverVertexInfo && !hoverVertexInfo.extendable && !drawing();
10410
+ gui.container.findChild('.map-layers').classed('dragging', useArrow);
10411
+ }
10412
+
10413
+ function vertexIsEndpoint(info, target) {
10414
+ var vId = info.ids[0];
10415
+ return internal.vertexIsArcStart(vId, target.gui.displayArcs) ||
10416
+ internal.vertexIsArcEnd(vId, target.gui.displayArcs);
10417
+ }
10418
+
10419
+ // info: optional vertex info object
10420
+ function deleteActiveVertex(e, infoArg) {
10421
+ var info = infoArg || findDraggableVertices(e);
10422
+ if (!info || info.type != 'vertex') return;
10021
10423
  var vId = info.ids[0];
10022
10424
  var target = hit.getHitTarget();
10023
- if (internal.vertexIsArcStart(vId, target.arcs) ||
10024
- internal.vertexIsArcEnd(vId, target.arcs)) {
10025
- // TODO: support removing arc endpoints
10026
- return;
10027
- }
10425
+ if (vertexIsEndpoint(info, target)) return;
10028
10426
  gui.dispatchEvent('vertex_delete', {
10029
10427
  target: target,
10030
10428
  vertex_id: vId
@@ -10050,25 +10448,27 @@
10050
10448
  var dist2 = dist / Math.sqrt(2);
10051
10449
  var minDist = Infinity;
10052
10450
  var cands = [
10053
- {x: x0, y: y0 + dist},
10054
- {x: x0, y: y0 - dist},
10055
- {x: x0 + dist, y: y0},
10056
- {x: x0 - dist, y: y0},
10057
- {x: x0 + dist2, y: y0 + dist2},
10058
- {x: x0 + dist2, y: y0 - dist2},
10059
- {x: x0 - dist2, y: y0 + dist2},
10060
- {x: x0 - dist2, y: y0 - dist2}
10451
+ {dx: 0, dy: dist},
10452
+ {dx: 0, dy: -dist},
10453
+ {dx: dist, dy: 0},
10454
+ {dx: -dist, dy: 0},
10455
+ {dx: dist2, dy: dist2},
10456
+ {dx: dist2, dy: -dist2},
10457
+ {dx: -dist2, dy: dist2},
10458
+ {dx: -dist2, dy: -dist2}
10061
10459
  ];
10062
10460
  var snapped = cands.reduce(function(memo, cand) {
10063
- var dist = geom.distance2D(thisEvt.x, thisEvt.y, cand.x, cand.y);
10461
+ var dist = geom.distance2D(thisEvt.x, thisEvt.y, x0 + cand.dx, y0 + cand.dy);
10064
10462
  if (dist < minDist) {
10065
10463
  minDist = dist;
10066
10464
  return cand;
10067
10465
  }
10068
10466
  return memo;
10069
10467
  }, null);
10070
- thisEvt.x = snapped.x;
10071
- thisEvt.y = snapped.y;
10468
+ thisEvt.x = x0 + snapped.dx;
10469
+ thisEvt.y = y0 + snapped.dy;
10470
+
10471
+ return null;
10072
10472
  }
10073
10473
 
10074
10474
  function finishCurrentPath() {
@@ -10089,7 +10489,7 @@
10089
10489
  var p1 = hoverVertexInfo?.point || p2;
10090
10490
  appendNewPath(target, p1, p2);
10091
10491
  gui.dispatchEvent('path_add', {target, p1, p2});
10092
- drawingId = target.layer.shapes.length - 1;
10492
+ drawingId = target.shapes.length - 1;
10093
10493
  hit.setDrawingId(drawingId);
10094
10494
  }
10095
10495
 
@@ -10103,11 +10503,11 @@
10103
10503
  error$1('Defective path');
10104
10504
  }
10105
10505
  if (finish && polygonMode()) {
10106
- shapes1 = target.layer.shapes.slice(initialShapeCount);
10107
- shapes2 = tryToClosePath(shapes1);
10506
+ shapes1 = target.shapes.slice(initialShapeCount);
10507
+ shapes2 = convertClosedPaths(shapes1);
10108
10508
  }
10109
10509
  if (shapes2) {
10110
- replaceShapes(shapes2);
10510
+ replaceDrawnShapes(shapes2);
10111
10511
  gui.dispatchEvent('path_extend', {target, p, shapes1, shapes2});
10112
10512
  clearDrawingInfo();
10113
10513
  fullRedraw();
@@ -10117,13 +10517,27 @@
10117
10517
  gui.dispatchEvent('path_extend', {target, p});
10118
10518
  hit.triggerChangeEvent(); // trigger overlay redraw
10119
10519
  }
10520
+ }
10120
10521
 
10522
+ function replaceDrawnShapes(shapes) {
10523
+ var target = hit.getHitTarget();
10524
+ var records = target.data?.getRecords();
10525
+ var prevLen = target.shapes.length;
10526
+ var newLen = initialShapeCount + shapes.length;
10527
+ var recordCount = records?.length || 0;
10528
+ target.shapes = target.shapes.slice(0, initialShapeCount).concat(shapes);
10529
+ while (records && records.length > newLen) {
10530
+ records.pop();
10531
+ }
10532
+ while (records && records.length < newLen) {
10533
+ appendNewDataRecord(target);
10534
+ }
10121
10535
  }
10122
10536
 
10123
10537
  // p: [x, y] source data coordinates
10124
10538
  function updatePathEndpoint(p) {
10125
10539
  var target = hit.getHitTarget();
10126
- var i = target.arcs.getPointCount() - 1;
10540
+ var i = target.gui.displayArcs.getPointCount() - 1;
10127
10541
  if (hoverVertexInfo) {
10128
10542
  p = hoverVertexInfo.point; // snap to selected point
10129
10543
  }
@@ -10133,8 +10547,8 @@
10133
10547
 
10134
10548
  function findPathStartInfo(e) {
10135
10549
  var target = hit.getHitTarget();
10136
- var arcId = target.arcs.size() - 1;
10137
- var data = target.arcs.getVertexData();
10550
+ var arcId = target.gui.displayArcs.size() - 1;
10551
+ var data = target.gui.displayArcs.getVertexData();
10138
10552
  var i = data.ii[arcId];
10139
10553
  var x = data.xx[i];
10140
10554
  var y = data.yy[i];
@@ -10142,7 +10556,7 @@
10142
10556
  var dist = geom.distance2D(p[0], p[1], x, y);
10143
10557
  var pathLen = data.nn[arcId];
10144
10558
  var pixelDist = dist / ext.getPixelSize();
10145
- if (pixelDist > HOVER_THRESHOLD$1 || pathLen < 4) {
10559
+ if (pixelDist > HOVER_THRESHOLD || pathLen < 4) {
10146
10560
  return null;
10147
10561
  }
10148
10562
  var point = translateDisplayPoint(target, [x, y]);
@@ -10156,41 +10570,38 @@
10156
10570
  //
10157
10571
  function findDraggableVertices(e) {
10158
10572
  var target = hit.getHitTarget();
10159
- var shp = target.layer.shapes[e.id];
10573
+ var shp = target.shapes[e.id];
10160
10574
  var p = ext.translatePixelCoords(e.x, e.y);
10161
- var ids = internal.findNearestVertices(p, shp, target.arcs);
10162
- var p2 = target.arcs.getVertex2(ids[0]);
10575
+ var ids = internal.findNearestVertices(p, shp, target.gui.displayArcs);
10576
+ var p2 = target.gui.displayArcs.getVertex2(ids[0]);
10163
10577
  var dist = geom.distance2D(p[0], p[1], p2[0], p2[1]);
10164
10578
  var pixelDist = dist / ext.getPixelSize();
10165
- if (pixelDist > HOVER_THRESHOLD$1) {
10579
+ if (pixelDist > HOVER_THRESHOLD) {
10166
10580
  return null;
10167
10581
  }
10168
10582
  var point = getVertexCoords(target, ids[0]); // data coordinates
10169
10583
  // find out if the vertex is the endpoint of a single path
10170
10584
  // (which could be extended by a newly drawn path)
10171
10585
  var extendable = ids.length == 1 &&
10172
- internal.vertexIsArcEndpoint(ids[0], target.arcs);
10173
- var displayPoint = target.arcs.getVertex2(ids[0]);
10586
+ internal.vertexIsArcEndpoint(ids[0], target.gui.displayArcs);
10587
+ var displayPoint = target.gui.displayArcs.getVertex2(ids[0]);
10174
10588
  return {target, ids, extendable, point, displayPoint, type: 'vertex'};
10175
10589
  }
10176
10590
 
10591
+
10177
10592
  function findInterpolatedPoint(e) {
10178
10593
  var target = hit.getHitTarget();
10179
10594
  //// vertex insertion not supported with simplification
10180
10595
  // if (!target.arcs.isFlat()) return null;
10181
10596
  var p = ext.translatePixelCoords(e.x, e.y);
10182
10597
  var minDist = Infinity;
10183
- var shp = target.layer.shapes[e.id];
10598
+ var shp = target.shapes[e.id];
10184
10599
  var closest;
10185
- internal.forEachSegmentInShape(shp, target.arcs, function(i, j, xx, yy) {
10600
+ internal.forEachSegmentInShape(shp, target.gui.displayArcs, function(i, j, xx, yy) {
10186
10601
  var x1 = xx[i],
10187
10602
  y1 = yy[i],
10188
10603
  x2 = xx[j],
10189
10604
  y2 = yy[j],
10190
- // switching from midpoint to nearest point to the mouse
10191
- // cx = (x1 + x2) / 2,
10192
- // cy = (y1 + y2) / 2,
10193
- // p2 = [cx, cy],
10194
10605
  p2 = internal.findClosestPointOnSeg(p[0], p[1], x1, y1, x2, y2, 0),
10195
10606
  dist = geom.distance2D(p2[0], p2[1], p[0], p[1]);
10196
10607
  if (dist < minDist) {
@@ -10203,7 +10614,7 @@
10203
10614
  }
10204
10615
  });
10205
10616
 
10206
- if (closest.distance / ext.getPixelSize() > HOVER_THRESHOLD$1) {
10617
+ if (closest.distance / ext.getPixelSize() > HOVER_THRESHOLD) {
10207
10618
  return null;
10208
10619
  }
10209
10620
  closest.point = translateDisplayPoint(target, closest.displayPoint);
@@ -10212,49 +10623,39 @@
10212
10623
  return closest;
10213
10624
  }
10214
10625
 
10215
- // shapes: shapes that have been drawn in the current session
10216
- //
10217
- function tryToClosePath(shapes) {
10626
+ // Try to form polygon shapes from an array of path shapes
10627
+ // shapes: array of all shapes that have been drawn in the current session
10628
+ function convertClosedPaths(shapes) {
10218
10629
  var target = hit.getHitTarget();
10630
+ // try to convert paths to polygons
10631
+ // NOTE: added "no_cuts" option to prevent polygons function from modifying
10632
+ // arcs, which would break undo/redo and cause other problems
10219
10633
  var tmpLyr = {
10220
10634
  geometry_type: 'polyline',
10221
10635
  shapes: shapes.concat()
10222
10636
  };
10223
- // create a temp dataset containing tmp layer and original layers
10224
- // (so original arcs are retained)
10225
- var tmpDataset = Object.assign({}, target.source.dataset);
10226
- tmpDataset.layers = tmpDataset.layers.concat(tmpLyr);
10227
- // NOTE: added "no_cuts" option to prevent polygons function from modifying
10228
- // arcs, which would break undo/redo and cause other problems
10229
- var outputLyr = mapshaper.cmd.polygons([tmpLyr], tmpDataset, {no_cuts: true})[0];
10230
- var isOpenPath = getOpenPathTest(outputLyr.shapes);
10231
- var shapes2 = [];
10232
- shapes.forEach(function(shp) {
10233
- if (isOpenPath(shp)) {
10234
- shapes2.push(shp);
10235
- }
10236
- });
10237
- return shapes2.concat(outputLyr.shapes);
10637
+ var output = mapshaper.cmd.polygons([tmpLyr], target.gui.source.dataset, {no_cuts: true});
10638
+ var closedShapes = output[0].shapes;
10639
+
10640
+ // find paths that were not convertible to polygons
10641
+ var isOpenPath = getOpenPathTest(closedShapes);
10642
+ var openShapes = shapes.filter(function(shp) { return isOpenPath(shp); });
10643
+
10644
+ // retain both converted polygons and unconverted polylines
10645
+ return openShapes.concat(closedShapes);
10238
10646
  }
10239
10647
 
10240
10648
  // Returns a function for testing if a shape is an unclosed path, and doesn't
10241
10649
  // overlap with an array of polygon shapes
10242
- // polygons: array of polygon shapes
10243
- function getOpenPathTest(polygons) {
10244
- var arcs = [];
10245
- internal.forEachArcId(polygons, function(arcId) {
10246
- if (arcId < 0) arcId = ~arcId;
10247
- arcs.push(arcId);
10650
+ function getOpenPathTest(polygonShapes) {
10651
+ var polygonArcs = [];
10652
+ internal.forEachArcId(polygonShapes, function(arcId) {
10653
+ polygonArcs.push(internal.absArcId(arcId));
10248
10654
  });
10655
+
10249
10656
  return function(shp) {
10250
- // assume that compound shapes are already polygons
10251
- var isOpen = false;
10252
- if (shapeHasOneFwdArc(shp)) {
10253
- var arcId = shp[0][0];
10254
- if (arcId < 0) arcId = ~arcId;
10255
- isOpen = !arcs.includes(arcId);
10256
- }
10257
- return isOpen;
10657
+ // assume that any compound shape is a polygon
10658
+ return shapeHasOneFwdArc(shp) && !polygonArcs.includes(shp[0][0]);
10258
10659
  };
10259
10660
  }
10260
10661
 
@@ -10266,213 +10667,29 @@
10266
10667
  function initPointDrawing(gui, ext, hit) {
10267
10668
  var mouse = gui.map.getMouse();
10268
10669
 
10269
- gui.on('interaction_mode_change', function(e) {
10270
- gui.container.findChild('.map-layers').classed('add-points', e.mode === 'add-points');
10271
- });
10272
-
10273
- function active() {
10274
- return gui.interaction.getMode() == 'add-points';
10275
- }
10276
-
10277
- mouse.on('click', function(e) {
10278
- if (!active()) return;
10279
- addPoint(e.x, e.y);
10280
- gui.dispatchEvent('map-needs-refresh');
10281
- });
10282
-
10283
- // x, y: pixel coordinates
10284
- function addPoint(x, y) {
10285
- var p = ext.translatePixelCoords(x, y);
10286
- var target = hit.getHitTarget();
10287
- var lyr = target.layer;
10288
- var fid = lyr.shapes.length;
10289
- var d = appendNewDataRecord(lyr);
10290
- if (d) {
10291
- // this seems to work even for projected layers -- the data tables
10292
- // of projected and original data seem to be shared.
10293
- lyr.data.getRecords()[fid] = d;
10294
- }
10295
- lyr.shapes[fid] = [p];
10296
- updatePointCoords(target, fid);
10297
- }
10298
-
10299
-
10300
- }
10301
-
10302
- // pointer thresholds for hovering near a vertex or segment midpoint
10303
- var HOVER_THRESHOLD = 8;
10304
- var MIDPOINT_THRESHOLD = 11;
10305
-
10306
- function initVertexDragging(gui, ext, hit) {
10307
- var insertionPoint;
10308
- var dragInfo;
10309
-
10310
- function active() {
10311
- return gui.interaction.getMode() == 'vertices';
10312
- }
10313
-
10314
- function dragging() {
10315
- return active() && !!dragInfo;
10316
- }
10317
-
10318
- function setHoverVertex(id) {
10319
- var target = hit.getHitTarget();
10320
- hit.setHoverVertex(target.arcs.getVertex2(id));
10321
- }
10322
-
10323
- function clearHoverVertex() {
10324
- hit.clearHoverVertex();
10325
- }
10326
-
10327
- // return data on the nearest vertex (or identical vertices) to the pointer
10328
- // (if within a distance threshold)
10329
- //
10330
- function findDraggableVertices(e) {
10331
- var target = hit.getHitTarget();
10332
- var shp = target.layer.shapes[e.id];
10333
- var p = ext.translatePixelCoords(e.x, e.y);
10334
- var nearestIds = internal.findNearestVertices(p, shp, target.arcs);
10335
- var p2 = target.arcs.getVertex2(nearestIds[0]);
10336
- var dist = geom.distance2D(p[0], p[1], p2[0], p2[1]);
10337
- var pixelDist = dist / ext.getPixelSize();
10338
- if (pixelDist > HOVER_THRESHOLD) {
10339
- return null;
10340
- }
10341
- var points = nearestIds.map(function(i) {
10342
- return getVertexCoords(target, i); // data coordinates
10343
- });
10344
- return {
10345
- target: target,
10346
- ids: nearestIds,
10347
- points: points
10348
- };
10349
- }
10350
-
10351
- function findVertexInsertionPoint(e) {
10352
- var target = hit.getHitTarget();
10353
- //// vertex insertion not supported with simplification
10354
- // if (!target.arcs.isFlat()) return null;
10355
- var p = ext.translatePixelCoords(e.x, e.y);
10356
- var midpoint = findNearestMidpoint(p, e.id, target);
10357
- if (!midpoint ||
10358
- midpoint.distance / ext.getPixelSize() > MIDPOINT_THRESHOLD) return null;
10359
- return midpoint;
10360
- }
10361
-
10362
- hit.on('dragstart', function(e) {
10363
- if (!active()) return;
10364
- if (insertionPoint) {
10365
- var target = hit.getHitTarget();
10366
- insertVertex$1(target, insertionPoint.i, insertionPoint.point);
10367
- dragInfo = {
10368
- target: target,
10369
- insertion: true,
10370
- ids: [insertionPoint.i],
10371
- points: [insertionPoint.point]
10372
- };
10373
- insertionPoint = null;
10374
- } else {
10375
- dragInfo = findDraggableVertices(e);
10376
- }
10377
- if (dragInfo) {
10378
- setHoverVertex(dragInfo.ids[0]);
10379
- }
10380
- });
10381
-
10382
- hit.on('drag', function(e) {
10383
- if (!dragging()) return;
10384
- var target = hit.getHitTarget();
10385
- var p = ext.translatePixelCoords(e.x, e.y);
10386
- if (gui.keyboard.shiftIsPressed()) {
10387
- internal.snapPointToArcEndpoint(p, dragInfo.ids, target.arcs);
10388
- }
10389
- internal.snapVerticesToPoint(dragInfo.ids, p, target.arcs);
10390
- setHoverVertex(dragInfo.ids[0]);
10391
- // redrawing the whole map updates the data layer as well as the overlay layer
10392
- // gui.dispatchEvent('map-needs-refresh');
10393
- });
10394
-
10395
- hit.on('dragend', function(e) {
10396
- if (!dragging()) return;
10397
- // kludge to get dataset to recalculate internal bounding boxes
10398
- hit.getHitTarget().arcs.transformPoints(function() {});
10399
- clearHoverVertex();
10400
- updateVertexCoords(dragInfo.target, dragInfo.ids);
10401
- gui.dispatchEvent('vertex_dragend', dragInfo);
10402
- gui.dispatchEvent('map-needs-refresh');
10403
- dragInfo = null;
10670
+ gui.on('interaction_mode_change', function(e) {
10671
+ gui.container.findChild('.map-layers').classed('add-points', e.mode === 'add-points');
10404
10672
  });
10405
10673
 
10406
- hit.on('dblclick', function(e) {
10674
+ function active() {
10675
+ return gui.interaction.getMode() == 'add-points';
10676
+ }
10677
+
10678
+ mouse.on('click', function(e) {
10407
10679
  if (!active()) return;
10408
- var info = findDraggableVertices(e); // same selection criteria as for dragging
10409
- if (!info) return;
10680
+ var p = pixToDataCoords(e.x, e.y);
10410
10681
  var target = hit.getHitTarget();
10411
- var vId = info.ids[0];
10412
- if (internal.vertexIsArcStart(vId, target.arcs) ||
10413
- internal.vertexIsArcEnd(vId, target.arcs)) {
10414
- // TODO: support removing arc endpoints
10415
- return;
10416
- }
10417
- gui.dispatchEvent('vertex_delete', {
10418
- target: target,
10419
- vertex_id: vId
10420
- });
10421
- deleteVertex$1(target, vId);
10422
- clearHoverVertex();
10682
+ appendNewPoint(target, p);
10683
+ gui.dispatchEvent('point_add', {p, target});
10423
10684
  gui.dispatchEvent('map-needs-refresh');
10424
10685
  });
10425
10686
 
10426
- // highlight hit vertex in path edit mode
10427
- hit.on('hover', function(e) {
10428
- insertionPoint = null;
10429
- if (!active() || dragging()) return; // no hover effect while dragging
10430
- var info = findDraggableVertices(e);
10431
- if (info) {
10432
- // hovering near a vertex: highlight the vertex
10433
- setHoverVertex(info.ids[0]);
10434
- return;
10435
- }
10436
- // if hovering near a segment midpoint: show the midpoint and save midpoint info
10437
- insertionPoint = findVertexInsertionPoint(e);
10438
- if (insertionPoint) {
10439
- hit.setHoverVertex(insertionPoint.displayPoint);
10440
- } else {
10441
- // pointer is not over a vertex: clear any hover effect
10442
- clearHoverVertex();
10443
- }
10444
- }, null, 100);
10445
- }
10446
10687
 
10688
+ function pixToDataCoords(x, y) {
10689
+ var target = hit.getHitTarget();
10690
+ return translateDisplayPoint(target, ext.translatePixelCoords(x, y));
10691
+ }
10447
10692
 
10448
- // Given a location @p (e.g. corresponding to the mouse pointer location),
10449
- // find the midpoint of two vertices on @shp suitable for inserting a new vertex
10450
- function findNearestMidpoint(p, fid, target) {
10451
- var arcs = target.arcs;
10452
- var shp = target.layer.shapes[fid];
10453
- var minDist = Infinity, v;
10454
- internal.forEachSegmentInShape(shp, arcs, function(i, j, xx, yy) {
10455
- var x1 = xx[i],
10456
- y1 = yy[i],
10457
- x2 = xx[j],
10458
- y2 = yy[j],
10459
- cx = (x1 + x2) / 2,
10460
- cy = (y1 + y2) / 2,
10461
- midpoint = [cx, cy],
10462
- dist = geom.distance2D(cx, cy, p[0], p[1]);
10463
- if (dist < minDist) {
10464
- minDist = dist;
10465
- v = {
10466
- i: (i < j ? i : j) + 1, // insertion point
10467
- segment: [i, j],
10468
- segmentLen: geom.distance2D(x1, y1, x2, y2),
10469
- displayPoint: midpoint,
10470
- point: translateDisplayPoint(target, midpoint),
10471
- distance: dist
10472
- };
10473
- }
10474
- });
10475
- return v || null;
10476
10693
  }
10477
10694
 
10478
10695
  function initInteractiveEditing(gui, ext, hit) {
@@ -10480,7 +10697,6 @@
10480
10697
  initPointDragging(gui, ext, hit);
10481
10698
  initPointDrawing(gui, ext, hit);
10482
10699
  initLineEditing(gui, ext, hit);
10483
- initVertexDragging(gui, ext, hit);
10484
10700
  }
10485
10701
 
10486
10702
  function MapExtent(_position) {
@@ -10493,26 +10709,32 @@
10493
10709
 
10494
10710
  _position.on('resize', function(e) {
10495
10711
  if (ready()) {
10496
- triggerChangeEvent({resize: true});
10712
+ // triggerChangeEvent({resize: true});
10713
+ triggerChangeEvent();
10497
10714
  }
10498
10715
  });
10499
10716
 
10500
10717
  function ready() { return !!_fullBounds; }
10501
10718
 
10502
- this.reset = function() {
10719
+ this.reset = function(fire) {
10503
10720
  if (!ready()) return;
10504
- recenter(_fullBounds.centerX(), _fullBounds.centerY(), 1, {reset: true});
10721
+ recenter(_fullBounds.centerX(), _fullBounds.centerY(), 1);
10722
+ if (fire) {
10723
+ triggerChangeEvent();
10724
+ }
10505
10725
  };
10506
10726
 
10507
10727
  this.home = function() {
10508
10728
  if (!ready()) return;
10509
10729
  recenter(_fullBounds.centerX(), _fullBounds.centerY(), 1);
10730
+ triggerChangeEvent();
10510
10731
  };
10511
10732
 
10512
10733
  this.pan = function(xpix, ypix) {
10513
10734
  if (!ready()) return;
10514
10735
  var t = this.getTransform();
10515
10736
  recenter(_cx - xpix / t.mx, _cy - ypix / t.my);
10737
+ triggerChangeEvent();
10516
10738
  };
10517
10739
 
10518
10740
  // Zoom to @w (width of the map viewport in coordinates)
@@ -10535,6 +10757,7 @@
10535
10757
  cx = fx + dx2,
10536
10758
  cy = fy + dy2;
10537
10759
  recenter(cx, cy, scale);
10760
+ triggerChangeEvent();
10538
10761
  };
10539
10762
 
10540
10763
  this.zoomByPct = function(pct, xpct, ypct) {
@@ -10626,11 +10849,10 @@
10626
10849
  return this.getTransform().invert().transform(x, y);
10627
10850
  };
10628
10851
 
10629
- function recenter(cx, cy, scale, data) {
10852
+ function recenter(cx, cy, scale) {
10630
10853
  scale = scale ? limitScale(scale) : _scale;
10631
10854
  if (cx == _cx && cy == _cy && scale == _scale) return;
10632
10855
  navigate(cx, cy, scale);
10633
- triggerChangeEvent(data);
10634
10856
  }
10635
10857
 
10636
10858
  function navigate(cx, cy, scale) {
@@ -10657,9 +10879,8 @@
10657
10879
  _scale = scale;
10658
10880
  }
10659
10881
 
10660
- function triggerChangeEvent(data) {
10661
- data = data || {};
10662
- _self.dispatchEvent('change', data);
10882
+ function triggerChangeEvent() {
10883
+ _self.dispatchEvent('change');
10663
10884
  }
10664
10885
 
10665
10886
  // stop zooming before rounding errors become too obvious
@@ -10854,47 +11075,46 @@
10854
11075
  }
10855
11076
 
10856
11077
  // TODO: consider moving this upstream
10857
- function getArcsForRendering(obj, ext) {
10858
- var dataset = obj.source.dataset;
11078
+ function getArcsForRendering(lyr, ext) {
11079
+ var dataset = lyr.gui.source.dataset;
10859
11080
  var sourceArcs = dataset.arcs;
10860
- if (obj.geographic && dataset.gui?.displayArcs) {
11081
+ if (lyr.gui.geographic && dataset.gui?.displayArcs) {
10861
11082
  return dataset.gui.displayArcs.getScaledArcs(ext);
10862
11083
  }
10863
- return obj.arcs;
11084
+ return lyr.gui.displayArcs;
10864
11085
  }
10865
11086
 
10866
- function drawOutlineLayerToCanvas(obj, canv, ext) {
11087
+ function drawOutlineLayerToCanvas(lyr, canv, ext) {
10867
11088
  var arcs;
10868
- var style = obj.style;
11089
+ var style = lyr.gui.style;
11090
+ var arcCounts = lyr.gui.arcCounts;
10869
11091
  var darkStyle = {strokeWidth: style.strokeWidth, strokeColor: style.strokeColors[1]},
10870
11092
  lightStyle = {strokeWidth: style.strokeWidth, strokeColor: style.strokeColors[0]};
10871
11093
  var filter;
10872
- if (internal.layerHasPaths(obj.layer)) {
10873
- if (!obj.arcCounts) {
10874
- obj.arcCounts = new Uint8Array(obj.arcs.size());
10875
- internal.countArcsInShapes(obj.layer.shapes, obj.arcCounts);
11094
+ if (internal.layerHasPaths(lyr.gui.displayLayer)) {
11095
+ if (!arcCounts) {
11096
+ arcCounts = lyr.gui.arcCounts = new Uint8Array(lyr.gui.displayArcs.size());
11097
+ internal.countArcsInShapes(lyr.gui.displayLayer.shapes, arcCounts);
10876
11098
  }
10877
- if (obj.arcCounts) {
10878
- arcs = getArcsForRendering(obj, ext);
10879
- if (lightStyle.strokeColor) {
10880
- filter = getArcFilter(arcs, ext, false, obj.arcCounts);
10881
- canv.drawArcs(arcs, lightStyle, filter);
10882
- }
10883
- if (darkStyle.strokeColor && obj.layer.geometry_type != 'point') {
10884
- filter = getArcFilter(arcs, ext, true, obj.arcCounts);
10885
- canv.drawArcs(arcs, darkStyle, filter);
10886
- }
11099
+ arcs = getArcsForRendering(lyr, ext);
11100
+ if (lightStyle.strokeColor) {
11101
+ filter = getArcFilter(arcs, ext, false, arcCounts);
11102
+ canv.drawArcs(arcs, lightStyle, filter);
11103
+ }
11104
+ if (darkStyle.strokeColor && lyr.gui.displayLayer.geometry_type != 'point') {
11105
+ filter = getArcFilter(arcs, ext, true, arcCounts);
11106
+ canv.drawArcs(arcs, darkStyle, filter);
10887
11107
  }
10888
11108
  }
10889
- if (obj.layer.geometry_type == 'point') {
10890
- canv.drawSquareDots(obj.layer.shapes, style);
11109
+ if (lyr.gui.displayLayer.geometry_type == 'point') {
11110
+ canv.drawSquareDots(lyr.gui.displayLayer.shapes, style);
10891
11111
  }
10892
11112
  }
10893
11113
 
10894
- function drawStyledLayerToCanvas(obj, canv, ext) {
11114
+ function drawStyledLayerToCanvas(lyr, canv, ext) {
10895
11115
  // TODO: add filter for out-of-view shapes
10896
- var style = obj.style;
10897
- var layer = obj.layer;
11116
+ var style = lyr.gui.style;
11117
+ var layer = lyr.gui.displayLayer;
10898
11118
  var arcs, filter;
10899
11119
  if (layer.geometry_type == 'point') {
10900
11120
  if (style.type == 'styled') {
@@ -10903,7 +11123,7 @@
10903
11123
  canv.drawSquareDots(layer.shapes, style);
10904
11124
  }
10905
11125
  } else {
10906
- arcs = getArcsForRendering(obj, ext);
11126
+ arcs = getArcsForRendering(lyr, ext);
10907
11127
  filter = getShapeFilter(arcs, ext);
10908
11128
  canv.drawStyledPaths(layer.shapes, arcs, style, filter);
10909
11129
  if (style.vertices) {
@@ -11497,12 +11717,12 @@
11497
11717
  }
11498
11718
  };
11499
11719
 
11500
- el.reposition = function(target, type) {
11720
+ el.reposition = function(lyr, type) {
11501
11721
  resize(ext);
11502
- reposition(target, type, ext);
11722
+ reposition(lyr, type, ext);
11503
11723
  };
11504
11724
 
11505
- el.drawLayer = function(target, type) {
11725
+ el.drawLayer = function(lyr, type) {
11506
11726
  var g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
11507
11727
  var html = '';
11508
11728
  // generate a unique id so layer can be identified when symbols are repositioned
@@ -11510,24 +11730,25 @@
11510
11730
  var id = utils$1.getUniqueName();
11511
11731
  var classNames = [id, 'mapshaper-svg-layer', 'mapshaper-' + type + '-layer'];
11512
11732
  g.setAttribute('class', classNames.join(' '));
11513
- target.svg_id = id;
11733
+ lyr.gui.svg_id = id;
11734
+ lyr.gui.svg_container = g;
11514
11735
  resize(ext);
11515
11736
  if (type == 'label' || type == 'symbol') {
11516
- html = renderSymbols(target.layer, ext);
11737
+ html = renderSymbols(lyr.gui.displayLayer, ext);
11517
11738
  } else if (type == 'furniture') {
11518
- html = renderFurniture(target.layer, ext);
11739
+ html = renderFurniture(lyr.gui.displayLayer, ext);
11519
11740
  }
11520
11741
  g.innerHTML = html;
11521
11742
  svg.append(g);
11522
11743
 
11523
11744
  // prevent svg hit detection on inactive layers
11524
- if (!target.active) {
11745
+ if (!lyr.active) {
11525
11746
  g.style.pointerEvents = 'none';
11526
11747
  }
11527
11748
  };
11528
11749
 
11529
- function reposition(target, type, ext) {
11530
- var container = el.findChild('.' + target.svg_id);
11750
+ function reposition(lyr, type, ext) {
11751
+ var container = el.findChild('.' + lyr.gui.svg_id);
11531
11752
  if (!container || !container.node()) {
11532
11753
  console.error('[reposition] missing SVG container');
11533
11754
  return;
@@ -11535,9 +11756,9 @@
11535
11756
  var elements;
11536
11757
  if (type == 'symbol') {
11537
11758
  elements = El.findAll('.mapshaper-svg-symbol', container.node());
11538
- repositionSymbols(elements, target.layer, ext);
11759
+ repositionSymbols(elements, lyr.gui.displayLayer, ext);
11539
11760
  } else if (type == 'furniture') {
11540
- repositionFurniture(container.node(), target.layer, ext);
11761
+ repositionFurniture(container.node(), lyr.gui.displayLayer, ext);
11541
11762
  } else {
11542
11763
  // container.getElementsByTagName('text')
11543
11764
  error('Unsupported symbol type:', type);
@@ -11552,8 +11773,10 @@
11552
11773
  return el;
11553
11774
  }
11554
11775
 
11555
- function LayerRenderer(gui, container, ext, mouse) {
11776
+ function LayerRenderer(gui, container) {
11556
11777
  var el = El(container),
11778
+ ext = gui.map.getExtent(),
11779
+ mouse = gui.map.getMouse(),
11557
11780
  _mainCanv = new DisplayCanvas().appendTo(el),
11558
11781
  _overlayCanv = new DisplayCanvas().appendTo(el),
11559
11782
  _svg = new SvgDisplayLayer(gui, ext, mouse).appendTo(el),
@@ -11571,7 +11794,7 @@
11571
11794
  _svg.clear();
11572
11795
  }
11573
11796
  layers.forEach(function(lyr) {
11574
- var isSvgLayer = internal.layerHasSvgSymbols(lyr.layer) || internal.layerHasLabels(lyr.layer);
11797
+ var isSvgLayer = internal.layerHasSvgSymbols(lyr) || internal.layerHasLabels(lyr);
11575
11798
  if (isSvgLayer && !needSvgRedraw) {
11576
11799
  _svg.reposition(lyr, 'symbol');
11577
11800
  } else if (isSvgLayer) {
@@ -11620,7 +11843,7 @@
11620
11843
 
11621
11844
  function drawCanvasLayer(lyr, canv) {
11622
11845
  if (!lyr) return;
11623
- if (lyr.style.type == 'outline') {
11846
+ if (lyr.gui.style.type == 'outline') {
11624
11847
  drawOutlineLayerToCanvas(lyr, canv, ext);
11625
11848
  } else {
11626
11849
  drawStyledLayerToCanvas(lyr, canv, ext);
@@ -11688,13 +11911,12 @@
11688
11911
  gui.on('interaction_mode_change', function(e) {
11689
11912
  if (e.mode === 'box') {
11690
11913
  gui.enterMode('box_tool');
11691
- } else if (gui.getMode() == 'box_tool') {
11692
- gui.clearMode();
11914
+ } else if (_on) {
11915
+ turnOff();
11693
11916
  }
11694
11917
  });
11695
11918
 
11696
11919
  gui.on('shift_drag_start', function() {
11697
- // box.classed('zooming', inZoomMode());
11698
11920
  hideCoords();
11699
11921
  });
11700
11922
 
@@ -11760,358 +11982,101 @@
11760
11982
  var el = popup.container();
11761
11983
  el.addClass('option-menu');
11762
11984
  var html = `<p>Enter a width in px, cm or inches to create a frame layer
11763
- for setting the size of the map for symbol scaling in the
11764
- 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>
11765
- <div tabindex="0" class="btn dialog-btn">Create</div></span>`;
11766
- el.html(html);
11767
- var input = el.findChild('.frame-width');
11768
- input.node().focus();
11769
- var btn = el.findChild('.btn').on('click', function() {
11770
- var widthStr = input.node().value.trim();
11771
- if (parseFloat(widthStr) > 0 === false) {
11772
- // invalid input
11773
- input.node().value = '';
11774
- return;
11775
- }
11776
- var cmd = `-rectangle + name=frame bbox='${bbox.join(',')}' width='${widthStr}'`;
11777
- runCommand(cmd);
11778
- popup.close();
11779
- });
11780
- }
11781
-
11782
- return self;
11783
- }
11784
-
11785
- function RectangleControl(gui, hit) {
11786
- var box = new HighlightBox(gui, {name: 'rectangle-tool', persistent: true, handles: true, classname: 'rectangles', draggable: false});
11787
- var _on = false;
11788
- var dragInfo;
11789
-
11790
- gui.addMode('rectangle_tool', turnOn, turnOff);
11791
-
11792
- gui.on('interaction_mode_change', function(e) {
11793
- if (e.mode === 'rectangles') {
11794
- gui.enterMode('rectangle_tool');
11795
- } else if (gui.getMode() == 'rectangle_tool') {
11796
- gui.clearMode();
11797
- }
11798
- });
11799
-
11800
- hit.on('change', function(e) {
11801
- if (!_on) return;
11802
- // TODO: handle multiple hits (see gui-inspection-control)
11803
- var id = e.id;
11804
- if (e.id > -1 && e.pinned) {
11805
- var target = hit.getHitTarget();
11806
- var path = target.layer.shapes[e.id][0];
11807
- var bbox = target.arcs.getSimpleShapeBounds(path).toArray();
11808
- box.setDataCoords(bbox);
11809
- dragInfo = {
11810
- id: e.id,
11811
- target: target,
11812
- ids: [],
11813
- points: []
11814
- };
11815
- var iter = target.arcs.getShapeIter(path);
11816
- while (iter.hasNext()) {
11817
- dragInfo.points.push([iter.x, iter.y]);
11818
- dragInfo.ids.push(iter._arc.i);
11819
- }
11820
- gui.container.findChild('.map-layers').classed('dragging', true);
11821
-
11822
- } else if (dragInfo) {
11823
- gui.dispatchEvent('rectangle_dragend', dragInfo); // save undo state
11824
- gui.container.findChild('.map-layers').classed('dragging', false);
11825
- reset();
11826
- } else {
11827
- box.hide();
11828
- }
11829
-
11830
- });
11831
-
11832
- box.on('handle_drag', function(e) {
11833
- if (!_on || !dragInfo) return;
11834
- var coords = internal.bboxToCoords(box.getDataCoords());
11835
- setRectangleCoords(dragInfo.target, dragInfo.ids, coords);
11836
- gui.dispatchEvent('map-needs-refresh');
11837
- });
11838
-
11839
- function turnOn() {
11840
- box.turnOn();
11841
- _on = true;
11842
- }
11843
-
11844
- function turnOff() {
11845
- box.turnOff();
11846
- if (gui.interaction.getMode() == 'rectangles') {
11847
- // mode change was not initiated by interactive menu -- turn off interactivity
11848
- gui.interaction.turnOff();
11849
- }
11850
- _on = false;
11851
- reset();
11852
- }
11853
-
11854
- function reset() {
11855
- box.hide();
11856
- dragInfo = null;
11857
- }
11858
- }
11859
-
11860
- // Create low-detail versions of large arc collections for faster rendering
11861
- // at zoomed-out scales.
11862
- function enhanceArcCollectionForDisplay(unfilteredArcs) {
11863
- var size = unfilteredArcs.getPointCount(),
11864
- filteredArcs, filteredSegLen;
11865
-
11866
- // Only generate low-detail arcs for larger datasets
11867
- if (size > 5e5) {
11868
- update();
11869
- }
11870
-
11871
- function update() {
11872
- if (unfilteredArcs.getVertexData().zz) {
11873
- // Use precalculated simplification data for vertex filtering, if available
11874
- filteredArcs = initFilteredArcs(unfilteredArcs);
11875
- filteredSegLen = internal.getAvgSegment(filteredArcs);
11876
- } else {
11877
- // Use fast simplification as a fallback
11878
- filteredSegLen = internal.getAvgSegment(unfilteredArcs) * 4;
11879
- filteredArcs = internal.simplifyArcsFast(unfilteredArcs, filteredSegLen);
11880
- }
11881
- }
11882
-
11883
- function initFilteredArcs(arcs) {
11884
- var filterPct = 0.08;
11885
- var nth = Math.ceil(arcs.getPointCount() / 5e5);
11886
- var currInterval = arcs.getRetainedInterval();
11887
- var filterZ = arcs.getThresholdByPct(filterPct, nth);
11888
- var filteredArcs = arcs.setRetainedInterval(filterZ).getFilteredCopy();
11889
- arcs.setRetainedInterval(currInterval); // reset current simplification
11890
- return filteredArcs;
11891
- }
11892
-
11893
- // TODO: better job of detecting arc change... e.g. revision number
11894
- unfilteredArcs.getScaledArcs = function(ext) {
11895
- // check for changes in the number of arcs (probably due to editing)
11896
- if (filteredArcs && filteredArcs.size() != unfilteredArcs.size()) {
11897
- // arc count has changed... probably due to editing
11898
- update();
11899
- if (filteredArcs.size() != unfilteredArcs.size()) {
11900
- throw Error('Internal error');
11901
- }
11902
- }
11903
- if (filteredArcs) {
11904
- // match simplification of unfiltered arcs
11905
- filteredArcs.setRetainedInterval(unfilteredArcs.getRetainedInterval());
11906
- }
11907
- // switch to filtered version of arcs at small scales
11908
- var unitsPerPixel = 1/ext.getTransform().mx,
11909
- useFiltering = filteredArcs && unitsPerPixel > filteredSegLen * 1.5;
11910
- return useFiltering ? filteredArcs : unfilteredArcs;
11911
- };
11912
- }
11913
-
11914
- function getDisplayLayerForTable(table) {
11915
- var n = table.size(),
11916
- cellWidth = 12,
11917
- cellHeight = 5,
11918
- gutter = 6,
11919
- arcs = [],
11920
- shapes = [],
11921
- aspectRatio = 1.1,
11922
- x, y, col, row, blockSize;
11923
-
11924
- if (n > 10000) {
11925
- arcs = null;
11926
- gutter = 0;
11927
- cellWidth = 4;
11928
- cellHeight = 4;
11929
- aspectRatio = 1.45;
11930
- } else if (n > 5000) {
11931
- cellWidth = 5;
11932
- gutter = 3;
11933
- aspectRatio = 1.45;
11934
- } else if (n > 1000) {
11935
- gutter = 3;
11936
- cellWidth = 8;
11937
- aspectRatio = 1.3;
11938
- }
11939
-
11940
- if (n < 25) {
11941
- blockSize = n;
11942
- } else {
11943
- blockSize = Math.sqrt(n * (cellWidth + gutter) / cellHeight / aspectRatio) | 0;
11944
- }
11945
-
11946
- for (var i=0; i<n; i++) {
11947
- row = i % blockSize;
11948
- col = Math.floor(i / blockSize);
11949
- x = col * (cellWidth + gutter);
11950
- y = cellHeight * (blockSize - row);
11951
- if (arcs) {
11952
- arcs.push(getArc(x, y, cellWidth, cellHeight));
11953
- shapes.push([[i]]);
11954
- } else {
11955
- shapes.push([[x, y]]);
11956
- }
11957
- }
11958
-
11959
- function getArc(x, y, w, h) {
11960
- return [[x, y], [x + w, y], [x + w, y - h], [x, y - h], [x, y]];
11961
- }
11962
-
11963
- return {
11964
- layer: {
11965
- geometry_type: arcs ? 'polygon' : 'point',
11966
- shapes: shapes,
11967
- data: table
11968
- },
11969
- arcs: arcs ? new internal.ArcCollection(arcs) : null
11970
- };
11971
- }
11972
-
11973
- // displayCRS: CRS to use for display, or null (which clears any current display CRS)
11974
- function projectDisplayLayer(lyr, displayCRS) {
11975
- var crsInfo = getDatasetCrsInfo(lyr.source.dataset);
11976
- var sourceCRS = crsInfo.crs;
11977
- var lyr2;
11978
- //if (!lyr.geographic || !sourceCRS) {
11979
- // let getDisplayLayer() handle case of unprojectable source
11980
- if (!lyr.geographic) {
11981
- return;
11982
- }
11983
- if (lyr.dynamic_crs && internal.crsAreEqual(sourceCRS, lyr.dynamic_crs)) {
11984
- return;
11985
+ for setting the size of the map for symbol scaling in the
11986
+ 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>
11987
+ <div tabindex="0" class="btn dialog-btn">Create</div></span>`;
11988
+ el.html(html);
11989
+ var input = el.findChild('.frame-width');
11990
+ input.node().focus();
11991
+ var btn = el.findChild('.btn').on('click', function() {
11992
+ var widthStr = input.node().value.trim();
11993
+ if (parseFloat(widthStr) > 0 === false) {
11994
+ // invalid input
11995
+ input.node().value = '';
11996
+ return;
11997
+ }
11998
+ var cmd = `-rectangle + name=frame bbox='${bbox.join(',')}' width='${widthStr}'`;
11999
+ runCommand(cmd);
12000
+ popup.close();
12001
+ });
11985
12002
  }
11986
- lyr2 = getDisplayLayer(lyr.source.layer, lyr.source.dataset, {crs: displayCRS});
11987
- // kludge: copy projection-related properties to original layer
11988
- lyr.dynamic_crs = lyr2.dynamic_crs;
11989
- lyr.layer = lyr2.layer;
11990
12003
 
11991
- if (lyr.style && lyr.style.ids) {
11992
- // re-apply layer filter
11993
- lyr.layer = filterLayerByIds(lyr.layer, lyr.style.ids);
11994
- }
11995
- lyr.invertPoint = lyr2.invertPoint;
11996
- lyr.projectPoint = lyr2.projectPoint;
11997
- lyr.bounds = lyr2.bounds;
11998
- lyr.arcs = lyr2.arcs;
12004
+ return self;
11999
12005
  }
12000
12006
 
12007
+ function RectangleControl(gui, hit) {
12008
+ var box = new HighlightBox(gui, {name: 'rectangle-tool', persistent: true, handles: true, classname: 'rectangles', draggable: false});
12009
+ var _on = false;
12010
+ var dragInfo;
12001
12011
 
12002
- // Wrap a layer in an object along with information needed for rendering
12003
- function getDisplayLayer(layer, dataset, opts) {
12004
- var obj = {
12005
- layer: null,
12006
- arcs: null,
12007
- // display_arcs: null,
12008
- style: null,
12009
- invertPoint: null,
12010
- projectPoint: null,
12011
- source: {
12012
- layer: layer,
12013
- dataset: dataset
12014
- },
12015
- empty: internal.getFeatureCount(layer) === 0
12016
- };
12017
-
12018
- var displayCRS = opts.crs || null;
12019
- // display arcs may have been generated when another layer in the dataset was converted for display... re-use if available
12020
- var displayArcs = dataset.gui?.displayArcs || null;
12021
- var sourceCRS;
12022
- var emptyArcs;
12012
+ gui.addMode('rectangle_tool', turnOn, turnOff);
12023
12013
 
12024
- if (displayCRS && layer.geometry_type) {
12025
- var crsInfo = getDatasetCrsInfo(dataset);
12026
- if (crsInfo.error) {
12027
- // unprojectable dataset -- return empty layer
12028
- obj.empty = true;
12029
- obj.geographic = true;
12030
- } else {
12031
- sourceCRS = crsInfo.crs;
12014
+ gui.on('interaction_mode_change', function(e) {
12015
+ if (e.mode === 'rectangles') {
12016
+ gui.enterMode('rectangle_tool');
12017
+ } else if (_on) {
12018
+ turnOff();
12032
12019
  }
12033
- }
12020
+ });
12034
12021
 
12035
- // Assume that dataset.displayArcs is in the display CRS
12036
- // (it must be deleted upstream if reprojection is needed)
12037
- // if (!obj.empty && dataset.arcs && !displayArcs) {
12038
- if (dataset.arcs && !displayArcs) {
12039
- // project arcs, if needed
12040
- if (needReprojectionForDisplay(sourceCRS, displayCRS)) {
12041
- displayArcs = projectArcsForDisplay(dataset.arcs, sourceCRS, displayCRS);
12022
+ hit.on('change', function(e) {
12023
+ if (!_on) return;
12024
+ // TODO: handle multiple hits (see gui-inspection-control)
12025
+ var id = e.id;
12026
+ if (e.id > -1 && e.pinned) {
12027
+ var target = hit.getHitTarget();
12028
+ var path = target.shapes[e.id][0];
12029
+ var bbox = target.gui.displayArcs.getSimpleShapeBounds(path).toArray();
12030
+ box.setDataCoords(bbox);
12031
+ dragInfo = {
12032
+ id: e.id,
12033
+ target: target,
12034
+ ids: [],
12035
+ points: []
12036
+ };
12037
+ var iter = target.gui.displayArcs.getShapeIter(path);
12038
+ while (iter.hasNext()) {
12039
+ dragInfo.points.push([iter.x, iter.y]);
12040
+ dragInfo.ids.push(iter._arc.i);
12041
+ }
12042
+ gui.container.findChild('.map-layers').classed('dragging', true);
12043
+
12044
+ } else if (dragInfo) {
12045
+ gui.dispatchEvent('rectangle_dragend', dragInfo); // save undo state
12046
+ gui.container.findChild('.map-layers').classed('dragging', false);
12047
+ reset();
12042
12048
  } else {
12043
- // Use original arcs for display if there is no dynamic reprojection
12044
- displayArcs = dataset.arcs;
12049
+ box.hide();
12045
12050
  }
12046
12051
 
12047
- enhanceArcCollectionForDisplay(displayArcs);
12048
- dataset.gui = {displayArcs}; // stash these in the dataset for other layers to use
12049
- }
12052
+ });
12050
12053
 
12051
- if (internal.layerHasFurniture(layer)) {
12052
- obj.layer = layer;
12053
- obj.tabular = true;
12054
- // TODO: consider how to render furniture in GUI
12055
- // obj.furniture = true;
12056
- // obj.furniture_type = internal.getFurnitureLayerType(layer);
12057
- // treating furniture layers (other than frame) as tabular for now,
12058
- // so there is something to show if they are selected
12059
- // obj.tabular = obj.furniture_type != 'frame';
12060
- } else if (layer.geometry_type) {
12061
- obj.geographic = true;
12062
- obj.layer = layer;
12063
- obj.arcs = displayArcs;
12064
- } else if (!obj.empty) {
12065
- obj.tabular = true;
12066
- } else {
12067
- obj.layer = {shapes: []}; // ideally we should avoid empty layers
12068
- }
12054
+ box.on('handle_drag', function(e) {
12055
+ if (!_on || !dragInfo) return;
12056
+ var coords = internal.bboxToCoords(box.getDataCoords());
12057
+ setRectangleCoords(dragInfo.target, dragInfo.ids, coords);
12058
+ gui.dispatchEvent('map-needs-refresh');
12059
+ });
12069
12060
 
12070
- if (obj.tabular) {
12071
- utils$1.extend(obj, getDisplayLayerForTable(layer.data));
12061
+ function turnOn() {
12062
+ box.turnOn();
12063
+ _on = true;
12072
12064
  }
12073
12065
 
12074
- // dynamic reprojection (arcs were already reprojected above)
12075
- if (obj.geographic && needReprojectionForDisplay(sourceCRS, displayCRS)) {
12076
- obj.dynamic_crs = displayCRS;
12077
- obj.invertPoint = internal.getProjTransform2(displayCRS, sourceCRS);
12078
- obj.projectPoint = internal.getProjTransform2(sourceCRS, displayCRS);
12079
- if (internal.layerHasPoints(layer)) {
12080
- obj.layer = projectPointsForDisplay(layer, sourceCRS, displayCRS);
12081
- } else if (internal.layerHasPaths(layer)) {
12082
- emptyArcs = findEmptyArcs(displayArcs);
12083
- if (emptyArcs.length > 0) {
12084
- // Don't try to draw paths containing coordinates that failed to project
12085
- obj.layer = internal.filterPathLayerByArcIds(obj.layer, emptyArcs);
12086
- }
12066
+ function turnOff() {
12067
+ box.turnOff();
12068
+ if (gui.interaction.getMode() == 'rectangles') {
12069
+ // mode change was not initiated by interactive menu -- turn off interactivity
12070
+ gui.interaction.turnOff();
12087
12071
  }
12072
+ _on = false;
12073
+ reset();
12088
12074
  }
12089
12075
 
12090
- obj.bounds = getDisplayBounds(obj.layer, obj.arcs);
12091
- return obj;
12092
- }
12093
-
12094
-
12095
- function getDisplayBounds(lyr, arcs) {
12096
- var bounds = internal.getLayerBounds(lyr, arcs) || new Bounds();
12097
- if (lyr.geometry_type == 'point' && arcs && bounds.hasBounds() && bounds.area() > 0 === false) {
12098
- // if a point layer has no extent (e.g. contains only a single point),
12099
- // then merge with arc bounds, to place the point in context.
12100
- bounds = bounds.mergeBounds(arcs.getBounds());
12101
- }
12102
- return bounds;
12103
- }
12104
-
12105
- // Returns an array of ids of empty arcs (arcs can be set to empty if errors occur while projecting them)
12106
- function findEmptyArcs(arcs) {
12107
- var nn = arcs.getVertexData().nn;
12108
- var ids = [];
12109
- for (var i=0, n=nn.length; i<n; i++) {
12110
- if (nn[i] === 0) {
12111
- ids.push(i);
12112
- }
12076
+ function reset() {
12077
+ box.hide();
12078
+ dragInfo = null;
12113
12079
  }
12114
- return ids;
12115
12080
  }
12116
12081
 
12117
12082
  function loadScript(url, cb) {
@@ -12164,15 +12129,9 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12164
12129
  turnOff();
12165
12130
  });
12166
12131
 
12167
- // hideBtn.on('mousedown', function() {
12168
- // if (activeStyle) {
12169
- // mapEl.css('visibility', 'hidden');
12170
- // hidden = true;
12171
- // }
12172
- // })
12173
12132
  clearBtn.on('click', function() {
12174
12133
  if (activeStyle) {
12175
- updateStyle(null);
12134
+ turnOffBasemap();
12176
12135
  updateButtons();
12177
12136
  }
12178
12137
  });
@@ -12196,28 +12155,34 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12196
12155
  params.styles.forEach(function(style) {
12197
12156
  var btn = El('div').html(`<div class="basemap-style-btn"><img src="${style.icon}"></img></div><div class="basemap-style-label">${style.name}</div>`);
12198
12157
  btn.findChild('.basemap-style-btn').on('click', function() {
12199
- updateStyle(style == activeStyle ? null : style);
12158
+ if (style == activeStyle) {
12159
+ turnOffBasemap();
12160
+ } else {
12161
+ showBasemap(style);
12162
+ }
12200
12163
  updateButtons();
12201
12164
  });
12202
12165
  btn.appendTo(list);
12203
12166
  });
12204
12167
  }
12205
12168
 
12206
- function updateStyle(style) {
12207
- activeStyle = style || null;
12208
- // TODO: consider enabling this
12169
+ function turnOffBasemap() {
12170
+ activeStyle = null;
12171
+ gui.map.setDisplayCRS(null);
12172
+ refresh();
12173
+ }
12174
+
12175
+ function showBasemap(style) {
12176
+ activeStyle = style;
12177
+ // TODO: consider enabling dark basemap mode
12209
12178
  // Make sure that the selected layer style gets updated in gui-map.js
12210
12179
  // gui.state.dark_basemap = style && style.dark || false;
12211
- if (!style) {
12212
- gui.map.setDisplayCRS(null);
12213
- refresh();
12214
- } else if (map) {
12180
+ if (map) {
12215
12181
  map.setStyle(style.url);
12216
12182
  refresh();
12217
- } else {
12183
+ } else if (prepareMapView()) {
12218
12184
  initMap();
12219
12185
  }
12220
-
12221
12186
  }
12222
12187
 
12223
12188
  function updateButtons() {
@@ -12227,8 +12192,9 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12227
12192
  }
12228
12193
 
12229
12194
  function turnOn() {
12230
- var activeLyr = gui.model.getActiveLayer();
12231
- var info = getDatasetCrsInfo(activeLyr.dataset);
12195
+ // TODO: show basemap even if there is no data
12196
+ var activeLyr = gui.model.getActiveLayer(); // may be null
12197
+ var info = getDatasetCrsInfo(activeLyr?.dataset); // defaults to wgs84
12232
12198
  var dataCRS = info.crs || null;
12233
12199
  var displayCRS = gui.map.getDisplayCRS();
12234
12200
  var warning;
@@ -12271,9 +12237,9 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12271
12237
 
12272
12238
  function getLonLatBounds() {
12273
12239
  var bbox = ext.getBounds().toArray();
12274
- var tr = fromWebMercator(bbox[2], bbox[3]);
12275
- var bl = fromWebMercator(bbox[0], bbox[1]);
12276
- return bl.concat(tr);
12240
+ var bbox2 = fromWebMercator(bbox[0], bbox[1])
12241
+ .concat(fromWebMercator(bbox[2], bbox[3]));
12242
+ return bbox2;
12277
12243
  }
12278
12244
 
12279
12245
  function initMap() {
@@ -12331,6 +12297,15 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12331
12297
  return true;
12332
12298
  }
12333
12299
 
12300
+ function prepareMapView() {
12301
+ var crs = gui.map.getDisplayCRS();
12302
+ if (!crs) return false;
12303
+ if (!internal.isWebMercator(crs)) {
12304
+ gui.map.setDisplayCRS(internal.parseCrsString('webmercator'));
12305
+ }
12306
+ return true;
12307
+ }
12308
+
12334
12309
  function refresh() {
12335
12310
  var crs = gui.map.getDisplayCRS();
12336
12311
  var off = !crs || !enabled() || !map || loading || !activeStyle;
@@ -12342,9 +12317,7 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12342
12317
  return;
12343
12318
  }
12344
12319
 
12345
- if (!internal.isWebMercator(crs)) {
12346
- gui.map.setDisplayCRS(internal.parseCrsString('webmercator'));
12347
- }
12320
+ prepareMapView();
12348
12321
  var bbox = getLonLatBounds();
12349
12322
  if (!checkBounds(bbox)) {
12350
12323
  // map does not display outside these bounds
@@ -12402,13 +12375,19 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12402
12375
 
12403
12376
  model.on('update', onUpdate);
12404
12377
 
12378
+ document.addEventListener('visibilitychange', function(e) {
12379
+ // refresh map when browser tab is re-activated (Chrome on mac has been
12380
+ // blanking the canvas after several other tabs are visited)
12381
+ if (document.visibilityState == 'visible') drawLayers();
12382
+ });
12405
12383
 
12406
12384
  // Update display of segment intersections
12407
12385
  this.setIntersectionLayer = function(lyr, dataset) {
12408
12386
  if (lyr == _intersectionLyr) return; // no change
12409
12387
  if (lyr) {
12410
- _intersectionLyr = getDisplayLayer(lyr, dataset, getDisplayOptions());
12411
- _intersectionLyr.style = getIntersectionStyle(_intersectionLyr.layer, getGlobalStyleOptions());
12388
+ enhanceLayerForDisplay(lyr, dataset, getDisplayOptions());
12389
+ _intersectionLyr = lyr;
12390
+ _intersectionLyr.gui.style = getIntersectionStyle(_intersectionLyr.gui.displayLayer, getGlobalStyleOptions());
12412
12391
  } else {
12413
12392
  _intersectionLyr = null;
12414
12393
  }
@@ -12437,9 +12416,16 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12437
12416
  };
12438
12417
 
12439
12418
  this.getDisplayCRS = function() {
12440
- if (!_activeLyr || !_activeLyr.geographic) return null;
12441
- if (_activeLyr.dynamic_crs) return _activeLyr.dynamic_crs;
12442
- var info = getDatasetCrsInfo(_activeLyr.source.dataset);
12419
+ if (!_activeLyr) {
12420
+ return _dynamicCRS || internal.parseCrsString('wgs84');
12421
+ }
12422
+ if (!_activeLyr.gui.geographic) {
12423
+ return null;
12424
+ }
12425
+ if (_activeLyr.gui.dynamic_crs) {
12426
+ return _activeLyr.gui.dynamic_crs;
12427
+ }
12428
+ var info = getDatasetCrsInfo(_activeLyr.gui.source.dataset);
12443
12429
  return info.crs || null;
12444
12430
  };
12445
12431
 
@@ -12471,14 +12457,14 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12471
12457
  var newCRS = utils$1.isString(crs) ? internal.parseCrsString(crs) : crs;
12472
12458
  // TODO: handle case that old and new CRS are the same
12473
12459
  _dynamicCRS = newCRS;
12474
- if (!_activeLyr) return; // stop here if no layers have been selected
12460
+ // if (!_activeLyr) return; // stop here if no layers have been selected
12475
12461
 
12476
12462
  // clear any stored FilteredArcs objects (so they will be recreated with the desired projection)
12477
12463
  clearAllDisplayArcs();
12478
12464
 
12479
12465
  // Reproject all visible map layers
12480
12466
  getContentLayers().forEach(function(lyr) {
12481
- projectDisplayLayer(lyr, newCRS);
12467
+ projectLayerForDisplay(lyr, newCRS);
12482
12468
  });
12483
12469
 
12484
12470
  // kludge to make sure all layers have styles
@@ -12489,6 +12475,34 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12489
12475
  updateFullBounds();
12490
12476
  };
12491
12477
 
12478
+ // Initialization just before displaying the map for the first time
12479
+ this.init = function() {
12480
+ if (_renderer) return;
12481
+ _ext.setFullBounds(calcFullBounds());
12482
+ _ext.resize();
12483
+ _renderer = new LayerRenderer(gui, el);
12484
+ gui.buttons.show();
12485
+
12486
+ if (opts.inspectorControl) {
12487
+ _hit = new HitControl(gui, _ext, _mouse),
12488
+ new InspectionControl2(gui, _hit);
12489
+ new SelectionTool(gui, _ext, _hit),
12490
+ new BoxTool(gui, _ext, _nav),
12491
+ new RectangleControl(gui, _hit),
12492
+ initInteractiveEditing(gui, _ext, _hit);
12493
+ _hit.on('change', updateOverlayLayer);
12494
+ }
12495
+
12496
+ _ext.on('change', function(e) {
12497
+ if (_basemap) _basemap.refresh(); // keep basemap synced up (if enabled)
12498
+ drawLayers(e.redraw ? '' : 'nav');
12499
+ });
12500
+
12501
+ gui.on('resize', function() {
12502
+ position.update(); // kludge to detect new map size after console toggle
12503
+ });
12504
+ };
12505
+
12492
12506
  function getGlobalStyleOptions(opts) {
12493
12507
  var mode = gui.state.interaction_mode;
12494
12508
  return Object.assign({
@@ -12503,9 +12517,8 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12503
12517
  var prevLyr = _activeLyr || null;
12504
12518
  var fullBounds;
12505
12519
  var needReset;
12506
-
12507
12520
  if (!prevLyr) {
12508
- initMap(); // first call
12521
+ // initMap(); // first call
12509
12522
  }
12510
12523
 
12511
12524
  if (arcsMayHaveChanged(e.flags)) {
@@ -12529,9 +12542,14 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12529
12542
  return;
12530
12543
  }
12531
12544
 
12532
- _activeLyr = getDisplayLayer(e.layer, e.dataset, getDisplayOptions());
12533
- _activeLyr.style = getActiveLayerStyle(_activeLyr.layer, getGlobalStyleOptions());
12534
- _activeLyr.active = true;
12545
+ if (e.layer) {
12546
+ enhanceLayerForDisplay(e.layer, e.dataset, getDisplayOptions());
12547
+ _activeLyr = e.layer;
12548
+ _activeLyr.gui.style = getActiveLayerStyle(_activeLyr.gui.displayLayer, getGlobalStyleOptions());
12549
+ _activeLyr.active = true;
12550
+ } else {
12551
+ _activeLyr = null;
12552
+ }
12535
12553
 
12536
12554
  if (popupCanStayOpen(e.flags)) {
12537
12555
  // data may have changed; if popup is open, it needs to be refreshed
@@ -12544,7 +12562,11 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12544
12562
  updateVisibleMapLayers();
12545
12563
  fullBounds = calcFullBounds();
12546
12564
 
12547
- if (!prevLyr || prevLyr.tabular || _activeLyr.tabular) {
12565
+ if (prevLyr?.gui.tabular || _activeLyr?.gui.tabular) {
12566
+ needReset = true;
12567
+ } else if (_activeLyr && internal.layerIsEmpty(_activeLyr)) {
12568
+ needReset = false;
12569
+ } else if (!prevLyr) {
12548
12570
  needReset = true;
12549
12571
  } else {
12550
12572
  needReset = mapNeedsReset(fullBounds, _ext.getFullBounds(), _ext.getBounds(), e.flags);
@@ -12554,45 +12576,19 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12554
12576
 
12555
12577
  if (needReset) {
12556
12578
  _ext.reset();
12579
+ if (_basemap) _basemap.refresh();
12557
12580
  }
12558
12581
  drawLayers();
12559
12582
  map.dispatchEvent('updated');
12560
12583
  }
12561
12584
 
12562
- // Initialization just before displaying the map for the first time
12563
- function initMap() {
12564
- _ext.resize();
12565
- _renderer = new LayerRenderer(gui, el, _ext, _mouse);
12566
- gui.buttons.show();
12567
-
12568
- if (opts.inspectorControl) {
12569
- _hit = new HitControl(gui, _ext, _mouse),
12570
- new InspectionControl2(gui, _hit);
12571
- new SelectionTool(gui, _ext, _hit),
12572
- new BoxTool(gui, _ext, _nav),
12573
- new RectangleControl(gui, _hit),
12574
- initInteractiveEditing(gui, _ext, _hit);
12575
- _hit.on('change', updateOverlayLayer);
12576
- }
12577
-
12578
- _ext.on('change', function(e) {
12579
- if (_basemap) _basemap.refresh(); // keep basemap synced up (if enabled)
12580
- if (e.reset) return; // don't need to redraw map here if extent has been reset
12581
- drawLayers('nav');
12582
- });
12583
-
12584
- gui.on('resize', function() {
12585
- position.update(); // kludge to detect new map size after console toggle
12586
- });
12587
- }
12588
12585
 
12589
12586
  function updateOverlayLayer(e) {
12590
- var style = getOverlayStyle(_activeLyr.layer, e, getGlobalStyleOptions());
12587
+ var style = getOverlayStyle(_activeLyr.gui.displayLayer, e, getGlobalStyleOptions());
12591
12588
  if (style) {
12592
- _overlayLyr = utils$1.defaults({
12593
- layer: filterLayerByIds(_activeLyr.layer, style.ids),
12594
- style: style
12595
- }, _activeLyr);
12589
+ var displayLayer = filterLayerByIds(_activeLyr.gui.displayLayer, style.ids);
12590
+ var gui = Object.assign({}, _activeLyr.gui, {style, displayLayer});
12591
+ _overlayLyr = utils$1.defaults({gui}, _activeLyr);
12596
12592
  } else {
12597
12593
  _overlayLyr = null;
12598
12594
  }
@@ -12623,12 +12619,13 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12623
12619
  var b = new Bounds();
12624
12620
  var layers = getContentLayers();
12625
12621
  layers.forEach(function(lyr) {
12626
- b.mergeBounds(lyr.bounds);
12622
+ b.mergeBounds(lyr.gui.bounds);
12627
12623
  });
12628
12624
 
12629
12625
  if (!b.hasBounds()) {
12630
12626
  // assign bounds to empty layers, to prevent rendering errors downstream
12631
- b.setBounds(0,0,0,0);
12627
+ // b.setBounds(0,0,0,0);
12628
+ b.setBounds(projectLatLonBBox([11.28,33.43,32.26,46.04], _dynamicCRS));
12632
12629
  }
12633
12630
  return b;
12634
12631
  }
@@ -12646,19 +12643,18 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12646
12643
  var widthPx = _ext.width();
12647
12644
  var marginPct = widthPx < 700 && 3.5 || widthPx < 800 && 3 || 2.5;
12648
12645
  if (isTableView()) {
12649
- var n = internal.getFeatureCount(_activeLyr.layer);
12646
+ var n = internal.getFeatureCount(_activeLyr);
12650
12647
  marginPct = n < 5 && 20 || n < 100 && 10 || 4;
12651
12648
  }
12652
12649
  b.scale(1 + marginPct / 100 * 2);
12653
12650
 
12654
12651
  // Inflate display bounding box by a tiny amount (gives extent to single-point layers and collapsed shapes)
12655
12652
  b.padBounds(1e-4, 1e-4, 1e-4, 1e-4);
12656
-
12657
12653
  return b;
12658
12654
  }
12659
12655
 
12660
12656
  function isActiveLayer(lyr) {
12661
- return _activeLyr && lyr == _activeLyr.source.layer || false;
12657
+ return _activeLyr && lyr == _activeLyr || false;
12662
12658
  }
12663
12659
 
12664
12660
  function isVisibleLayer(lyr) {
@@ -12666,12 +12662,12 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12666
12662
  }
12667
12663
 
12668
12664
  function isTableView() {
12669
- return !!_activeLyr.tabular;
12665
+ return !!_activeLyr?.gui.tabular;
12670
12666
  }
12671
12667
 
12672
12668
  function findFrameLayer() {
12673
12669
  return getVisibleMapLayers().find(function(lyr) {
12674
- return internal.isFrameLayer(lyr.layer, lyr.arcs);
12670
+ return internal.isFrameLayer(lyr.gui.displayLayer, lyr.gui.displayArcs);
12675
12671
  });
12676
12672
  }
12677
12673
 
@@ -12682,10 +12678,13 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12682
12678
 
12683
12679
  function getFrameData() {
12684
12680
  var lyr = findFrameLayer();
12685
- return lyr && internal.getFrameLayerData(lyr.layer, lyr.arcs) || null;
12681
+ return lyr && internal.getFrameLayerData(lyr, lyr.gui.displayArcs) || null;
12686
12682
  }
12687
12683
 
12688
12684
  function clearAllDisplayArcs() {
12685
+ model.forEachLayer(function(lyr) {
12686
+ if (lyr.gui) delete lyr.gui.arcCounts;
12687
+ });
12689
12688
  model.getDatasets().forEach(function(o) {
12690
12689
  delete o.gui;
12691
12690
  });
@@ -12698,7 +12697,8 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12698
12697
  if (isActiveLayer(o.layer)) {
12699
12698
  layers.push(_activeLyr);
12700
12699
  } else if (!isTableView()) {
12701
- layers.push(getDisplayLayer(o.layer, o.dataset, getDisplayOptions()));
12700
+ enhanceLayerForDisplay(o.layer, o.dataset, getDisplayOptions());
12701
+ layers.push(o.layer);
12702
12702
  }
12703
12703
  });
12704
12704
  _visibleLayers = layers;
@@ -12720,13 +12720,13 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12720
12720
  return findActiveLayer(layers);
12721
12721
  }
12722
12722
  return layers.filter(function(o) {
12723
- return !!o.geographic;
12723
+ return !!o.gui.geographic;
12724
12724
  });
12725
12725
  }
12726
12726
 
12727
12727
  function getDrawableContentLayers() {
12728
12728
  return getContentLayers().filter(function(lyr) {
12729
- if (isActiveLayer(lyr.layer) && lyr.layer.hidden) return false;
12729
+ if (isActiveLayer(lyr) && lyr.hidden) return false;
12730
12730
  return true;
12731
12731
  });
12732
12732
  }
@@ -12740,31 +12740,33 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12740
12740
 
12741
12741
  function updateLayerStyles(layers) {
12742
12742
  layers.forEach(function(mapLayer, i) {
12743
+ var style;
12743
12744
  if (mapLayer.active) {
12744
12745
  // regenerating active style everytime, to support style change when
12745
12746
  // switching between outline and preview modes.
12746
- mapLayer.style = getActiveLayerStyle(mapLayer.layer, getGlobalStyleOptions());
12747
- if (mapLayer.style.type != 'styled' && layers.length > 1 && mapLayer.style.strokeColors) {
12747
+ style = getActiveLayerStyle(mapLayer.gui.displayLayer, getGlobalStyleOptions());
12748
+ if (style.type != 'styled' && layers.length > 1 && style.strokeColors) {
12748
12749
  // kludge to hide ghosted layers when reference layers are present
12749
12750
  // TODO: consider never showing ghosted layers (which appear after
12750
12751
  // commands like dissolve and filter).
12751
- mapLayer.style = utils$1.defaults({
12752
- strokeColors: [null, mapLayer.style.strokeColors[1]]
12753
- }, mapLayer.style);
12752
+ style = utils$1.defaults({
12753
+ strokeColors: [null, style.strokeColors[1]]
12754
+ }, style);
12754
12755
  }
12755
12756
  } else {
12756
- if (mapLayer.layer == _activeLyr.layer) {
12757
+ if (mapLayer == _activeLyr) {
12757
12758
  console.error("Error: shared map layer");
12758
12759
  }
12759
- mapLayer.style = getReferenceLayerStyle(mapLayer.layer, getGlobalStyleOptions());
12760
+ style = getReferenceLayerStyle(mapLayer.gui.displayLayer, getGlobalStyleOptions());
12760
12761
  }
12762
+ mapLayer.gui.style = style;
12761
12763
  });
12762
12764
  }
12763
12765
 
12764
12766
  function sortMapLayers(layers) {
12765
12767
  layers.sort(function(a, b) {
12766
12768
  // assume that each layer has a menu_order (assigned by updateLayerStackOrder())
12767
- return a.source.layer.menu_order - b.source.layer.menu_order;
12769
+ return a.menu_order - b.menu_order;
12768
12770
  });
12769
12771
  }
12770
12772
 
@@ -12844,6 +12846,102 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12844
12846
  });
12845
12847
  }
12846
12848
 
12849
+ function ContextMenu() {
12850
+ var body = document.querySelector('body');
12851
+ var menu = El('div').addClass('contextmenu rollover').appendTo(body);
12852
+ var _open = false;
12853
+ var _openCount = 0;
12854
+ document.addEventListener('mousedown', close);
12855
+
12856
+ this.isOpen = function() {
12857
+ return _open;
12858
+ };
12859
+
12860
+ function close() {
12861
+ var count = _openCount;
12862
+ if (!_open) return;
12863
+ setTimeout(function() {
12864
+ if (count == _openCount) {
12865
+ menu.hide();
12866
+ _open = false;
12867
+ }
12868
+ }, 300);
12869
+ }
12870
+
12871
+ function addMenuItem(label, func) {
12872
+ El('div')
12873
+ .appendTo(menu)
12874
+ .addClass('contextmenu-item')
12875
+ .html(label)
12876
+ .on('click', func)
12877
+ .show();
12878
+ }
12879
+
12880
+ this.open = function(e, lyr) {
12881
+ if (!lyr || !lyr.gui.geographic) return;
12882
+ _open = true;
12883
+ _openCount++;
12884
+ var rspace = body.clientWidth - e.pageX;
12885
+ var xoffs = 10;
12886
+ menu.empty().show();
12887
+ if (rspace > 150) {
12888
+ menu.css('left', e.pageX + xoffs + 'px');
12889
+ menu.css('right', null);
12890
+ } else {
12891
+ menu.css('right', (body.clientWidth - e.pageX + xoffs) + 'px');
12892
+ menu.css('left', null);
12893
+ }
12894
+ menu.css('top', (e.pageY - 15) + 'px');
12895
+
12896
+ // menu contents
12897
+ if (e.coordinates) {
12898
+ addCopyCoords();
12899
+ }
12900
+ if (e.deleteVertex) {
12901
+ addMenuItem('Delete vertex', e.deleteVertex);
12902
+ }
12903
+ if (e.ids?.length) {
12904
+ addMenuItem('Copy as GeoJSON', copyGeoJSON);
12905
+ }
12906
+
12907
+ function addCopyCoords() {
12908
+ var bbox = internal.getLayerBounds(lyr, lyr.gui.source.dataset.arcs).toArray();
12909
+ var coordStr = internal.getRoundedCoordString(e.coordinates, internal.getBoundsPrecisionForDisplay(bbox));
12910
+ var displayStr = '• &nbsp;' + coordStr.replace(/-/g, '–').replace(',', ', ');
12911
+ addMenuItem(displayStr, function() {
12912
+ saveFileContentToClipboard(coordStr);
12913
+ });
12914
+ }
12915
+
12916
+ function copyGeoJSON() {
12917
+ var opts = {
12918
+ no_replace: true,
12919
+ ids: e.ids,
12920
+ quiet: true
12921
+ };
12922
+ var dataset = lyr.gui.source.dataset;
12923
+ var layer = mapshaper.cmd.filterFeatures(lyr, dataset.arcs, opts);
12924
+ // the drawing tool can send open paths with 'polygon' geometry type,
12925
+ // should be changed to 'polyline'
12926
+ if (layerHasOpenPaths(layer, dataset.arcs)) {
12927
+ layer.geometry_type = 'polyline';
12928
+ }
12929
+ var features = internal.exportLayerAsGeoJSON(layer, dataset, {rfc7946: true, prettify: true}, true, 'string');
12930
+ var str = internal.geojson.formatCollection({"type": "FeatureCollection"}, features);
12931
+ saveFileContentToClipboard(str);
12932
+ }
12933
+ };
12934
+ }
12935
+
12936
+
12937
+ function layerHasOpenPaths(layer, arcs) {
12938
+ var retn = false;
12939
+ internal.editShapes(layer.shapes, function(part) {
12940
+ if (!geom.pathIsClosed(part, arcs)) retn = true;
12941
+ });
12942
+ return retn;
12943
+ }
12944
+
12847
12945
  function GuiInstance(container, opts) {
12848
12946
  var gui = new ModeSwitcher();
12849
12947
  opts = utils$1.extend({
@@ -12862,20 +12960,21 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12862
12960
  gui.model = new Model(gui);
12863
12961
  gui.keyboard = new KeyboardEvents(gui);
12864
12962
  gui.buttons = new SidebarButtons(gui);
12865
- gui.map = new MshpMap(gui);
12866
- gui.interaction = new InteractionMode(gui);
12867
12963
  gui.session = new SessionHistory(gui);
12964
+ gui.contextMenu = new ContextMenu();
12868
12965
  gui.undo = new Undo(gui);
12869
- gui.state = {};
12870
-
12966
+ gui.map = new MshpMap(gui);
12871
12967
  if (opts.saveControl) {
12872
12968
  new SessionSnapshots(gui);
12873
12969
  }
12970
+ gui.interaction = new InteractionMode(gui);
12971
+ gui.state = {};
12874
12972
 
12875
12973
  var msgCount = 0;
12876
12974
  var clearMsg;
12877
12975
 
12878
12976
  initModeRules(gui);
12977
+ gui.map.init();
12879
12978
 
12880
12979
  gui.showProgressMessage = function(msg) {
12881
12980
  if (!gui.progressMessage) {
@@ -12988,9 +13087,10 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12988
13087
  importOpts = getImportOpts(manifest),
12989
13088
  gui = new GuiInstance('body');
12990
13089
 
12991
- if (manifest.blurb) {
12992
- El('#splash-screen-blurb').text(manifest.blurb);
12993
- }
13090
+ // TODO: re-enable the "blurb"
13091
+ // if (manifest.blurb) {
13092
+ // El('#splash-screen-blurb').text(manifest.blurb);
13093
+ // }
12994
13094
 
12995
13095
  new AlertControl(gui);
12996
13096
  new RepairControl(gui);
@@ -13021,17 +13121,19 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
13021
13121
  });
13022
13122
 
13023
13123
  // Initial display configuration
13024
- gui.model.on('select', function() {
13124
+ gui.on('mode', function(e) {
13025
13125
  if (dataLoaded) return;
13026
13126
  dataLoaded = true;
13127
+ gui.buttons.show();
13027
13128
  El('#mode-buttons').show();
13129
+ El('#splash-buttons').hide();
13130
+ El('body').addClass('map-view');
13028
13131
  if (importOpts.display_all) {
13029
13132
  gui.model.getLayers().forEach(function(o) {
13030
13133
  gui.map.setLayerPinning(o, true);
13031
13134
  });
13032
13135
  }
13033
13136
  gui.console.runInitialCommands(getInitialConsoleCommands());
13034
-
13035
13137
  });
13036
13138
  };
13037
13139