mapshaper 0.6.119 → 0.6.120

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/README.md CHANGED
@@ -98,7 +98,7 @@ Run `npm test` to run mapshaper's tests.
98
98
 
99
99
  Mapshaper is free and open source. If it's useful to you or your organization, please consider supporting its continued development:
100
100
 
101
- * [Ko-fi](https://ko-fi.com/mapshaper) — one-time or recurring donations, no account required.
101
+ * [Ko-fi](https://ko-fi.com/mapshaper) — one-time or recurring contributions, no account required.
102
102
  * [GitHub Sponsors](https://github.com/sponsors/mbloch) — recurring sponsorship.
103
103
 
104
104
  You can also help by [reporting bugs](https://github.com/mbloch/mapshaper/issues) or [telling me what you'd like to see improved](https://tally.so/r/44Njok).
package/mapshaper.js CHANGED
@@ -51131,7 +51131,7 @@ ${svg}
51131
51131
  });
51132
51132
  }
51133
51133
 
51134
- var version = "0.6.119";
51134
+ var version = "0.6.120";
51135
51135
 
51136
51136
  // Parse command line args into commands and run them
51137
51137
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mapshaper",
3
- "version": "0.6.119",
3
+ "version": "0.6.120",
4
4
  "description": "A tool for editing vector datasets for mapping and GIS.",
5
5
  "keywords": [
6
6
  "shapefile",
package/www/index.html CHANGED
@@ -63,7 +63,7 @@
63
63
  <span class="console-btn header-btn btn">Console</span><span class="display-btn header-btn btn">Display</span><span class="separator"></span><span class="separator"></span></span><span class="simplify-btn header-btn btn">Simplify</span><span class="separator"></span><span class="export-btn header-btn btn">Export</span>
64
64
  </div>
65
65
  <div id="splash-buttons" class="page-header-buttons">
66
- <a href="https://github.com/mbloch/mapshaper/wiki"><span id="wiki-btn" class="header-btn btn">Wiki</span></a><span class="separator"></span><a href="https://github.com/mbloch/mapshaper"><span id="github-btn" class="header-btn btn">GitHub</span></a><span class="separator"></span><a href="donate.html"><span id="donate-btn" class="header-btn btn">Donate</span></a>
66
+ <a href="https://github.com/mbloch/mapshaper/wiki"><span id="wiki-btn" class="header-btn btn">Wiki</span></a><span class="separator"></span><a href="https://github.com/mbloch/mapshaper"><span id="github-btn" class="header-btn btn">GitHub</span></a><span class="separator"></span><a href="sponsor.html"><span id="sponsor-btn" class="header-btn btn"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg> Sponsor</span></a>
67
67
  </div>
68
68
  </div>
69
69
 
@@ -3079,13 +3079,12 @@
3079
3079
  throw Error('Internal error');
3080
3080
  }
3081
3081
  }
3082
+ // switch to filtered version of arcs at small scales
3083
+ var useFiltering = filteredArcs && 1/ext.getTransform().mx > filteredSegLen * 1.5;
3082
3084
  if (filteredArcs) {
3083
3085
  // match simplification of unfiltered arcs
3084
3086
  filteredArcs.setRetainedInterval(unfilteredArcs.getRetainedInterval());
3085
3087
  }
3086
- // switch to filtered version of arcs at small scales
3087
- var unitsPerPixel = 1/ext.getTransform().mx,
3088
- useFiltering = filteredArcs && unitsPerPixel > filteredSegLen * 1.5;
3089
3088
  return useFiltering ? filteredArcs : unfilteredArcs;
3090
3089
  };
3091
3090
  }
@@ -8931,6 +8930,9 @@
8931
8930
  var pct = ease ? ease(e.pct) : e.pct,
8932
8931
  val = end * pct + start * (1 - pct);
8933
8932
  self.dispatchEvent('change', {value: val});
8933
+ if (e.done) {
8934
+ self.dispatchEvent('done');
8935
+ }
8934
8936
  }
8935
8937
  }
8936
8938
 
@@ -9095,6 +9097,7 @@
9095
9097
  }
