mapshaper 0.7.34 → 0.7.36

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mapshaper",
3
- "version": "0.7.34",
3
+ "version": "0.7.36",
4
4
  "description": "A tool for editing geospatial data for mapping and GIS.",
5
5
  "keywords": [
6
6
  "shapefile",
package/www/index.html CHANGED
@@ -222,8 +222,13 @@ precision=0.001. Click to see all options.</div></div></div></a>
222
222
  <div class="close2-btn"></div>
223
223
  <h3>Display options</h3>
224
224
 
225
- <div class=""><label><input type="checkbox" id="intersections-opt" class="checkbox intersections-opt">detect line intersections</label></div>
226
- <div class=""><label><input type="checkbox" id="ghost-opt" class="checkbox ghost-opt">show reference lines</label></div>
225
+ <div class=""><label><input type="checkbox" id="intersections-opt" class="checkbox intersections-opt">detect line intersections</label><div class="tip-button">?<div class="tip-anchor">
226
+ <div class="tip">Mark points where a layer's lines
227
+ cross or overlap.</div></div></div></div>
228
+ <div class=""><label><input type="checkbox" id="compare-opt" class="checkbox compare-opt">compare with original</label><div class="tip-button">?<div class="tip-anchor">
229
+ <div class="tip">Overlay the original shapes to compare
230
+ before and after an edit. Only works with
231
+ the buffer, smooth and simplify commands.</div></div></div></div>
227
232
 
228
233
  <div class="basemap-opts">
229
234
  <h4>Basemaps</h4>
@@ -9773,6 +9773,9 @@
9773
9773
  if (undoTransaction) {
9774
9774
  setActiveUndoTransaction(undoTransaction);
9775
9775
  }
9776
+ // Notify listeners (e.g. comparison overlay) before the model is mutated,
9777
+ // so they can snapshot the pre-command state of the active layer.
9778
+ gui.dispatchEvent('command_start', {commands: commands});
9776
9779
  try {
9777
9780
  internal.runParsedCommands(commands, job, onCommandsDone);
9778
9781
  } catch(e) {
@@ -10337,6 +10340,116 @@
10337
10340
  }
10338
10341
  }
10339
10342
 
