Skip to content

Commit d7e162a

Browse files
committed
Update website
1 parent 09b5c8b commit d7e162a

4 files changed

Lines changed: 226 additions & 27 deletions

File tree

donate.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -185,13 +185,13 @@ <h1>Support mapshaper</h1>
185185
<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>
186186

187187
<div class="donate-buttons">
188-
<a href="https://ko-fi.com/mapshaper" class="btn">Donate via Ko-fi</a>
188+
<a href="https://ko-fi.com/mapshaper" class="btn">Support via Ko-fi</a>
189189
<a href="https://github.com/sponsors/mbloch" class="btn btn-secondary">Sponsor on GitHub</a>
190190
</div>
191191

192-
<p class="donate-note">Ko-fi accepts one-time or recurring donations &mdash; no account required. GitHub Sponsors offers recurring sponsorship.</p>
192+
<p class="donate-note">Ko-fi accepts one-time or recurring contributions &mdash; no account required. GitHub Sponsors offers recurring sponsorship.</p>
193193

194-
<h2>What your donation supports</h2>
194+
<h2>What your contribution supports</h2>
195195
<ul class="support-list">
196196
<li>Development time &mdash; bug fixes, new features, performance improvements</li>
197197
<li>Responding to user issues and requests</li>

index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070
<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>
7171
</div>
7272
<div id="splash-buttons" class="page-header-buttons">
73-
<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>
73+
<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"><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>
7474
</div>
7575
</div>
7676

mapshaper-gui.js

Lines changed: 215 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3079,14 +3079,25 @@
30793079
throw Error('Internal error');
30803080
}
30813081
}
3082-
if (filteredArcs) {
3083-
// match simplification of unfiltered arcs
3084-
filteredArcs.setRetainedInterval(unfilteredArcs.getRetainedInterval());
3085-
}
3082+
var userInterval = unfilteredArcs.getRetainedInterval();
30863083
// switch to filtered version of arcs at small scales
30873084
var unitsPerPixel = 1/ext.getTransform().mx,
30883085
useFiltering = filteredArcs && unitsPerPixel > filteredSegLen * 1.5;
3089-
return useFiltering ? filteredArcs : unfilteredArcs;
3086+
if (useFiltering) {
3087+
// Dynamic LOD: drop any vertex whose Visvalingam triangle-area
3088+
// contribution is smaller than half a square pixel at the current
3089+
// zoom. Visually imperceptible, but can remove a large fraction of
3090+
// vertices on very zoomed-out views of highly detailed arcs.
3091+
// Never go below the user's own simplification setting.
3092+
var displayInterval = unitsPerPixel * unitsPerPixel * 0.5;
3093+
filteredArcs.setRetainedInterval(Math.max(userInterval, displayInterval));
3094+
return filteredArcs;
3095+
}
3096+
if (filteredArcs) {
3097+
// match simplification of unfiltered arcs, in case we switch back
3098+
filteredArcs.setRetainedInterval(userInterval);
3099+
}
3100+
return unfilteredArcs;
30903101
};
30913102
}
30923103

@@ -8931,6 +8942,9 @@
89318942
var pct = ease ? ease(e.pct) : e.pct,
89328943
val = end * pct + start * (1 - pct);
89338944
self.dispatchEvent('change', {value: val});
8945+
if (e.done) {
8946+
self.dispatchEvent('done');
8947+
}
89348948
}
89358949
}
89368950

@@ -9095,6 +9109,7 @@
90959109
}
90969110
if (evt.done) {
90979111
active = false;
9112+
self.dispatchEvent('mousewheelend');
90989113
} else {
90999114
if (fadeFactor > 0) {
91009115
// Decelerate towards the end of the sustain interval (for smoother zooming)
@@ -9688,12 +9703,26 @@
96889703

96899704
gui.on('map_reset', function() {
96909705
ext.reset(true);
9706+
// ext.reset() synchronously triggers a 'nav' draw; signal end-of-
9707+
// interaction so the LayerRenderer settles on a sharp frame
9708+
// immediately instead of waiting on the fallback timer.
9709+
gui.dispatchEvent('map_interaction_end');
96919710
});
96929711

96939712
zoomTween.on('change', function(e) {
96949713
ext.zoomToExtent(e.value, _fx, _fy);
96959714
});
96969715

9716+
// Signal end-of-interaction so downstream modules (e.g. LayerRenderer) can
9717+
// trigger a settle/redraw immediately, instead of waiting on a debounce.
9718+
zoomTween.on('done', function() {
9719+
gui.dispatchEvent('map_interaction_end');
9720+
});
9721+
9722+
wheel.on('mousewheelend', function() {
9723+
gui.dispatchEvent('map_interaction_end');
9724+
});
9725+
96979726
mouse.on('click', function(e) {
96989727
gui.dispatchEvent('map_click', e);
96999728
});
@@ -9744,6 +9773,7 @@
97449773
zoomBox.turnOff();
97459774
} else {
97469775
El('body').removeClass('panning').removeClass('pan');
9776+
gui.dispatchEvent('map_interaction_end');
97479777
}
97489778
});
97499779