9096
9098
  if (evt.done) {
9097
9099
  active = false;
9100
+ self.dispatchEvent('mousewheelend');
9098
9101
  } else {
9099
9102
  if (fadeFactor > 0) {
9100
9103
  // Decelerate towards the end of the sustain interval (for smoother zooming)
@@ -9688,12 +9691,26 @@
9688
9691
 
9689
9692
  gui.on('map_reset', function() {
9690
9693
  ext.reset(true);
9694
+ // ext.reset() synchronously triggers a 'nav' draw; signal end-of-
9695
+ // interaction so the LayerRenderer settles on a sharp frame
9696
+ // immediately instead of waiting on the fallback timer.
9697
+ gui.dispatchEvent('map_interaction_end');
9691
9698
  });
9692
9699
 
9693
9700
  zoomTween.on('change', function(e) {
9694
9701
  ext.zoomToExtent(e.value, _fx, _fy);
9695
9702
  });
9696
9703
 
9704
+ // Signal end-of-interaction so downstream modules (e.g. LayerRenderer) can
9705
+ // trigger a settle/redraw immediately, instead of waiting on a debounce.
9706
+ zoomTween.on('done', function() {
9707
+ gui.dispatchEvent('map_interaction_end');
9708
+ });
9709
+
9710
+ wheel.on('mousewheelend', function() {
9711
+ gui.dispatchEvent('map_interaction_end');
9712
+ });
9713
+
9697
9714
  mouse.on('click', function(e) {
9698
9715
  gui.dispatchEvent('map_click', e);
9699
9716
  });
@@ -9744,6 +9761,7 @@
9744
9761
  zoomBox.turnOff();
9745
9762
  } else {
9746
9763
  El('body').removeClass('panning').removeClass('pan');
9764
+ gui.dispatchEvent('map_interaction_end');
9747
9765
  }
9748
9766
  });
9749
9767
 
@@ -12279,7 +12297,6 @@
12279
12297
  }
12280
12298
 
12281
12299
  function drawStyledLayerToCanvas(lyr, canv, ext) {
12282
- // TODO: add filter for out-of-view shapes
12283
12300
  var style = lyr.gui.style;
12284
12301
  var layer = lyr.gui.displayLayer;
12285
12302
  var arcs, filter;
@@ -12291,7 +12308,7 @@
12291
12308
  }
12292
12309
  } else {
12293
12310
  arcs = getArcsForRendering(lyr, ext);
12294
- filter = getShapeFilter(arcs, ext);
12311
+ filter = getShapeFilter(arcs, layer.shapes, ext);
12295
12312
  canv.drawStyledPaths(layer.shapes, arcs, style, filter);
12296
12313
  if (style.vertices) {
12297
12314
  canv.drawVertices(layer.shapes, arcs, style, filter);
@@ -12300,11 +12317,12 @@
12300
12317
  canv.clearStyles();
12301
12318
  }
12302
12319
 
12303
-
12304
12320
  // Return a function for testing if an arc should be drawn in the current view
12305
12321
  function getArcFilter(arcs, ext, usedFlag, arcCounts) {
12306
- var MIN_PATH_LEN = 0.1;
12307
- var minPathLen = ext.getPixelSize() * MIN_PATH_LEN, // * 0.5
12322
+ // Arcs whose bbox is smaller than this pixel threshold collapse to a dot
12323
+ // under roundToPix anyway; skipping them saves per-arc iteration.
12324
+ var MIN_PATH_LEN = 0.35;
12325
+ var minPathLen = ext.getPixelSize() * MIN_PATH_LEN,
12308
12326
  geoBounds = ext.getBounds(),
12309
12327
  geoBBox = geoBounds.toArray(),
12310
12328
  allIn = geoBounds.contains(arcs.getBounds()),
@@ -12324,15 +12342,20 @@
12324
12342
  };
12325
12343
  }
12326
12344
 
