我正在使用傳單和小冊子標籤。有時標記重疊,這是不好的UX,因此,我已實現了以下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有填充和邊界,但我將能夠添加所需的值,如果我能讀的內部寬度和高度莫名其妙。
」在元素添加到DOM後使用window.getComputedStyle。「沒有選擇。正如我在問題中已經描述的那樣,我需要在繪製標籤之前獲取標籤的大小。你建議我畫他們,然後閱讀他們的尺寸。但我需要他們的尺寸來知道在哪裏繪製它們,所以如果它們已經繪製完成,那麼我不再對它們的尺寸感興趣。 –
「您可以自由從DOM中移除該元素」不是一個選項。如果它已經在那裏了,那麼我就沒有興趣去確定它的位置,我有充分的理由不這樣做,爲了保持簡單,我不會深究。長話短說:我想知道在繪製小冊子標籤之前是否有任何方法獲得小冊子的尺寸。你的意見是,沒有辦法做到這一點。如果沒有更好的選項並重復使用這些尺寸,我可以測量標籤並查看它們有多寬,如果一個parapgraph包含n個字母,實際上有一種方法,但涉及勞動密集型研究。 –
「這就是Leaflet.LayerGroup.Collision的工作原理」我沒有從DOM中讀取數據就進行了數學計算,當我知道在哪裏放置東西時,我繪製了標記。如果我們忽略與標籤有關的問題,這完美地工作。我可以預先測量不同情況下,不同瀏覽器和不同風格的標籤,但這需要一段時間。有一個解決方案,但它是痛苦的。我明白你的意見是沒有別的辦法。你可能是對的,但我們還要等待其他答案,看看是否還有一些額外的信息。 –