Re-size circle using paperJS - javascript

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.

Related

Phaser 2 determining which unit is active

I have 3 clickable objects. When one is clicked, this becomes the 'selected unit'.
I am trying to create some generic actions for the units such as moving to a point.
In my create function I initialize the units, when a unit is clicked on - this is supposed to become the 'selected unit' so that my movement and direction function applies to the this unit. However, the script is not able to recognize which unit intend for example I get this error:
Uncaught TypeError: Cannot read property 'velocity' of undefined.
Is there a way to use a variable to indicate selected users and pass that to the functions?
window.onload = function() {
var block_count = 0;
var block = '';
var selected_unit = '';
var unit_clicked = 0;
var tank1 = null;
var game = new Phaser.Game(800, 600, Phaser.AUTO, '', { preload: preload, create: create, update: update, render: render});
function preload () {
game.load.image('block', 'block.png');
game.load.image('tank1', 'tank.png');
game.load.image('baddie', 'tank.png');
game.load.image('mouse_btn', 'block.png');
game.input.mouse.capture = true;
}
function create () {
game.physics.startSystem(Phaser.Physics.ARCADE);
mouse_btn = game.add.sprite(30,30, 'mouse_btn');
mouse_btn.anchor.setTo(0.5, 0.5);
//T1
tank1 = game.add.sprite(30,30, 'tank1');
initialise_player(tank1);
game.physics.enable(tank1, Phaser.Physics.ARCADE);
//T2
tank2 = game.add.sprite(30,60, 'tank1');
initialise_player(tank2);
game.physics.enable(tank2, Phaser.Physics.ARCADE);
game.world.setBounds(0, 0, 2000, 2000);
game.camera.follow(tank1);
}
function update () {
if(selected_unit == '') {
mouse_btn.x = game.input.mousePointer.worldX
mouse_btn.y = game.input.mousePointer.worldY
}
if(game.input.activePointer.leftButton.isDown && block_count == 0 && unit_clicked == 0) {
game.input.activePointer.leftButton.stop(event);
block_count =1;
block = game.add.sprite(game.input.mousePointer.worldX, game.input.mousePointer.worldY, 'block');
game.physics.enable(block, Phaser.Physics.ARCADE);
block.anchor.setTo(0.5, 0.5)
lookAtObject(selected_unit, block, 0.005);
}
if(block.alive){
game.physics.arcade.moveToObject(selected_unit, block, 260, 0)
} else {
console.log(selected_unit)
selected_unit.body.velocity.x = 0;
selected_unit.body.velocity.y = 0;
}
if(game.physics.arcade.collide(selected_unit, block)) {
block_count--;
block.kill();
}
}
function render(){
//console.log(game.physics.arcade.collide(tank1, block))
}
function lookAtObject(obj, target, rotspeed){
var angle = Math.atan2(block.y - tank1.y, block.x - tank1.body.x);
tank1.rotation = angle + game.math.degToRad(90);
}
function initialise_player(tank1){
tank1.anchor.setTo(0.5, 0.5);
tank1.inputEnabled = true;
tank1.input.useHandCursor = true;
tank1.events.onInputDown.add(t1_clicked,this);
tank1.events.onInputOver.add(t1_over, this)
tank1.events.onInputOut.add(t1_out, this)
}
function t1_clicked() {
selected_unit = tank1;
}
function t1_over() {
unit_clicked = 1
}
function t1_out () {
unit_clicked = 0
}
};
The error you're getting on initial load is because in update you're making an assumption that selected_unit exists and has a body.
Update your third if to make sure selected_unit is defined.
if (selected_unit !== '') {
selected_unit.body.velocity.x = 0;
selected_unit.body.velocity.y = 0;
}
However, a better option would be to put this a few lines down, where you kill the block instead.
if(game.physics.arcade.collide(selected_unit, block)) {
selected_unit.body.velocity.x = 0;
selected_unit.body.velocity.y = 0;
block_count--;
block.kill();
}
if (block.alive); moveToObject is also expecting selected_unit to exist and have a body, which may not be the case; wrap it with a check.
if (selected_unit !== '') {
game.physics.arcade.moveToObject(selected_unit, block, 260, 0)
}
That now allows tank1 to rotate to look at the item you just placed, but it doesn't move it until it or tank2 have been clicked on.
This also points out that there are a number of tweaks you'll want to make to your code in general, since you're ignoring arguments that are being passed in. For example, t1_clicked isn't using the sprite that's been clicked on, but is instead just hard-coding tank1. lookAtObject isn't using obj or target, but again has values hard-coded in.
One other thing you may want to change is the following:
if(selected_unit == '') {
mouse_btn.x = game.input.mousePointer.worldX
mouse_btn.y = game.input.mousePointer.worldY
}
If you make that the following, you won't end up with an extra sprite hanging about on the screen.
if (block_count === 0) {
mouse_btn.x = game.input.mousePointer.worldX;
mouse_btn.y = game.input.mousePointer.worldY;
}

