mapshaper 0.6.76 → 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.
@@ -1475,6 +1475,7 @@
1475
1475
 
1476
1476
  async function saveSnapshot(gui) {
1477
1477
  var obj = await captureSnapshot(gui);
1478
+ if (!obj) return;
1478
1479
  // storing an unpacked object is usually a bit faster (~20%)
1479
1480
  // note: we don't know the size of unpacked snapshot objects
1480
1481
  // obj = internal.pack(obj);
@@ -1554,12 +1555,13 @@
1554
1555
  }
1555
1556
 
1556
1557
  async function captureSnapshot(gui) {
1557
- var datasets = gui.model.getDatasets();
1558
- var lyr = gui.model.getActiveLayer().layer;
1558
+ var lyr = gui.model.getActiveLayer()?.layer;
1559
+ if (!lyr) return null; // no data -- no snapshot
1560
+ lyr.active = true;
1559
1561
  // compact: true applies compression to vector coordinates, for ~30% reduction
1560
1562
  // in file size in a typical polygon or polyline file, but longer processing time
1561
1563
  var opts = {compact: false};
1562
- lyr.active = true;
1564
+ var datasets = gui.model.getDatasets();
1563
1565
  // console.time('msx');
1564
1566
  var obj = await internal.exportDatasetsToPack(datasets, opts);
1565
1567
  // console.timeEnd('msx')
@@ -1752,7 +1754,7 @@
1752
1754
  }],
1753
1755
  info: {}
1754
1756
  };
1755
- if (type == 'polygon' || type == 'point') {
1757
+ if (type == 'polygon' || type == 'polyline') {
1756
1758
  dataset.arcs = new internal.ArcCollection();
1757
1759
  }
1758
1760
  if (crsInfo) {
@@ -1864,7 +1866,6 @@
1864
1866
  var initialImport = true;
1865
1867
  var importCount = 0;
1866
1868
  var importTotal = 0;
1867
- var overQuickView = false;
1868
1869
  var useQuickView = false;
1869
1870
  var queuedFiles = [];
1870
1871
  var manifestFiles = opts.files || [];
@@ -1874,50 +1875,48 @@
1874
1875
  catalog = new CatalogControl(gui, opts.catalog, downloadFiles);
1875
1876
  }
1876
1877
 
1877
- new SimpleButton('#import-buttons .submit-btn').on('click', importQueuedFiles);
1878
- new SimpleButton('#import-buttons .cancel-btn').on('click', gui.clearMode);
1879
- new DropControl(gui, 'body', receiveFiles);
1880
- new FileChooser('#file-selection-btn', receiveFiles);
1881
- new FileChooser('#import-buttons .add-btn', receiveFiles);
1878
+ var submitBtn = new SimpleButton('#import-options .submit-btn').on('click', importQueuedFiles);
1879
+ new SimpleButton('#import-options .cancel-btn').on('click', gui.clearMode);
1880
+ new DropControl(gui, 'body', receiveFilesWithOption);
1881
+ new FileChooser('#import-options .add-btn', receiveFilesWithOption);
1882
1882
  new FileChooser('#add-file-btn', receiveFiles);
1883
1883
  new SimpleButton('#add-empty-btn').on('click', function() {
1884
1884
  gui.clearMode(); // close import dialog
1885
1885
  openAddLayerPopup(gui);
1886
1886
  });
1887
- initDropArea('#import-quick-drop', true);
1888
- initDropArea('#import-drop');
1887
+ // initDropArea('#import-quick-drop', true);
1888
+ // initDropArea('#import-drop');
1889
1889
  gui.keyboard.onMenuSubmit(El('#import-options'), importQueuedFiles);
1890
1890
 
1891
1891
  gui.addMode('import', turnOn, turnOff);
1892
1892
  gui.enterMode('import');
1893
1893
 
1894
- gui.on('mode', function(e) {
1895
- // re-open import opts if leaving alert or console modes and nothing has been imported yet
1896
- if (!e.name && model.isEmpty()) {
1897
- gui.enterMode('import');
1898
- }
1899
- });
1900
-
1901
- function initDropArea(el, isQuick) {
1902
- var area = El(el)
1903
- .on('dragleave', onout)
1904
- .on('dragover', function() {overQuickView = !!isQuick; onover();})
1905
- .on('mouseover', onover)
1906
- .on('mouseout', onout);
1907
-
1908
- function onover() {
1909
- area.addClass('dragover');
1894
+ function turnOn() {
1895
+ if (manifestFiles.length > 0) {
1896
+ downloadFiles(manifestFiles, true);
1897
+ manifestFiles = [];
1898
+ } else if (model.isEmpty()) {
1899
+ showImportMenu();
1910
1900
  }
1901
+ }
1911
1902
 
1912
- function onout() {
1913
- overQuickView = false;
1914
- area.removeClass('dragover');
1903
+ function turnOff() {
1904
+ var target;
1905
+ if (catalog) catalog.reset(); // re-enable clickable catalog
1906
+ if (importCount > 0) {
1907
+ onImportComplete();
1908
+ importTotal += importCount;
1909
+ importCount = 0;
1915
1910
  }
1911
+ gui.clearProgressMessage();
1912
+ initialImport = false; // unset 'quick view' mode, if on
1913
+ clearQueuedFiles();
1914
+ hideImportMenu();
1916
1915
  }
1917
1916
 
1918
1917
  async function importQueuedFiles() {
1919
- gui.container.removeClass('queued-files');
1920
- gui.container.removeClass('splash-screen');
1918
+ // gui.container.removeClass('queued-files');
1919
+ hideImportMenu();
1921
1920
  var files = queuedFiles;
1922
1921
  try {
1923
1922
  if (files.length > 0) {
@@ -1934,27 +1933,7 @@
1934
1933
  }
1935
1934
  }
1936
1935
 
1937
- function turnOn() {
1938
- if (manifestFiles.length > 0) {
1939
- downloadFiles(manifestFiles, true);
1940
- manifestFiles = [];
1941
- } else if (model.isEmpty()) {
1942
- gui.container.addClass('splash-screen');
1943
- }
1944
- }
1945
1936
 
1946
- function turnOff() {
1947
- var target;
1948
- if (catalog) catalog.reset(); // re-enable clickable catalog
1949
- if (importCount > 0) {
1950
- onImportComplete();
1951
- importTotal += importCount;
1952
- importCount = 0;
1953
- }
1954
- gui.clearProgressMessage();
1955
- initialImport = false; // unset 'quick view' mode, if on
1956
- clearQueuedFiles();
1957
- }
1958
1937
 
1959
1938
  function onImportComplete() {
1960
1939
  // display last layer of last imported dataset
@@ -1987,6 +1966,7 @@
1987
1966
  }, []);
1988
1967
  }
1989
1968
 
1969
+
1990
1970
  function showQueuedFiles() {
1991
1971
  var list = gui.container.findChild('.dropped-file-list').empty();
1992
1972
  queuedFiles.forEach(function(f) {
@@ -2006,13 +1986,20 @@
2006
1986
  }
2007
1987
  });
2008
1988
  });
1989
+ submitBtn.classed('disabled', queuedFiles.length === 0);
1990
+ }
1991
+
1992
+ function receiveFilesWithOption(files) {
1993
+ var quickView = !El('.advanced-import-options').node().checked;
1994
+ receiveFiles(files, quickView);
2009
1995
  }
2010
1996
 
2011
- async function receiveFiles(files) {
1997
+ async function receiveFiles(files, quickView) {
2012
1998
  var names = getFileNames(files);
2013
1999
  var expanded = [];
2014
2000
  if (files.length === 0) return;
2015
- useQuickView = importTotal === 0 && (opts.quick_view || overQuickView);
2001
+ useQuickView = importTotal === 0 && (opts.quick_view ||
2002
+ quickView);
2016
2003
  try {
2017
2004
  expanded = await expandFiles(files);
2018
2005
  } catch(e) {
@@ -2030,12 +2017,23 @@
2030
2017
  if (useQuickView) {
2031
2018
  await importQueuedFiles();
2032
2019
  } else {
2033
- gui.container.addClass('queued-files');
2034
- El('#path-import-options').classed('hidden', !filesMayContainPaths(queuedFiles));
2035
- showQueuedFiles();
2020
+ showImportMenu();
2036
2021
  }
2037
2022
  }
2038
2023
 
2024
+ function showImportMenu() {
2025
+ // gui.container.addClass('queued-files');
2026
+ El('#import-options').show();
2027
+ gui.container.classed('queued-files', queuedFiles.length > 0);
2028
+ El('#path-import-options').classed('hidden', !filesMayContainPaths(queuedFiles));
2029
+ showQueuedFiles();
2030
+ }
2031
+
2032
+ function hideImportMenu() {
2033
+ // gui.container.removeClass('queued-files');
2034
+ El('#import-options').hide();
2035
+ }
2036
+
2039
2037
  function getFileNames(files) {
2040
2038
  return Array.from(files).map(function(f) {return f.name;});
2041
2039
  }
@@ -2531,7 +2529,11 @@
2531
2529
  // prevent GUI message popup on error
2532
2530
  internal.setLoggingForCLI();
2533
2531
  try {
2534
- crs = internal.getDatasetCRS(dataset);
2532
+ if (!dataset || internal.datasetIsEmpty(dataset)) {
2533
+ crs = internal.parseCrsString('wgs84');
2534
+ } else {
2535
+ crs = internal.getDatasetCRS(dataset);
2536
+ }
2535
2537
  } catch(e) {
2536
2538
  err = e.message;
2537
2539
  }
@@ -2923,235 +2925,699 @@
2923
2925
  return internal.findPropertiesBySymbolGeom(fields, lyr.geometry_type);
2924
2926
  }
2925
2927
 
2926
- function flattenArcs(lyr) {
2927
- lyr.gui.source.dataset.arcs.flatten();
2928
- if (isProjectedLayer(lyr)) {
2929
- lyr.gui.displayArcs.flatten();
2930
- }
2931
- }
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;
2932
2933
 
2933
- function setZ(lyr, z) {
2934
- lyr.gui.source.dataset.arcs.setRetainedInterval(z);
2935
- if (isProjectedLayer(lyr)) {
2936
- lyr.gui.displayArcs.setRetainedInterval(z);
2934
+ // Only generate low-detail arcs for larger datasets
2935
+ if (size > 5e5) {
2936
+ update();
2937
2937
  }
2938
- }
2939
2938
 
2940
- function updateZ(lyr) {
2941
- if (isProjectedLayer(lyr) && !lyr.gui.source.dataset.arcs.isFlat()) {
2942
- lyr.gui.displayArcs.setThresholds(lyr.gui.source.dataset.arcs.getVertexData().zz);
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
+ }
2943
2949
  }
2944
- }
2945
2950
 
2946
- function appendNewDataRecord(layer) {
2947
- if (!layer.data) return null;
2948
- var fields = layer.data.getFields();
2949
- var d = getEmptyDataRecord(layer.data);
2950
- // TODO: handle SVG symbol layer
2951
- if (internal.layerHasLabels(layer)) {
2952
- d['label-text'] = 'TBD'; // without text, new labels will be invisible
2953
- } else if (layer.geometry_type == 'point' && fields.includes('r')) {
2954
- d.r = 3; // show a black circle if layer is styled
2955
- }
2956
- if (layer.geometry_type == 'polyline' || layer.geometry_type == 'polygon') {
2957
- if (fields.includes('stroke')) d.stroke = 'black';
2958
- if (fields.includes('stroke-width')) d['stroke-width'] = 1;
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;
2959
2959
  }
2960
- if (layer.geometry_type == 'polygon') {
2961
- if (fields.includes('fill')) {
2962
- d.fill = 'rgba(0,0,0,0.10)'; // 'rgba(249,120,249,0.20)';
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
+ }
2963
2970
  }
2964
- }
2965
- // TODO: better styling
2966
- layer.data.getRecords().push(d);
2967
- 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
+ };
2968
2980
  }
