I try to make a 2D graph by using 3D Force Directed Graph (that is using D3.js, https://github.com/vasturiano/3d-force-graph) as in this example https://bl.ocks.org/vasturiano/f59675656258d3f490e9faa40828c0e7 and i try to make it start as 2D and with the camera rotation disabled. Each user will have the possibility to switch to 3d with the Orbit controlType and with the rotation enabled.
I have tried several ways to make this happen, for example, i have tried to use three.js like this:
Graph.camera().enableRotate = false; // nothing happens
or like this:
... .nodeLabel('id').camera().nodeAutoColorBy('group') ... // error
I think that the easiest way to solve this would be to use panning on both mouse buttons, that would disable the rotation. I couldn't find any example of how to do that.
Is there any way that i could change the mouse left click function? or is there any example of how can i use three.js .camera() with three.js .enableRotate? or is there any other way to disable rotation on 2D version of the graph?
The complete code that i use is:
let highlightNodes = [];
let highlightLink = null;
const elem = document.getElementById('3d-graph');
const Graph = ForceGraph3D({controlType:'orbit'})
(elem)
//.forceEngine('ngraph')
.jsonUrl('https://api.codetabs.com/v1/proxy?quest=https://bl.ocks.org/vasturiano/raw/f59675656258d3f490e9faa40828c0e7/.miserables.json')
.nodeLabel('id')
.nodeAutoColorBy('group')
.enableNodeDrag(true)
.onNodeHover(
node => {
elem.style.cursor = node ? 'pointer' : null;
if ((!node && !highlightNodes.length) || (highlightNodes.length === 1 && highlightNodes[0] === node)) return;
highlightNodes = node ? [node] : [];
updateGeometries();
}
)
.onNodeClick(
node => {
// Aim at node from outside it
if(Graph.numDimensions() == 3){
Graph.cameraPosition(
//{ x: node.x - 66, y: node.y - 66, z: node.z - 66 }, // new position
{ x: Graph.cameraPosition().x /2, y: Graph.cameraPosition().y /2, z: Graph.cameraPosition().z /2}, // new position
node, // lookAt ({ x, y, z })
2000 // ms transition duration
)
} else if(Graph.numDimensions() == 2){
Graph.cameraPosition(
node, //new position
node, // lookAt ({ x, y, z })
1000 // ms transition duration
)
}
}
)
.onLinkHover(
link => {
// no state change
if (highlightLink === link) return;
highlightLink = link;
highlightNodes = link ? [link.source, link.target] : [];
updateGeometries();
}
)
//.linkColor('rgba(255,255,255,1)')
.showNavInfo(false)
.numDimensions(2)
//.enableNavigationControls(false)
.enableNodeDrag(false)
.nodeColor(node => highlightNodes.indexOf(node) === -1 ? 'rgba(0,255,255,0.6)' : 'rgb(255,0,0,1)')
.linkWidth(link => link === highlightLink ? 4 : 1)
.linkDirectionalParticles(link => link === highlightLink ? 4 : 0)
.linkDirectionalParticleWidth(4)
/*
.nodeThreeObject(
node => {
var map = new THREE.TextureLoader().load( "https://picsum.photos/100/100/?random" );
map.minFilter = THREE.LinearFilter;
var material = new THREE.SpriteMaterial( { map: map } );
var sprite = new THREE.Sprite( material );
sprite.scale.set(22,22,1);
return sprite;
}
)
*/
;
Graph.camera().enableRotate == false;
function updateGeometries() {
//setTimeout(function(){
Graph.nodeRelSize(4);
//}, 500);
}
let numDim = 2;
const toggleDimensions = function(numDimensions) {
numDim = numDimensions;
Graph.numDimensions(numDim);
};
Thank you.
I have solved it like this:
var GraphCanvas = document.getElementsByTagName('canvas')[0];
GraphCanvas.addEventListener('mousedown', function(e) {
if(window.event.which==1 && numDim == 2){
Graph.enableNavigationControls(false);
}
});
GraphCanvas.addEventListener('mouseup', function(e) {
if(window.event.which==1 && numDim == 2){
Graph.enableNavigationControls(false);
Graph.enableNavigationControls(true);
}
});
I disable the controls for the left click on 'mousedown' and restore it on 'mouseup'. All the other mouse operations are working as before, this will disable the rotation of the scene on left click. It works only if numDim is 2 (only if is 2D not 3D).
I am using leaflet with leaflet label. Sometimes the markers overlap, which is bad for the UX and therefore I have implemented the following Spiderfier functionality:
/*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);
}
}
Explanation: This calculates the graphical coordinates of the marker's rectangle and finds out which rectangles belong to a group. A group will be a top half circle where markers are displayed at the periphery and when we have such half-circles, they are checked against each-other, so if they intersect each-other, then they are merged into a new group. If a group contains a single marker, then its rectangle is taken into consideration rather than its top half circle. At the end markers are translated to their desired location on their group (top half circle periphery).
This works perfectly, however, the problem is that this takes into account only the markers' rectangles and does not take into account label sizes at all (a label is displayed to the right of the corresponding marker and the two together should be considered as a single rectangle). The reason for this is very simple: I can gather label sizes, but only after everything was drawn. My question is as follows: If I know what the label will contain, is there a reliable formula with which I can gather the bounds and limits of the label container, so that I could check for it to not overlap with other markers or labels as well?
After labels are generated, this extremely hacky way is how I could gather information about the labels' size:
function getLabelSize(index) {
var labelContext = $(".leaflet-map-pane .leaflet-label:eq(" + index + ")");
return {width: labelContext.width(), height: labelContext.height()};
}
To draw the markers, measure the labels and then redraw the markers just to get the label sizes this way is so hacky that I rather allow labels to intersect markers or other labels, which is a sad decision. Therefore I wonder: is there a way to get the width and height of a label which was not yet drawn based on its future content?
The content looks like this:
<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>
Of course, that div has padding and border, but I will be able to add the needed values if I am able to read the inner width and height somehow.
If I know what the label will contain, is there a reliable formula with which I can gather the bounds and limits of the label container?
No.
In HTML, you cannot know the computed dimensions of a block element before that block element gets added to the DOM. This is due to a variety of reasons; mainly the fact that there might be lots of different (non-explicit) CSS selectors that might apply to that block element when it gets added. Plus user-agent stylesheets, GPU font rendering, DPI font size, etc etc etc.
I researched this when working on Leaflet.LayerGroup.Collision.
this extremely hacky way is how I could gather information about the labels' size
Please don't. Use window.getComputedStyle after the element has been added to the DOM.
You are free to remove that element from the DOM (or to remove the Leaflet layer, as it will have the same effect) within the same render frame. You can add an element to the DOM, get its computed dimensions, remove that element to the DOM, and do it so fast that the browser does not hit a frame render in between (among other things, because the browser UI thread is blocked while doing so).
This is how Leaflet.LayerGroup.Collision works: Add everything to the DOM (add all the leaflet layers to the map), get the computed style for everything, add the bounding boxes to the rbush structure, compute collisions, remove elements from the DOM (layers from the map) within one frame.
Well i have this SVG canvas element, i've got to the point so far that once a user clicks and drags the canvas is moved about and off-screen elements become on screen etc....
However i have this is issue in which when ever the user then goes and click and drags again then the translate co-ords reset to 0, which makes the canvas jump back to 0,0.
Here is the code that i've Got for those of you whio don't wanna use JS fiddle
Here is the JSfiddle demo - https://jsfiddle.net/2cu2jvbp/2/
edit: Got the solution - here is a JSfiddle DEMO https://jsfiddle.net/hsqnzh5w/
Any and all sugesstion will really help.
var states = '', stateOrigin;
var root = document.getElementById("svgCanvas");
var viewport = root.getElementById("viewport");
var storeCo =[];
function setAttributes(element, attribute)
{
for(var n in attribute) //rool through all attributes that have been created.
{
element.setAttributeNS(null, n, attribute[n]);
}
}
function setupEventHandlers() //self explanatory;
{
setAttributes(root, {
"onmousedown": "mouseDown(evt)", //do function
"onmouseup": "mouseUp(evt)",
"onmousemove": "mouseMove(evt)",
});
}
setupEventHandlers();
function setTranslate(element, x,y,scale) {
var m = "translate(" + x + "," + y+")"+ "scale"+"("+scale+")";
element.setAttribute("transform", m);
}
function getMousePoint(evt) { //this creates an SVG point object with the co-ords of where the mouse has been clicked.
var points = root.createSVGPoint();
points.x = evt.clientX;
points.Y = evt.clientY;
return points;
}
function mouseDown(evt)
{
var value;
if(evt.target == root || viewport)
{
states = "pan";
stateOrigin = getMousePoint(evt);
console.log(value);
}
}
function mouseMove(evt)
{
var pointsLive = getMousePoint(evt);
if(states == "pan")
{
setTranslate(viewport,pointsLive.x - stateOrigin.x, pointsLive.Y - stateOrigin.Y, 1.0); //is this re-intializing every turn?
storeCo[0] = pointsLive.x - stateOrigin.x
storeCo[1] = pointsLive.Y - stateOrigin.Y;
}
else if(states == "store")
{
setTranslate(viewport,storeCo[0],storeCo[1],1); // store the co-ords!!!
stateOrigin = pointsLive; //replaces the old stateOrigin with the new state
states = "stop";
}
}
function mouseUp(evt)
{
if(states == "pan")
{
states = "store";
if(states == "stop")
{
states ='';
}
}
}
In your mousedown function, you are not accounting for the fact that the element might already have a transform and you are just overwriting it.
You are going to need to either look for, and parse, any existing transform. Or an easier approach would be to keep a record of the old x and y offsets and when a new mousedown happens add them to the new offset.
For example I have a multipolygon consisting of two polygons. I can delete vertices with the modifyFeature control and the delete-key. However, the modifyFeature control does not allow me to delete a polygon completely because it has to contain at least 3 vertices. How do I delete a polygon completely from a multipolygon feature (with the delete-key)? Is there an appropriate control or a plugin available for OpenLayers?
I tried it myself with patching the control but that resulted in multiple minor bugs. So I thought I ask first if someone already did that before I spend a lot of time to fix the issues.
removeComponent: function(point) {
var removed = this.components && (this.components.length > 3);
if (removed) {
//remove last point
this.components.pop();
//remove our point
OpenLayers.Geometry.Collection.prototype.removeComponent.apply(this,
arguments);
//append copy of first point
var firstPoint = this.components[0];
OpenLayers.Geometry.Collection.prototype.addComponent.apply(this,
[firstPoint]);
//bm extension: possibility to delete geometry from multigeometries
} else if (this.components && this.parent && this.parent.parent && this.parent.parent.CLASS_NAME === "OpenLayers.Geometry.MultiPolygon") {
//polygon with hole
if (this.parent.components.length > 1) {
for (var i = 0; i < this.parent.components.length && this.parent.components.length > 1; i++) {
if (this.parent.components[i].id === this.id) {
OpenLayers.Util.removeItem(this.parent.components, this.parent.components[i]);
this.parent.clearBounds();
return true;
}
}
}
//seperate polygon
for (var i = 0; i < this.parent.parent.components.length && this.parent.parent.components.length > 1; i++) {
if (this.parent.parent.components[i].id === this.parent.id) {
OpenLayers.Util.removeItem(this.parent.parent.components, this.parent.parent.components[i]);
this.parent.parent.clearBounds();
return true;
}
}
}
return removed;
},
I am trying to re-size a circle using papeJS but since i used two onMouseDrag function it if conflicting. I am unable to create it. Can anyone help me. Here is the fiddle with circle
Here is the code.
<script type="text/paperscript" canvas="canvas">
var raster = new Raster({
source: 'Chrysanthemum.jpg',
position: view.center
});
var path = null;
var circles = [];
var isDrawing = false;
var draggingIndex = -1;
var segment, movePath;
var resize = false;
project.activeLayer.selected = false;
function onMouseDrag(event) {
if (!isDrawing && circles.length > 0) {
for (var ix = 0; ix < circles.length; ix++) {
if (circles[ix].contains(event.point)) {
draggingIndex = ix;
break;
}
}
}
if (draggingIndex > -1) {
circles[draggingIndex].position = event.point;
} else {
path = new Path.Circle({
center: event.point,
radius: (event.downPoint - event.point).length,
fillColor: null,
strokeColor: 'black',
strokeWidth: 10
});
path.removeOnDrag();
isDrawing = true;
}
}
;
function onMouseUp(event) {
if (isDrawing) {
circles.push(path);
}
isDrawing = false;
draggingIndex = -1;
}
;
function onMouseMove(event) {
project.activeLayer.selected = false;
if (event.item)
event.item.selected = true;
resize = true;
}
var segment, path;
var movePath = false;
function onMouseDown(event) {
segment = path = null;
var hitResult = project.hitTest(event.point, hitOptions);
if (!hitResult)
return;
if (hitResult) {
path = hitResult.item;
if (hitResult.type == 'segment') {
segment = hitResult.segment;
} else if (hitResult.type == 'stroke') {
var location = hitResult.location;
segment = path.insert(location.index + 1, event.point);
path.smooth();
}
}
movePath = hitResult.type == 'fill';
if (movePath)
project.activeLayer.addChild(hitResult.item);
}
</script>
First, your code (on jsfiddle) does not run.
The paperjs external resource returned a 404. https://raw.github.com/paperjs/paper.js/master/dist/paper.js works for paperjs.
The raster source was for a local file, not a URI.
In onMouseDown, project.hitTest references an undefined hitOptions.
It seems from your question that you want to be able to drag the circle segments to resize the circle, and you tried using two onMouseDrag functions to do that, which would not work. Instead, both operations should be in the same onMouseDrag, using if-then-else to choose between them. To make this work as expected, the item that was hit should be stored in onMouseDown instead of whatever circle your code finds at the beginning of onMouseDrag. For example, here onMouseDrag can either "move" or "resize" (jsfiddle here):
<script type="text/paperscript" canvas="myCanvas">
var raster = new Raster({
source: 'http://i140.photobucket.com/albums/r10/Array39/Chrysanthemum.jpg',
position: view.center
});
var circles = [];
var hitItem = null;
var currentAction = null;
function onMouseMove(event) {
project.activeLayer.selected = false;
if (event.item) {
event.item.selected = true;
}
}
function onMouseDown(event) {
hitItem = null;
var aColor = new Color('black');
for (var i = 0; i < circles.length; i++) {
circles[i].fillColor = aColor;
}
view.draw();
var hitResult = project.hitTest(event.point);
for (var i = 0; i < circles.length; i++) {
circles[i].fillColor = null;
}
view.draw();
if (!hitResult) {
return; //only happens if we don't even hit the raster
}
hitItem = hitResult.item;
if (circles.indexOf(hitItem) < 0) {
var newCircle = new Path.Circle({
center: event.point,
radius: 2,
strokeColor: 'black',
strokeWidth: 10
});
hitItem = newCircle;
circles.push(hitItem);
currentAction = 'resize';
return;
}
if (hitResult.type == 'segment') {
currentAction = 'resize';
} else if (hitResult.type == 'stroke') {
hitItem.insert(hitResult.location.index + 1, event.point);
hitItem.smooth();
currentAction = 'resize';
} else if (hitResult.type == 'fill') {
currentAction = 'move';
}
}
function onMouseDrag(event) {
if (!hitItem) {
return;
}
if (currentAction == 'move') {
hitItem.position = event.point;
} else if (currentAction == 'resize') {
if ((event.downPoint - event.point).length >= 1) {
hitItem.fitBounds(new Rectangle(event.downPoint, event.point), true);
}
}
};
</script>
<canvas id="myCanvas"></canvas>
Also note:
In onMouseDown, the function returns if !hitResult, so you do not need to test if (hitResult) right after that return.
Naming variables the same as objects makes searching more difficult, e.g., in your code path is an instance of Path.
Using the same variable for different purposes makes code more difficult to parse, e.g., in your code path is used to create new circles as well as to store which circle has been selected.
You have multiple variables defined twice: path, movePath, and segment.
If a variable will only be used in a single function, e.g., movePath and segment, then it makes the code more readable if the variable is defined in that function. Also, movePath is only used in a single if-statement, which just adds items back to the layer, but the only items not in the layer have been removed when the circle was originally drawn. Since those items cannot be hit, the item that was hit must already be in the layer.
The variable segment is not used.
It makes the code flow/read better if the functions are ordered logically. In this case, onMouseMove should go first because it happens before the button is clicked. Then onMouseDown goes next because it must happen before the other actions. Then onMouseDrag, and finally onMouseUp.
Instead of creating new circles in onMouseDrag and then throwing them away on the next drag, it makes more sense to create one in onMouseDown if there was no item hit, or if the hit item is not a circle. Then in onMouseDown, you just resize that circle. Path.scale or Path.fitBounds can be used for such resizing.
Instead of using multiple boolean variables to keep track of the current action (e.g., resize vs move), it is more logical to have a single variable keeping track of the current action.
Instead of your code to find whether the point is within a circle, the code I am using temporarily sets the circles' fillColor, do the hitTest, and then clears the circles' fillColor. I did this because when you hit a stroke, the shape of the circle changes, for which your code to find the draggingIndex does not account.