@@ -12279,7 +12309,6 @@
1227912309
}
1228012310

1228112311
function drawStyledLayerToCanvas(lyr, canv, ext) {
12282-
// TODO: add filter for out-of-view shapes
1228312312
var style = lyr.gui.style;
1228412313
var layer = lyr.gui.displayLayer;
1228512314
var arcs, filter;
@@ -12291,7 +12320,7 @@
1229112320
}
1229212321
} else {
1229312322
arcs = getArcsForRendering(lyr, ext);
12294-
filter = getShapeFilter(arcs, ext);
12323+
filter = getShapeFilter(arcs, layer.shapes, ext);
1229512324
canv.drawStyledPaths(layer.shapes, arcs, style, filter);
1229612325
if (style.vertices) {
1229712326
canv.drawVertices(layer.shapes, arcs, style, filter);
@@ -12300,11 +12329,12 @@
1230012329
canv.clearStyles();
1230112330
}
1230212331

12303-
1230412332
// Return a function for testing if an arc should be drawn in the current view
1230512333
function getArcFilter(arcs, ext, usedFlag, arcCounts) {
12306-
var MIN_PATH_LEN = 0.1;
12307-
var minPathLen = ext.getPixelSize() * MIN_PATH_LEN, // * 0.5
12334+
// Arcs whose bbox is smaller than this pixel threshold collapse to a dot
12335+
// under roundToPix anyway; skipping them saves per-arc iteration.
12336+
var MIN_PATH_LEN = 0.5;
12337+
var minPathLen = ext.getPixelSize() * MIN_PATH_LEN,
1230812338
geoBounds = ext.getBounds(),
1230912339
geoBBox = geoBounds.toArray(),
1231012340
allIn = geoBounds.contains(arcs.getBounds()),
@@ -12324,15 +12354,20 @@
1232412354
};
1232512355
}
1232612356

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);
12357+
// Return a function for testing if a shape should be drawn in the current
12358+
// view. The filter takes a shape index and tests the shape's bbox against
12359+
// the viewport. At nearly full extent the test is a no-op, so we return
12360+
// null to let the caller skip it entirely.
12361+
function getShapeFilter(arcs, shapes, ext) {
12362+
if (ext.scale() < 1.1) return null;
12363+
var view = ext.getBounds();
12364+
var b = new Bounds();
12365+
return function(i) {
12366+
var shp = shapes[i];
12367+
if (!shp) return false;
12368+
b.empty();
12369+
arcs.getMultiShapeBounds(shp, b);
12370+
return view.intersects(b);
1233612371
};
1233712372
}
1233812373