2969
2981
 
2970
- function getEmptyDataRecord(table) {
2971
- return table.getFields().reduce(function(memo, name) {
2972
- memo[name] = null;
2973
- return memo;
2974
- }, {});
2975
- }
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;
2976
2992
 
2977
- function deleteLastPath(lyr) {
2978
- var arcId = lyr.gui.displayArcs.size() - 1;
2979
- if (lyr.data) {
2980
- lyr.data.getRecords().pop();
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;
2981
3007
  }
2982
- var shp = lyr.shapes.pop();
2983
- internal.deleteLastArc(lyr.gui.displayArcs);
2984
- if (isProjectedLayer(lyr)) {
2985
- internal.deleteLastArc(lyr.gui.source.dataset.arcs);
3008
+
3009
+ if (n < 25) {
3010
+ blockSize = n;
3011
+ } else {
3012
+ blockSize = Math.sqrt(n * (cellWidth + gutter) / cellHeight / aspectRatio) | 0;
2986
3013
  }
2987
- }
2988
3014
 
2989
- // p1, p2: two points in source data CRS coords.
2990
- function appendNewPath(lyr, p1, p2) {
2991
- var arcId = lyr.gui.displayArcs.size();
2992
- internal.appendEmptyArc(lyr.gui.displayArcs);
2993
- lyr.shapes.push([[arcId]]);
2994
- if (isProjectedLayer(lyr)) {
2995
- internal.appendEmptyArc(lyr.gui.source.dataset.arcs);
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
+ }
2996
3026
  }
2997
- appendVertex$1(lyr, p1);
2998
- appendVertex$1(lyr, p2);
2999
- appendNewDataRecord(lyr);
3000
- }
3001
3027
 
3002
- // p: point in source data CRS coords.
3003
- function insertVertex$1(lyr, id, p) {
3004
- internal.insertVertex(lyr.gui.source.dataset.arcs, id, p);
3005
- if (isProjectedLayer(lyr)) {
3006
- internal.insertVertex(lyr.gui.displayArcs, id, lyr.gui.projectPoint(p[0], p[1]));
3028
+ function getArc(x, y, w, h) {
3029
+ return [[x, y], [x + w, y], [x + w, y - h], [x, y - h], [x, y]];
3007
3030
  }
3008
- }
3009
3031
 
3010
- function appendVertex$1(lyr, p) {
3011
- var n = lyr.gui.source.dataset.arcs.getPointCount();
3012
- insertVertex$1(lyr, n, p);
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
+ };
3013
3040
  }
3014
3041
 
3015
- // TODO: make sure we're not also removing an entire arc
3016
- function deleteLastVertex(lyr) {
3017
- deleteVertex$1(lyr, lyr.gui.displayArcs.getPointCount() - 1);
3018
- }
3042
+ var R = 6378137;
3043
+ var D2R = Math.PI / 180;
3044
+ var R2D = 180 / Math.PI;
3019
3045
 
3020
- function deleteVertex$1(lyr, id) {
3021
- internal.deleteVertex(lyr.gui.displayArcs, id);
3022
- if (isProjectedLayer(lyr)) {
3023
- internal.deleteVertex(lyr.gui.source.dataset.arcs, id);
3046
+ // Assumes projections are available
3047
+
3048
+ function needReprojectionForDisplay(sourceCRS, displayCRS) {
3049
+ if (!sourceCRS || !displayCRS) {
3050
+ return false;
3051
+ }
3052
+ if (internal.crsAreEqual(sourceCRS, displayCRS)) {
3053
+ return false;
3024
3054
  }
3055
+ return true;
3025
3056
  }
3026
3057
 
3027
- function getLastArcCoords(target) {
3028
- var arcId = target.gui.source.dataset.arcs.size() - 1;
3029
- return internal.getUnfilteredArcCoords(arcId, target.gui.source.dataset.arcs);
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();
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;
3030
3067
  }
3031
3068
 
3032
- function getLastVertexCoords(target) {
3033
- var arcs = target.gui.source.dataset.arcs;
3034
- return internal.getVertexCoords(arcs.getPointCount() - 1, arcs);
3035
- }
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
+ }
3036
3075
 
3037
- function getLastArcLength(target) {
3038
- var arcId = target.gui.source.dataset.arcs.size() - 1;
3039
- return internal.getUnfilteredArcLength(arcId, target.gui.source.dataset.arcs);
3076
+ var wgs84 = internal.parseCrsString('wgs84');
3077
+ var toWGS84 = internal.getProjTransform2(src, wgs84);
3078
+ var fromWGS84 = internal.getProjTransform2(wgs84, dest);
3079
+
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;
3040
3104
  }
3041
3105
 
3042
- function getPointCoords(lyr, fid) {
3043
- return internal.cloneShape(lyr.shapes[fid]);
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
+ });
3044
3115
  }
3045
3116
 
3046
- function getVertexCoords(lyr, id) {
3047
- return lyr.gui.source.dataset.arcs.getVertex2(id);
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;
3048
3123
  }
3049
3124
 
3050
- // set data coords (not display coords) of one or more vertices.
3051
- function setVertexCoords(lyr, ids, dataPoint) {
3052
- internal.snapVerticesToPoint(ids, dataPoint, lyr.gui.source.dataset.arcs);
3053
- if (isProjectedLayer(lyr)) {
3054
- var p = lyr.gui.projectPoint(dataPoint[0], dataPoint[1]);
3055
- internal.snapVerticesToPoint(ids, p, lyr.gui.displayArcs);
3056
- }
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];
3057
3131
  }
3058
3132
 
3059
- // coords: [x, y] point in data CRS (not display CRS)
3060
- function setPointCoords(lyr, fid, coords) {
3061
- lyr.shapes[fid] = coords;
3062
- if (isProjectedLayer(lyr)) {
3063
- lyr.shapes[fid] = projectPointCoords(coords, lyr.gui.projectPoint);
3064
- }
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];
3065
3137
  }
3066
3138
 
3067
- function updateVertexCoords(lyr, ids) {
3068
- if (!isProjectedLayer(lyr)) return;
3069
- var p = lyr.gui.displayArcs.getVertex2(ids[0]);
3070
- internal.snapVerticesToPoint(ids, lyr.gui.invertPoint(p[0], p[1]), lyr.gui.source.dataset.arcs);
3139
+ function scaleToZoom(metersPerPix) {
3140
+ return Math.log(40075017 / 512 / metersPerPix) / Math.log(2);
3071
3141
  }
3072
3142
 
3073
- function setRectangleCoords(lyr, ids, coords) {
3074
- ids.forEach(function(id, i) {
3075
- var p = coords[i];
3076
- internal.snapVerticesToPoint([id], p, lyr.gui.source.dataset.arcs);
3077
- if (isProjectedLayer(lyr)) {
3078
- internal.snapVerticesToPoint([id], lyr.gui.projectPoint(p[0], p[1]), lyr.gui.displayArcs);
3079
- }
3080
- });
3143
+ function getMapboxBounds() {
3144
+ var ymax = toWebMercator(0, 84)[1];
3145
+ var ymin = toWebMercator(0, -84)[1];
3146
+ return [-Infinity, ymin, Infinity, ymax];
3081
3147
  }
3082
3148
 
3083
- // Update source data coordinates by projecting display coordinates
3084
- function updatePointCoords(lyr, fid) {
3085
- if (!isProjectedLayer(lyr)) return;
3086
- var displayShp = lyr.shapes[fid];
3087
- lyr.shapes[fid] = projectPointCoords(displayShp, lyr.gui.invertPoint);
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});
3088
3181
  }
3089
3182
 
3090
- function projectPointCoords(src, proj) {
3091
- var dest = [], p;
3092
- for (var i=0; i<src.length; i++) {
3093
- p = proj(src[i][0], src[i][1]);
3094
- if (p) dest.push(p);
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);
3095
3190
  }
3096
- return dest.length ? dest : null;
3097
3191
  }
3098
3192
 
3099
- /*
3100
- How changes in the simplify control should affect other components
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
+ }
3101
3203
 
3102
- data calculated, 100% simplification
3103
- -> [map] filtered arcs update
3204
+ function formatLayerNameForDisplay(name) {
3205
+ return name || '[unnamed]';
3206
+ }
3104
3207
 
3105
- data calculated, <100% simplification
3106
- -> [map] filtered arcs update, redraw; [repair] intersection update
3208
+ function cleanLayerName(raw) {
3209
+ return raw.replace(/[\n\t/\\]/g, '')
3210
+ .replace(/^[.\s]+/, '').replace(/[.\s]+$/, '');
3211
+ }
3107
3212
 
3108
- change via text field
3109
- -> [map] redraw; [repair] intersection update
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
+ }
3110
3228
 
3111
- slider drag start
3112
- -> [repair] hide display
3229
+ function sortLayersForMenuDisplay(layers) {
3230
+ layers = updateLayerStackOrder(layers);
3231
+ return layers.reverse();
3232
+ }
3113
3233
 
3114
- slider drag
3115
- -> [map] redraw
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
+ }
3116
3253
 
3117
- slider drag end
3118
- -> [repair] intersection update
3119
3254
 
3120
- */
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
+ };
3121
3269
 
3122
- var SimplifyControl = function(gui) {
3123
- var model = gui.model;
3124
- var control = {};
3125
- var _value = 1;
3126
- var el = gui.container.findChild('.simplify-control-wrapper');
3127
- var menu = gui.container.findChild('.simplify-options');
3128
- var slider, text, fromPct;
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;
3129
3277
 
3130
- // init settings menu
3131
- new SimpleButton(menu.findChild('.submit-btn').addClass('default-btn')).on('click', onSubmit);
3132
- new SimpleButton(menu.findChild('.close2-btn')).on('click', function() {
3133
- if (el.visible()) {
3134
- // cancel just hides menu if slider is visible
3135
- menu.hide();
3136
- } else {
3137
- gui.clearMode();
3138
- }
3139
- });
3140
- new SimpleButton(el.findChild('.simplify-settings-btn')).on('click', function() {
3141
- if (menu.visible()) {
3142
- menu.hide();
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;
3143
3283
  } else {
3144
- showMenu();
3284
+ sourceCRS = crsInfo.crs;
3145
3285
  }
3146
- });
3147
- gui.keyboard.onMenuSubmit(menu, onSubmit);
3286
+ }
3148
3287
 