Is there a way to get the labels' width and height before drawing?

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.

SVG Camera Pan - Translate keeps resetting to 0 evertime?

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.

Javascript memory leak deleting Raphael path objects

I have a memory leak in my code and I cannot figure out what is causing it. I am receiving target data from a WebSocket which contains basically an ID and an X,Y coordinate. The data is passed to a HandleData function which creates a circle for each target and a line (which is updated) to show where the target has moved from/to.
If a target no longer appears in the WebSocket stream it is removed. On testing I have found the webpage quickly amasses hundreds of mb of data in spite of me removing these items. Upon using Chrome's memory profiler it seems Raphael (or something) is holding onto all the path information in spite of me deleting it.
If anyone can help me in anyway I would be very grateful. It does seem that Raphael is holding onto the data but there is every chance I have made a mistake somewhere in my code :)
var arrayColours = ["#f33", "#3f3", "#33f", "#f66", "#6f6", "#66f"];
var targetStructArray = [];
function HandleTargetData(data) {
//Go through all our existing targets and mark them as not updated
for (var i = 0; i < targetStructArray.length; i++) {
targetStructArray[i].updated = false;
}
for (var i = 0; i < data.targets.length; i++) {
var targetData = data.targets[i];
var targetStruct = undefined;
//find our targetStruct
for (var j = 0; j < targetStructArray.length; j++) {
if (targetStructArray[j].id == targetData.id) {
targetStruct = targetStructArray[j];
break;
}
}
//If it doesnt exist, create it
if (!targetStruct) {
var path = paper.path();
path.attr({ "stroke-width": "2", "stroke": "#888" });
path.addPart(['M', targetData.x, targetData.y]);
var circle = paper.circle(targetData.x, targetData.y, 15, 15).attr({
stroke: "none",
fill: arrayColours[Math.floor(Math.random() * arrayColours.length)] //random colour
});
//Create our struct
targetStruct = {
circle: circle,
path: path,
id: targetData.id,
updated: false
};
targetStructArray.push(targetStruct);
}
else {
targetStruct.circle.attr({ cx: targetData.x, cy: targetData.y });
targetStruct.path.addPart(['L', targetData.x, targetData.y]);
}
//ensure we are set as updated
targetStruct.updated = true;
}
//Go through all our existing targets and delete any that werent updated
for (var i = targetStructArray.length - 1; i >= 0; i--) {
if (!targetStructArray[i].updated) {
targetStructArray[i].circle.remove();
targetStructArray[i].path.remove();
targetStructArray[i].circle.removeData();
targetStructArray[i].path.removeData();
targetStructArray[i].circle = null;
targetStructArray[i].path = null;
targetStructArray[i] = null;
targetStructArray.remove(i);
}
}
}
I use two functions not listed here which are John Resig's Array.Remove and a Raphael helper function from a different question

How to move nodes to a desired position?

I want to move a specific node (say i have the node reference), to a desired location (say to the top left corner of the canvas). I tried doing something similar to one in the sample project, where they move nodes while dragging with the mouse. But it doesn't seem to work. I am not seeing the node move as i expected. This is the code i have.
$("#someElement").click(function() {
sys.eachNode(function(node, pt) {
if (node.name === "specificNode") {
// moveToOrigin
var s = arbor.Point(1, 1);
var p = sys.fromScreen(s);
node.fixed = true;
node.p = p;
node.fixed = false;
node.tempMass = 1000;
}
}
});
To move a node to a desired position get the desired position relative to the canvas and set it via particle system's fromScreen(...) function:
var point = point;
var pos = canvas.offset();
var s = arbor.Point(point.x-pos.left, point.x-pos.top);
node.p = particleSystem.fromScreen(s);
You need to set position of node first, then again iterate each loop for nodes.
redraw: function () {
gfx.clear()
particleSystem.eachNode(function (node, pt) {
//var node = particleSystem.getNode("Carrol Wahi")
if (node.data.color == "yellow") {
var pos = $(canvas).offset();
var point = particleSystem.fromScreen(arbor.Point(pos.left + 150, pos.top));
node._fixed = true;
node._p = point;
//console.log("x=" + point.x + ", y=" + point.y);
node.tempMass = .1
}
});
particleSystem.eachEdge(function (edge, pt1, pt2) {
// your code goes here
}
particleSystem.eachNode(function (node, pt) {
//your code goes here
})
}

Categories

Resources