Я пытаюсь использовать определенные пользователем значки svg для маркеров на листовке, но я думаю, что вся задача становится слишком сложной для моего браузера.
До сих пор я использовал L.circleMarker, но теперь мне нужно использовать маркеры, такие как звездочки, стрелки, звезды и т. д., Поэтому я решил сделать их как путь svg, а затем подключить их вместо моих круговых маркеров. Чтобы усложнить задачу, у меня более 300 тысяч баллов. С помощью circleMarkers я смог создать работоспособную диаграмму, не молниеносно, но вполне приемлемо, особенно когда использовался довольно глубокий зум, чтобы можно было различать отдельные точки (в противном случае все было похоже на большую каплю и бесполезно для изучения).
Однако с маркерами svg диаграмма становится настолько тяжелой с точки зрения вычислений, что браузер просто зависает. Я играл со 100, 1000 и 10000 очков, и даже с 1000 очков разница становится очевидной. Есть ли какое-то решение для этого, пожалуйста, кто-нибудь использовал маркеры svg с большим количеством точек данных? Я думаю, что холст правильно используется в моем коде, особенно для circleMarkers, но я могу ошибаться. Любая помощь очень ценится. Код во фрагменте, прокомментируйте / раскомментируйте несколько строк внизу:
return L.circleMarker(p, style(feature));
или
console.info("Starting markers.")
return L.marker(p, {
renderer: myRenderer,
icon: makeIcon('6-pointed-star', style(feature).color),
});
переключиться с маркеров circleMarkers на svg.
Большое спасибо!
PS. С маркерами svg код прерывается событием выделения, но я вполне понял, что не так ... он отлично работает с 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/[email protected]/dist/leaflet.css" />
<script src = "https://unpkg.com/[email protected]/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.info("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 не отображаются холстом. Взгляните на DevTools, и вы увидите, что это теги img с svg в кодировке base-64 в качестве источника. Если у вас много маркеров, это замедлит работу средства визуализации HTML.
Метки CircleMarkers отображаются на холсте.
Создав новый подкласс L.Path, вы можете нарисовать любой маркер на холсте и позволить листовке делать то, что у нее получается лучше всего. Внесите эти изменения в буклет перед любым другим кодом 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/[email protected]/dist/leaflet.css" />
<script src = "https://unpkg.com/[email protected]/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.info("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 маркеров выполняется довольно быстро. Вы можете наложить плитку для каждого слоя / группы, которую хотите показать. Все обслуживаются с одного тайлового сервера.
rioV8, это просто потрясающе. У меня работает нормально, хотя и несколько медленнее, чем у CircleMarkers в листовке (на 300К баллов). Возможно, мне это сойдет с рук. Что касается другого подхода, у вас есть указатель (пожалуйста, не просите вас писать его, просто если вы где-то его видели, чтобы я мог увидеть, как это делается). Кроме того, как вы сделали путь svg для 6-конечной звезды, мне нужно будет сделать еще несколько фигур. Я могу понять типичные пути svg, но для меня это в новинку. Layer._radius, пожалуйста, откуда это взялось? Мне нужно сделать маркеры больше, и я думаю, это то, что я должен установить.
@Aenaon: он использует те же свойства, что и CircleMarker. Вы устанавливаете radius в методах style и highlight, он сохраняется в layer._radius конструктором circleMarker. Поскольку у нас больше вызовов ctx и нам нужно заполнить более сложную форму по сравнению с кругом, это занимает немного больше времени. Найдите web map tile service wmts или WMS. Посмотрите примеры листовок для облицовки плиткой. Относительные координаты в пути svg не имели для меня значения (goto-relative 13,13 | goto-relative 0,5 [где эта точка ??]), поэтому я пересчитал их все относительно средней точки.
Отличный ответ - возможно, это мои версии листовки v1.5.1, но мне пришлось заменить this._drawnLayers[layer._leaflet_id] = layer; на this._layers[layer._leaflet_id] = layer;. _drawnLayers был undefined
@walking_the_cow: Понятия не имею, какую версию я использовал для ответа, и, возможно, они изменили этот внутренний API. '_' означает непубличный материал
@walking_the_cow: Я использую v1.3.1 и отлично работает. Не пробовал обновляться и не планирую, так как не вижу причин для этого. Еще раз большое спасибо rioV8!
@ rioV8: Если вы это видите, что касается ваших комментариев в последнем абзаце вашего ответа еще в августе 2018 года о предварительном преобразовании маркеров в плитки в виде прозрачных PNG-файлов, есть ли у вас какие-либо указатели, которые я мог бы прочитать и посмотреть, как это делается?
@Aenaon. Вам необходимо реализовать веб-сервер, как вы используете для тайлов ландшафта. Подавать изображение на основе веб-запроса, содержащего x, y, масштабирование. Это определенно больше работы, чем SO-ответ.
@ rioV8, да, конечно, я не ожидаю, что вы напишете код для этого здесь (или где-нибудь еще). На всякий случай, если вы наткнулись на какой-то материал (блоги, книги, действительно что-нибудь), обсуждающий этот подход, чтобы увидеть, как это делается. Я думаю, что мне не хватает того, как сделать эти прозрачные плитки с маркерами, уже предварительно отрендеренными на них как png. Думаю, мне нужно еще много чего, чего я не знаю. Еще раз большое спасибо
@Aenaon: веб-сервер использует некоторую проекцию, это может быть всемирный WGS84 или местный, например, UK-grid. Затем спроецируйте свои местоположения на используемые координаты проекции и нарисуйте маркер на плитках, помните, что маркер перекрывает несколько плиток (до 4), если ваш маркер меньше плитки. Сделайте это для каждого уровня масштабирования, поддерживаемого сервером листов. Вы можете делать "на лету" или предварительно запечь плитки для статических наборов данных. Может быть, делать ежедневную предварительную выпечку.
Используйте меньше точек или переходите на холст в качестве технологии рендеринга.