10343
+ // GUI-only "compare with original" feature.
10344
+ //
10345
+ // When the Display option is enabled, an eligible edit (a -buffer/-smooth/
10346
+ // -simplify console command, or entering the simplify slider) snapshots
10347
+ // the pre-edit shapes of the active polygon/polyline layer into a throwaway
10348
+ // dataset and draws them on top of the map as an outline. The overlay
10349
+ // survives panning/zooming but is torn down as soon as the next edit occurs
10350
+ // (or the option is disabled).
10351
+ function CompareControl(gui) {
10352
+ var map = gui.map,
10353
+ model = gui.model,
10354
+ eligibleCommands = {buffer: true, smooth: true, simplify: true},
10355
+ pendingSnapshot = null,
10356
+ simplifyModeActive = false;
10357
+
10358
+ // Snapshot the pre-command state before an eligible console command mutates
10359
+ // the model (dispatched by the console just before running commands).
10360
+ gui.on('command_start', function(e) {
10361
+ if (!compareEnabled()) return;
10362
+ var commands = (e && e.commands) || [];
10363
+ var hasEligible = commands.some(function(cmd) {
10364
+ return !!eligibleCommands[cmd.name];
10365
+ });
10366
+ if (!hasEligible) return;
10367
+ pendingSnapshot = captureSnapshot();
10368
+ });
10369
+
10370
+ // After the model is mutated, promote a pending snapshot to the overlay, or
10371
+ // clear the overlay when a subsequent (non-comparison) edit occurs.
10372
+ model.on('update', function(e) {
10373
+ var flags = (e && e.flags) || {};
10374
+ // The simplify slider drives its own lifecycle (see the 'mode' handler);
10375
+ // ignore its live redraws so the overlay stays visible during dragging.
10376
+ if (simplifyModeActive) return;
10377
+ if (flags.simplify_amount || flags.redraw_only) return;
10378
+ if (pendingSnapshot) {
10379
+ showSnapshot(pendingSnapshot);
10380
+ pendingSnapshot = null;
10381
+ } else {
10382
+ map.setCompareLayer(null);
10383
+ }
10384
+ });
10385
+
10386
+ // Simplify slider: snapshot on entering simplify mode, keep the overlay alive
10387
+ // through slider drags, and clear it on leaving the mode.
10388
+ gui.on('mode', function(e) {
10389
+ if (e.name == 'simplify' && compareEnabled() && isComparableLayer(getActiveLayer())) {
10390
+ simplifyModeActive = true;
10391
+ showSnapshot(captureSnapshot());
10392
+ } else if (e.prev == 'simplify' && simplifyModeActive) {
10393
+ simplifyModeActive = false;
10394
+ clearOverlay();
10395
+ }
10396
+ });
10397
+
10398
+ // Disabling the Display option removes any live overlay immediately.
10399
+ gui.on('compare-clear', function() {
10400
+ simplifyModeActive = false;
10401
+ clearOverlay();
10402
+ });
10403
+
10404
+ function compareEnabled() {
10405
+ return !!(gui.display && gui.display.getOptions().compareOn);
10406
+ }
10407
+
10408
+ function getActiveLayer() {
10409
+ var o = model.getActiveLayer();
10410
+ return o ? o.layer : null;
10411
+ }
10412
+
10413
+ function isComparableLayer(lyr) {
10414
+ return !!lyr && (lyr.geometry_type == 'polygon' || lyr.geometry_type == 'polyline');
10415
+ }
10416
+
10417
+ // Build a standalone, throwaway dataset holding a geometry-only copy of the
10418
+ // active layer (no attribute data, and therefore no style fields). The arcs
10419
+ // are deep-copied so later edits to the source don't alter the overlay.
10420
+ function captureSnapshot() {
10421
+ var active = model.getActiveLayer();
10422
+ if (!active || !isComparableLayer(active.layer)) return null;
10423
+ var src = active.dataset;
10424
+ var dataset = internal.copyDataset({
10425
+ layers: [active.layer],
10426
+ arcs: src.arcs,
10427
+ info: src.info
10428
+ });
10429
+ var lyr = dataset.layers[0];
10430
+ lyr.data = null; // drop attributes (and any style fields)
10431
+ lyr.name = 'before';
10432
+ delete lyr.gui; // force fresh display enhancement
10433
+ // Preserve the source's current simplification level in the copy, so the
10434
+ // overlay reflects the geometry as it was when the edit began.
10435
+ if (dataset.arcs && src.arcs) {
10436
+ dataset.arcs.setRetainedInterval(src.arcs.getRetainedInterval());
10437
+ }
10438
+ return {layer: lyr, dataset: dataset};
10439
+ }
10440
+
10441
+ function showSnapshot(snapshot) {
10442
+ if (snapshot) {
10443
+ map.setCompareLayer(snapshot.layer, snapshot.dataset);
10444
+ }
10445
+ }
10446
+
10447
+ function clearOverlay() {
10448
+ pendingSnapshot = null;
10449
+ map.setCompareLayer(null);
10450
+ }
10451
+ }
10452
+
10340
10453
  async function saveFileContentToClipboard(content) {
10341
10454
  var str = utils$1.isString(content) ? content : content.toString();
10342
10455
  await navigator.clipboard.writeText(str);
@@ -10798,6 +10911,9 @@
10798
10911
  if (e.deleteLayer) {
10799
10912
  addMenuItem('delete layer', e.deleteLayer, '');
10800
10913
  }
10914
+ if (e.duplicateLayer) {
10915
+ addMenuItem('duplicate layer', e.duplicateLayer, '');
10916
+ }
10801
10917
  if (e.styleLayer) {
10802
10918
  addMenuItem('style layer', e.styleLayer, '');
10803
10919
  }
@@ -10915,6 +11031,22 @@
10915
11031
  return retn;
10916
11032
  }
10917
11033
 