3149
- // init simplify button and mode
3150
- gui.addMode('simplify', turnOn, turnOff, gui.container.findChild('.simplify-btn'));
3151
- model.on('select', function() {
3152
- if (gui.getMode() == 'simplify') gui.clearMode();
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);
3539
+ if (isProjectedLayer(lyr)) {
3540
+ internal.snapVerticesToPoint([id], lyr.gui.projectPoint(p[0], p[1]), lyr.gui.displayArcs);
3541
+ }
3542
+ });
3543
+ }
3544
+
3545
+ // Update source data coordinates by projecting display coordinates
3546
+ function updatePointCoords(lyr, fid) {
3547
+ if (!isProjectedLayer(lyr)) return;
3548
+ var displayShp = lyr.shapes[fid];
3549
+ lyr.shapes[fid] = projectPointCoords(displayShp, lyr.gui.invertPoint);
3550
+ }
3551
+
3552
+ function projectPointCoords(src, proj) {
3553
+ var dest = [], p;
3554
+ for (var i=0; i<src.length; i++) {
3555
+ p = proj(src[i][0], src[i][1]);
3556
+ if (p) dest.push(p);
3557
+ }
3558
+ return dest.length ? dest : null;
3559
+ }
3560
+
3561
+ /*
3562
+ How changes in the simplify control should affect other components
3563
+
3564
+ data calculated, 100% simplification
3565
+ -> [map] filtered arcs update
3566
+
3567
+ data calculated, <100% simplification
3568
+ -> [map] filtered arcs update, redraw; [repair] intersection update
3569
+
3570
+ change via text field
3571
+ -> [map] redraw; [repair] intersection update
3572
+
3573
+ slider drag start
3574
+ -> [repair] hide display
3575
+
3576
+ slider drag
3577
+ -> [map] redraw
3578
+
3579
+ slider drag end
3580
+ -> [repair] intersection update
3581
+
3582
+ */
3583
+
3584
+ var SimplifyControl = function(gui) {
3585
+ var model = gui.model;
3586
+ var control = {};
3587
+ var _value = 1;
3588
+ var el = gui.container.findChild('.simplify-control-wrapper');
3589
+ var menu = gui.container.findChild('.simplify-options');
3590
+ var slider, text, fromPct;
3591
+ var menuBtn = gui.container.findChild('.simplify-btn').addClass('disabled');
3592
+
3593
+ // init settings menu
3594
+ new SimpleButton(menu.findChild('.submit-btn').addClass('default-btn')).on('click', onSubmit);
3595
+ new SimpleButton(menu.findChild('.close2-btn')).on('click', function() {
3596
+ if (el.visible()) {
3597
+ // cancel just hides menu if slider is visible
3598
+ menu.hide();
3599
+ } else {
3600
+ gui.clearMode();
3601
+ }
3602
+ });
3603
+ new SimpleButton(el.findChild('.simplify-settings-btn')).on('click', function() {
3604
+ if (menu.visible()) {
3605
+ menu.hide();
3606
+ } else {
3607
+ showMenu();
3608
+ }
3609
+ });
3610
+ gui.keyboard.onMenuSubmit(menu, onSubmit);
3611
+
3612
+ // init simplify button and mode
3613
+ gui.addMode('simplify', turnOn, turnOff, menuBtn);
3614
+
3615
+ model.on('update', function() {
3616
+ menuBtn.classed('disabled', !model.getActiveLayer());
3617
+ if (gui.getMode() == 'simplify') gui.clearMode();
3153
3618
  });
3154
3619
 
3620
+
3155
3621
  // exit simplify mode when user clicks off the visible part of the menu
3156
3622
  menu.on('click', GUI.handleDirectEvent(gui.clearMode));
3157
3623
 
@@ -3331,144 +3797,6 @@
3331
3797
  }
3332
3798
  };
3333
3799
 
3334
- var R = 6378137;
3335
- var D2R = Math.PI / 180;
3336
- var R2D = 180 / Math.PI;
3337
-
3338
- // Assumes projections are available
3339
-
3340
- function needReprojectionForDisplay(sourceCRS, displayCRS) {
3341
- if (!sourceCRS || !displayCRS) {
3342
- return false;
3343
- }
3344
- if (internal.crsAreEqual(sourceCRS, displayCRS)) {
3345
- return false;
3346
- }
3347
- return true;
3348
- }
3349
-
3350
- function projectArcsForDisplay(arcs, src, dest) {
3351
- var copy = arcs.getCopy(); // need to flatten first?
3352
- var destIsWebMerc = internal.isWebMercator(dest);
3353
- if (destIsWebMerc && internal.isWebMercator(src)) {
3354
- return copy;
3355
- }
3356
-
3357
- var wgs84 = internal.parseCrsString('wgs84');
3358
- var toWGS84 = internal.getProjTransform2(src, wgs84);
3359
- var fromWGS84 = internal.getProjTransform2(wgs84, dest);
3360
-
3361
- try {
3362
- // first try projectArcs() -- it's fast and preserves arc ids
3363
- // (so vertex editing doesn't break)
3364
- if (!internal.isWGS84(src)) {
3365
- // use wgs84 as a pivot CRS, so we can handle polar coordinates
3366
- // that can't be projected to Mercator
3367
- internal.projectArcs(copy, toWGS84);
3368
- }
3369
- if (destIsWebMerc) {
3370
- // handle polar points by clamping them to they will project
3371
- // (downside: may cause unexpected behavior when editing vertices interactively)
3372
- clampY(copy);
3373
- }
3374
- internal.projectArcs(copy, fromWGS84);
3375
- } catch(e) {
3376
- console.error(e);
3377
- // use the more robust projectArcs2 if projectArcs throws an error
3378
- // downside: projectArcs2 discards Z values and changes arc indexing,
3379
- // which will break vertex editing.
3380
- var reproject = internal.getProjTransform2(src, dest);
3381
- copy = arcs.getCopy();
3382
- internal.projectArcs2(copy, reproject);
3383
- }
3384
- return copy;
3385
- }
3386
-
3387
- function clampY(arcs) {
3388
- var max = 89.9,
3389
- min = -89.9,
3390
- bbox = arcs.getBounds().toArray();
3391
- if (bbox[1] >= min && bbox[3] <= max) return;
3392
- arcs.transformPoints(function(x, y) {
3393
- if (y > max) return [x, max];
3394
- if (y < min) return [x, min];
3395
- });
3396
- }
3397
-
3398
- function projectPointsForDisplay(lyr, src, dest) {
3399
- var copy = utils$1.extend({}, lyr);
3400
- var proj = internal.getProjTransform2(src, dest);
3401
- copy.shapes = internal.cloneShapes(lyr.shapes);
3402
- internal.projectPointLayer(copy, proj);
3403
- return copy;
3404
- }
3405
-
3406
-
3407
- function toWebMercator(lng, lat) {
3408
- var k = Math.cos(lat * D2R);
3409
- var x = R * lng * D2R;
3410
- var y = R * Math.log(Math.tan(Math.PI * 0.25 + lat * D2R * 0.5));
3411
- return [x, y];
3412
- }
3413
-
3414
- function fromWebMercator(x, y) {
3415
- var lon = x / R * R2D;
3416
- var lat = R2D * (Math.PI * 0.5 - 2 * Math.atan(Math.exp(-y / R)));
3417
- return [lon, lat];
3418
- }
3419
-
3420
- function scaleToZoom(metersPerPix) {
3421
- return Math.log(40075017 / 512 / metersPerPix) / Math.log(2);
3422
- }
3423
-
3424
- function getMapboxBounds() {
3425
- var ymax = toWebMercator(0, 84)[1];
3426
- var ymin = toWebMercator(0, -84)[1];
3427
- return [-Infinity, ymin, Infinity, ymax];
3428
- }
3429
-
3430
-
3431
- // Update map extent and trigger redraw, after a new display CRS has been applied
3432
- function projectMapExtent(ext, src, dest, newBounds) {
3433
- var oldBounds = ext.getBounds();
3434
- var oldScale = ext.scale();
3435
- var newCP, proj, strictBounds;
3436
-
3437
- if (dest && internal.isWebMercator(dest)) {
3438
- // clampToMapboxBounds(newBounds);
3439
- strictBounds = getMapboxBounds();
3440
- }
3441
-
3442
- // if source or destination CRS is unknown, show full extent
3443
- // if map is at full extent, show full extent
3444
- // TODO: handle case that scale is 1 and map is panned away from center
3445
- if (ext.scale() == 1 || !dest) {
3446
- ext.setFullBounds(newBounds, strictBounds);
3447
- ext.home(); // sets full extent and triggers redraw
3448
- } else {
3449
- // if map is zoomed, stay centered on the same geographic location, at the same relative scale
3450
- proj = internal.getProjTransform2(src, dest);
3451
- newCP = proj(oldBounds.centerX(), oldBounds.centerY());
3452
- ext.setFullBounds(newBounds, strictBounds);
3453
- if (!newCP) {
3454
- // projection of center point failed; use center of bounds
3455
- // (also consider just resetting the view using ext.home())
3456
- newCP = [newBounds.centerX(), newBounds.centerY()];
3457
- }
3458
- ext.recenter(newCP[0], newCP[1], oldScale);
3459
- }
3460
- }
3461
-
3462
- // Called from console; for testing dynamic crs
3463
- function setDisplayProjection(gui, cmd) {
3464
- var arg = cmd.replace(/^projd[ ]*/, '');
3465
- if (arg) {
3466
- gui.map.setDisplayCRS(internal.parseCrsString(arg));
3467
- } else {
3468
- gui.map.setDisplayCRS(null);
3469
- }
3470
- }
3471
-
3472
3800
  function Console(gui) {
3473
3801
  var model = gui.model;
3474
3802
  var CURSOR = '$ ';
@@ -3537,7 +3865,8 @@
3537
3865
  }
3538
3866
 
