Skip to content

Instantly share code, notes, and snippets.

@Sumbera
Last active December 22, 2021 08:13
  • Star 2 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save Sumbera/7e8e57368175a1433791 to your computer and use it in GitHub Desktop.
SVG scaled overlay in Leaflet

more info also here: http://blog.sumbera.com/2015/08/14/svg-fast-scaled-overlay-on-leaflet-1-0/ compare to default SVG circle overlay here:http://bl.ocks.org/Sumbera/dcfcc3887ff56a9e1928

  • scaled SVG draw prototype on top of Leaflet 1.0 beta
  • note it uses L.map patch to get it working right
  • SVG data are not modified, only scaled and optionaly radius/stroke width etc. can be specified on onScaleChange callback
  • this approach make it faster for zoom-in/out than default leaflet implementation or default approach that re-calculates point view coordinates with each scal echange
  • "S" in SVG is for "Scalable" so why not to leverage it
  • very experimental, this is already 3rd attempt, 1st one was using absolute pixel coordinates, but had problems on IE and FF with large numbera (>1M) in transform
  • this sample is using 24T real-data sample points, some points are overlapping so they are brighter
  • best performance in Chrome, FF is much worse and Ie is very bad

compare to Canvas based rendering with same data here:http://bl.ocks.org/Sumbera/11114288

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>SVG Simple overlay</title>
<link rel="stylesheet" href="http://www.sumbera.com/gist/js/leaflet/svg/scaled/leaflet.css" />
<style>
body {
margin: 0px;
}
#map {
position: absolute;
height: 100%;
width: 100%;
background-color: #333;
}
</style>
</head>
<body>
<!-- Base libraries-->
<script src="http://www.sumbera.com/gist/js/leaflet/svg/scaled/d3-3.5.5.min.js"></script>
<script src="http://www.sumbera.com/gist/js/leaflet/svg/scaled/leaflet.js"></script>
<script src="L.SvgScaleOverlay.js"></script>
<script src="http://www.sumbera.com/gist/data.js" charset="utf-8"></script>
<div id="map">
</div>
<div id="tooltip" style="width:230px; height:100px;">
</div>
<script>
var lmap = new L.map('map').setView([50.00, 14.44], 9)
.addLayer(L.tileLayer("http://{s}.sm.mapstack.stamen.com/(toner-lite,$fff[difference],$fff[@23],$fff[hsl-saturation@20])/{z}/{x}/{y}.png"));
var circles;
var svgOverlay = L.SvgScaleOverlay();
var radius = 3;
//------------------------------------------------------------
svgOverlay.onInitData = function () {
if (!circles) {
var g = d3.select(this._g);
circles = g.selectAll("circle")
.data(data)
.enter().append('circle');
// -- opacity based on grouping optimization in point data
circles.style("fill-opacity", 0.8);
circles.style("fill", "rgba(255,116,116, 0.5)")
}
circles.each(function (d) {
var elem = d3.select(this);
var point = lmap.project(L.latLng(new L.LatLng(d[0], d[1])))._subtract(lmap.getPixelOrigin());
//var point = lmap.latLngToLayerPoint(new L.LatLng(d.geometry.coordinates[1], d.geometry.coordinates[0]));
elem.attr('cx', point.x)
elem.attr('cy', point.y)
elem.attr('r', radius)
})
};
svgOverlay.onScaleChange = function (scaleDiff) {
if (scaleDiff > 0.5) {
var newRadius = radius * 1 / scaleDiff;
var currentRadius = d3.select('circle').attr("r");
if (currentRadius != newRadius) {
d3.selectAll("circle").attr('r', newRadius);
}
}
}
lmap.addLayer(svgOverlay);
/***********************/
</script>
</body>
</html>
/*
Stanislav Sumbera, August , 2015
- scaled SVG draw prototype on top of Leaflet 1.0 beta
- note it uses L.map patch to get it working right
- SVG data are not modified, only scaled and optionaly radius/stroke width etc. can be specified on onScaleChange callback
- very experimental
*/
//-- Patch to get leaflet properly zoomed
L.Map.prototype.latLngToLayerPoint = function (latlng) { // (LatLng)
var projectedPoint = this.project(L.latLng(latlng));//._round();
return projectedPoint._subtract(this.getPixelOrigin());
};
L.Map.prototype._getNewPixelOrigin = function (center, zoom) {
var viewHalf = this.getSize()._divideBy(2);
return this.project(center, zoom)._subtract(viewHalf)._add(this._getMapPanePos());//._round();
};
// -- from leaflet 1.0 to get this working right on v 0.7 too
L.Map.prototype.getZoomScale = function (toZoom, fromZoom) {
var crs = this.options.crs;
fromZoom = fromZoom === undefined ? this._zoom : fromZoom;
return crs.scale(toZoom) / crs.scale(fromZoom);
};
L.SVGScaleOverlay = L.Class.extend({
options: {
pane: 'overlayPane',
nonBubblingEvents: [], // Array of events that should not be bubbled to DOM parents (like the map)
// how much to extend the clip area around the map view (relative to its size)
// e.g. 0.1 would be 10% of map view in each direction; defaults to clip with the map view
padding: 0
},
isLeafletVersion1: function () {
return L.Layer ? true : false;
},
initialize: function (options) {
L.setOptions(this, options);
L.stamp(this);
},
// ----------------------------------------------------------------------------------
getScaleDiff:function(zoom) {
var zoomDiff = this._groundZoom - zoom;
var scale = (zoomDiff < 0 ? Math.pow(2, Math.abs(zoomDiff)) : 1 / (Math.pow(2, zoomDiff)));
return scale;
},
initSvgContainer: function () {
var xmlns = "http://www.w3.org/2000/svg";
this._svg = document.createElementNS(xmlns, "svg");
this._g = document.createElementNS(xmlns, "g");
if (!this.isLeafletVersion1()) {
L.DomUtil.addClass(this._g, 'leaflet-zoom-hide');
}
var size = this._map.getSize();
this._svgSize = size;
this._svg.setAttribute('width', size.x);
this._svg.setAttribute('height', size.y);
this._svg.appendChild(this._g);
this._groundZoom = this._map.getZoom();
this._shift = new L.Point(0, 0);
this._lastZoom = this._map.getZoom();
var bounds = this._map.getBounds();
this._lastTopLeftlatLng = new L.LatLng(bounds.getNorth(), bounds.getWest()); ////this._initialTopLeft = this._map.layerPointToLatLng(this._lastLeftLayerPoint);
},
resize: function (e) {
var size = this._map.getSize();
this._svgSize = size;
this._svg.setAttribute('width', size.x);
this._svg.setAttribute('height', size.y);
},
moveEnd: function (e) {
var bounds = this._map.getBounds();
var topLeftLatLng = new L.LatLng(bounds.getNorth(), bounds.getWest());
var topLeftLayerPoint = this._map.latLngToLayerPoint(topLeftLatLng);
var lastLeftLayerPoint = this._map.latLngToLayerPoint(this._lastTopLeftlatLng);
var zoom = this._map.getZoom();
var scaleDelta = this._map.getZoomScale(zoom, this._lastZoom);
var scaleDiff = this.getScaleDiff(zoom);
if (this._lastZoom != zoom) {
if (typeof (this.onScaleChange) == 'function') {
this.onScaleChange(scaleDiff);
}
}
this._lastZoom = zoom;
var delta = lastLeftLayerPoint.subtract(topLeftLayerPoint);
this._lastTopLeftlatLng = topLeftLatLng;
L.DomUtil.setPosition(this._svg, topLeftLayerPoint);
this._shift._multiplyBy(scaleDelta)._add(delta);
this._g.setAttribute("transform", "translate(" + this._shift.x + "," + this._shift.y + ") scale(" + scaleDiff + ")"); // --we use viewBox instead
},
animateSvgZoom: function (e) {
var scale = this._map.getZoomScale(e.zoom, this._lastZoom),
offset = this._map._latLngToNewLayerPoint(this._lastTopLeftlatLng, e.zoom, e.center);
L.DomUtil.setTransform(this._svg, offset, scale);
},
getEvents: function () {
var events = {
resize: this.resize,
moveend: this.moveEnd
};
if (this._zoomAnimated && this.isLeafletVersion1()) {
// events.zoomanim = this.animateSvgZoom;
}
return events;
},
/* from Layer , extension to get it worked on lf 1.0, this is not called on ,1. versions */
_layerAdd: function (e) { this.onAdd(e.target); },
/*end Layer */
onAdd: function (map) {
// -- from _layerAdd
// check in case layer gets added and then removed before the map is ready
if (!map.hasLayer(this)) { return; }
this._map = map;
this._zoomAnimated = map._zoomAnimated;
// --onAdd leaflet 1.0
if (!this._svg) {
this.initSvgContainer();
if (this._zoomAnimated) {
//L.DomUtil.addClass(this._svg, 'leaflet-zoom-animated');
L.DomUtil.addClass(this._svg, 'leaflet-zoom-hide');
}
}
var pane = this._map.getPanes().overlayPane;
pane.appendChild(this._svg);
if (typeof (this.onInitData) == 'function') {
this.onInitData();
}
//---------- from _layerAdd
if (this.getAttribution && this._map.attributionControl) {
this._map.attributionControl.addAttribution(this.getAttribution());
}
if (this.getEvents) {
map.on(this.getEvents(), this);
}
map.fire('layeradd', { layer: this });
},
onRemove: function () {
L.DomUtil.remove(this._svg);
}
});
L.SvgScaleOverlay = function (options) {
return new L.SVGScaleOverlay(options);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment