Leaflet.js:自定义 svg 标记太慢(还有很多点) [英] leaflet.js: too slow with custom svg markers (and a lot of points)

查看:21
本文介绍了Leaflet.js:自定义 svg 标记太慢(还有很多点)的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试使用一些用户定义的 svg 图标作为传单上的标记,但我认为整个任务对我的浏览器来说太繁重了.

I am trying to use some user defined svg icons for markers on leaflet but I think the whole task gets too heavy for my browser.

直到现在我一直在使用 L.circleMarker 但我现在必须使用星号、箭头、星号等标记,所以我决定将它们作为 svg 路径,然后将它们插入而不是我的圆圈标记.为了让事情变得更复杂,我有超过 300K 点.使用 circleMarkers,我可以制作一个可行的图表,虽然不是闪电般快速,但完全可以接受,尤其是当使用相当深度的缩放来区分各个点时(否则一切都像一个大斑点,对研究毫无用处).

Until now I was using L.circleMarker but I now have to use markers like asterisks, arrows, stars etc instead so I decided to do them as svg path and then plug them in instead of my circleMarkers. To make things more complicated I have more than 300K points. With the circleMarkers I was able to make a workable chart, not lightning fast but quite acceptable especially when a fairly deep zoom was used to be able to distinguish individual points (otherwise everything was like a big blob and useless to study).

但是,使用 svg 标记时,图表的计算量会变得非常大,以至于浏览器会挂起.我玩过 100、1000 和 10000 点,即使是 1000 点,差异也很明显.请问有什么解决办法吗,有没有人用过很多数据点的svg标记?我认为画布在我的代码中使用得当,尤其是 circleMarkers 但我可能弄错了.任何帮助高度赞赏.代码段中的代码,注释/取消注释底部的几行:

With the svg markers however the chart becomes so computationally heavy that the browser just hangs. I have played around with 100, 1000 and 10000 points and even with 1000 points the difference becomes apparent. Is there any solution to this please, has anyone used svg markers with lots of data points? I think canvas is properly employed in my code, especially for the circleMarkers but I might be mistaken. Any help highly appreciated. Code in the snippet, comment/uncomment the few lines towards the bottom:

return L.circleMarker(p, style(feature));

console.log("Starting markers.")
return L.marker(p, {
    renderer: myRenderer,
    icon: makeIcon('6-pointed-star', style(feature).color),
    });

circleMarkers 切换到 svg 标记.非常感谢!

to switch from the circleMarkers to svg markers. Many thanks!

PS.使用 svg 标记,代码会因突出显示事件而中断,但我已经完全理解出了什么问题..它与 circleMarkers

PS. With svg markers the code breaks with the highlight event but I have quite understood whats wrong..it works fine with circleMarkers

<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="utf-8">

<title>Chart</title>
    <style>
        #tooltip {
          position:absolute;
          background-color: #2B292E;
          color: white;
          font-family: sans-serif;
          font-size: 15px;
          pointer-events: none; /*dont trigger events on the tooltip*/
          padding: 15px 20px 10px 20px;
          text-align: center;
          opacity: 0;
          border-radius: 4px;
        }

		html, body {
			height: 100%;
			margin: 0;
		}
		#map {
			width: 600px;
			height: 600px;
		}
    </style>

<!-- Reference style.css -->
<!--    <link rel="stylesheet" type="text/css" href="style.css">-->

<!-- Reference minified version of D3 -->
    <script src='https://d3js.org/d3.v4.min.js' type='text/javascript'></script>
    <script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'></script>
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.1/dist/leaflet.css" />
    <script src="https://unpkg.com/leaflet@1.3.1/dist/leaflet.js"></script>
</head>

<body>



<div id="map"></div>