12327
- // Return a function for testing if a shape should be drawn in the current view
12328
- function getShapeFilter(arcs, ext) {
12329
- var viewBounds = ext.getBounds();
12330
- var bounds = new Bounds();
12331
- if (ext.scale() < 1.1) return null; // full or almost-full zoom: no filter
12332
- return function(shape) {
12333
- bounds.empty();
12334
- arcs.getMultiShapeBounds(shape, bounds);
12335
- return viewBounds.intersects(bounds);
12345
+ // Return a function for testing if a shape should be drawn in the current
12346
+ // view. The filter takes a shape index and tests the shape's bbox against
12347
+ // the viewport. At nearly full extent the test is a no-op, so we return
12348
+ // null to let the caller skip it entirely.
12349
+ function getShapeFilter(arcs, shapes, ext) {
12350
+ if (ext.scale() < 1.1) return null;
12351
+ var view = ext.getBounds();
12352
+ var b = new Bounds();
12353
+ return function(i) {
12354
+ var shp = shapes[i];
12355
+ if (!shp) return false;
12356
+ b.empty();
12357
+ arcs.getMultiShapeBounds(shp, b);
12358
+ return view.intersects(b);
12336
12359
  };
12337
12360
  }
12338
12361
 
@@ -12405,7 +12428,7 @@
12405
12428
  _ctx.fillStyle = color;
12406
12429
  for (i=0; i<shapes.length; i++) {
12407
12430
  var shp = shapes[i];
12408
- if (!shp || filter && !filter(shp)) continue;
12431
+ if (!shp || filter && !filter(i)) continue;
12409
12432
  for (j=0; j<shp.length; j++) {
12410
12433
  iter.init(shp[j]);
12411
12434
  while (iter.hasNext()) {
@@ -12431,14 +12454,14 @@
12431
12454
  // Optimized to draw paths in same-style batches (faster Canvas drawing)
12432
12455
  _self.drawStyledPaths = function(shapes, arcs, style, filter) {
12433
12456
  var styleIndex = {};
12434
- var batchSize = 1500;
12457
+ var batchSize = 100;
12435
12458
  var startPath = getPathStart(_ext, getScaledLineScale(_ext, style));
12436
12459
  var draw = getShapePencil(arcs, _ext);
12437
12460
  var key, item, shp;
12438
12461
  var styler = style.styler || null;
12439
12462
  for (var i=0; i<shapes.length; i++) {
12440
12463
  shp = shapes[i];
12441
- if (!shp || filter && !filter(shp)) continue;
12464
+ if (!shp || filter && !filter(i)) continue;
12442
12465
  if (styler) {
12443
12466
  styler(style, i);
12444
12467
  }
@@ -12606,7 +12629,8 @@
12606
12629
  var startPath = getPathStart(_ext, getLineScale(_ext)),
12607
12630
  t = getScaledTransform(_ext),
12608
12631
  ctx = _ctx,
12609
- batch = 25, // render paths in batches of this size (an optimization)
12632
+ // Larger batches reduce the number of stroke() flushes.
12633
+ batch = 100,
12610
12634
  count = 0,
12611
12635
  n = arcs.size(),
12612
12636
  i, iter;
@@ -12963,12 +12987,68 @@
12963
12987
  _furniture = new SvgDisplayLayer(gui, ext, null).appendTo(el),
12964
12988
  _ext = ext;
12965
12989
 
12990
+ // Fast-nav config: when the most recent render cycle exceeded SLOW_FRAME_MS,
12991
+ // subsequent 'nav' actions transform the previously rendered bitmap via CSS
12992
+ // instead of re-drawing vector content. This trades visible scaling/edge
12993
+ // artifacts for smoother panning and zooming on large datasets.
12994
+ //
12995
+ // Canvas 2D paint ops are normally rasterized asynchronously, so JS usually
12996
+ // returns long before the pixels are committed to the compositor. We force
12997
+ // a synchronous flush at the end of a full render (see flushCanvas) so that
12998
+ // (a) the elapsed time reflects the *true* frame cost (JS + rasterization)
12999
+ // rather than just the JS portion, and
13000
+ // (b) subsequent fast-nav CSS transforms composite cleanly rather than on
13001
+ // top of a still-committing bitmap.
13002
+ // We use only the most recent sample because previous renders may have
13003
+ // drawn a different scene (e.g. a simpler layer that is no longer active).
13004
+ var SLOW_FRAME_MS = 100;
13005
+ // Fallback delay before triggering a redraw if no explicit end-of-interaction
13006
+ // event arrives. The primary trigger is gui 'map_interaction_end' (mouse
13007
+ // release, wheel timeout, zoom tween done); this timer only fires if that
13008
+ // signal is somehow missed, so it's set generously.
13009
+ var FAST_SETTLE_FALLBACK_MS = 2000;
13010
+ var _lastFrameMs = 0; // duration of the most recent full render cycle
13011
+ var _snapshot = null; // {bounds, width, height, pixRatio}
13012
+ var _fastActive = false;
13013
+ var _settleTimer = null;
13014
+ var _redrawPending = false;
13015
+ var _settleRequested = false;
13016
+
13017
+ // 'map_interaction_end' may arrive before the in-flight 'nav' draw runs,
13018
+ // because drawLayers() schedules its work via requestAnimationFrame.
13019
+ // We latch the intent so that whichever order things happen in, the
13020
+ // settle fires immediately rather than waiting on the fallback timer.
13021
+ gui.on('map_interaction_end', function() {
13022
+ if (_redrawPending) {
13023
+ settleNow();
13024
+ } else {
13025
+ _settleRequested = true;
13026
+ }
13027
+ });
13028
+
12966
13029
  // don't let furniture container block events to symbol layers
12967
13030
  _furniture.css('pointer-events', 'none');
12968
13031
 
12969
13032
  this.drawMainLayers = function(layers, action) {
12970
- var needSvgRedraw = action != 'nav' && action != 'hover';
12971
13033
  if (skipMainLayerRedraw(action)) return;
13034
+ if (action == 'nav' && shouldUseFastNav()) {
13035
+ applyFastTransform();
13036
+ // SVG symbol reposition is already cheap; keep labels/symbols accurate
13037
+ // while the canvas is being transformed.
13038
+ layers.forEach(function(lyr) {
13039
+ if (internal.layerHasSvgSymbols(lyr) || internal.layerHasLabels(lyr)) {
13040
+ _svg.reposition(lyr, 'symbol');
13041
+ }
13042
+ });
13043
+ markRedrawPending();
13044
+ return;
13045
+ }
13046
+ var startTime = performance.now();
13047
+ var needSvgRedraw = action != 'nav' && action != 'hover';
13048
+ cancelSettle();
13049
+ _redrawPending = false;
13050
+ _settleRequested = false; // a full render satisfies any pending settle
13051
+ clearFastTransform();
12972
13052
  _mainCanv.prep(_ext);
12973
13053
  if (needSvgRedraw) {
12974
13054
  _svg.clear();
@@ -12983,6 +13063,12 @@
12983
13063
  drawCanvasLayer(lyr, _mainCanv);
12984
13064
  }
12985
13065
  });
13066
+ // Force synchronous rasterization so performance.now() reflects the true
13067
+ // frame cost (including GPU paint-op commit), and so subsequent fast-nav
13068
+ // CSS transforms don't composite over a still-pending canvas commit.
13069
+ flushCanvas(_mainCanv);
13070
+ _lastFrameMs = performance.now() - startTime;
13071
+ captureSnapshot();
12986
13072
  };
12987
13073
 
12988
13074
  // Draw highlight effect for hover and selection
@@ -13034,6 +13120,18 @@
13034
13120
  }
13035
13121
  }
13036
13122
 
13123
+ // Reading back a single pixel forces Chrome/Firefox to synchronously complete
13124
+ // any queued paint operations before returning. This converts an unbounded
13125
+ // deferred "Commit" phase into in-line wall time we can measure, and ensures
13126
+ // the canvas pixels are actually on screen before the caller returns.
13127
+ function flushCanvas(canv) {
13128
+ try {
13129
+ canv.node().getContext('2d').getImageData(0, 0, 1, 1);
13130
+ } catch (e) {
13131
+ // e.g. cross-origin-tainted canvas (shouldn't happen here, but safe)
13132
+ }
13133
+ }
13134
+
13037
13135
  function getSvgLayerType(layer) {
13038
13136
  var type = null;
13039
13137
  if (internal.layerHasSvgSymbols(layer)) {
@@ -13043,6 +13141,87 @@
13043
13141
  }
13044
13142
  return type;
13045
13143
  }
13144
+
13145
+ function captureSnapshot() {
13146
+ _snapshot = {
13147
+ bounds: _ext.getBounds(),
13148
+ width: _ext.width(),
13149
+ height: _ext.height(),
13150
+ pixRatio: GUI.getPixelRatio()
13151
+ };
13152
+ }
13153
+
13154
+ function shouldUseFastNav() {
13155
+ if (!_snapshot) return false;
13156
+ if (_lastFrameMs <= SLOW_FRAME_MS) return false;
13157
+ if (_snapshot.width != _ext.width() || _snapshot.height != _ext.height()) return false;
13158
+ if (_snapshot.pixRatio != GUI.getPixelRatio()) return false;
13159
+ return true;
13160
+ }
13161
+
13162
+ // Apply a CSS transform to the main canvas so its previously rendered
13163
+ // contents line up with the current map extent. The canvas bitmap is not
13164
+ // redrawn.
13165
+ function applyFastTransform() {
13166
+ var t = _ext.getTransform();
13167
+ var b = _snapshot.bounds;
13168
+ var tl = t.transform(b.xmin, b.ymax);
13169
+ var br = t.transform(b.xmax, b.ymin);
13170
+ var sx = (br[0] - tl[0]) / _snapshot.width;
13171
+ var sy = (br[1] - tl[1]) / _snapshot.height;
13172
+ // On retina the canvas bitmap is already scaled down to CSS pixels via a
13173
+ // CSS transform from the .retina class; overriding `transform` here
13174
+ // replaces that rule so we must bake the pixRatio scale back in.
13175
+ var k = 1 / _snapshot.pixRatio;
13176
+ setFastTransform(_mainCanv.node(), tl[0], tl[1], sx * k, sy * k);
13177
+ _fastActive = true;
13178
+ }
13179
+
13180
+ function clearFastTransform() {
13181
+ if (!_fastActive) return;
13182
+ var node = _mainCanv.node();
13183
+ node.style.transform = '';
13184
+ node.style.transformOrigin = '';
13185
+ _fastActive = false;
13186
+ }
13187
+
13188
+ function setFastTransform(node, tx, ty, sx, sy) {
13189
+ node.style.transformOrigin = 'top left';
13190
+ node.style.transform = 'translate(' + tx + 'px,' + ty + 'px) scale(' + sx + ',' + sy + ')';
13191
+ }
13192
+
13193
+ // Record that a fast-nav frame is showing so that the next end-of-interaction
13194
+ // event (or the fallback timer) can trigger a real redraw. Each fast-nav
13195
+ // frame resets the fallback timer so it only fires after the user truly
13196
+ // stops interacting, as a backstop for any interaction path that doesn't
13197
+ // emit 'map_interaction_end'. If 'map_interaction_end' was already
13198
+ // received before this frame ran (e.g. the home button reset, which
13199
+ // dispatches synchronously while the nav draw is still queued in rAF),
13200
+ // settle right now instead of waiting.
13201
+ function markRedrawPending() {
13202
+ _redrawPending = true;
13203
+ cancelSettle();
13204
+ if (_settleRequested) {
13205
+ _settleRequested = false;
13206
+ settleNow();
13207
+ } else {
13208
+ _settleTimer = setTimeout(settleNow, FAST_SETTLE_FALLBACK_MS);
13209
+ }
13210
+ }
13211
+
13212
+ function settleNow() {
13213
+ cancelSettle();
13214
+ if (!_redrawPending) return;
13215
+ _redrawPending = false;
13216
+ gui.dispatchEvent('map-needs-refresh');
13217
+ }
13218
+
13219
+ function cancelSettle() {
13220
+ if (_settleTimer) {
13221
+ clearTimeout(_settleTimer);
13222
+ _settleTimer = null;
13223
+ }
13224
+ }
13046
13225
  }
13047
13226
 
13048
13227
  // Controls the shift-drag box editing tool
@@ -13778,6 +13957,7 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
13778
13957
 
13779
13958
  // RENDERING
13780
13959
  // draw main content layers
13960
+
13781
13961
  _renderer.drawMainLayers(contentLayers, action);
13782
13962
 
13783
13963
  // draw hover & selection overlay
package/www/mapshaper.js CHANGED
@@ -51131,7 +51131,7 @@ ${svg}
51131
51131
  });
