我正在使用带有传单标签的传单。有时标记会重叠,这对用户体验很不利,因此我实现了以下Spiderfier功能:
/*Geometry*/
//Abstract Shape function capable to check intersection
function Shape(params) {
Initializable.call(this, params);
this.initialize("Type", "Default");
//Let's know whether intersection is symmetric
this.initialize("Symmetric", true);
this.initialize("Intersects", function (shape) {
return false;
});
}
//These rectangles have two horizontal and two vertical sides
function HorizontalVerticalRectangle(params) {
params.Type = "HorizontalVerticalRectangle";
var self = this;
if (typeof params.Intersects !== "function") {
//Default Intersects function
params.Intersects = function (shape) {
//If the two shapes have the same types and self is not to the right, left, bottom or top compared to shape then they intersect each-other
if (shape.Type === self.Type) {
return !((self.TopLeft.x > shape.BottomRight.x) ||
(self.BottomRight.x < shape.TopLeft.x) ||
(self.TopLeft.y > shape.BottomRight.y) ||
(self.BottomRight.y < shape.TopLeft.y));
//In case of top half circles, we need to make sure that the horizontal square collides the circle and in the top half
} else if (shape.Type === "TopHalfCircle") {
return (self.TopLeft.y <= shape.Center.y) && HorizontalVerticalRectangle.prototype.CollidesCircle(self, shape.Center.x, shape.Center.y, shape.Diameter / 2);
}
//Not implemented
return false;
};
}
Shape.call(this, params);
this.initialize("TopLeft", { x: 0, y: 0 });
this.initialize("BottomRight", { x: 0, y: 0 });
//Make sure the x and y coordinates are kept as floats
this.TopLeft.x = parseFloat(this.TopLeft.x);
this.TopLeft.y = parseFloat(this.TopLeft.y);
this.BottomRight.x = parseFloat(this.BottomRight.x);
this.BottomRight.y = parseFloat(this.BottomRight.y);
//Coordinate setters
this.setTopLeftX = function (x) {
self.TopLeft.x = parseFloat(x);
};
this.setTopLeftY = function (y) {
self.TopLeft.y = parseFloat(y);
};
this.setBottomRightX = function (x) {
self.BottomRight.x = parseFloat(x);
};
this.setBottomRightY = function (y) {
self.BottomRight.y = parseFloat(y);
};
}
HorizontalVerticalRectangle.prototype.CollidesCircle = function (horizontalRectangle, centerX, centerY, radius) {
var deltaX = centerX - Math.max(horizontalRectangle.TopLeft.x, Math.min(centerX, horizontalRectangle.BottomRight.x));
var deltaY = centerY - Math.max(horizontalRectangle.TopLeft.y, Math.min(centerY, horizontalRectangle.BottomRight.y));
return Math.pow(deltaX, 2) + Math.pow(deltaY, 2) <= Math.pow(radius, 2);
};
//These are circles where the center has the maximum y and the shape is upwards on screens
function TopHalfCircle(params) {
params.Type = "TopHalfCircle";
var self = this;
if (typeof params.Intersects !== "function") {
//Default Intersects function
params.Intersects = function (shape) {
//If the two shapes have identical type, none of them is above (below in coordinates) the other by more than the other's radius and the full circles intersect,
//then the half circles intersect each-other
if (shape.Type === self.Type) {
return ((self.Center.y - shape.Center.y) < (self.Diameter / 2)) &&
((shape.Center.y - self.Center.y) < (shape.Diameter / 2)) &&
(Math.pow(self.Center.x - shape.Center.x, 2) + Math.pow(self.Center.y - shape.Center.y, 2) < Math.pow(((self.Diameter + shape.Diameter) / 2), 2));
//In case of top horizontal vertical rectangle, we need to make sure that the horizontal square collides the circle and in the top half
} else if (shape.Type === "HorizontalVerticalRectangle") {
return (shape.TopLeft.y <= self.Center.y) && HorizontalVerticalRectangle.prototype.CollidesCircle(shape, self.Center.x, self.Center.y, self.Diameter / 2);
}
//Not Implemented
return false;
};
}
Shape.call(this, params);
this.initialize("Center", { x: 0, y: 0 });
this.initialize("Diameter", 0);
//Make sure the coordinates and diameter are kept as floats
this.Center.x = parseFloat(this.Center.x);
this.Center.y = parseFloat(this.Center.y);
this.Diameter = parseFloat(this.Diameter);
//Setters
this.setCenterX = function (x) {
self.Center.x = parseFloat(x);
};
this.setCenterY = function (y) {
self.Center.y = parseFloat(y);
};
this.setDiameter = function (d) {
self.Diameter = parseFloat(d);
};
}
//Placement strategies for markers, but they can be used for different purposes as well
var PlacementStrategies = {
//This function finds groups of shapes seeing which shape intersects which other shape
Group: function (shapes, comparator) {
if (typeof comparator !== "function") {
comparator = function () {
return true;
};
}
//This variable is empty at start, but at the end will hold the shape groups
var groups = [];
//Traverse the shapes to build the groups
for (var shapeIndex in shapes) {
//This variable will hold false if the shape does not fit into any existing group and the group index otherwise
var foundGroup = false;
//Traverse the groups to find whether a group where the shape fits in already exists
for (var groupIndex = 0; groupIndex < groups.length; groupIndex++) {
//Traverse the shapes of the current group to see whether any of them intersects the shape
for (var innerShapeIndex = 0; (groupIndex < groups.length) && (innerShapeIndex < groups[groupIndex].length) ; innerShapeIndex++) {
//If shape intersects with the current group's current shape, then set foundGroup and exit two for cycles
if (Shape.prototype.intersects(shapes[shapeIndex], shapes[groups[groupIndex][innerShapeIndex]])) {
foundGroup = groupIndex;
innerShapeIndex = groups[groupIndex].length;
groupIndex = groups.length;
}
}
}
//If the shape does not fit into any groups, then we create its own group
if (foundGroup === false) {
groups.push([shapeIndex]);
//Otherwise we search for the location where the shape fits best
} else {
//Desired location. If it results in false, then the shape will be pushed to the end, otherwise it will be inserted at insertIndex
var insertIndex = false;
//Traverse the shapes of the found group to find the desired location to insert
for (var innerShapeIndex = 0; innerShapeIndex < groups[foundGroup].length; innerShapeIndex++) {
//If the shape to be inserted is "smaller" than the found group's current shape, then store the index and quit the cycle
if (!comparator(shapes[groups[foundGroup][innerShapeIndex]], shapes[shapeIndex])) {
insertIndex = innerShapeIndex;
innerShapeIndex = groups[foundGroup].length;
}
}
//Insert the shape into the desired location or to the end if there was no desired middle location
if (insertIndex === false) {
groups[foundGroup].push(shapeIndex);
} else {
groups[foundGroup].splice(insertIndex, 0, shapeIndex);
}
}
}
return groups;
},
//This function merges shape groups if they intersect each-other
MergeGroup: function (shapes, groups, merged, comparator) {
if (typeof comparator !== "function") {
comparator = function () {
return true;
};
}
//This time we merge the contents of the groups into the first index
mergeIssued = true;
while (mergeIssued) {
//There was no merge issued yet
mergeIssued = false;
//Traverse the main groups
for (var mergeIndex in merged) {
//Traverse the groups to merge with
for (var innerMergeIndex in merged[mergeIndex]) {
//If the group to merge with is empty, then it was already parsed
if ((merged[merged[mergeIndex][innerMergeIndex]]) && (merged[merged[mergeIndex][innerMergeIndex]].length > 0)) {
//Traverse the inner groups of the inner group
for (var toMove in merged[merged[mergeIndex][innerMergeIndex]]) {
//Move them if they are not yet present in the main merge group
if (merged[mergeIndex].indexOf(merged[merged[mergeIndex][innerMergeIndex]][toMove]) === -1) {
merged[mergeIndex].push(merged[merged[mergeIndex][innerMergeIndex]][toMove]);
mergeIssued = true;
}
//Remove the content of the inner group to avoid duplicates
merged[merged[mergeIndex][innerMergeIndex]] = [];
}
}
}
}
}
//Traverse the merge groups to move the shapes
for (var mergeIndex in merged) {
//Traverse the inner groups where we read the shapes from
for (var innerMergeIndex in merged[mergeIndex]) {
//Traverse the shapes of the inner group
for (var shapeIndex in groups[merged[mergeIndex][innerMergeIndex]]) {
//If the shape is not yet present in the target group, we move it
if (groups[mergeIndex].indexOf(groups[merged[mergeIndex][innerMergeIndex]][shapeIndex]) === -1) {
//A variable which will hold the index of insertion or false, if the element should be the lasts
var insertLocation = false;
//Traverse the shapes of the target group to find the correct location
for (var targetIndex = 0; (insertLocation === false) && (targetIndex < groups[mergeIndex].length) ; targetIndex++) {
//If the shape located at the current index is not "smaller" than the shape to be inserted, then we found the target location
if (!comparator(shapes[groups[mergeIndex][targetIndex]], shapes[groups[merged[mergeIndex][innerMergeIndex]][shapeIndex]])) {
insertLocation = targetIndex;
}
}
//If there was no "bigger" element, then push at the end of the array
if (insertLocation === false) {
groups[mergeIndex].push(groups[merged[mergeIndex][innerMergeIndex]][shapeIndex]);
//Otherwise insert it to the correct location
} else {
groups[mergeIndex].splice(insertLocation, 0, groups[merged[mergeIndex][innerMergeIndex]][shapeIndex]);
}
}
}
//Clear the group where we moved the shapes from
groups[merged[mergeIndex][innerMergeIndex]] = [];
}
}
//We copy the non-empty groups into another container
var finalGroups = [];
for (var groupIndex in groups) {
if (groups[groupIndex].length > 0) {
finalGroups.push(groups[groupIndex]);
}
}
//And return it
return finalGroups;
},
//This strategy moves rectangles inside a group into a semi circle upwards on the screen
SemiCircleHorizontalRectangles: function (shapes, groups) {
//If groups is falsy, then this is the first try
if (!groups) {
//Which means that we need to create it by calling PlacementStrategies.Group with the comparator desired here
groups = PlacementStrategies.Group(shapes, function (shape1, shape2) {
//The shapes to the left are "smaller" to minimize line collisions
return shape1.TopLeft.x < shape2.TopLeft.x;
});
}
//This will hold top circles of the groups of shapes
var groupTopCircles = [];
//Traverse the raw groups
for (var groupIndex in groups) {
//We need to know the center of the circle, which will be the middle point of the horizontal coordinates and the lowest point in the circle
var maxY = false;
var minX = false;
var maxX = false;
//We need to know the half periphery to calculate the diameter
var halfPeriphery = 0;
//Traverse the shapes in the group
for (var innerShapeIndex in groups[groupIndex]) {
//Calculate the values where we calculate the center coordinates from
if ((minX === false) || (minX > shapes[groups[groupIndex][innerShapeIndex]].TopLeft.x)) {
minX = shapes[groups[groupIndex][innerShapeIndex]].TopLeft.x;
}
if ((maxX === false) || (maxX < shapes[groups[groupIndex][innerShapeIndex]].BottomRight.x)) {
maxX = shapes[groups[groupIndex][innerShapeIndex]].BottomRight.x;
}
if ((maxY === false) || (maxY < shapes[groups[groupIndex][innerShapeIndex]].BottomRight.y)) {
maxY = shapes[groups[groupIndex][innerShapeIndex]].BottomRight.y;
}
//Add the length of the diagonal of the shape to halfPeriphery
halfPeriphery += Math.sqrt(Math.pow(shapes[groups[groupIndex][innerShapeIndex]].BottomRight.x - shapes[groups[groupIndex][innerShapeIndex]].TopLeft.x, 2) + Math.pow(shapes[groups[groupIndex][innerShapeIndex]].BottomRight.y - shapes[groups[groupIndex][innerShapeIndex]].TopLeft.y, 2));
}
//Add the half circle to the container
groupTopCircles[groupIndex] = new TopHalfCircle({ Center: { x: (minX + maxX) / 2, y: maxY }, Diameter: 2 * halfPeriphery / Math.PI });
}
//Container for groups to be merged
var merged;
//Traverse all the shapes
for (var halfCircleIndex = 0; halfCircleIndex < groupTopCircles.length; halfCircleIndex++) {
var s1 = (groups[halfCircleIndex].length === 1) ? shapes[groups[halfCircleIndex][0]] : groupTopCircles[halfCircleIndex];
//Traverse the "later" shapes
for (var secondHalfCircleIndex = halfCircleIndex + 1; secondHalfCircleIndex < groupTopCircles.length; secondHalfCircleIndex++) {
var s2 = (groups[secondHalfCircleIndex].length === 1) ? shapes[groups[secondHalfCircleIndex][0]] : groupTopCircles[secondHalfCircleIndex];
//If the two half circles intersect each-other, then merge them
if (Shape.prototype.intersects(s1, s2)) {
if (!merged) {
merged = {};
}
if (!merged[halfCircleIndex]) {
merged[halfCircleIndex] = [];
}
//We always merge into the first group
merged[halfCircleIndex].push(secondHalfCircleIndex);
}
}
}
//If there was a merge then we do the effective merging and repeat this strategy for the resulting half-circles
if (merged) {
return PlacementStrategies.SemiCircleHorizontalRectangles(shapes, PlacementStrategies.MergeGroup(shapes, groups, merged, function (shape1, shape2) {
//We will order horizontal-verticle rectangles here, we might refactor this function to get a comparator instead later
return shape1.TopLeft.x < shape2.TopLeft.x;
}));
}
//Angle iterator for the half circle
var angle;
//The amount of step with the angle iterator
var angleStep;
//Traverse the groups to change the coordinates
for (var groupIndex in groups) {
//If the group has a single element, then we jump over it
if (groups[groupIndex].length > 1) {
//Initialize the angle iterator and calculate its step size
angle = Math.PI;
angleStep = angle / (groups[groupIndex].length - 1);
//Traverse the shapes
for (var shapeIndex in groups[groupIndex]) {
//The translation is calculated based on circle coordinates
var translation = {
x: groupTopCircles[groupIndex].Center.x + (groupTopCircles[groupIndex].Diameter * Math.cos(angle) / 2),
y: groupTopCircles[groupIndex].Center.y + (groupTopCircles[groupIndex].Diameter * Math.sin(angle) / 2)
};
//The middle of the rectangles will place at the desired point and we need the middle coordinates for that
var halfDiffX = (shapes[groups[groupIndex][shapeIndex]].BottomRight.x - shapes[groups[groupIndex][shapeIndex]].TopLeft.x) / 2;
var halfDiffY = (shapes[groups[groupIndex][shapeIndex]].BottomRight.y - shapes[groups[groupIndex][shapeIndex]].TopLeft.y) / 2;
//Calculate the new bounds of the rectangle and step the iterator
shapes[groups[groupIndex][shapeIndex]].setTopLeftX(translation.x - halfDiffX);
shapes[groups[groupIndex][shapeIndex]].setTopLeftY(translation.y - halfDiffY);
shapes[groups[groupIndex][shapeIndex]].setBottomRightX(translation.x + halfDiffX);
shapes[groups[groupIndex][shapeIndex]].setBottomRightY(translation.y + halfDiffY);
angle += angleStep;
}
}
}
return shapes;
}
};
//General intersects function for shapes, which gets two shapes and checks whether they intersect each-other
Shape.prototype.intersects = function (shape1, shape2) {
//If the first shape is symmetric and the types of shapes match, it is enough to check a single direction of intersection
//Otherwise we need to check both directions
return ((shape1.Symmetric) && (shape1.Type === shape2.Type)) ? (shape1.Intersects(shape2)) : (shape1.Intersects(shape2) || shape2.Intersects(shape1));
};
/*Geometry*/
/*Spiderfier*/
function Spiderfier(params) {
Initializable.call(this, params);
var self = this;
var isSpiderfied = false;
this.defaultFunction = function () { };
//Custom Spiderfy Events
this.initialize("OnSpiderfy", this.defaultFunction, true);
this.initialize("OnUnspiderfy", this.defaultFunction, true);
this.initialize("rows", [], true);
this.initialize("cm", function () {
return cachedMarkers;
}, true);
this.initialize("options", {});
this.SpiderLines = {};
this.isCurrentlySpiderfied = function () {
return isSpiderfied;
};
this.refreshRows = function (r, stopRefresh) {
rows = r;
if (isSpiderfied && (!stopRefresh)) {
self.spiderfy();
}
};
this.spiderfy = function (r) {
if (r) {
self.refreshRows(r, true);
}
params.OnSpiderfy(rows, self);
isSpiderfied = true;
};
this.unspiderfy = function (r) {
if (r) {
self.refreshRows(r, true);
}
params.OnUnspiderfy(rows, self);
isSpiderfied = false;
};
//Handles marker draw and spiderfying
this.drawAndSpiderfy = function (r, o) {
//First handle the spiderfy thing
if (o) {
self.options = o;
}
if (self.isCurrentlySpiderfied()) {
self.spiderfy(r, params.cm());
drawSpiderMarkers(r, params.cm(), self);
} else {
self.unspiderfy(r, params.cm());
}
//And then draw the markers
drawMarkers(rows, options);
};
}
//Gets the rectangles of the markers
function markersToRectangles(rows) {
var shapes = [];
var lowPoint;
for (var rowIndex in rows) {
//Convert the geographical point of the marker into graphical point
lowPoint = map.latLngToLayerPoint(L.latLng(rows[rowIndex].RealLat, rows[rowIndex].RealLon));
shapes.push(new HorizontalVerticalRectangle({
TopLeft: { x: lowPoint.x - 18, y: lowPoint.y - 44 },
BottomRight: { x: lowPoint.x + 18 + 0, y: lowPoint.y }
}));
}
return shapes;
}
//Spiderfies rectangles with half circle strategy
function RectangleHalfCircleSpiderfy(rows, spdfr) {
//Initialize real latitude and longitude if not already done so
for (var rowIndex in rows) {
if (!rows[rowIndex].RealLat) {
rows[rowIndex].RealLat = rows[rowIndex].Lat;
rows[rowIndex].RealLon = rows[rowIndex].Lon;
}
}
//Gather the desired rectangles
var rectangles = PlacementStrategies.SemiCircleHorizontalRectangles(markersToRectangles(rows));
//Store the geographic coordinates
for (var rowIndex in rectangles) {
//Convert graphical coordinates into geographic coordinates
var location = map.layerPointToLatLng(L.point(rectangles[rowIndex].TopLeft.x + 14, rectangles[rowIndex].BottomRight.y));
rows[rowIndex].Lat = location.lat;
rows[rowIndex].Lon = location.lng;
}
}
function normalUnspiderfy(rows, spiderfier) {
for (var rowIndex in rows) {
if (rows[rowIndex].RealLat !== undefined) {
rows[rowIndex].Lat = rows[rowIndex].RealLat;
rows[rowIndex].Lon = rows[rowIndex].RealLon;
delete rows[rowIndex].RealLat;
delete rows[rowIndex].RealLon;
}
}
for (var lineIndex in spiderfier.SpiderLines) {
map.removeLayer(spiderfier.SpiderLines[lineIndex].polyLine);
}
spiderfier.SpiderLines = {};
}
//Draws spider markers
function drawSpiderMarkers(rows, cachedMarkers, spiderfier) {
//For each row...
for (var i = 0; i < rows.length; i++) {
//If real location exists and differs from the display location and there is either no spider line yet or points to a different location than the expected one
if (rows[i].RealLat && rows[i].RealLon &&
((rows[i].Lat != rows[i].RealLat) || (rows[i].Lon != rows[i].RealLon)) &&
((!spiderfier.SpiderLines[i]) || (spiderfier.SpiderLines[i].location.Lat != rows[i].Lat) || (spiderfier.SpiderLines[i].location.Lon != rows[i].Lon))
) {
//Then check whether the spider line exists and remove it if so
if (spiderfier.SpiderLines[i]) {
map.removeLayer(spiderfier.SpiderLines[i].polyLine);
}
//And generate a new spider line
spiderfier.SpiderLines[i] = { location: new L.LatLng(rows[i].Lat, rows[i].Lon), realLocation: new L.LatLng(rows[i].RealLat, rows[i].RealLon) };
spiderfier.SpiderLines[i].polyLine = L.polyline([spiderfier.SpiderLines[i].location, spiderfier.SpiderLines[i].realLocation]);
spiderfier.SpiderLines[i].polyLine.options.weight = 2;
spiderfier.SpiderLines[i].polyLine.options.color = "#5f0df1";
spiderfier.SpiderLines[i].polyLine.addTo(map);
}
}
}
var spiderfier;
/*Spiderfier*/
function getStrategyName(code) {
switch (code) {
case 2: return "Grouped";
case 3: return "RectangleHalfCircleSpiderfy";
default: return "Unspecified";
}
}
function drawStrategicMarkers(rows, drawOpt) {
if (drawOpt.strategy < 3) {
if (drawOpt.strategy === 2) {
drawOpt.grouped = true;
}
return drawMarkers(rows, drawOpt);
} else {
if (!spiderfier) {
window["spiderfier"] = new Spiderfier({
OnSpiderfy: window[getStrategyName(drawOpt.strategy)],
OnUnspiderfy: normalUnspiderfy,
});
}
spiderfier.drawAndSpiderfy(rows);
}
}
说明:这将计算标记矩形的图形坐标,并找出哪些矩形属于一个组。一个组将是一个上半圈,其中标记显示在外围,当我们有这样的半圆时,它们相互检查,所以如果它们相互交叉,那么它们将合并为一个新组。如果一个组包含单个标记,则考虑其矩形而不是其上半部分。最后,标记被转换为它们在其组(上半圆周边)上的所需位置。
这很有效,但问题是这只考虑了标记的矩形而根本没有考虑标签尺寸(标签显示在相应标记的右侧,两者一起应该是被视为单个矩形)。原因很简单:我可以收集标签尺寸,但只能在绘制完所有内容之后。我的问题如下:如果我知道标签将包含什么,是否有一个可靠的公式,我可以收集标签容器的边界和限制,以便我可以检查它不与其他标记或标签重叠好?
生成标签后,这种非常黑客的方式是我如何收集有关标签大小的信息:
function getLabelSize(index) {
var labelContext = $(".leaflet-map-pane .leaflet-label:eq(" + index + ")");
return {width: labelContext.width(), height: labelContext.height()};
}
要绘制标记,测量标记,然后重新绘制标记只是为了获得标签大小这种方式是如此hacky我宁愿允许标签交叉标记或其他标签,这是一个悲伤的决定。因此我想知道:有没有办法获得一个尚未根据其未来内容绘制的标签的宽度和高度?
内容如下:
<div class="leaflet-label leaflet-zoom-animated leaflet-label-right" style="z-index: 540; transform: translate3d(912px, 500px, 0px); opacity: 1;">
<p class="orange">34534</p>
<p>3343453</p>
</div>
当然,这个div有填充和边框,但是如果我能够以某种方式读取内部宽度和高度,我将能够添加所需的值。
答案 0 :(得分:4)
如果我知道标签会包含什么,是否有可靠的公式可以用来收集标签容器的边界和限制?
没有
在HTML中,在将该块元素添加到DOM之前,您无法知道块元素的computed dimensions。这是由于各种原因造成的;主要是这样的事实:可能是许多不同的(非显式的)CSS选择器,可能在添加时应用于该块元素。加上用户代理样式表,GPU字体渲染,DPI字体大小等等。
我在处理Leaflet.LayerGroup.Collision时研究过这个问题。
这种非常黑客的方式是我如何收集有关标签大小的信息
请不要。在将元素添加到DOM 之后使用window.getComputedStyle
。
您可以在同一个渲染帧中自由地从DOM中删除该元素(或删除Leaflet图层,因为它将具有相同的效果)。您可以向DOM添加元素,获取其计算的维度,将该元素移除到DOM,并快速执行以至于浏览器不在其间点击帧渲染(除此之外,因为执行此操作时阻止浏览器UI线程。
这是Leaflet.LayerGroup.Collision的工作方式:将所有内容添加到DOM(将所有传单图层添加到地图中),获取所有内容的计算样式,将边界框添加到rbush
结构,计算碰撞,从DOM中删除元素(地图中的图层)在一个框架内。