<script>
    var data = [];
    var NumOfPoints = 100
    for (let i = 0; i < NumOfPoints; i++) {
        data.push({
            num: i,
            x: Math.random(),
            y: Math.random(),
            year: Math.floor(100*Math.random())
        })
    }
    
    renderChart(data);


    function make_dots(data) {
        var arr = [];
        var nest = d3.nest()
            .key(function (d) {
                return Math.floor(d.year / 10);;
            })
            .entries(data);

        for (var k = 0; k < nest.length; ++k) {
            arr[k] = helper(nest[k].values);
        }
        return arr;
    }

    function helper(data) {
        dots = {
            type: "FeatureCollection",
            features: []
        };
        for (var i = 0; i < data.length; ++i) {
            x = data[i].x;
            y = data[i].y;
            var g = {
                "type": "Point",
                "coordinates": [x, y]
            };

            //create feature properties
            var p = {
                "id": i,
                "popup": "Dot_" + i,
                "year": parseInt(data[i].year),
                "size": 30 // Fixed size
            };

            //create features with proper geojson structure
            dots.features.push({
                "geometry": g,
                "type": "Feature",
                "properties": p
            });
        }
        return dots;
    }


    //////////////////////////////////////////////////////////////////////////////////////////////
    //styling and displaying the data as circle markers//
    //////////////////////////////////////////////////////////////////////////////////////////////

    //create color ramp
    function getColor(y) {
        return y > 90 ? '#6068F0' :
            y > 80 ? '#6B64DC' :
            y > 70 ? '#7660C9' :
            y > 60 ? '#815CB6' :
            y > 50 ? '#8C58A3' :
            y > 40 ? '#985490' :
            y > 30 ? '#A3507C' :
            y > 20 ? '#AE4C69' :
            y > 10 ? '#B94856' :
            y > 0 ? '#C44443' :
            '#D04030';
    }

    //calculate radius so that resulting circles will be proportional by area
    function getRadius(y) {
        r = Math.sqrt(y / Math.PI)
        return r;
    }

    // This is very important! Use a canvas otherwise the chart is too heavy for the browser when
    // the number of points is too high, as in this case where we have around 300K points to plot
    var myRenderer = L.canvas({
        padding: 0.5
    });

    //create style, with fillColor picked from color ramp
    function style(feature) {
        return {
            radius: getRadius(feature.properties.size),
            fillColor: getColor(feature.properties.year),
            color: "#000",
            weight: 0,
            opacity: 1,
            fillOpacity: 0.9,
            renderer: myRenderer
        };
    }

    //create highlight style, with darker color and larger radius
    function highlightStyle(feature) {
        return {
            radius: getRadius(feature.properties.size) + 1.5,
            fillColor: "#FFCE00",
            color: "#FFCE00",
            weight: 1,
            opacity: 1,
            fillOpacity: 0.9
        };
    }

    //attach styles and popups to the marker layer
    function highlightDot(e) {
        var layer = e.target;
        dotStyleHighlight = highlightStyle(layer.feature);
        layer.setStyle(dotStyleHighlight);
        if (!L.Browser.ie && !L.Browser.opera) {
            layer.bringToFront();
        }
    }

    function resetDotHighlight(e) {
        var layer = e.target;
        dotStyleDefault = style(layer.feature);
        layer.setStyle(dotStyleDefault);
    }

    function onEachDot(feature, layer) {
        layer.on({
            mouseover: highlightDot,
            mouseout: resetDotHighlight
        });
        var popup = '<table style="width:110px"><tbody><tr><td><div><b>Marker:</b></div></td><td><div>' + feature.properties.popup +
            '</div></td></tr><tr class><td><div><b>Group:</b></div></td><td><div>' + feature.properties.year +
            '</div></td></tr><tr><td><div><b>X:</b></div></td><td><div>' + feature.geometry.coordinates[0] +
            '</div></td></tr><tr><td><div><b>Y:</b></div></td><td><div>' + feature.geometry.coordinates[1] +
            '</div></td></tr></tbody></table>'

        layer.bindPopup(popup);
    }
    
    function makeIcon(name, color) {

    if (name == "diamond") {
        // here's the SVG for the marker
        var icon = "<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='20' height='20'> " +
            "<path stroke=" + "'" + color + "'" + " stroke-width='3' fill='none' " +
            " d='M10,1 5,10 10,19, 15,10Z'/></svg>";
    }


    // Based on http://www.smiffysplace.com/stars.html
    if (name == "6-pointed-star") {
        // here's the SVG for the marker
        var icon = "<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='28' height='28'> " +
            "<path stroke=" + "'" + color + "'" + " stroke-width='3' fill='none' " +
            " d='m13 13m0 5l5 3.6599999999999966l-0.6700000000000017 -6.159999999999997l5.670000000000002 -2.5l-5.670000000000002 -2.5l0.6700000000000017 -6.159999999999997l-5 3.6599999999999966l-5 -3.6599999999999966l0.6700000000000017 6.159999999999997l-5.670000000000002 2.5l5.670000000000002 2.5l-0.6700000000000017 6.159999999999997z'/></svg>";

    }


    // here's the trick, base64 encode the URL
    var svgURL = "data:image/svg+xml;base64," + btoa(icon);

    // create icon
    var svgIcon = L.icon({
        iconUrl: svgURL,
        iconSize: [20, 20],
        shadowSize: [12, 10],
        iconAnchor: [5, 5],
        popupAnchor: [5, -5]
    });

    return svgIcon
}
    

    function renderChart(data) {
        var myDots = make_dots(data);

        var minZoom = 0,
            maxZoom = 15;

        var map = L.map('map', {
            minZoom: minZoom,
            maxZoom: maxZoom
        }).setView([0.5, 0.5], 10);

        L.tileLayer("http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
            continuousWorld: false,
            minZoom: 0,
            noWrap: true
        }).addTo(map);

        var myRenderer = L.canvas({
            padding: 0.5
        });

        // Define an array to keep layerGroups
        var dotlayer = [];

        //create marker layer and display it on the map
        for (var i = 0; i < myDots.length; i += 1) {
            dotlayer[i] = L.geoJson(myDots[i], {
                pointToLayer: function (feature, latlng) {
                    var p = latlng;
//                    return L.circleMarker(p, style(feature));
                    console.log("Starting markers.")
                    return L.marker(p, {
                        renderer: myRenderer,
                        icon: makeIcon('6-pointed-star', style(feature).color),
                    });
                },
                onEachFeature: onEachDot
            }).addTo(map);
        }


        var cl = L.control.layers(null, {}).addTo(map);
        for (j = 0; j < dotlayer.length; j += 1) {
            var name = "Group " + j + "0-" + j + "9";
            cl.addOverlay(dotlayer[j], name);
        }

    }