@@ -12405,7 +12440,7 @@
1240512440
_ctx.fillStyle = color;
1240612441
for (i=0; i<shapes.length; i++) {
1240712442
var shp = shapes[i];
12408-
if (!shp || filter && !filter(shp)) continue;
12443+
if (!shp || filter && !filter(i)) continue;
1240912444
for (j=0; j<shp.length; j++) {
1241012445
iter.init(shp[j]);
1241112446
while (iter.hasNext()) {
@@ -12438,7 +12473,7 @@
1243812473
var styler = style.styler || null;
1243912474
for (var i=0; i<shapes.length; i++) {
1244012475
shp = shapes[i];
12441-
if (!shp || filter && !filter(shp)) continue;
12476+
if (!shp || filter && !filter(i)) continue;
1244212477
if (styler) {
1244312478
styler(style, i);
1244412479
}
@@ -12606,7 +12641,8 @@
1260612641
var startPath = getPathStart(_ext, getLineScale(_ext)),
1260712642
t = getScaledTransform(_ext),
1260812643
ctx = _ctx,
12609-
batch = 25, // render paths in batches of this size (an optimization)
12644+
// Larger batches reduce the number of stroke() flushes.
12645+
batch = 100,
1261012646
count = 0,
1261112647
n = arcs.size(),
1261212648
i, iter;
@@ -12963,12 +12999,68 @@
1296312999
_furniture = new SvgDisplayLayer(gui, ext, null).appendTo(el),
1296413000
_ext = ext;
1296513001

13002+
// Fast-nav config: when the most recent render cycle exceeded SLOW_FRAME_MS,
13003+
// subsequent 'nav' actions transform the previously rendered bitmap via CSS
13004+
// instead of re-drawing vector content. This trades visible scaling/edge
13005+
// artifacts for smoother panning and zooming on large datasets.
13006+
//
13007+
// Canvas 2D paint ops are normally rasterized asynchronously, so JS usually
13008+
// returns long before the pixels are committed to the compositor. We force
13009+
// a synchronous flush at the end of a full render (see flushCanvas) so that
13010+
// (a) the elapsed time reflects the *true* frame cost (JS + rasterization)
13011+
// rather than just the JS portion, and
13012+
// (b) subsequent fast-nav CSS transforms composite cleanly rather than on
13013+
// top of a still-committing bitmap.
13014+
// We use only the most recent sample because previous renders may have
13015+
// drawn a different scene (e.g. a simpler layer that is no longer active).
13016+
var SLOW_FRAME_MS = 100;
13017+
// Fallback delay before triggering a redraw if no explicit end-of-interaction
13018+
// event arrives. The primary trigger is gui 'map_interaction_end' (mouse
13019+
// release, wheel timeout, zoom tween done); this timer only fires if that
13020+
// signal is somehow missed, so it's set generously.
13021+
var FAST_SETTLE_FALLBACK_MS = 2000;
13022+
var _lastFrameMs = 0; // duration of the most recent full render cycle
13023+
var _snapshot = null; // {bounds, width, height, pixRatio}
13024+
var _fastActive = false;
13025+
var _settleTimer = null;
13026+
var _redrawPending = false;
13027+
var _settleRequested = false;
13028+
13029+
// 'map_interaction_end' may arrive before the in-flight 'nav' draw runs,
13030+
// because drawLayers() schedules its work via requestAnimationFrame.
13031+
// We latch the intent so that whichever order things happen in, the
13032+
// settle fires immediately rather than waiting on the fallback timer.
13033+
gui.on('map_interaction_end', function() {
13034+
if (_redrawPending) {
13035+
settleNow();
13036+
} else {
13037+
_settleRequested = true;
13038+
}
13039+
});
13040+
1296613041
// don't let furniture container block events to symbol layers
1296713042
_furniture.css('pointer-events', 'none');
1296813043

1296913044
this.drawMainLayers = function(layers, action) {
12970-
var needSvgRedraw = action != 'nav' && action != 'hover';
1297113045
if (skipMainLayerRedraw(action)) return;
13046+
if (action == 'nav' && shouldUseFastNav()) {
13047+
applyFastTransform();
13048+
// SVG symbol reposition is already cheap; keep labels/symbols accurate
13049+
// while the canvas is being transformed.
13050+
layers.forEach(function(lyr) {
13051+
if (internal.layerHasSvgSymbols(lyr) || internal.layerHasLabels(lyr)) {
13052+
_svg.reposition(lyr, 'symbol');
13053+
}
13054+
});
13055+
markRedrawPending();
13056+
return;
13057+
}
13058+
var startTime = performance.now();
13059+
var needSvgRedraw = action != 'nav' && action != 'hover';
13060+
cancelSettle();
13061+
_redrawPending = false;
13062+
_settleRequested = false; // a full render satisfies any pending settle
13063+
clearFastTransform();
1297213064
_mainCanv.prep(_ext);
1297313065
if (needSvgRedraw) {
1297413066
_svg.clear();
@@ -12983,6 +13075,12 @@
1298313075
drawCanvasLayer(lyr, _mainCanv);
1298413076
}
1298513077
});
13078+
// Force synchronous rasterization so performance.now() reflects the true
13079+
// frame cost (including GPU paint-op commit), and so subsequent fast-nav
13080+
// CSS transforms don't composite over a still-pending canvas commit.
13081+
flushCanvas(_mainCanv);
13082+
_lastFrameMs = performance.now() - startTime;
13083+
captureSnapshot();
1298613084
};
1298713085

1298813086
// Draw highlight effect for hover and selection
@@ -13034,6 +13132,18 @@
1303413132
}
1303513133
}
1303613134