11034
+ function runGuiEditCommand(gui, cmd, optsArg) {
11035
+ var opts = optsArg || {};
11036
+ if (!gui.console) return;
11037
+ gui.console.runMapshaperCommands(cmd, function(err, flags) {
11038
+ if (err) {
11039
+ showPopupAlert(err.message || String(err), opts.title || 'Command error');
11040
+ if (opts.onError) opts.onError(err);
11041
+ } else if (opts.onSuccess) {
11042
+ opts.onSuccess(flags);
11043
+ }
11044
+ if (opts.onDone) {
11045
+ opts.onDone(err, flags);
11046
+ }
11047
+ });
11048
+ }
11049
+
10918
11050
  function LayerControl(gui) {
10919
11051
  var model = gui.model;
10920
11052
  var map = gui.map;
@@ -10977,6 +11109,12 @@
10977
11109
 
10978
11110
  model.on('update', function(e) {
10979
11111
  updateMenuBtn();
11112
+ // Skip re-rendering the layer list for display-only updates. The simplify
11113
+ // slider fires 'simplify_amount' continuously while dragging; these updates
11114
+ // don't change layer names, counts, geometry types or icons, so a full
11115
+ // re-render is wasteful and makes the panel's icons flicker.
11116
+ var flags = e.flags || {};
11117
+ if (flags.simplify_amount || flags.redraw_only) return;
10980
11118
  if (isOpen) render();
10981
11119
  });
10982
11120
 
@@ -11220,6 +11358,28 @@
11220
11358
  }
11221
11359
  }
11222
11360
 
11361
+ // Duplicate a layer by running an equivalent command, so the copy is
11362
+ // recorded in session history and can be undone. Uses '-filter true +'
11363
+ // to append a copy without replacing the source, keeps the same name
11364
+ // (if any), and targets the source layer explicitly so the menu works on
11365
+ // layers that aren't currently active. The '+' output becomes the new
11366
+ // default target, which selects the copy as the active layer.
11367
+ function duplicateLayer() {
11368
+ var target = findLayerById(id);
11369
+ var parts = ['-filter true +'];
11370
+ if (!target) return;
11371
+ if (target.layer.name) {
11372
+ parts.push('name=' + internal.formatOptionValue(target.layer.name));
11373
+ }
11374
+ if (!map.isActiveLayer(target.layer)) {
11375
+ parts.push('target=' +
11376
+ internal.formatOptionValue(internal.getLayerTargetId(model, target.layer)));
11377
+ }
11378
+ runGuiEditCommand(gui, parts.join(' '), {
11379
+ title: 'Duplicate layer'
11380
+ });
11381
+ }
11382
+
11223
11383
  function selectLayer() {
11224
11384
  var target = findLayerById(id);
11225
11385
  // don't select if user is typing or dragging
@@ -11262,6 +11422,7 @@
11262
11422
  };
11263
11423
  }
11264
11424
  menuEvent.deleteLayer = deleteLayer;
11425
+ menuEvent.duplicateLayer = duplicateLayer;
11265
11426
  menuEvent.showLayerInfo = showLayerInfo;
11266
11427
  if (target && layerCanBeStyled(target.layer)) {
11267
11428
  menuEvent.styleLayer = styleLayer;
@@ -14204,22 +14365,6 @@
14204
14365
  return aName < bName ? -1 : aName > bName ? 1 : 0;
14205
14366
  }
14206
14367
 
14207
- function runGuiEditCommand(gui, cmd, optsArg) {
14208
- var opts = optsArg || {};
14209
- if (!gui.console) return;
14210
- gui.console.runMapshaperCommands(cmd, function(err, flags) {
14211
- if (err) {
14212
- showPopupAlert(err.message || String(err), opts.title || 'Command error');
14213
- if (opts.onError) opts.onError(err);
14214
- } else if (opts.onSuccess) {
14215
- opts.onSuccess(flags);
14216
- }
14217
- if (opts.onDone) {
14218
- opts.onDone(err, flags);
14219
- }
14220
- });
14221
- }
14222
-
14223
14368
  var fontField = 'font-family';
14224
14369
  var fontSizeField = 'font-size';
14225
14370
  var fontStyleField = 'font-style';
@@ -20170,17 +20315,16 @@
20170
20315
  }
20171
20316
 
20172
20317
  var darkStroke = "#334",
20173
- lightStroke = "#b7d9ea",
20174
20318
  activeStyle = { // outline style for the active layer
20175
20319
  type: 'outline',
20176
- strokeColors: [lightStroke, darkStroke],
20320
+ strokeColors: [null, darkStroke],
20177
20321
  strokeWidth: 0.8,
20178
20322
  dotColor: "#223",
20179
20323
  dotSize: 1
20180
20324
  },