</script>
</body>

</html>

推荐答案

Canvas 不会渲染您的 svg 6 点标记.看看 DevTools,你会发现它们是 img 标签,以 base-64 编码的 svg 作为源.如果您有大量标记,这会降低 HTML 渲染器的速度.

Your svg 6-point markers are not rendered by the Canvas. Have a look with the DevTools and you see they are img tags with a base-64 encoded svg as source. If you have a large number of markers this will slow down the HTML renderer.

圆形标记在画布上呈现.

The CircleMarkers are rendered on the canvas.

通过创建一个新的 L.Path 子类,您可以在画布上绘制您想要的任何标记,并让小册子做它最擅长的事情.在任何其他 JS 代码之前进行这些传单修改,否则它会抱怨它不是构造函数.

By creating a new L.Path subclass you can draw any marker you want on the canvas and let leaflet do what it does best. Make these leaflet modifications before any other JS code otherwise it will complain that it is not a constructor.

L.Canvas.include({
    _updateMarker6Point: function (layer) {
        if (!this._drawing || layer._empty()) { return; }

        var p = layer._point,
            ctx = this._ctx,
            r = Math.max(Math.round(layer._radius), 1);

        this._drawnLayers[layer._leaflet_id] = layer;

        ctx.beginPath();
        ctx.moveTo(p.x + r     , p.y );
        ctx.lineTo(p.x + 0.43*r, p.y + 0.25 * r);
        ctx.lineTo(p.x + 0.50*r, p.y + 0.87 * r);
        ctx.lineTo(p.x         , p.y + 0.50 * r);
        ctx.lineTo(p.x - 0.50*r, p.y + 0.87 * r);
        ctx.lineTo(p.x - 0.43*r, p.y + 0.25 * r);
        ctx.lineTo(p.x -      r, p.y );
        ctx.lineTo(p.x - 0.43*r, p.y - 0.25 * r);
        ctx.lineTo(p.x - 0.50*r, p.y - 0.87 * r);
        ctx.lineTo(p.x         , p.y - 0.50 * r);
        ctx.lineTo(p.x + 0.50*r, p.y - 0.87 * r);
        ctx.lineTo(p.x + 0.43*r, p.y - 0.25 * r);
        ctx.closePath();
        this._fillStroke(ctx, layer);
    }
});