3539
3867
  function turnOn() {
3540
- if (!_isOpen && !model.isEmpty()) {
3868
+ // if (!_isOpen && !model.isEmpty()) {
3869
+ if (!_isOpen) {
3541
3870
  btn.addClass('active');
3542
3871
  _isOpen = true;
3543
3872
  // use console for messages while open
@@ -3876,8 +4205,8 @@
3876
4205
 
3877
4206
  function applyParsedCommands(commands, done) {
3878
4207
  var active = model.getActiveLayer(),
3879
- prevArcs = active.dataset.arcs,
3880
- prevTable = active.layer.data,
4208
+ prevArcs = active?.dataset.arcs,
4209
+ prevTable = active?.layer.data,
3881
4210
  prevTableSize = prevTable ? prevTable.size() : 0,
3882
4211
  prevArcCount = prevArcs ? prevArcs.size() : 0,
3883
4212
  job = new internal.Job(model);
@@ -3886,9 +4215,9 @@
3886
4215
  internal.runParsedCommands(commands, job, function(err) {
3887
4216
  var flags = getCommandFlags(commands),
3888
4217
  active2 = model.getActiveLayer(),
3889
- postArcs = active2.dataset.arcs,
4218
+ postArcs = active2?.dataset.arcs,
3890
4219
  postArcCount = postArcs ? postArcs.size() : 0,
3891
- postTable = active2.layer.data,
4220
+ postTable = active2?.layer.data,
3892
4221
  postTableSize = postTable ? postTable.size() : 0,
3893
4222
  sameTable = prevTable == postTable && prevTableSize == postTableSize,
3894
4223
  sameArcs = prevArcs == postArcs && postArcCount == prevArcCount;
@@ -3901,7 +4230,7 @@
3901
4230
  if (sameTable) {
3902
4231
  flags.same_table = true;
3903
4232
  }
3904
- if (active.layer != active2.layer) {
4233
+ if (active && active?.layer == active2?.layer) {
3905
4234
  // this can get set after some commands that don't set a new target
3906
4235
  // (e.g. -dissolve)
3907
4236
  flags.select = true;
@@ -3916,10 +4245,10 @@
3916
4245
  function onError(err) {
3917
4246
  if (utils$1.isString(err)) {
3918
4247
  consoleStop(err);
3919
- } else if (err.name == 'UserError') {
4248
+ } else if (err.name == 'UserError' ) {
3920
4249
  // stop() has already been called, don't need to log
4250
+ console.error(err.stack);
3921
4251
  } else if (err.name) {
3922
- // log stack trace to browser console
3923
4252
  console.error(err.stack);
3924
4253
  // log to console window
3925
4254
  consoleWarning(err.message);
@@ -4102,6 +4431,7 @@
4102
4431
  return;
4103
4432
  }
4104
4433
  var e = model.getActiveLayer();
4434
+ if (!e) return;
4105
4435
  if (internal.layerHasPaths(e.layer)) {
4106
4436
  el.show();
4107
4437
  checkBtn.show();
@@ -4180,47 +4510,6 @@
4180
4510
 
4181
4511
  utils$1.inherit(RepairControl, EventDispatcher);
4182
4512
 
4183
- function filterLayerByIds(lyr, ids) {
4184
- var shapes;
4185
- if (lyr.shapes) {
4186
- shapes = ids.map(function(id) {
4187
- return lyr.shapes[id];
4188
- });
4189
- return utils$1.defaults({shapes: shapes, data: null}, lyr);
4190
- }
4191
- return lyr;
4192
- }
4193
-
4194
- function formatLayerNameForDisplay(name) {
4195
- return name || '[unnamed]';
4196
- }
4197
-
4198
- function cleanLayerName(raw) {
4199
- return raw.replace(/[\n\t/\\]/g, '')
4200
- .replace(/^[.\s]+/, '').replace(/[.\s]+$/, '');
4201
- }
4202
-
4203
- function updateLayerStackOrder(layers) {
4204
- // 1. assign ascending ids to unassigned layers above the range of other layers
4205
- layers.forEach(function(o, i) {
4206
- if (!o.layer.menu_order) o.layer.menu_order = 1e6 + i;
4207
- });
4208
- // 2. sort in ascending order
4209
- layers.sort(function(a, b) {
4210
- return a.layer.menu_order - b.layer.menu_order;
4211
- });
4212
- // 3. assign consecutve ids
4213
- layers.forEach(function(o, i) {
4214
- o.layer.menu_order = i + 1;
4215
- });
4216
- return layers;
4217
- }
4218
-
4219
- function sortLayersForMenuDisplay(layers) {
4220
- layers = updateLayerStackOrder(layers);
4221
- return layers.reverse();
4222
- }
4223
-
4224
4513
  async function saveFileContentToClipboard(content) {
4225
4514
  var str = utils$1.isString(content) ? content : content.toString();
4226
4515
  await navigator.clipboard.writeText(str);
@@ -4233,7 +4522,7 @@
4233
4522
  var menu = gui.container.findChild('.export-options').on('click', GUI.handleDirectEvent(gui.clearMode));
4234
4523
  var layersArr = [];
4235
4524
  var toggleBtn = null; // checkbox <input> for toggling layer selection
4236
- var exportBtn = gui.container.findChild('.export-btn');
4525
+ var exportBtn = gui.container.findChild('.export-btn').addClass('disabled');
4237
4526
  var ofileName = gui.container.findChild('#ofile-name');
4238
4527
  new SimpleButton(menu.findChild('.close2-btn')).on('click', gui.clearMode);
4239
4528
 
@@ -4248,6 +4537,10 @@
4248
4537
  return;
4249
4538
  }
4250
4539
 
4540
+ model.on('update', function() {
4541
+ exportBtn.classed('disabled', !model.getActiveLayer());
4542
+ });
4543
+
4251
4544
  new SimpleButton(menu.findChild('#export-btn').addClass('default-btn')).on('click', onExportClick);
4252
4545
  gui.addMode('export', turnOn, turnOff, exportBtn);
4253
4546
  gui.keyboard.onMenuSubmit(menu, onExportClick);
@@ -4578,6 +4871,14 @@
4578
4871
  var layerOrderSlug;
4579
4872
 
4580
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
+
4581
4882
  model.on('update', function(e) {
4582
4883
  updateMenuBtn();
4583
4884
  if (isOpen) render();
@@ -4668,8 +4969,9 @@
4668
4969
  }
4669
4970
 
4670
4971
  function updateMenuBtn() {
4671
- var lyrName = model.getActiveLayer().layer.name || '';
4672
- 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]';
4673
4975
  var pageTitle = lyrName || 'mapshaper';
4674
4976
  btn.classed('active', 'true').findChild('.layer-name').html(menuTitle + " &nbsp;&#9660;");
4675
4977
  window.document.title = pageTitle;
@@ -4681,6 +4983,8 @@
4681
4983
  }
4682
4984
 
4683
4985
  function renderSourceFileList() {
4986
+ el.findChild('.no-layer-note').classed('hidden', model.getActiveLayer());
4987
+ el.findChild('.source-file-section').classed('hidden', !model.getActiveLayer());
4684
4988
  var list = el.findChild('.file-list');
4685
4989
  var files = [];
4686
4990
  list.empty();
@@ -5186,14 +5490,24 @@
5186
5490
  addHistoryState(undo, redo);
5187
5491
  });
5188
5492
 
5189
- gui.on('vertex_delete', function(e) {
5190
- // get vertex coords in data coordinates (not display coordinates);
5191
- var p = getVertexCoords(e.data.target, e.vertex_id);
5493
+ gui.on('vertex_delete', function(e) {
5494
+ // get vertex coords in data coordinates (not display coordinates);
5495
+ var p = getVertexCoords(e.data.target, e.vertex_id);
5496
+ var redo = function() {
5497
+ deleteVertex$1(e.data.target, e.vertex_id);
5498
+ };
5499
+ var undo = function() {
5500
+ insertVertex$1(e.data.target, e.vertex_id, p);
5501
+ };
5502
+ addHistoryState(undo, redo);
5503
+ });
5504
+
5505
+ gui.on('point_add', function(e) {
5192
5506
  var redo = function() {
5193
- deleteVertex$1(e.data.target, e.vertex_id);
5507
+ appendNewPoint(e.data.target, e.p);
5194
5508
  };
5195
5509
  var undo = function() {
5196
- insertVertex$1(e.data.target, e.vertex_id, p);
5510
+ deleteLastPoint(e.data.target);
5197
5511
  };
5198
5512
  addHistoryState(undo, redo);
5199
5513
  });
@@ -5266,6 +5580,7 @@
5266
5580
  var _hidden = true;
5267
5581
  gui.on('active', updateVisibility);
5268
5582
  gui.on('inactive', updateVisibility);
5583
+ gui.model.on('update', updateVisibility);
5269
5584
 
5270
5585
  // @iconRef: selector for an (svg) button icon
5271
5586
  this.addButton = function(iconRef) {
@@ -5294,7 +5609,7 @@
5294
5609
  };
5295
5610
 
5296
5611
  function updateVisibility() {
5297
- if (GUI.isActiveInstance(gui) && !_hidden) {
5612
+ if (GUI.isActiveInstance(gui) && !_hidden && !!gui.model.getActiveLayer()) {
5298
5613
  buttons.show();
5299
5614
  } else {
5300
5615
  buttons.hide();
@@ -5315,6 +5630,7 @@
5315
5630
  });
5316
5631
 
5317
5632
  btn.on('click', function() {
5633
+ if (btn.hasClass('disabled')) return;
5318
5634
  modes.enterMode(active ? null : name);
5319
5635
  });
5320
5636
  }
@@ -5389,14 +5705,16 @@
5389
5705
  var ctrlDown = false;
5390
5706
  document.addEventListener('keyup', function(e) {
5391
5707
  if (!GUI.isActiveInstance(gui)) return;
5392
- 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;
5393
5711
  if (e.keyCode == 17) ctrlDown = false;
5394
5712
  self.dispatchEvent('keyup', getEventData(e));
5395
5713
  });
5396
5714
 
5397
5715
  document.addEventListener('keydown', function(e) {
5398
5716
  if (!GUI.isActiveInstance(gui)) return;
5399
- if (e.keyCode == 16) shiftDown = true;
5717
+ shiftDown = e.shiftKey;
5400
5718
  if (e.keyCode == 17) ctrlDown = true;
5401
5719
  self.dispatchEvent('keydown', getEventData(e));
5402
5720
  });
@@ -7621,9 +7939,12 @@
7621
7939
  var priority = 2;
7622
7940
 
7623
7941
  mouse.on('contextmenu', function(e) {
7624
- if (isOverMap(e)) {
7625
- e.originalEvent.preventDefault();
7942
+ // shift key enables default menu (for development)
7943
+ if (gui.keyboard.shiftIsPressed()) {
7944
+ return;
7626
7945
  }
7946
+ e.originalEvent.preventDefault();
7947
+ if (!targetLayer) return; // TODO: enable menu on empty map
7627
7948
  triggerHitEvent('contextmenu', e);
7628
7949
  }, false);
7629
7950
 
@@ -7667,7 +7988,6 @@
7667
7988
  };
7668
7989
 
7669
7990
  function updateHitTest(featureFilter) {
7670
- if (!hoverable()) return;
7671
7991
  hitTest = getPointerHitTest(targetLayer, ext, interactionMode, featureFilter);
7672
7992
  }
7673
7993
 
@@ -7688,7 +8008,7 @@
7688
8008
  }
7689
8009
 
7690
8010
  function hoverable() {
7691
- return true;
8011
+ return !!interactionMode;
7692
8012
  }
7693
8013
 
7694
8014
  function selectable() {
@@ -7864,6 +8184,7 @@
7864
8184
 
7865
8185
  // Hits are re-detected on 'hover' (if hit detection is active)
7866
8186
  mouse.on('hover', function(e) {
8187
+ if (gui.contextMenu.isOpen()) return;
7867
8188
  handlePointerEvent(e);
7868
8189
  if (storedData.pinned || !hitTest || !active) return;
7869
8190
  if (e.hover && isOverMap(e)) {
@@ -7979,10 +8300,6 @@
7979
8300
  // check if an event is used in the current interaction mode
7980
8301
  function eventIsEnabled(type) {
7981
8302
  if (!active) return false;
7982
- if (interactionMode == 'drawing' && (type == 'hover' || type == 'dblclick')) {
7983
- return true; // special case -- using hover for line drawing animation
7984
- }
7985
-
7986
8303
  if (type == 'click' && gui.keyboard.ctrlIsPressed()) {
7987
8304
  return false; // don't fire if context menu might open
7988
8305
  }
@@ -7993,6 +8310,10 @@
7993
8310
  return true; // click events are triggered even if no shape is hit
7994
8311
  }
7995
8312
 
8313
+ if (interactionMode == 'drawing' && (type == 'hover' || type == 'dblclick')) {
8314
+ return true; // special case -- using hover for line drawing animation
8315
+ }
8316
+
7996
8317
  // ignore pointer events when no features are being hit
7997
8318
  // (don't block pan and other navigation when events aren't being used for editing)
7998
8319
  var hitId = self.getHitId();
@@ -8848,7 +9169,7 @@
8848
9169
  }
8849
9170
 
8850
9171
  gui.on('map_reset', function() {
8851
- ext.home();
9172
+ ext.reset(true);
8852
9173
  });
8853
9174
 
8854
9175
  zoomTween.on('change', function(e) {
@@ -8866,8 +9187,9 @@
8866
9187
 
8867
9188
  mouse.on('dragstart', function(e) {
8868
9189
  if (disabled()) return;
8869
- if (!internal.layerHasGeometry(gui.model.getActiveLayer().layer)) return;
8870
- // zoomDrag = !!e.metaKey || !!e.ctrlKey; // meta is command on mac, windows key on windows
9190
+ // allow drawing rectangles if active layer is empty
9191
+ // var lyr = gui.model.getActiveLayer()?.layer;
9192
+ // if (lyr && !internal.layerHasGeometry(lyr)) return;
8871
9193
  shiftDrag = !!e.shiftKey;
8872
9194
  if (shiftDrag) {
8873
9195
  if (useBoxZoom()) zoomBox.turnOn();
@@ -10355,25 +10677,18 @@
10355
10677
 
10356
10678
  mouse.on('click', function(e) {
10357
10679
  if (!active()) return;
10358
- addPoint(e.x, e.y);
10680
+ var p = pixToDataCoords(e.x, e.y);
10681
+ var target = hit.getHitTarget();
10682
+ appendNewPoint(target, p);
10683
+ gui.dispatchEvent('point_add', {p, target});
10359
10684
  gui.dispatchEvent('map-needs-refresh');
10360
10685
  });
10361
10686
 
10362
- // x, y: pixel coordinates
10363
- function addPoint(x, y) {
10364
- var p = ext.translatePixelCoords(x, y);
10365
- var lyr = hit.getHitTarget();
10366
- var fid = lyr.shapes.length;
10367
- var d = appendNewDataRecord(lyr);
10368
- if (d) {
10369
- // this seems to work even for projected layers -- the data tables
10370
- // of projected and original data seem to be shared.
10371
- lyr.data.getRecords()[fid] = d;
10372
- }
10373
- lyr.shapes[fid] = [p];
10374
- updatePointCoords(lyr, fid);
10375
- }
10376
10687
 
10688
+ function pixToDataCoords(x, y) {
10689
+ var target = hit.getHitTarget();
10690
+ return translateDisplayPoint(target, ext.translatePixelCoords(x, y));
10691
+ }
10377
10692
 
10378
10693
  }
10379
10694
 
@@ -10394,26 +10709,32 @@
10394
10709
 
10395
10710
  _position.on('resize', function(e) {
10396
10711
  if (ready()) {
10397
- triggerChangeEvent({resize: true});
10712
+ // triggerChangeEvent({resize: true});
10713
+ triggerChangeEvent();
10398
10714
  }
10399
10715
  });
10400
10716
 
10401
10717
  function ready() { return !!_fullBounds; }
10402
10718
 
10403
- this.reset = function() {
10719
+ this.reset = function(fire) {
10404
10720
  if (!ready()) return;
10405
- recenter(_fullBounds.centerX(), _fullBounds.centerY(), 1, {reset: true});
10721
+ recenter(_fullBounds.centerX(), _fullBounds.centerY(), 1);
10722
+ if (fire) {
10723
+ triggerChangeEvent();
10724
+ }
10406
10725
  };
10407
10726
 
10408
10727
  this.home = function() {
10409
10728
  if (!ready()) return;
10410
10729
  recenter(_fullBounds.centerX(), _fullBounds.centerY(), 1);
10730
+ triggerChangeEvent();
10411
10731
  };
10412
10732
 
10413
10733
  this.pan = function(xpix, ypix) {
10414
10734
  if (!ready()) return;
10415
10735
  var t = this.getTransform();
10416
10736
  recenter(_cx - xpix / t.mx, _cy - ypix / t.my);
10737
+ triggerChangeEvent();
10417
10738
  };
10418
10739
 
10419
10740
  // Zoom to @w (width of the map viewport in coordinates)
@@ -10436,6 +10757,7 @@
10436
10757
  cx = fx + dx2,
10437
10758
  cy = fy + dy2;
10438
10759
  recenter(cx, cy, scale);
10760
+ triggerChangeEvent();
10439
10761
  };
10440
10762
 
10441
10763
  this.zoomByPct = function(pct, xpct, ypct) {
@@ -10527,11 +10849,10 @@
10527
10849
  return this.getTransform().invert().transform(x, y);
10528
10850
  };
10529
10851
 
10530
- function recenter(cx, cy, scale, data) {
10852
+ function recenter(cx, cy, scale) {
10531
10853
  scale = scale ? limitScale(scale) : _scale;
10532
10854
  if (cx == _cx && cy == _cy && scale == _scale) return;
10533
10855
  navigate(cx, cy, scale);
10534
- triggerChangeEvent(data);
10535
10856
  }
10536
10857
 
10537
10858
  function navigate(cx, cy, scale) {
@@ -10558,9 +10879,8 @@
10558
10879
  _scale = scale;
10559
10880
  }
10560
10881
 
10561
- function triggerChangeEvent(data) {
10562
- data = data || {};
10563
- _self.dispatchEvent('change', data);
10882
+ function triggerChangeEvent() {
10883
+ _self.dispatchEvent('change');
10564
10884
  }
10565
10885
 
10566
10886
  // stop zooming before rounding errors become too obvious
@@ -11453,8 +11773,10 @@
11453
11773
  return el;
11454
11774
  }
11455
11775
 
11456
- function LayerRenderer(gui, container, ext, mouse) {
11776
+ function LayerRenderer(gui, container) {
11457
11777
  var el = El(container),
11778
+ ext = gui.map.getExtent(),
11779
+ mouse = gui.map.getMouse(),
11458
11780
  _mainCanv = new DisplayCanvas().appendTo(el),
11459
11781
  _overlayCanv = new DisplayCanvas().appendTo(el),
11460
11782
  _svg = new SvgDisplayLayer(gui, ext, mouse).appendTo(el),
@@ -11507,499 +11829,254 @@
11507
11829
  layers.forEach(function(lyr) {
11508
11830
  if (noRedraw) {
11509
11831
  _furniture.reposition(lyr, 'furniture');
11510
- } else {
11511
- _furniture.drawLayer(lyr, 'furniture');
11512
- }
11513
- });
11514
- };
11515
-
11516
- // kludge: skip rendering base layers if hovering, except on first hover
11517
- // (because highlight shapes may be rendered to the main canvas)
11518
- function skipMainLayerRedraw(action) {
11519
- return action == 'hover' && _overlayCanv.visible();
11520
- }
11521
-
11522
- function drawCanvasLayer(lyr, canv) {
11523
- if (!lyr) return;
11524
- if (lyr.gui.style.type == 'outline') {
11525
- drawOutlineLayerToCanvas(lyr, canv, ext);
11526
- } else {
11527
- drawStyledLayerToCanvas(lyr, canv, ext);
11528
- }
11529
- }
11530
-
11531
- function getSvgLayerType(layer) {
11532
- var type = null;
11533
- if (internal.layerHasSvgSymbols(layer)) {
11534
- type = 'symbol'; // also label + symbol
11535
- } else if (internal.layerHasLabels(layer)) {
11536
- type = 'symbol';
11537
- }
11538
- return type;
11539
- }
11540
- }
11541
-
11542
- // Controls the shift-drag box editing tool
11543
- //
11544
- function BoxTool(gui, ext, nav) {
11545
- var self = new EventDispatcher();
11546
- var box = new HighlightBox(gui, {name: 'box-tool', persistent: true, handles: true, draggable: true});
11547
- var popup = gui.container.findChild('.box-tool-options');
11548
- var coords = popup.findChild('.box-coords');
11549
- var _on = false;
11550
-
11551
- var infoBtn = new SimpleButton(popup.findChild('.info-btn')).on('click', function() {
11552
- if (coords.visible()) hideCoords(); else showCoords();
11553
- });
11554
-
11555
- new SimpleButton(popup.findChild('.cancel-btn')).on('click', function() {
11556
- reset();
11557
- });
11558
-
11559
- new SimpleButton(popup.findChild('.select-btn')).on('click', function() {
11560
- var coords = box.getDataCoords();
11561
- if (!coords) return;
11562
- gui.enterMode('selection_tool');
11563
- gui.interaction.setMode('selection');
11564
- // kludge to pass bbox to the selection tool
11565
- gui.dispatchEvent('selection_bridge', {
11566
- map_data_bbox: coords
11567
- });
11568
- });
11569
-
11570
- new SimpleButton(popup.findChild('.clip-btn')).on('click', function() {
11571
- runCommand('-clip bbox=' + box.getDataCoords().join(','));
11572
- });
11573
-
11574
- new SimpleButton(popup.findChild('.erase-btn')).on('click', function() {
11575
- runCommand('-erase bbox=' + box.getDataCoords().join(','));
11576
- });
11577
-
11578
- new SimpleButton(popup.findChild('.rect-btn')).on('click', function() {
11579
- var cmd = '-rectangle + bbox=' + box.getDataCoords().join(',');
11580
- runCommand(cmd);
11581
- });
11582
-
11583
- new SimpleButton(popup.findChild('.frame-btn')).on('click', function() {
11584
- openAddFramePopup(gui, box.getDataCoords());
11585
- });
11586
-
11587
- gui.addMode('box_tool', turnOn, turnOff);
11588
-
11589
- gui.on('interaction_mode_change', function(e) {
11590
- if (e.mode === 'box') {
11591
- gui.enterMode('box_tool');
11592
- } else if (_on) {
11593
- turnOff();
11594
- }
11595
- });
11596
-
11597
- gui.on('shift_drag_start', function() {
11598
- // box.classed('zooming', inZoomMode());
11599
- hideCoords();
11600
- });
11601
-
11602
- box.on('dragend', function(e) {
11603
- if (_on) popup.show();
11604
- });
11605
-
11606
- box.on('handle_drag', function() {
11607
- if (coords.visible()) {
11608
- showCoords();
11609
- }
11610
- });
11611
-
11612
- function inZoomMode() {
11613
- return !_on && gui.getMode() != 'selection_tool';
11614
- }
11615
-
11616
- function runCommand(cmd) {
11617
- if (gui.console) {
11618
- gui.console.runMapshaperCommands(cmd, function(err) {
11619
- reset();
11620
- gui.clearMode();
11621
- });
11622
- }
11623
- // reset(); // TODO: exit interactive mode
11624
- }
11625
-
11626
- function showCoords() {
11627
- El(infoBtn.node()).addClass('selected-btn');
11628
- coords.text(box.getDataCoords().join(','));
11629
- coords.show();
11630
- GUI.selectElement(coords.node());
11631
- }
11632
-
11633
- function hideCoords() {
11634
- El(infoBtn.node()).removeClass('selected-btn');
11635
- coords.hide();
11636
- }
11637
-
11638
- function turnOn() {
11639
- box.turnOn();
11640
- _on = true;
11641
- }
11642
-
11643
- function turnOff() {
11644
- box.turnOff();
11645
- if (gui.interaction.getMode() == 'box') {
11646
- // mode change was not initiated by interactive menu -- turn off interactivity
11647
- gui.interaction.turnOff();
11648
- }
11649
- _on = false;
11650
- reset();
11651
- }
11652
-
11653
- function reset() {
11654
- box.hide();
11655
- popup.hide();
11656
- hideCoords();
11657
- }
11658
-
11659
- function openAddFramePopup(gui, bbox) {
11660
- var popup = showPopupAlert('', 'Add a map frame');
11661
- var el = popup.container();
11662
- el.addClass('option-menu');
11663
- var html = `<p>Enter a width in px, cm or inches to create a frame layer
11664
- for setting the size of the map for symbol scaling in the
11665
- GUI and setting the size and crop of SVG output.</p><div><input type="text" class="frame-width text-input" placeholder="examples: 600px 5in"></div>
11666
- <div tabindex="0" class="btn dialog-btn">Create</div></span>`;
11667
- el.html(html);
11668
- var input = el.findChild('.frame-width');
11669
- input.node().focus();
11670
- var btn = el.findChild('.btn').on('click', function() {
11671
- var widthStr = input.node().value.trim();
11672
- if (parseFloat(widthStr) > 0 === false) {
11673
- // invalid input
11674
- input.node().value = '';
11675
- return;
11676
- }
11677
- var cmd = `-rectangle + name=frame bbox='${bbox.join(',')}' width='${widthStr}'`;
11678
- runCommand(cmd);
11679
- popup.close();
11680
- });
11681
- }
11682
-
11683
- return self;
11684
- }
11685
-
11686
- function RectangleControl(gui, hit) {
11687
- var box = new HighlightBox(gui, {name: 'rectangle-tool', persistent: true, handles: true, classname: 'rectangles', draggable: false});
11688
- var _on = false;
11689
- var dragInfo;
11690
-
11691
- gui.addMode('rectangle_tool', turnOn, turnOff);
11692
-
11693
- gui.on('interaction_mode_change', function(e) {
11694
- if (e.mode === 'rectangles') {
11695
- gui.enterMode('rectangle_tool');
11696
- } else if (_on) {
11697
- turnOff();
11698
- }
11699
- });
11700
-
11701
- hit.on('change', function(e) {
11702
- if (!_on) return;
11703
- // TODO: handle multiple hits (see gui-inspection-control)
11704
- var id = e.id;
11705
- if (e.id > -1 && e.pinned) {
11706
- var target = hit.getHitTarget();
11707
- var path = target.shapes[e.id][0];
11708
- var bbox = target.gui.displayArcs.getSimpleShapeBounds(path).toArray();
11709
- box.setDataCoords(bbox);
11710
- dragInfo = {
11711
- id: e.id,
11712
- target: target,
11713
- ids: [],
11714
- points: []
11715
- };
11716
- var iter = target.gui.displayArcs.getShapeIter(path);
11717
- while (iter.hasNext()) {
11718
- dragInfo.points.push([iter.x, iter.y]);
11719
- dragInfo.ids.push(iter._arc.i);
11720
- }
11721
- gui.container.findChild('.map-layers').classed('dragging', true);
11722
-
11723
- } else if (dragInfo) {
11724
- gui.dispatchEvent('rectangle_dragend', dragInfo); // save undo state
11725
- gui.container.findChild('.map-layers').classed('dragging', false);
11726
- reset();
11727
- } else {
11728
- box.hide();
11729
- }
11730
-
11731
- });
11732
-
11733
- box.on('handle_drag', function(e) {
11734
- if (!_on || !dragInfo) return;
11735
- var coords = internal.bboxToCoords(box.getDataCoords());
11736
- setRectangleCoords(dragInfo.target, dragInfo.ids, coords);
11737
- gui.dispatchEvent('map-needs-refresh');
11738
- });
11832
+ } else {
11833
+ _furniture.drawLayer(lyr, 'furniture');
11834
+ }
11835
+ });
11836
+ };
11739
11837
 
11740
- function turnOn() {
11741
- box.turnOn();
11742
- _on = true;
11838
+ // kludge: skip rendering base layers if hovering, except on first hover
11839
+ // (because highlight shapes may be rendered to the main canvas)
11840
+ function skipMainLayerRedraw(action) {
11841
+ return action == 'hover' && _overlayCanv.visible();
11743
11842
  }
11744
11843
 
11745
- function turnOff() {
11746
- box.turnOff();
11747
- if (gui.interaction.getMode() == 'rectangles') {
11748
- // mode change was not initiated by interactive menu -- turn off interactivity
11749
- gui.interaction.turnOff();
11844
+ function drawCanvasLayer(lyr, canv) {
11845
+ if (!lyr) return;
11846
+ if (lyr.gui.style.type == 'outline') {
11847
+ drawOutlineLayerToCanvas(lyr, canv, ext);
11848
+ } else {
11849
+ drawStyledLayerToCanvas(lyr, canv, ext);
11750
11850
  }
11751
- _on = false;
11752
- reset();
11753
11851
  }
11754
11852
 
11755
- function reset() {
11756
- box.hide();
11757
- dragInfo = null;
11853
+ function getSvgLayerType(layer) {
11854
+ var type = null;
11855
+ if (internal.layerHasSvgSymbols(layer)) {
11856
+ type = 'symbol'; // also label + symbol
11857
+ } else if (internal.layerHasLabels(layer)) {
11858
+ type = 'symbol';
11859
+ }
11860
+ return type;
11758
11861
  }
11759
11862
  }
11760
11863
 
11761
- // Create low-detail versions of large arc collections for faster rendering
11762
- // at zoomed-out scales.
11763
- function enhanceArcCollectionForDisplay(unfilteredArcs) {
11764
- var size = unfilteredArcs.getPointCount(),
11765
- filteredArcs, filteredSegLen;
11766
-
11767
- // Only generate low-detail arcs for larger datasets
11768
- if (size > 5e5) {
11769
- update();
11770
- }
11864
+ // Controls the shift-drag box editing tool
11865
+ //
11866
+ function BoxTool(gui, ext, nav) {
11867
+ var self = new EventDispatcher();
11868
+ var box = new HighlightBox(gui, {name: 'box-tool', persistent: true, handles: true, draggable: true});
11869
+ var popup = gui.container.findChild('.box-tool-options');
11870
+ var coords = popup.findChild('.box-coords');
11871
+ var _on = false;
11771
11872
 
11772
- function update() {
11773
- if (unfilteredArcs.getVertexData().zz) {
11774
- // Use precalculated simplification data for vertex filtering, if available
11775
- filteredArcs = initFilteredArcs(unfilteredArcs);
11776
- filteredSegLen = internal.getAvgSegment(filteredArcs);
11777
- } else {
11778
- // Use fast simplification as a fallback
11779
- filteredSegLen = internal.getAvgSegment(unfilteredArcs) * 4;
11780
- filteredArcs = internal.simplifyArcsFast(unfilteredArcs, filteredSegLen);
11781
- }
11782
- }
11873
+ var infoBtn = new SimpleButton(popup.findChild('.info-btn')).on('click', function() {
11874
+ if (coords.visible()) hideCoords(); else showCoords();
11875
+ });
11783
11876
 
11784
- function initFilteredArcs(arcs) {
11785
- var filterPct = 0.08;
11786
- var nth = Math.ceil(arcs.getPointCount() / 5e5);
11787
- var currInterval = arcs.getRetainedInterval();
11788
- var filterZ = arcs.getThresholdByPct(filterPct, nth);
11789
- var filteredArcs = arcs.setRetainedInterval(filterZ).getFilteredCopy();
11790
- arcs.setRetainedInterval(currInterval); // reset current simplification
11791
- return filteredArcs;
11792
- }
11877
+ new SimpleButton(popup.findChild('.cancel-btn')).on('click', function() {
11878
+ reset();
11879
+ });
11793
11880
 
11794
- // TODO: better job of detecting arc change... e.g. revision number
11795
- unfilteredArcs.getScaledArcs = function(ext) {
11796
- // check for changes in the number of arcs (probably due to editing)
11797
- if (filteredArcs && filteredArcs.size() != unfilteredArcs.size()) {
11798
- // arc count has changed... probably due to editing
11799
- update();
11800
- if (filteredArcs.size() != unfilteredArcs.size()) {
11801
- throw Error('Internal error');
11802
- }
11803
- }
11804
- if (filteredArcs) {
11805
- // match simplification of unfiltered arcs
11806
- filteredArcs.setRetainedInterval(unfilteredArcs.getRetainedInterval());
11807
- }
11808
- // switch to filtered version of arcs at small scales
11809
- var unitsPerPixel = 1/ext.getTransform().mx,
11810
- useFiltering = filteredArcs && unitsPerPixel > filteredSegLen * 1.5;
11811
- return useFiltering ? filteredArcs : unfilteredArcs;
11812
- };
11813
- }
11881
+ new SimpleButton(popup.findChild('.select-btn')).on('click', function() {
11882
+ var coords = box.getDataCoords();
11883
+ if (!coords) return;
11884
+ gui.enterMode('selection_tool');
11885
+ gui.interaction.setMode('selection');
11886
+ // kludge to pass bbox to the selection tool
11887
+ gui.dispatchEvent('selection_bridge', {
11888
+ map_data_bbox: coords
11889
+ });
11890
+ });
11814
11891
 
11815
- function getDisplayLayerForTable(tableArg) {
11816
- var table = tableArg || new internal.DataTable(0),
11817
- n = table.size(),
11818
- cellWidth = 12,
11819
- cellHeight = 5,
11820
- gutter = 6,
11821
- arcs = [],
11822
- shapes = [],
11823
- aspectRatio = 1.1,
11824
- x, y, col, row, blockSize;
11892
+ new SimpleButton(popup.findChild('.clip-btn')).on('click', function() {
11893
+ runCommand('-clip bbox=' + box.getDataCoords().join(','));
11894
+ });
11825
11895
 
11826
- if (n > 10000) {
11827
- arcs = null;
11828
- gutter = 0;
11829
- cellWidth = 4;
11830
- cellHeight = 4;
11831
- aspectRatio = 1.45;
11832
- } else if (n > 5000) {
11833
- cellWidth = 5;
11834
- gutter = 3;
11835
- aspectRatio = 1.45;
11836
- } else if (n > 1000) {
11837
- gutter = 3;
11838
- cellWidth = 8;
11839
- aspectRatio = 1.3;
11840
- }
11896
+ new SimpleButton(popup.findChild('.erase-btn')).on('click', function() {
11897
+ runCommand('-erase bbox=' + box.getDataCoords().join(','));
11898
+ });
11841
11899
 
11842
- if (n < 25) {
11843
- blockSize = n;
11844
- } else {
11845
- blockSize = Math.sqrt(n * (cellWidth + gutter) / cellHeight / aspectRatio) | 0;
11846
- }
11900
+ new SimpleButton(popup.findChild('.rect-btn')).on('click', function() {
11901
+ var cmd = '-rectangle + bbox=' + box.getDataCoords().join(',');
11902
+ runCommand(cmd);
11903
+ });
11847
11904
 
11848
- for (var i=0; i<n; i++) {
11849
- row = i % blockSize;
11850
- col = Math.floor(i / blockSize);
11851
- x = col * (cellWidth + gutter);
11852
- y = cellHeight * (blockSize - row);
11853
- if (arcs) {
11854
- arcs.push(getArc(x, y, cellWidth, cellHeight));
11855
- shapes.push([[i]]);
11856
- } else {
11857
- shapes.push([[x, y]]);
11858
- }
11859
- }
11905
+ new SimpleButton(popup.findChild('.frame-btn')).on('click', function() {
11906
+ openAddFramePopup(gui, box.getDataCoords());
11907
+ });
11860
11908
 
11861
- function getArc(x, y, w, h) {
11862
- return [[x, y], [x + w, y], [x + w, y - h], [x, y - h], [x, y]];
11863
- }
11909
+ gui.addMode('box_tool', turnOn, turnOff);
11864
11910
 
11865
- return {
11866
- layer: {
11867
- geometry_type: arcs ? 'polygon' : 'point',
11868
- shapes: shapes,
11869
- data: table
11870
- },
11871
- arcs: arcs ? new internal.ArcCollection(arcs) : null
11872
- };
11873
- }
11911
+ gui.on('interaction_mode_change', function(e) {
11912
+ if (e.mode === 'box') {
11913
+ gui.enterMode('box_tool');
11914
+ } else if (_on) {
11915
+ turnOff();
11916
+ }
11917
+ });
11874
11918
 
11875
- // lyr: a map layer with gui property
11876
- // displayCRS: CRS to use for display, or null (which clears any current display CRS)
11877
- function projectLayerForDisplay(lyr, displayCRS) {
11878
- var crsInfo = getDatasetCrsInfo(lyr.gui.source.dataset);
11879
- var sourceCRS = crsInfo.crs || null; // let enhanceLayerForDisplay() handle null case
11880
- if (!lyr.gui.geographic) {
11881
- return;
11882
- }
11883
- if (lyr.gui.dynamic_crs && internal.crsAreEqual(sourceCRS, lyr.gui.dynamic_crs)) {
11884
- return;
11885
- }
11886
- enhanceLayerForDisplay(lyr, lyr.gui.source.dataset, {crs: displayCRS});
11887
- if (lyr.gui.style?.ids) {
11888
- // re-apply layer filter
11889
- lyr.gui.displayLayer = filterLayerByIds(lyr.gui.displayLayer, lyr.gui.style.ids);
11890
- }
11891
- }
11919
+ gui.on('shift_drag_start', function() {
11920
+ hideCoords();
11921
+ });
11892
11922
 
11923
+ box.on('dragend', function(e) {
11924
+ if (_on) popup.show();
11925
+ });
11893
11926
 
11894
- // Supplement a layer with information needed for rendering
11895
- function enhanceLayerForDisplay(layer, dataset, opts) {
11896
- var gui = {
11897
- empty: internal.getFeatureCount(layer) === 0,
11898
- geographic: false,
11899
- displayArcs: null,
11900
- displayLayer: null,
11901
- source: {dataset},
11902
- bounds: null,
11903
- style: null,
11904
- dynamic_crs: null,
11905
- invertPoint: null,
11906
- projectPoint: null
11907
- };
11927
+ box.on('handle_drag', function() {
11928
+ if (coords.visible()) {
11929
+ showCoords();
11930
+ }
11931
+ });
11908
11932
 
11909
- var displayCRS = opts.crs || null;
11910
- // display arcs may have been generated when another layer in the dataset
11911
- // was converted for display... re-use if available
11912
- var displayArcs = dataset.gui?.displayArcs;
11913
- var unprojectable = false;
11914
- var sourceCRS;
11915
- var emptyArcs;
11933
+ function inZoomMode() {
11934
+ return !_on && gui.getMode() != 'selection_tool';
11935
+ }
11916
11936
 
11917
- if (displayCRS && layer.geometry_type) {
11918
- var crsInfo = getDatasetCrsInfo(dataset);
11919
- if (crsInfo.error) {
11920
- // unprojectable dataset -- return empty layer
11921
- gui.unprojectable = true;
11922
- } else {
11923
- sourceCRS = crsInfo.crs;
11937
+ function runCommand(cmd) {
11938
+ if (gui.console) {
11939
+ gui.console.runMapshaperCommands(cmd, function(err) {
11940
+ reset();
11941
+ gui.clearMode();
11942
+ });
11924
11943
  }
11944
+ // reset(); // TODO: exit interactive mode
11945
+ }
11946
+
11947
+ function showCoords() {
11948
+ El(infoBtn.node()).addClass('selected-btn');
11949
+ coords.text(box.getDataCoords().join(','));
11950
+ coords.show();
11951
+ GUI.selectElement(coords.node());
11925
11952
  }
11926
11953
 
11927
- // Assume that dataset.displayArcs is in the display CRS
11928
- // (it must be deleted upstream if reprojection is needed)
11929
- // if (!obj.empty && dataset.arcs && !displayArcs) {
11930
- if (dataset.arcs && !displayArcs && !gui.unprojectable) {
11931
- // project arcs, if needed
11932
- if (needReprojectionForDisplay(sourceCRS, displayCRS)) {
11933
- displayArcs = projectArcsForDisplay(dataset.arcs, sourceCRS, displayCRS);
11934
- } else {
11935
- // Use original arcs for display if there is no dynamic reprojection
11936
- displayArcs = dataset.arcs;
11937
- }
11954
+ function hideCoords() {
11955
+ El(infoBtn.node()).removeClass('selected-btn');
11956
+ coords.hide();
11957
+ }
11938
11958
 
11939
- enhanceArcCollectionForDisplay(displayArcs);
11940
- dataset.gui = {displayArcs}; // stash these in the dataset for other layers to use
11959
+ function turnOn() {
11960
+ box.turnOn();
11961
+ _on = true;
11941
11962
  }
11942
11963
 
11943
- if (internal.layerHasFurniture(layer)) {
11944
- // TODO: consider how to render furniture in GUI
11945
- // treating furniture layers (other than frame) as tabular for now,
11946
- // so there is something to show if they are selected
11964
+ function turnOff() {
11965
+ box.turnOff();
11966
+ if (gui.interaction.getMode() == 'box') {
11967
+ // mode change was not initiated by interactive menu -- turn off interactivity
11968
+ gui.interaction.turnOff();
11969
+ }
11970
+ _on = false;
11971
+ reset();
11947
11972
  }
11948
11973
 
11949
- if (gui.unprojectable) {
11950
- gui.displayLayer = {shapes: []}; // TODO: improve
11951
- } else if (layer.geometry_type) {
11952
- gui.geographic = true;
11953
- gui.displayLayer = layer;
11954
- gui.displayArcs = displayArcs;
11955
- } else {
11956
- var table = getDisplayLayerForTable(layer.data);
11957
- gui.tabular = true;
11958
- gui.displayLayer = table.layer;
11959
- gui.displayArcs = table.arcs;
11974
+ function reset() {
11975
+ box.hide();
11976
+ popup.hide();
11977
+ hideCoords();
11960
11978
  }
11961
11979
 
11962
- // dynamic reprojection (arcs were already reprojected above)
11963
- if (gui.geographic && needReprojectionForDisplay(sourceCRS, displayCRS)) {
11964
- gui.dynamic_crs = displayCRS;
11965
- gui.invertPoint = internal.getProjTransform2(displayCRS, sourceCRS);
11966
- gui.projectPoint = internal.getProjTransform2(sourceCRS, displayCRS);
11967
- if (internal.layerHasPoints(layer)) {
11968
- gui.displayLayer = projectPointsForDisplay(layer, sourceCRS, displayCRS);
11969
- } else if (internal.layerHasPaths(layer)) {
11970
- emptyArcs = findEmptyArcs(displayArcs);
11971
- if (emptyArcs.length > 0) {
11972
- // Don't try to draw paths containing coordinates that failed to project
11973
- gui.displayLayer = internal.filterPathLayerByArcIds(gui.displayLayer, emptyArcs);
11980
+ function openAddFramePopup(gui, bbox) {
11981
+ var popup = showPopupAlert('', 'Add a map frame');
11982
+ var el = popup.container();
11983
+ el.addClass('option-menu');
11984
+ var html = `<p>Enter a width in px, cm or inches to create a frame layer
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;
11974
11997
  }
11975
- }
11998
+ var cmd = `-rectangle + name=frame bbox='${bbox.join(',')}' width='${widthStr}'`;
11999
+ runCommand(cmd);
12000
+ popup.close();
12001
+ });
11976
12002
  }
11977
12003
 
11978
- gui.bounds = getDisplayBounds(gui.displayLayer, gui.displayArcs);
11979
- layer.gui = gui;
12004
+ return self;
11980
12005
  }
11981
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;
11982
12011
 
11983
- function getDisplayBounds(lyr, arcs) {
11984
- var bounds = internal.getLayerBounds(lyr, arcs) || new Bounds();
11985
- if (lyr.geometry_type == 'point' && arcs && bounds.hasBounds() && bounds.area() > 0 === false) {
11986
- // if a point layer has no extent (e.g. contains only a single point),
11987
- // then merge with arc bounds, to place the point in context.
11988
- bounds = bounds.mergeBounds(arcs.getBounds());
12012
+ gui.addMode('rectangle_tool', turnOn, turnOff);
12013
+
12014
+ gui.on('interaction_mode_change', function(e) {
12015
+ if (e.mode === 'rectangles') {
12016
+ gui.enterMode('rectangle_tool');
12017
+ } else if (_on) {
12018
+ turnOff();
12019
+ }
12020
+ });
12021
+
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();
12048
+ } else {
12049
+ box.hide();
12050
+ }
12051
+
12052
+ });
12053
+
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
+ });
12060
+
12061
+ function turnOn() {
12062
+ box.turnOn();
12063
+ _on = true;
11989
12064
  }
11990
- return bounds;
11991
- }
11992
12065
 
11993
- // Returns an array of ids of empty arcs (arcs can be set to empty if errors occur while projecting them)
11994
- function findEmptyArcs(arcs) {
11995
- var nn = arcs.getVertexData().nn;
11996
- var ids = [];
11997
- for (var i=0, n=nn.length; i<n; i++) {
11998
- if (nn[i] === 0) {
11999
- ids.push(i);
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();
12000
12071
  }
12072
+ _on = false;
12073
+ reset();
12074
+ }
12075
+
12076
+ function reset() {
12077
+ box.hide();
12078
+ dragInfo = null;
12001
12079
  }
12002
- return ids;
12003
12080
  }
12004
12081
 
12005
12082
  function loadScript(url, cb) {
@@ -12052,15 +12129,9 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12052
12129
  turnOff();
12053
12130
  });
12054
12131
 
12055
- // hideBtn.on('mousedown', function() {
12056
- // if (activeStyle) {
12057
- // mapEl.css('visibility', 'hidden');
12058
- // hidden = true;
12059
- // }
12060
- // })
12061
12132
  clearBtn.on('click', function() {
12062
12133
  if (activeStyle) {
12063
- updateStyle(null);
12134
+ turnOffBasemap();
12064
12135
  updateButtons();
12065
12136
  }
12066
12137
  });
@@ -12084,28 +12155,34 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12084
12155
  params.styles.forEach(function(style) {
12085
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>`);
12086
12157
  btn.findChild('.basemap-style-btn').on('click', function() {
12087
- updateStyle(style == activeStyle ? null : style);
12158
+ if (style == activeStyle) {
12159
+ turnOffBasemap();
12160
+ } else {
12161
+ showBasemap(style);
12162
+ }
12088
12163
  updateButtons();
12089
12164
  });
12090
12165
  btn.appendTo(list);
12091
12166
  });
12092
12167
  }
12093
12168
 
12094
- function updateStyle(style) {
12095
- activeStyle = style || null;
12096
- // 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
12097
12178
  // Make sure that the selected layer style gets updated in gui-map.js
12098
12179
  // gui.state.dark_basemap = style && style.dark || false;
12099
- if (!style) {
12100
- gui.map.setDisplayCRS(null);
12101
- refresh();
12102
- } else if (map) {
12180
+ if (map) {
12103
12181
  map.setStyle(style.url);
12104
12182
  refresh();
12105
- } else {
12183
+ } else if (prepareMapView()) {
12106
12184
  initMap();
12107
12185
  }
12108
-
12109
12186
  }
12110
12187
 
12111
12188
  function updateButtons() {
@@ -12115,8 +12192,9 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12115
12192
  }
12116
12193
 
12117
12194
  function turnOn() {
12118
- var activeLyr = gui.model.getActiveLayer();
12119
- 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
12120
12198
  var dataCRS = info.crs || null;
12121
12199
  var displayCRS = gui.map.getDisplayCRS();
12122
12200
  var warning;
@@ -12159,9 +12237,9 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12159
12237
 
12160
12238
  function getLonLatBounds() {
12161
12239
  var bbox = ext.getBounds().toArray();
12162
- var tr = fromWebMercator(bbox[2], bbox[3]);
12163
- var bl = fromWebMercator(bbox[0], bbox[1]);
12164
- return bl.concat(tr);
12240
+ var bbox2 = fromWebMercator(bbox[0], bbox[1])
12241
+ .concat(fromWebMercator(bbox[2], bbox[3]));
12242
+ return bbox2;
12165
12243
  }
12166
12244
 
12167
12245
  function initMap() {
@@ -12219,6 +12297,15 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12219
12297
  return true;
12220
12298
  }
12221
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
+
12222
12309
  function refresh() {
12223
12310
  var crs = gui.map.getDisplayCRS();
12224
12311
  var off = !crs || !enabled() || !map || loading || !activeStyle;
@@ -12230,9 +12317,7 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12230
12317
  return;
12231
12318
  }
12232
12319
 
12233
- if (!internal.isWebMercator(crs)) {
12234
- gui.map.setDisplayCRS(internal.parseCrsString('webmercator'));
12235
- }
12320
+ prepareMapView();
12236
12321
  var bbox = getLonLatBounds();
12237
12322
  if (!checkBounds(bbox)) {
12238
12323
  // map does not display outside these bounds
@@ -12331,8 +12416,15 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12331
12416
  };
12332
12417
 
12333
12418
  this.getDisplayCRS = function() {
12334
- if (!_activeLyr || !_activeLyr.gui.geographic) return null;
12335
- if (_activeLyr.gui.dynamic_crs) return _activeLyr.gui.dynamic_crs;
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
+ }
12336
12428
  var info = getDatasetCrsInfo(_activeLyr.gui.source.dataset);
12337
12429
  return info.crs || null;
12338
12430
  };
@@ -12365,7 +12457,7 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12365
12457
  var newCRS = utils$1.isString(crs) ? internal.parseCrsString(crs) : crs;
12366
12458
  // TODO: handle case that old and new CRS are the same
12367
12459
  _dynamicCRS = newCRS;
12368
- if (!_activeLyr) return; // stop here if no layers have been selected
12460
+ // if (!_activeLyr) return; // stop here if no layers have been selected
12369
12461
 
12370
12462
  // clear any stored FilteredArcs objects (so they will be recreated with the desired projection)
12371
12463
  clearAllDisplayArcs();
@@ -12383,6 +12475,34 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12383
12475
  updateFullBounds();
12384
12476
  };
12385
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
+
12386
12506
  function getGlobalStyleOptions(opts) {
12387
12507
  var mode = gui.state.interaction_mode;
12388
12508
  return Object.assign({
@@ -12398,7 +12518,7 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12398
12518
  var fullBounds;
12399
12519
  var needReset;
12400
12520
  if (!prevLyr) {
12401
- initMap(); // first call
12521
+ // initMap(); // first call
12402
12522
  }
12403
12523
 
12404
12524
  if (arcsMayHaveChanged(e.flags)) {
@@ -12422,10 +12542,14 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12422
12542
  return;
12423
12543
  }
12424
12544
 
12425
- enhanceLayerForDisplay(e.layer, e.dataset, getDisplayOptions());
12426
- _activeLyr = e.layer;
12427
- _activeLyr.gui.style = getActiveLayerStyle(_activeLyr.gui.displayLayer, getGlobalStyleOptions());
12428
- _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
+ }
12429
12553
 
12430
12554
  if (popupCanStayOpen(e.flags)) {
12431
12555
  // data may have changed; if popup is open, it needs to be refreshed
@@ -12438,7 +12562,11 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12438
12562
  updateVisibleMapLayers();
12439
12563
  fullBounds = calcFullBounds();
12440
12564
 
12441
- if (!prevLyr || prevLyr.gui.tabular || _activeLyr.gui.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) {
12442
12570
  needReset = true;
12443
12571
  } else {
12444
12572
  needReset = mapNeedsReset(fullBounds, _ext.getFullBounds(), _ext.getBounds(), e.flags);
@@ -12448,37 +12576,12 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12448
12576
 
12449
12577
  if (needReset) {
12450
12578
  _ext.reset();
12579
+ if (_basemap) _basemap.refresh();
12451
12580
  }
12452
12581
  drawLayers();
12453
12582
  map.dispatchEvent('updated');
12454
12583
  }
12455
12584
 
12456
- // Initialization just before displaying the map for the first time
12457
- function initMap() {
12458
- _ext.resize();
12459
- _renderer = new LayerRenderer(gui, el, _ext, _mouse);
12460
- gui.buttons.show();
12461
-
12462
- if (opts.inspectorControl) {
12463
- _hit = new HitControl(gui, _ext, _mouse),
12464
- new InspectionControl2(gui, _hit);
12465
- new SelectionTool(gui, _ext, _hit),
12466
- new BoxTool(gui, _ext, _nav),
12467
- new RectangleControl(gui, _hit),
12468
- initInteractiveEditing(gui, _ext, _hit);
12469
- _hit.on('change', updateOverlayLayer);
12470
- }
12471
-
12472
- _ext.on('change', function(e) {
12473
- if (_basemap) _basemap.refresh(); // keep basemap synced up (if enabled)
12474
- if (e.reset) return; // don't need to redraw map here if extent has been reset
12475
- drawLayers('nav');
12476
- });
12477
-
12478
- gui.on('resize', function() {
12479
- position.update(); // kludge to detect new map size after console toggle
12480
- });
12481
- }
12482
12585
 
12483
12586
  function updateOverlayLayer(e) {
12484
12587
  var style = getOverlayStyle(_activeLyr.gui.displayLayer, e, getGlobalStyleOptions());
@@ -12521,7 +12624,8 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12521
12624
 
12522
12625
  if (!b.hasBounds()) {
12523
12626
  // assign bounds to empty layers, to prevent rendering errors downstream
12524
- 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));
12525
12629
  }
12526
12630
  return b;
12527
12631
  }
@@ -12546,7 +12650,6 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12546
12650
 
12547
12651
  // Inflate display bounding box by a tiny amount (gives extent to single-point layers and collapsed shapes)
12548
12652
  b.padBounds(1e-4, 1e-4, 1e-4, 1e-4);
12549
-
12550
12653
  return b;
12551
12654
  }
12552
12655
 
@@ -12559,7 +12662,7 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12559
12662
  }
12560
12663
 
12561
12664
  function isTableView() {
12562
- return !!_activeLyr.gui.tabular;
12665
+ return !!_activeLyr?.gui.tabular;
12563
12666
  }
12564
12667
 
12565
12668
  function findFrameLayer() {
@@ -12769,12 +12872,13 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12769
12872
  El('div')
12770
12873
  .appendTo(menu)
12771
12874
  .addClass('contextmenu-item')
12772
- .text(label)
12875
+ .html(label)
12773
12876
  .on('click', func)
12774
12877
  .show();
12775
12878
  }
12776
12879
 
12777
12880
  this.open = function(e, lyr) {
12881
+ if (!lyr || !lyr.gui.geographic) return;
12778
12882
  _open = true;
12779
12883
  _openCount++;
12780
12884
  var rspace = body.clientWidth - e.pageX;
@@ -12782,27 +12886,29 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12782
12886
  menu.empty().show();
12783
12887
  if (rspace > 150) {
12784
12888
  menu.css('left', e.pageX + xoffs + 'px');
12889
+ menu.css('right', null);
12785
12890
  } else {
12786
12891
  menu.css('right', (body.clientWidth - e.pageX + xoffs) + 'px');
12892
+ menu.css('left', null);
12787
12893
  }
12788
12894
  menu.css('top', (e.pageY - 15) + 'px');
12789
12895
 
12790
12896
  // menu contents
12791
- if (e.deleteVertex) {
12792
- addMenuItem('Delete vertex', e.deleteVertex);
12793
- }
12794
12897
  if (e.coordinates) {
12795
12898
  addCopyCoords();
12796
12899
  }
12900
+ if (e.deleteVertex) {
12901
+ addMenuItem('Delete vertex', e.deleteVertex);
12902
+ }
12797
12903
  if (e.ids?.length) {
12798
12904
  addMenuItem('Copy as GeoJSON', copyGeoJSON);
12799
12905
  }
12800
12906
 
12801
12907
  function addCopyCoords() {
12802
12908
  var bbox = internal.getLayerBounds(lyr, lyr.gui.source.dataset.arcs).toArray();
12803
- var p = internal.getRoundedCoords(e.coordinates, internal.getBoundsPrecisionForDisplay(bbox));
12804
- var coordStr = p.join(',');
12805
- addMenuItem(coordStr, function() {
12909
+ var coordStr = internal.getRoundedCoordString(e.coordinates, internal.getBoundsPrecisionForDisplay(bbox));
12910
+ var displayStr = '• &nbsp;' + coordStr.replace(/-/g, '–').replace(',', ', ');
12911
+ addMenuItem(displayStr, function() {
12806
12912
  saveFileContentToClipboard(coordStr);
12807
12913
  });
12808
12914
  }
@@ -12868,6 +12974,7 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12868
12974
  var clearMsg;
12869
12975
 
12870
12976
  initModeRules(gui);
12977
+ gui.map.init();
12871
12978
 
12872
12979
  gui.showProgressMessage = function(msg) {
12873
12980
  if (!gui.progressMessage) {
@@ -12980,9 +13087,10 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
12980
13087
  importOpts = getImportOpts(manifest),
12981
13088
  gui = new GuiInstance('body');
12982
13089
 
12983
- if (manifest.blurb) {
12984
- El('#splash-screen-blurb').text(manifest.blurb);
12985
- }
13090
+ // TODO: re-enable the "blurb"
13091
+ // if (manifest.blurb) {
13092
+ // El('#splash-screen-blurb').text(manifest.blurb);
13093
+ // }
12986
13094
 
12987
13095
  new AlertControl(gui);
12988
13096
  new RepairControl(gui);
@@ -13013,17 +13121,19 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
13013
13121
  });
13014
13122
 
13015
13123
  // Initial display configuration
13016
- gui.model.on('select', function() {
13124
+ gui.on('mode', function(e) {
13017
13125
  if (dataLoaded) return;
13018
13126
  dataLoaded = true;
13127
+ gui.buttons.show();
13019
13128
  El('#mode-buttons').show();
13129
+ El('#splash-buttons').hide();
13130
+ El('body').addClass('map-view');
13020
13131
  if (importOpts.display_all) {
13021
13132
  gui.model.getLayers().forEach(function(o) {
13022
13133
  gui.map.setLayerPinning(o, true);
13023
13134
  });
13024
13135
  }
13025
13136
  gui.console.runInitialCommands(getInitialConsoleCommands());
13026
-
13027
13137
  });
13028
13138
  };
13029
13139