51132
51132
  }
51133
51133
 
51134
- var version = "0.6.119";
51134
+ var version = "0.6.120";
51135
51135
 
51136
51136
  // Parse command line args into commands and run them
51137
51137
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.
package/www/page.css CHANGED
@@ -196,10 +196,17 @@ body.map-view {
196
196
  background-color: black;
197
197
  }
198
198
 
199
- #donate-btn {
199
+ #sponsor-btn {
200
200
  color: #ffa;
201
201
  }
202
202
 
203
+ #sponsor-btn svg {
204
+ width: 12px;
205
+ height: 12px;
206
+ fill: currentColor;
207
+ vertical-align: -1px;
208
+ }
209
+
203
210
  .separator {
204
211
  border-left: 1px solid white;
205
212
  height: 10px;
@@ -81,7 +81,7 @@
81
81
  margin: 0 0 32px;
82
82
  }
83
83
 
84
- .donate-buttons {
84
+ .sponsor-buttons {
85
85
  display: flex;
86
86
  gap: 12px;
87
87
  flex-wrap: wrap;
@@ -113,7 +113,7 @@
113
113
  background-color: #222;
114
114
  }
115
115
 
116
- .donate-note {
116
+ .sponsor-note {
117
117
  font-size: 14px;
118
118
  color: #666;
119
119
  margin: 0 0 32px;
@@ -159,7 +159,7 @@
159
159
  @media (max-width: 480px) {
160
160
  main { padding: 24px 18px 60px; }
161
161
  h1 { font-size: 26px; }
162
- .donate-buttons { flex-direction: column; align-items: stretch; }
162
+ .sponsor-buttons { flex-direction: column; align-items: stretch; }
163
163
  .btn { text-align: center; }
164
164
  }
165
165
  </style>
@@ -177,20 +177,20 @@
177
177
 
178
178
  <p class="tagline">Mapshaper is free, open-source software for editing geographic data &mdash; used worldwide for everything from classroom exercises to professional map work.</p>
179
179
 
180
- <div class="donate-buttons">
181
- <a href="https://ko-fi.com/mapshaper" class="btn">Donate via Ko-fi</a>
180
+ <div class="sponsor-buttons">
181
+ <a href="https://ko-fi.com/mapshaper" class="btn">Support via Ko-fi</a>
182
182
  <a href="https://github.com/sponsors/mbloch" class="btn btn-secondary">Sponsor on GitHub</a>
183
183
  </div>
184
184
 
185
- <p class="donate-note">Ko-fi accepts one-time or recurring donations &mdash; no account required. GitHub Sponsors offers recurring sponsorship.</p>
185
+ <p class="sponsor-note">Ko-fi accepts one-time or recurring contributions &mdash; no account required. GitHub Sponsors offers recurring sponsorship.</p>
186
186
 
187
- <h2>What your donation supports</h2>
187
+ <h2>What your contribution supports</h2>
188
188
  <ul class="support-list">
189
189
  <li>Development time &mdash; bug fixes, new features, performance improvements</li>
190
190
  <li>Responding to user issues and requests</li>
191
191
  </ul>
192
192
 
193
- <h2>Recent improvements</h2>
193
+ <h2>Recent work</h2>
194
194
  <ul class="support-list">
195
195
  <li><strong>GeoPackage</strong> import and export</li>
196
196
  <li><strong>FlatGeobuf</strong> import and export</li>