var Marker6Point = L.CircleMarker.extend({
    _updatePath: function () {
        this._renderer._updateMarker6Point(this);
    }
});

你使用它和 circleMarker 一样

You use it the same as for the circleMarker

return new Marker6Point(p, style(feature));

在代码中有2个L.canvas实例和2个变量myRenderer.我保留了全局变量,但仅在 renderChart() 函数中构造了 L.map() 时才分配它.

In the code there are 2 L.canvas instances and there are 2 variables myRenderer. I have kept the global variable but assign it only when there is an L.map() constructed in the renderChart() function.

在演示中,我使用了带有标记的更大区域并使用了 10000 个标记.我的浏览器对此没有任何问题.我已将属性中的 size 增加到 500,因此我们得到了一个半径为 13 像素的标记,因此您可以清楚地看到星星.

For the demo I have used a larger area with markers and used 10000 markers. My browser has no problems with that. I have increased the size in the properties to 500 so we get a marker with radius 13 pixels, so you can see the star clearly.

我使用的是最新的传单版本 1.3.3.

I have used the latest leaflet version 1.3.3.

<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="utf-8">

<title>Chart</title>
<style>
#tooltip {
    position:absolute;
    background-color: #2B292E;
    color: white;
    font-family: sans-serif;
    font-size: 15px;
    pointer-events: none; /*dont trigger events on the tooltip*/
    padding: 15px 20px 10px 20px;
    text-align: center;
    opacity: 0;
    border-radius: 4px;
}

html, body {
    height: 100%;
    margin: 0;
}
#map {
    width: 600px;
    height: 600px;
}
</style>

<!-- Reference style.css -->
<!--    <link rel="stylesheet" type="text/css" href="style.css">-->

<!-- Reference minified version of D3 -->
<script src='https://d3js.org/d3.v4.min.js' type='text/javascript'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'></script>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.3/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.3.3/dist/leaflet.js"></script>
</head>

<body>
<div id="map"></div>
<script>
L.Canvas.include({
    _updateMarker6Point: function (layer) {
        if (!this._drawing || layer._empty()) { return; }

        var p = layer._point,
            ctx = this._ctx,
            r = Math.max(Math.round(layer._radius), 1);

        this._drawnLayers[layer._leaflet_id] = layer;

        ctx.beginPath();
        ctx.moveTo(p.x + r     , p.y );
        ctx.lineTo(p.x + 0.43*r, p.y + 0.25 * r);
        ctx.lineTo(p.x + 0.50*r, p.y + 0.87 * r);
        ctx.lineTo(p.x         , p.y + 0.50 * r);
        ctx.lineTo(p.x - 0.50*r, p.y + 0.87 * r);
        ctx.lineTo(p.x - 0.43*r, p.y + 0.25 * r);
        ctx.lineTo(p.x -      r, p.y );
        ctx.lineTo(p.x - 0.43*r, p.y - 0.25 * r);
        ctx.lineTo(p.x - 0.50*r, p.y - 0.87 * r);
        ctx.lineTo(p.x         , p.y - 0.50 * r);
        ctx.lineTo(p.x + 0.50*r, p.y - 0.87 * r);
        ctx.lineTo(p.x + 0.43*r, p.y - 0.25 * r);
        ctx.closePath();
        this._fillStroke(ctx, layer);
    }
});

