leaflet.js:使用自定义svg标记(以及很多要点)太慢了

时间:2018-08-14 21:52:22

标签: d3.js svg canvas leaflet mapbox

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

直到现在我一直在使用L.circleMarker,但是现在我不得不使用诸如星号,箭头,星星等标记,所以我决定将它们作为svg路径,然后将其插入而不是我的circleMarkers。为了使事情更复杂,我获得了超过30万的积分。使用circleMarkers,我可以制作一个可行的图表,虽然速度不快,但可以接受,尤其是当使用相当深的缩放比例来区分各个点时(否则,一切就像一个大斑点,没有用处)。

但是,使用svg标记时,图表的计算量变得很大,以至于浏览器只是挂起。我玩过100、1000和10000分,即使有1000分,差异也很明显。请问对此有什么解决方案,有人使用带有大量数据点的svg标记吗?我认为我的代码中正确使用了canvas,尤其是对于circleMarkers,我可能会误会。任何帮助,高度赞赏。摘录中的代码,对底部的几行进行注释/取消注释:

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标记。 非常感谢!

PS。使用svg标记,代码会因Highlight事件而中断,但是我已经很了解发生了什么错误。.它在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>

1 个答案:

答案 0 :(得分:6)

“画布”不会渲染您的SVG 6点标记。看一下DevTools,您会看到它们是img标签,其中包含base-64编码的svg作为源。如果您有很多标记,这会减慢HTML渲染器的速度。

CircleMarkers呈现在画布上。

通过创建新的L.Path子类,您可以在画布上绘制所需的任何标记,并让Leaflet发挥其最大作用。在任何其他JS代码之前进行这些传单的修改,否则它将抱怨它不是构造函数。

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相同

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

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

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

我使用了最新的传单版本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标记的渲染非常快。您可以为要显示的每个图层/组制作一个瓷砖叠加层。所有服务都来自同一个图块服务器。