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

查看:128
本文介绍了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路径,然后将其插入而不是我的circleMarkers.为了使事情变得更复杂,我获得了超过30万的积分.使用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标记吗?我认为我的代码中正确使用了canvas,尤其是对于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标记,代码会因Highlight事件而中断,但是我已经很了解发生了什么错误..它在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>

推荐答案

您的svg 6点标记不是由Canvas渲染的.看一下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.

CircleMarkers呈现在画布上.

The CircleMarkers are rendered on the canvas.

通过创建一个新的L.Path子类,您可以在画布上绘制所需的任何标记,并让Leaflet发挥其最大作用.在任何其他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天全站免登陆