var Marker6Point = L.CircleMarker.extend({
    _updatePath: function () {
        this._renderer._updateMarker6Point(this);
    }
});

var data = [];
var NumOfPoints = 10000;
for (let i = 0; i < NumOfPoints; i++) {
    data.push({
        num: i,
        x: Math.random()*60,
        y: Math.random()*60,
        year: Math.floor(100*Math.random())
    })
}

renderChart(data);

function make_dots(data) {
    var arr = [];
    var nest = d3.nest()
        .key(function (d) {
            return Math.floor(d.year / 10);
        })
        .entries(data);

    for (var k = 0; k < nest.length; ++k) {
        arr[k] = helper(nest[k].values);
    }
    return arr;
}

function helper(data) {
    dots = {
        type: "FeatureCollection",
        features: []
    };
    for (var i = 0; i < data.length; ++i) {
        x = data[i].x;
        y = data[i].y;
        var g = {
            "type": "Point",
            "coordinates": [x, y]
        };

        //create feature properties
        var p = {
            "id": i,
            "popup": "Dot_" + i,
            "year": parseInt(data[i].year),
            "size": 500 // Fixed size circle radius=~13
        };

        //create features with proper geojson structure
        dots.features.push({
            "geometry": g,
            "type": "Feature",
            "properties": p
        });
    }
    return dots;
}

//////////////////////////////////////////////////////////////////////////////////////////////
//styling and displaying the data as circle markers//
//////////////////////////////////////////////////////////////////////////////////////////////

//create color ramp
function getColor(y) {
    return y > 90 ? '#6068F0' :
        y > 80 ? '#6B64DC' :
        y > 70 ? '#7660C9' :
        y > 60 ? '#815CB6' :
        y > 50 ? '#8C58A3' :
        y > 40 ? '#985490' :
        y > 30 ? '#A3507C' :
        y > 20 ? '#AE4C69' :
        y > 10 ? '#B94856' :
        y > 0 ? '#C44443' :
        '#D04030';
}

//calculate radius so that resulting circles will be proportional by area
function getRadius(y) {
    r = Math.sqrt(y / Math.PI)
    return r;
}

// This is very important! Use a canvas otherwise the chart is too heavy for the browser when
// the number of points is too high, as in this case where we have around 300K points to plot
var myRenderer;
//  = L.canvas({
//     padding: 0.5
// });

//create style, with fillColor picked from color ramp
function style(feature) {
    return {
        radius: getRadius(feature.properties.size),
        fillColor: getColor(feature.properties.year),
        color: "#000",
        weight: 0,
        opacity: 1,
        fillOpacity: 0.9,
        renderer: myRenderer
    };
}

//create highlight style, with darker color and larger radius
function highlightStyle(feature) {
    return {
        radius: getRadius(feature.properties.size) + 1.5,
        fillColor: "#FFCE00",
        color: "#FFCE00",
        weight: 1,
        opacity: 1,
        fillOpacity: 0.9
    };
}

//attach styles and popups to the marker layer
function highlightDot(e) {
    var layer = e.target;
    dotStyleHighlight = highlightStyle(layer.feature);
    layer.setStyle(dotStyleHighlight);
    if (!L.Browser.ie && !L.Browser.opera) {
        layer.bringToFront();
    }
}

function resetDotHighlight(e) {
    var layer = e.target;
    dotStyleDefault = style(layer.feature);
    layer.setStyle(dotStyleDefault);
}