13135+
// Reading back a single pixel forces Chrome/Firefox to synchronously complete
13136+
// any queued paint operations before returning. This converts an unbounded
13137+
// deferred "Commit" phase into in-line wall time we can measure, and ensures
13138+
// the canvas pixels are actually on screen before the caller returns.
13139+
function flushCanvas(canv) {
13140+
try {
13141+
canv.node().getContext('2d').getImageData(0, 0, 1, 1);
13142+
} catch (e) {
13143+
// e.g. cross-origin-tainted canvas (shouldn't happen here, but safe)
13144+
}
13145+
}
13146+
1303713147
function getSvgLayerType(layer) {
1303813148
var type = null;
1303913149
if (internal.layerHasSvgSymbols(layer)) {
@@ -13043,6 +13153,87 @@
1304313153
}
1304413154
return type;
1304513155
}
13156+
13157+
function captureSnapshot() {
13158+
_snapshot = {
13159+
bounds: _ext.getBounds(),
13160+
width: _ext.width(),
13161+
height: _ext.height(),
13162+
pixRatio: GUI.getPixelRatio()
13163+
};
13164+
}
13165+
13166+
function shouldUseFastNav() {
13167+
if (!_snapshot) return false;
13168+
if (_lastFrameMs <= SLOW_FRAME_MS) return false;
13169+
if (_snapshot.width != _ext.width() || _snapshot.height != _ext.height()) return false;
13170+
if (_snapshot.pixRatio != GUI.getPixelRatio()) return false;
13171+
return true;
13172+
}
13173+
13174+
// Apply a CSS transform to the main canvas so its previously rendered
13175+
// contents line up with the current map extent. The canvas bitmap is not
13176+
// redrawn.
13177+
function applyFastTransform() {
13178+
var t = _ext.getTransform();
13179+
var b = _snapshot.bounds;
13180+
var tl = t.transform(b.xmin, b.ymax);
13181+
var br = t.transform(b.xmax, b.ymin);
13182+
var sx = (br[0] - tl[0]) / _snapshot.width;
13183+
var sy = (br[1] - tl[1]) / _snapshot.height;
13184+
// On retina the canvas bitmap is already scaled down to CSS pixels via a
13185+
// CSS transform from the .retina class; overriding `transform` here
13186+
// replaces that rule so we must bake the pixRatio scale back in.
13187+
var k = 1 / _snapshot.pixRatio;
13188+
setFastTransform(_mainCanv.node(), tl[0], tl[1], sx * k, sy * k);
13189+
_fastActive = true;
13190+
}
13191+
13192+
function clearFastTransform() {
13193+
if (!_fastActive) return;
13194+
var node = _mainCanv.node();
13195+
node.style.transform = '';
13196+
node.style.transformOrigin = '';
13197+
_fastActive = false;
13198+
}
13199+
13200+
function setFastTransform(node, tx, ty, sx, sy) {
13201+
node.style.transformOrigin = 'top left';
13202+
node.style.transform = 'translate(' + tx + 'px,' + ty + 'px) scale(' + sx + ',' + sy + ')';
13203+
}
13204+
13205+
// Record that a fast-nav frame is showing so that the next end-of-interaction
13206+
// event (or the fallback timer) can trigger a real redraw. Each fast-nav
13207+
// frame resets the fallback timer so it only fires after the user truly
13208+
// stops interacting, as a backstop for any interaction path that doesn't
13209+
// emit 'map_interaction_end'. If 'map_interaction_end' was already
13210+
// received before this frame ran (e.g. the home button reset, which
13211+
// dispatches synchronously while the nav draw is still queued in rAF),
13212+
// settle right now instead of waiting.
13213+
function markRedrawPending() {
13214+
_redrawPending = true;
13215+
cancelSettle();
13216+
if (_settleRequested) {
13217+
_settleRequested = false;
13218+
settleNow();
13219+
} else {
13220+
_settleTimer = setTimeout(settleNow, FAST_SETTLE_FALLBACK_MS);
13221+
}
13222+
}
13223+
13224+
function settleNow() {
13225+
cancelSettle();
13226+
if (!_redrawPending) return;
13227+
_redrawPending = false;
13228+
gui.dispatchEvent('map-needs-refresh');
13229+
}
13230+
13231+
function cancelSettle() {
13232+
if (_settleTimer) {
13233+
clearTimeout(_settleTimer);
13234+
_settleTimer = null;
13235+
}
13236+
}
1304613237
}
1304713238

1304813239
// Controls the shift-drag box editing tool
@@ -13778,6 +13969,7 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
1377813969

1377913970
// RENDERING
1378013971
// draw main content layers
13972+
1378113973
_renderer.drawMainLayers(contentLayers, action);
1378213974

1378313975
// draw hover & selection overlay

0 commit comments

Comments
 (0)