20181
20325
  activeStyleDarkMode = {
20182
20326
  type: 'outline',
20183
- strokeColors: [lightStroke, 'white'],
20327
+ strokeColors: [null, 'white'],
20184
20328
  strokeWidth: 0.9,
20185
20329
  dotColor: 'white',
20186
20330
  dotSize: 1
@@ -20199,12 +20343,24 @@
20199
20343
  intersectionStyle = {
20200
20344
  dotColor: "#FF421D",
20201
20345
  dotSize: 1.3
20346
+ },
20347
+ compareStyle = { // "before" overlay for the comparison feature
20348
+ type: 'outline',
20349
+ strokeColors: [null, 'rgba(185, 0, 178, 0.45)'],
20350
+ strokeWidth: 1.1,
20351
+ dotColor: 'rgba(185, 0, 178, 0.45)',
20352
+ dotSize: 1
20202
20353
  };
20203
20354
 
20204
20355
  function getIntersectionStyle(lyr, opts) {
20205
20356
  return copyBaseStyle(intersectionStyle);
20206
20357
  }
20207
20358
 
20359
+ // Style for the temporary "before" comparison overlay (original shapes).
20360
+ function getCompareLayerStyle(lyr, opts) {
20361
+ return copyBaseStyle(compareStyle);
20362
+ }
20363
+
20208
20364
  // Display style for unselected layers with visibility turned on
20209
20365
  // (may be fully styled or outlined)
20210
20366
  function getReferenceLayerStyle(lyr, opts) {
@@ -20233,11 +20389,6 @@
20233
20389
  } else {
20234
20390
  style = copyBaseStyle(activeStyle);
20235
20391
  }
20236
- // kludge: no ghosted lines if not enabled
20237
- if (style.strokeColors && !opts.ghostingOn) {
20238
- style.strokeColors = [null, style.strokeColors[1]];
20239
- }
20240
-
20241
20392
  return style;
20242
20393
  }
20243
20394
 
@@ -22415,7 +22566,7 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
22415
22566
  _ext = new MapExtent(position),
22416
22567
  _visibleLayers = [], // cached visible map layers
22417
22568
  _hit, _nav,
22418
- _intersectionLyr, _activeLyr, _overlayLayers,
22569
+ _intersectionLyr, _compareLyr, _activeLyr, _overlayLayers,
22419
22570
  _renderer, _dynamicCRS,
22420
22571
  _resizeRedrawTimer = null;
22421
22572
 
@@ -22464,6 +22615,22 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
22464
22615
  }
22465
22616
  };
22466
22617
 
22618
+ // Display a temporary "before" overlay of pre-edit shapes (comparison feature).
22619
+ // lyr, dataset: a standalone (non-catalog) layer + dataset, or null to clear.
22620
+ this.setCompareLayer = function(lyr, dataset, redraw) {
22621
+ if (lyr == _compareLyr) return; // no change
22622
+ if (lyr) {
22623
+ enhanceLayerForDisplay(lyr, dataset, getDisplayOptions());
22624
+ lyr.gui.style = getCompareLayerStyle(lyr.gui.displayLayer, getGlobalStyleOptions());
22625
+ _compareLyr = lyr;
22626
+ } else {
22627
+ _compareLyr = null;
22628
+ }
22629
+ if (redraw !== false) {
22630
+ drawLayers();
22631
+ }
22632
+ };
22633
+
22467
22634
  this.pixelCoordsToLngLatCoords = function(x, y) {
22468
22635
  var crsFrom = this.getDisplayCRS();
22469
22636
  if (!crsFrom) return null; // e.g. table view
@@ -22540,7 +22707,7 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
22540
22707
  clearAllDisplayArcs();
22541
22708
 
22542
22709
  // Reproject all visible map layers
22543
- getContentLayers().concat(_intersectionLyr || []).forEach(function(lyr) {
22710
+ getContentLayers().concat(_intersectionLyr || []).concat(_compareLyr || []).forEach(function(lyr) {
22544
22711
  projectLayerForDisplay(lyr, newCRS);
22545
22712
  });
22546
22713
 
@@ -22613,7 +22780,7 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
22613
22780
  darkMode: !!gui.state.dark_basemap,
22614
22781
  outlineMode: mode == 'vertices',
22615
22782
  interactionMode: mode
22616
- }, opts, gui.display.getOptions()); // intersectionsOn, ghostingOn
22783
+ }, opts, gui.display.getOptions()); // intersectionsOn, compareOn
22617
22784
  }
22618
22785
 
22619
22786
  // Refresh map display in response to data changes, layer selection, etc.
@@ -22853,13 +23020,6 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
22853
23020
  // regenerating active style everytime, to support style change when
22854
23021
  // switching between outline and preview modes.
22855
23022
  style = getActiveLayerStyle(mapLayer.gui.displayLayer, getGlobalStyleOptions());
22856
- // changed: ghosting is set in gui-layer-styler.mjs
22857
- // if (style.type != 'styled' && layers.length > 1 && style.strokeColors) {
22858
- // // kludge to hide ghosted layers when reference layers are present
22859
- // style = utils.defaults({
22860
- // strokeColors: [null, style.strokeColors[1]]
22861
- // }, style);
22862
- // }
22863
23023
  } else {
22864
23024
  if (mapLayer == _activeLyr) {
22865
23025
  console.error("Error: shared map layer");
@@ -22919,6 +23079,10 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
22919
23079
  if (_intersectionLyr) {
22920
23080
  contentLayers = contentLayers.concat(_intersectionLyr);
22921
23081
  }
23082
+ if (_compareLyr) {
23083
+ // draw the comparison overlay on top of everything else
23084
+ contentLayers = contentLayers.concat(_compareLyr);
23085
+ }
22922
23086
  // moved this below intersection layer addition, so intersection dots get scaled
22923
23087
 
22924
23088
  // Adjust dot size based on total visible dots TODO: move this
@@ -23405,11 +23569,11 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
23405
23569
  var menu = gui.container.findChild('.display-options');
23406
23570
  var closeBtn = new SimpleButton(menu.findChild('.close2-btn'));
23407
23571
  var xxBox = menu.findChild('.intersections-opt');
23408
- var ghostBox = menu.findChild('.ghost-opt');
23572
+ var compareBox = menu.findChild('.compare-opt');
23409
23573
 
23410
23574
  var savedOpts = GUI.getSavedValue('display_options') || {};
23411
23575
  xxBox.node().checked = savedOpts.intersectionsOn;
23412
- ghostBox.node().checked = savedOpts.ghostingOn;
23576
+ compareBox.node().checked = savedOpts.compareOn;
23413
23577
 
23414
23578
 
23415
23579
  gui.addMode('display_options', turnOn, turnOff, menuBtn);
@@ -23430,18 +23594,22 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
23430
23594
  });
23431
23595
  });
23432
23596
 
23433
- ghostBox.on('change', function() {
23597
+ compareBox.on('change', function() {
23598
+ var on = getOptions().compareOn;
23434
23599
  gui.dispatchEvent('display_option_change', {
23435
- option: 'ghostingOn',
23436
- value: getOptions().ghostingOn
23600
+ option: 'compareOn',
23601
+ value: on
23437
23602
  });
23438
- gui.dispatchEvent('map-needs-refresh');
23603
+ if (!on) {
23604
+ // tear down any live comparison overlay when the option is disabled
23605
+ gui.dispatchEvent('compare-clear');
23606
+ }
23439
23607
  });
23440
23608
 
23441
23609
  function getOptions() {
23442
23610
  return {
23443
23611
  intersectionsOn: xxBox.node().checked,
23444
- ghostingOn: ghostBox.node().checked
23612
+ compareOn: compareBox.node().checked
23445
23613
  };
23446
23614
  }
23447
23615
 
@@ -24206,6 +24374,7 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
24206
24374
  new LayerControl(gui);
24207
24375
  HeaderMenu();
24208
24376
  gui.console = new Console(gui);
24377
+ new CompareControl(gui);
24209
24378
  HistoryMenu(gui);
24210
24379
  window.mapshaper.getRuntimeStateContext = gui.getRuntimeStateContext;
24211
24380
  window.mapshaper.stringifyRuntimeStateContext = gui.stringifyRuntimeStateContext;