function onEachDot(feature, layer) {
    layer.on({
        mouseover: highlightDot,
        mouseout: resetDotHighlight
    });
    var popup = '<table style="width:110px"><tbody><tr><td><div><b>Marker:</b></div></td><td><div>' + feature.properties.popup +
        '</div></td></tr><tr class><td><div><b>Group:</b></div></td><td><div>' + feature.properties.year +
        '</div></td></tr><tr><td><div><b>X:</b></div></td><td><div>' + feature.geometry.coordinates[0] +
        '</div></td></tr><tr><td><div><b>Y:</b></div></td><td><div>' + feature.geometry.coordinates[1] +
        '</div></td></tr></tbody></table>'

    layer.bindPopup(popup);
}

function makeIcon(name, color) {

    if (name == "diamond") {
        // here's the SVG for the marker
        var icon = "<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='20' height='20'> " +
            "<path stroke=" + "'" + color + "'" + " stroke-width='3' fill='none' " +
            " d='M10,1 5,10 10,19, 15,10Z'/></svg>";
    }

    // Based on http://www.smiffysplace.com/stars.html
    if (name == "6-pointed-star") {
        // here's the SVG for the marker
        var icon = "<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='28' height='28'> " +
            "<path stroke=" + "'" + color + "'" + " stroke-width='3' fill='none' " +
            " d='m13 13m0 5l5 3.66l-0.67 -6.16l5.67 -2.5l-5.67 -2.5l0.67 -6.16l-5 3.66l-5 -3.66l0.67 6.16l-5.67 2.5l5.67 2.5l-0.67 6.16z'/></svg>";
    }

    // here's the trick, base64 encode the URL
    var svgURL = "data:image/svg+xml;base64," + btoa(icon);

    // create icon
    var svgIcon = L.icon({
        iconUrl: svgURL,
        iconSize: [20, 20],
        shadowSize: [12, 10],
        iconAnchor: [5, 5],
        popupAnchor: [5, -5]
    });

    return svgIcon
}

function renderChart(data) {
    var myDots = make_dots(data);

    var minZoom = 0,
        maxZoom = 15;

    var map = L.map('map', {
        minZoom: minZoom,
        maxZoom: maxZoom
    }).setView([0.5, 0.5], 5);

    L.tileLayer("http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
        continuousWorld: false,
        minZoom: 0,
        noWrap: true
    }).addTo(map);

    myRenderer = L.canvas({ padding: 0.5 });

    // Define an array to keep layerGroups
    var dotlayer = [];

    //create marker layer and display it on the map
    for (var i = 0; i < myDots.length; i += 1) {
        dotlayer[i] = L.geoJson(myDots[i], {
            pointToLayer: function (feature, latlng) {
                var p = latlng;
                // return L.circleMarker(p, style(feature));

                // console.log("Starting markers.")
                // return L.marker(p, {
                //     renderer: myRenderer,
                //     icon: makeIcon('6-pointed-star', style(feature).color),
                // });

                return new Marker6Point(p, style(feature));
            },
            onEachFeature: onEachDot
        }).addTo(map);
    }
    var cl = L.control.layers(null, {}).addTo(map);
    for (j = 0; j < dotlayer.length; j += 1) {
        var name = "Group " + j + "0-" + j + "9";
        cl.addOverlay(dotlayer[j], name);
    }
}
</script>
</body>

</html>

如果这还不够快,您可以随时设置自己的图块服务器并将标记烘焙到不同缩放级别的图块中(在透明 PNG 文件中).并将其用作地形图块顶部的单独图块层.您没有简单的弹出窗口,但 300K 标记的渲染速度非常快.您可以为要显示的每个图层/组制作平铺叠加层.全部由同一个磁贴服务器提供服务.

If this is not fast enough you can always set up your own tile server and bake the markers into the tiles at the different zoom levels (in transparent PNG files). And use this as a separate tile layer on top of the terrain tiles. You don't have easy popups but the rendering of 300K markers is pretty fast. You can make a tile overlay for each layer/group you want to show. All serviced from the same tile server.

这篇关于Leaflet.js:自定义 svg 标记太慢(还有很多点)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