Container With Most Water algorithm - using recursion - javascript

I am doing leetcode #11 Container With Most Water
https://leetcode.com/problems/container-with-most-water/
Given n non-negative integers a1, a2, ..., an , where each represents a point at coordinate (i, ai). n vertical lines are drawn such that the two endpoints of line i is at (i, ai) and (i, 0). Find two lines, which together with x-axis forms a container, such that the container contains the most water.
Note: You may not slant the container and n is at least 2.
var maxArea = function (height) {
var list = [];
for (var index = 1; index <= height.length; index++) {
var eachCorr = {
x_corr: index,
y_corr: height[index - 1]
}
list.push(eachCorr);
}
var mainResult = reCursion(list, list.length-1,0,1);
console.log(list);
console.log(mainResult);
return mainResult;
//last vertical line * each vertical line from index=1;
//x-corr*(last vertical - each vertical), y-corr*(smaller vertical line)
};
function reCursion(arr, index, x,y) {
//lastX and lastY use recursion to loop
var lastX = arr[index][x];
var lastY = arr[index][y];
var chosenY = 0;
var area = 0;
var result = [];
var maxAreaAns = 0;
for (var i = index - 1; i >= 0; i--) {
if (lastY > arr[i][1]) {
chosenY = arr[i][1];
} else {
chosenY = lastY;
}
area = (lastX - arr[i][0]) * chosenY;
console.log(`area = ${area} with i = ${i}, lastX=${lastX}, lastY=${lastY}`);
result.push(area);
}
if (index === 0) {
maxAreaAns = Math.max(...result);
return maxAreaAns;
} else {
return reCursion(arr, index - 1,0,1);
}
}
My approach is using recursion, first select the last vertical line, multiple to x-corr difference of
each vertical line before, then select the small y-corr of the vertical line when compared.
area = (x-corr difference of last vertical line and compared vertical line) * (y-coor of small vertical line)
Then use recursion to select the second last vertical line and so all until select the first vertical line.
Then I push all the area result into a array and find the maximum.
I want to know why this method can not execute( lastX, lastY, area variables are undefined).

Having analyzed your code, your
var lastX = arr[index][x];
var lastY = arr[index][y];
are both always undefined. Since arr[index] returns an object and not a list, you cannot get the values by indexing. You'll need to do
var lastX = arr[index].x_corr;
var lastY = arr[index].y_corr;
Which also goes for your
if (lastY > arr[i][1]) {
chosenY = arr[i][1];
Now you might have realized that your function always logs out -Infinity as its result.
This is because when the condition
if (index === 0) {
maxAreaAns = Math.max(...result);
return maxAreaAns;
}
is met and the code inside it is executed, the result array is always empty (try invoking the Math.max() function without any input. It will return -Infinity).
This is because when the index variable is equal to 0, the loop
for (var i = index - 1; i >= 0; i--) {
if (lastY > arr[i][1]) {
...
}
will not run (as i starts from -1), and the result stays as an empty array.
I am guessing that what you would want to do is to either set result array as a global variable, or to pass it to the next reCursion() function.
That being said, I actually don't see the point of solving this problem using recursion.
Instead of using recursion (which obviously makes it difficult to write and understand the code), why not just use a nested loop to check the combinations?

Related

Optimize order of objects within layer in Illustrator for reduced laser cutting time

I'm trying to optimize the layer order of paths in Illustrator so that when sent to a laser cutter, the end of one path is close to the start of the next path reducing the travel time of the laser between each cut.
I've come up with the following code, which works, but could be further optimized considering length of lines, or through an annealing process. I'm posting it here in case anyone else is Googling 'Laser cutting optimization' and doesn't want to write their own code. Also if anyone can suggest improvements to the below code, I'd love to hear them.
// For this script to work, all paths to be optimised need to be on layer 0.
// Create a new empty layer in position 1 in the layer heirarchy.
// Run the script, all paths will move from layer 0 to layer 1 in an optimized order.
// Further optimisation possible with 'Annealing', but this will be a good first run optimization.
// Load into Visual Studio Code, follow steps on this website
// https://medium.com/#jtnimoy/illustrator-scripting-in-visual-studio-code-cdcf4b97365d
// to get setup, then run code when linked to Illustrator.
function test() {
if (!app.documents.length) {
alert("You must have a document open.");
return;
}
var docRef = app.activeDocument;
function endToStartDistance(endPath, startPath) {
var endPoint = endPath.pathPoints[endPath.pathPoints.length - 1].anchor;
var startPoint = startPath.pathPoints[0].anchor;
var dx = (endPoint[0] - startPoint[0]);
var dy = (endPoint[1] - startPoint[1]);
var dist = Math.pow((Math.pow(dx, 2) + Math.pow(dy, 2)), 0.5);
return dist;
}
function Optimize(items) {
var lastPath, closest, minDist, delIndex, curItem, tempItems = [];
var topLayer = app.activeDocument.layers[0];
var newLayer = app.activeDocument.layers[1];
for (var x = 1, len = items.length; x < len; x++) {
tempItems.push(items[x]);
}
lastPath = items[0];
lastPath.move(newLayer, ElementPlacement.PLACEATBEGINNING);
while (tempItems.length) {
closest = tempItems[0];
minDist = endToStartDistance(lastPath, closest);
delIndex = 0;
for (var y = 1, len = tempItems.length; y < len; y++) {
curItem = tempItems[y];
if (endToStartDistance(lastPath, curItem) < minDist) {
closest = curItem;
minDist = endToStartDistance(lastPath, closest);
delIndex = y;
}
}
$.writeln(minDist);
//closest.zOrder(ZOrderMethod.BRINGTOFRONT);
closest.move(newLayer, ElementPlacement.PLACEATBEGINNING);
lastPath = closest;
tempItems.splice(delIndex, 1);
}
}
var allPaths = [];
for (var i = 0; i < documents[0].pathItems.length; i++) {
allPaths.push(documents[0].pathItems[i]);
//$.writeln(documents[0].pathItems[i].pathPoints[0].anchor[0])
}
Optimize(allPaths);
}
test();
Version 2 of the above code, some changes include the ability to reverse paths if this results in a reduced distance for the cutting head to move between paths, and added comments to make the code easier to read.
// Create a new empty layer in position 1 in the layer heirarchy.
// Run the script, all paths will move from their current layer to layer 1 in an optimized order.
// Further optimisation possible with 'Annealing', but this will be a good first run optimization.
// Load into Visual Studio Code, follow steps on this website
// https://medium.com/#jtnimoy/illustrator-scripting-in-visual-studio-code-cdcf4b97365d
// to get setup, then run code when linked to Illustrator.aa
function main() {
if (!app.documents.length) {
alert("You must have a document open.");
return;
}
var docRef = app.activeDocument;
// The below function gets the distance between the end of the endPath vector object
// and the start of the startPath vector object.
function endToStartDistance(endPath, startPath) {
var endPoint = endPath.pathPoints[endPath.pathPoints.length - 1].anchor;
var startPoint = startPath.pathPoints[0].anchor;
var dx = (endPoint[0] - startPoint[0]);
var dy = (endPoint[1] - startPoint[1]);
var dist = Math.pow((Math.pow(dx, 2) + Math.pow(dy, 2)), 0.5);
return dist;
}
// The below function gets the distance between the end of the endPath vector object
// and the end of the startPath vector object.
function endToEndDistance(endPath, startPath) {
var endPoint = endPath.pathPoints[endPath.pathPoints.length - 1].anchor;
var startPoint = startPath.pathPoints[startPath.pathPoints.length - 1].anchor;
var dx = (endPoint[0] - startPoint[0]);
var dy = (endPoint[1] - startPoint[1]);
var dist = Math.pow((Math.pow(dx, 2) + Math.pow(dy, 2)), 0.5);
return dist;
}
// The below function iterates over the supplied list of tempItems (path objects) and checks the distance between
// the end of path objects and the start/end of all other path objects, ordering the objects in the layer heirarchy
// so that there is the shortest distance between the end of one path and the start of the next.
// The function can reverse the direciton of a path if this results in a smaller distance to the next object.
function Optimize(tempItems) {
var lastPath, closest, minDist, delIndex, curItem;
var newLayer = app.activeDocument.layers[1]; // There needs to be an empty layer in position 2 in the layer heirarchy
// This is where the path objects are moved as they are sorted.
lastPath = tempItems[0]; // Arbitrarily take the first item in the list of supplied items
tempItems.splice(0, 1); // Remove the first item from the list of items to be iterated over
lastPath.move(newLayer, ElementPlacement.PLACEATBEGINNING); // Move the first item to the first position in the new layer
while (tempItems.length) { // Loop over all supplied items while the length of this array is not 0.
// Items are removed from the list once sorted.
closest = tempItems[0]; // Start by checking the distance to the first item in the list
minDist = Math.min(endToStartDistance(lastPath, closest), endToEndDistance(lastPath, closest));
// Find the smallest of the distances between the end of the previous path item
// and the start / end of this next item.
delIndex = 0; // The delIndex is the index to be removed from the tempItems list after iterating through
// the entire list.
for (var y = 1, len = tempItems.length; y < len; y++) {
// Iterate over all items in the list, starting at item 1 (item 0 already being used above)
curItem = tempItems[y];
if (endToStartDistance(lastPath, curItem) < minDist || endToEndDistance(lastPath, curItem) < minDist) {
// If either the end / start distance to the current item is smaller than the previously
// measured minDistance, then the current path item becomes the new smallest entry
closest = curItem;
minDist = Math.min(endToStartDistance(lastPath, closest), endToEndDistance(lastPath, closest));
// The new minDistace is set
delIndex = y; // And the item is marked for removal from the list at the end of the loop.
}
}
if (endToEndDistance(lastPath, closest) < endToStartDistance(lastPath, closest)) {
reversePaths(closest); // If the smallest distance is yielded from the end of the previous path
// To the end of the next path, reverse the next path so that the
// end-to-start distance between paths is minimised.
}
closest.move(newLayer, ElementPlacement.PLACEATBEGINNING); // Move the closest path item to the beginning of the new layer
lastPath = closest; // The moved path item becomes the next item in the chain, and is stored as the previous item
// (lastPath) for when the loop iterates again.
tempItems.splice(delIndex, 1); // Remove the item identified as closest in the previous loop from the list of
// items to iterate over. When there are no items left in the list
// The loop ends.
}
}
function reversePaths(theItems) { // This code taken / adapted from https://gist.github.com/Grsmto/bfe1541957a0bb17972d
if (theItems.typename == "PathItem" && !theItems.locked && !theItems.parent.locked && !theItems.layer.locked) {
pathLen = theItems.pathPoints.length;
for (k = 0; k < pathLen / 2; k++) {
h = pathLen - k - 1;
HintenAnchor = theItems.pathPoints[h].anchor;
HintenLeft = theItems.pathPoints[h].leftDirection;
HintenType = theItems.pathPoints[h].pointType;
HintenRight = theItems.pathPoints[h].rightDirection;
theItems.pathPoints[h].anchor = theItems.pathPoints[k].anchor;
theItems.pathPoints[h].leftDirection = theItems.pathPoints[k].rightDirection;
theItems.pathPoints[h].pointType = theItems.pathPoints[k].pointType;
theItems.pathPoints[h].rightDirection = theItems.pathPoints[k].leftDirection;
theItems.pathPoints[k].anchor = HintenAnchor;
theItems.pathPoints[k].leftDirection = HintenRight;
theItems.pathPoints[k].pointType = HintenType;
theItems.pathPoints[k].rightDirection = HintenLeft;
}
}
}
var allPaths = []; // Grab every line in the document
for (var i = 0; i < documents[0].pathItems.length; i++) {
allPaths.push(documents[0].pathItems[i]);
// This could be better changed to the selected objects, or to filter only objects below a certain
// stroke weight so that raster paths are not affected, but cut paths are.
}
Optimize(allPaths); // Feed all paths in the document into the optimize function.
}
main(); // Call the main function, executing the above code.

Mutual repulsion force in js

I have a 1-dimensional dataset (a list of elements with only a horizontal position (+ circle radius)).
I want to implement a simple layout algorithm to show this dataset as circles in a scale.
The problem is the collisions.
I want to implement a "simple" repulsion force to avoid collisions. I don't mind the circles won't have a precise position anymore.
The result I'm looking for is simple as that:
I'm not using D3, it is plain js (and svg.js), where to start looking for theoretical information about this layout? What is the common name with which this force is referred to? Is there any example of similar things?
I've added min_gap for minimum margin between elements. So the solution is to move two intersected elements with most intersected distance on a small step at a time.
const elements = [{pos:10, radius:5}, {pos:15, radius: 20}, {pos:20, radius:10}, {pos:150, radius:5}];
const field_size = [0, 300];
const min_gap = 5;
const step = 1;
moveIntersected(elements);
console.log(elements);
function detectCollisions(arr=[]){
const result = [];
for(let i=0; i < arr.length - 1; i++){
let dist = (arr[i+1].pos - arr[i+1].radius) - (arr[i].pos + arr[i].radius);
if(dist < min_gap){
result.push([i + 0.5, dist]);
}
}
return result;
}
function moveIntersected(arr=[]){
const collisions = detectCollisions(arr);
if(collisions.length < 1) return;
const most_intersected = collisions.sort((a,b) => a[1] - b[1])[0];
const left = arr[Math.floor(most_intersected[0])];
const right = arr[Math.ceil(most_intersected[0])];
if(left.pos - left.radius - step >= field_size[0]){
left.pos -= step;
} else {
right.pos += step*2;
}
if(right.pos + right.radius + step <= field_size[1]){
right.pos += step;
} else {
left.pos -= step*2;
}
moveIntersected(arr);
}

Why is my maze generator not detecting if a cell has been visited in p5.js?

I am trying to make a maze generator, and almost everything is working so far. I have been able to set my position to a random pos, and then I repeat the standard() function. In the function, I add pos to posList, and then I choose a random direction. Next, I check if the cell has been visited by running through all of the posList vectors in reverse. I haven't executed the code that backtracks yet. If visited = false then I move to the square and execute the yet-to-be-made path() function. However, for some reason, the mover just doesn't detect if a cell has been visited or not. I am using p5.js. What am I doing wrong?
var posList = [];
var pos;
var tempo;
var boole = false;
var direc;
var mka = 0;
function setup() {
createCanvas(400, 400);
//Set up position
pos = createVector(floor(random(3)), floor(random(3)));
frameRate(1)
}
//Choose a direction
function direct(dire) {
if(dire === 0) {
return(createVector(0, -1));
} else if(dire === 1) {
return(createVector(1, 0));
} else if(dire === 2) {
return(createVector(0, 1));
} else {
return(createVector(-1, 0));
}
}
/foLo stands fo forLoop
function foLo() {
//If we have checked less than three directions and know there is a possibility for moving
if(mka < 4) {
//tempoRARY, this is what we use to see if the cell has been visited
tempo = createVector(pos.x + direct(direc).x, pos.y + direct(direc).y);
//Go through posList backwards
for(var i = posList.length - 1; i >= 0; i --) {
//If the cell has been visited or the cell is off of the screen
if(tempo === posList[i]) {
//Change the direction
direc ++;
//Roll over direction value
if(direc === 4) {
direc = 0;
}
//Re-execute on next frame
foLo();
//The cell has been visited
boole = false;
//Debugging
console.log(direc)
mka++;
} else if(tempo.x < 0 || tempo.x > 2 || tempo.y < 0 || tempo.y > 2) {
direc ++;
if(direc === 4) {
direc = 0;
}
foLo();
boole = false;
console.log(direc)
mka++;
}
}
//If it wasn't visited (Happens every time for some reason)
if(boole === true) {
//position is now the temporary value
pos = tempo;
console.log("works")
mka = 0;
}
}
}
function standard() {
//Add pos to posList
posList.push(pos);
//Random direction
direc = floor(random(4));
//Convert to vector
direct(direc);
foLo();
//Tracks pos
fill(255, 255, 0);
rect(pos.x*100+50, pos.y*100+50, 50, 50)
}
function draw() {
background(255);
fill(0);
noStroke();
//draw grid
for(var i = 0; i < 4; i ++) {
rect(i*100,0,50,350);
rect(0, i*100, 350, 50);
}
standard();
boole = true;
console.log(pos)
console.log(posList);
}
Your issue is on the line where you compare two vectors if(tempo === posList[i]) {: This will never be true.
You can verify that with the following code (in setup() for example):
const v1 = new p5.Vector(1, 0);
const v2 = new p5.Vector(1, 0);
const v3 = new p5.Vector(1, 1);
console.log(v1 === v2) // false
console.log(v1 === v3) // false
This is because despite having the same value v1 and v2 are referencing two different objects.
What you could do is using the p5.Vector.equals function. The doc has the following example:
let v1 = createVector(10.0, 20.0, 30.0);
let v2 = createVector(10.0, 20.0, 30.0);
let v3 = createVector(0.0, 0.0, 0.0);
print(v1.equals(v2)); // true
print(v1.equals(v3)); // false
This might not give you a working algorithm because I suspect you have other logical errors (but I could be wrong or you will debug them later on) but at least this part of the code will do what you expect.
Another solution is to use a Set instead of your list of positions. The cons of this solution is that you will have to adapt your code to handle the "out of grid" position situation. However when you want to keep track of visited items a Set is usually a great solution because the access time is constant. That this means is that to define is a position has already been visited it will always take the same time (you'll do something like visitedSet.has(positionToCheck), whereas with your solution where you iterate through a list the more cells you have visited to longer it will take to check if the cell is in the list.
The Set solution will require that you transform your vectors before adding them to the set though sine, has I explained before you cannot simply compare vectors. So you could check for their string representation with something like this:
const visitedCells = new Set();
const vectorToString = (v) => `${v.x},{$v.y}` // function to get the vector representation
// ...
visitedCells.add(vectorToString(oneCell)); // Mark the cell as visited
visited = visitedCells.has(vectorToString(anotherCell))
Also has a general advice you should pay attention to your variables and functions name. For example
// foLo stands fo forLoop
function foLo() {
is a big smell: Your function name should be descriptive, when you see your function call foLo(); having to find the comment next to the function declaration makes the code less readable. You could call it generateMaze() and this way you'll know what it's doing without having to look at the function code.
Same for
//tempoRARY, this is what we use to see if the cell has been visited
tempo = createVector(pos.x + direct(direc).x, pos.y + direct(direc).y);
You could simply rename tempo to cellToVisit for example.
Or boole: naming a boolean boole doesn't convey a lot of information.
That could look like some minor details when you just wrote the code but when your code will be several hundred lines or when you read it again after taking several days of break, you'll thank past you for taking care of that.

How can I show the process of a Merge Sort similiarly to how I did to a Bubble Sort on canvas [duplicate]

This question already has answers here:
Using Canvas to animate a sorting algorithm in JS
(2 answers)
Closed 3 years ago.
Im taking a Highschool CompSci 30 class and I'm working on an assignment. I'm trying to make something that will sort arrays of HSL values and display it on a canvas. I'm using two different algorithms, Bubble sort and Merge sort. My Bubble Sort works just as I want it to, it sorts and shows the process as it's sorting. My Merge Sort also works but I want it to show the process just like my Bubble Sort does. How I got my Bubble Sort to work is by adding async before my function and adding await delay(ms) after each change is made so it draws a new version of the array after however many ms. The code for merge sort is a bit different since its recursive and I'm not sure where to add a draw function or a delay or if that approach would even work.
I've tried adding async and await like I did with my Bubble Sort but the Merge Sort code is more complex and I can't get it right
This is my draw function:
function draw(){
for(y=0;y<361;y++){
hue = cArray[y].slice(4,cArray[y].indexOf(",", 4));
ctx.fillStyle = `hsl(`+ hue + `,100%,50%)`;
ctx.fillRect(x,0,4,canvas.height);
x=x+3;} //draws small strips of color
x=0; //resets after every call
}
My Bubble Sort:
async function bubbleSort(array){
for(i=0;i<array.length;i++){
for(j=1;j<array.length;j++){
var hue1 = array[j-1].slice(4,array[j-1].indexOf(","));
var hue2 = array[j].slice(4,array[j].indexOf(","));
if(hueFromHsl(array[j-1]) > hueFromHsl(array[j])){
var temp = array[j-1];
array[j-1] = array[j];
array[j] = temp;
draw(array);
}
}
await delay(1);
}
return array;
}
My Merge Sort:
function mergeSort(array){
if (array.length < 2) {return array;}
var mid = Math.floor(array.length / 2);
var left = array.slice(0, mid);
var right = array.slice(mid,array.length);
return merge(mergeSort(left), mergeSort(right));
}
function merge(left,right){
var result = [];
var l = 0, r = 0;
while (l < left.length && r < right.length) {
if (hueFromHsl(left[l]) < hueFromHsl(right[r])) {result.push(left[l++]);}
else {result.push(right[r++]);}
}
return result.concat(left.slice(l)).concat(right.slice(r));
}
Also here is a js.do of the code: https://js.do/Brunsos/color-sort
The process should look similiar to the way my Bubble Sort looks when its used but it either finishes the sort instantly or doesnt work at all. What can I do?
Great code! The issue with displaying this is that it creates copies at each iteration using slice(), so the original array remains the same until the end. Instead of using return statements, just change the actual array. To do this, pass in indexes of the subarrays, then change the actual array. Just call draw(array) within the function. Notice now neither function returns anything, instead they change the array passed in...
async function mergeSort(array, leftIndex, rightIndex) {
length = rightIndex - leftIndex
if (length < 2) {
return array;
}
var mid = leftIndex + Math.floor(length / 2);
mergeSort(array, leftIndex, mid)
mergeSort(array, mid, rightIndex)
await delay(1000*Math.sqrt(rightIndex-leftIndex));
draw(array)
merge(array, leftIndex, mid, rightIndex)
}
function merge(array, leftIndex, mid, rightIndex) {
var result = [];
var l = leftIndex,
r = mid;
while (l < mid && r < rightIndex) {
if (array[l] < array[r]) {
result.push(array[l++]);
} else {
result.push(array[r++]);
}
}
result = result.concat(array.slice(l, mid)).concat(array.slice(r, rightIndex));
for (let i = 0; i < rightIndex - leftIndex; i++) {
array[leftIndex + i] = result[i]
}
}
Button Script:
<button id="mSort" class="sort" onclick=
"(async() => {
await mergeSort(cArray,0,360);
await delay(1600);
draw(cArray);
})()"
>Merge Sort</button>
</div>
This button script is to allow for the last draw, since the draw occurs before the final merge if you don't await then draw it will be stuck before the final merge...

How to find selected elements within a javascript marquee selection box without using a loop?

I am writing my own drag and drop file manager. This includes a javascript marquee selection box which when active calculates the elements (files) that are intersected and selects them by adding a class to them.
I currently perform the check during a mousemove handler, loop through an array of element coordinates and determine which ones are intersected by the drag and drop selection box.
The function currently looks like this:
selectItems : function(voidindex){
var self = this;
var coords = self.cache.selectioncoords;
for(var i=0, len = self.cache.items.length; i<len; i++){
var item = self.cache.items[i];
var itemcoords = item.box_pos;
if(coords.topleft.x < (itemcoords.x+201) && coords.topright.x > itemcoords.x && coords.topleft.y < (itemcoords.y+221) && coords.bottomleft.y > itemcoords.y){
if(!item.selected){
item.selected = true;
item.html.addClass('selected').removeClass('activebutton');
self.cache.selecteditems.push(i);
self.setInfo();
}
}
else{
if(item.selected){
item.selected = false;
if(!voidindex || voidindex !== i){
item.html.removeClass('selected');
}
var removeindex = self.cache.selecteditems.indexOf(i);
self.cache.selecteditems.splice(removeindex, 1);
self.setInfo();
}
}
}
},
There is lots of dirty logic in the code above which ensures that the DOM is only manipulated when the selection changes. This is not relevant to the question and can be exluded. The important part is the intersection logic which checks the coordinates of the element versus the coordinates of the marquee selection box.
Also please note that the item dimensions are fixed at 201px width by 221px height.
I have tested this and all works perfectly, however I have the need to support potentially thousands of files which would mean that at some point we will start seeing UI performance decrease.
I would like to know if there is anyway to perform intersection detection without looping through the coordinates of each element.
The coordinates of the marquee box are defined as follows at any given time:
selectioncoords : {
topleft : {
x : 0,
y : 0
},
topright : {
x : 0,
y : 0
},
bottomleft : {
x : 0,
y : 0
},
bottomright : {
x : 0,
y : 0
},
width : 0,
height : 0
}
And the coordinates of each item, stored in the self.cache.items array are defined as follows:
item : {
box_pos : {
x : 0,
y : 0
},
grid_pos : {
row : 1,
column : 1
}
}
So the information available will always be the actual grid position (row/column) as well as the physical item position (left and top offsets in pixels within the grid).
So to summarize, the question is, is there anyway to detect item intersection from a set of marquee selection box coordinates as defined above without looping through the whole array of item coordinates every time the mousemove event fires?
Thanks in advance for any help.
The following depends upon a locked grid with the dimensions as described.
You are comparing a mouse-defined rectangle against a grid with static edge sizes. Thus, given an x coordinate or a y coordinate, you should be able to derive pretty easily which column or row (respectively) the coordinate falls into.
When the user starts the select box, grab that x and y, and find the row/column of the start. When the mouse moves while pulling the select box, you find (and then update) the row/column of the finish. anything that is both within the rows defined by that box and within the columns defined by that box (inclusive) is selected. If you then keep your selectable elements in a two-dimensional array according to rows and columns, you should be able to just grab the ones you want that way.
Mind, how much more (or less) efficient this is depends on the size of your expected selection boxes as compared to the total size, and the degree to which you expect the grid to be populated. Certainly, if the average use case is selecting half or so of the objects at a time, there's not a whole lot you can do to cut down efficiently on the number of objects you have to look at each time.
Also, though it is kludgy, you can have the mousemove handler not fire every time. Letting it pause a bit between updates will reduce the responsiveness of this particular function a fair bit, but it'll cut down significantly on the amount of resources that are used.
There are several ways you could approach this. Here's one. First you need the items in some kind of organized structure that you can look up quickly by row and column. You could use a two-dimensional array, or for simplicity I'm going to use a hash table. You could do this at the same time that you create the self.cache.items, or later, something like this:
var cacheLookup = {};
function initCacheLookup() {
var items = self.cache.items;
for( var i = 0, n = items.length; i < n; i++ ) {
var item = items[i];
var key = [ item.grid_pos.row, item.grid_pos.column ].join(',');
cacheLookup[key] = item;
}
}
Then when you want to get the items intersecting the rectangle, you could do something like this:
var itemWidth = 201, itemHeight = 221;
var tl = selectioncoords.topleft, br = selectioncoords.bottomright;
var left = Math.floor( tl.x / itemWidth ) + 1;
var right = Math.floor( br.x / itemWidth ) + 1;
var top = Math.floor( tl.y / itemHeight ) + 1;
var bottom = Math.floor( br.y / itemHeight ) + 1;
var selecteditems = [];
for( var row = top; row <= bottom; row++ ) {
for( var col = left; col <= right; col++ ) {
var key = [ row, col ].join(',');
var item = cacheLookup[key];
if( item ) {
selecteditems.push( item );
}
}
}
// Now selecteditems has the items intersecting the rectangle
There's probably an off-by-one error or two here, but this should be close.
Well, as I said, that is one way to do it. And it has the possibly interesting property that it doesn't depend on the order of items in the self.cache.items array. But that cacheLookup hash table smells like it might not be the most efficient solution.
Let me take a guess: isn't that array already in the correct order by rows and columns (or vice versa)? For example, if your grid is four wide, then the top row would be array elements 0-3, the second row 4-7, the third row 8-11, etc. Or it could be a similar arrangement going down the columns.
Assuming it's in row-by-row order, then you don't need the hash table at all. That initCacheLookup() function goes away, and instead the search code looks like this:
var nCols = 4/*whatever*/; // defined somewhere else
var itemWidth = 201, itemHeight = 221;
var tl = selectioncoords.topleft, br = selectioncoords.bottomright;
var left = Math.floor( tl.x / itemWidth );
var right = Math.floor( br.x / itemWidth );
var top = Math.floor( tl.y / itemHeight ) * nCols;
var bottom = Math.floor( br.y / itemHeight ) * nCols;
var items = self.cache.items;
var selecteditems = [];
for( var iRow = top; iRow <= bottom; iRow += nCols ) {
for( var col = left; col <= right; col++ ) {
var index = iRow + col;
if( index < items.length ) {
selecteditems.push( items[index] );
}
}
}
// Now selecteditems has the items intersecting the rectangle
This code will be a little faster, and it's simpler too. Also it doesn't depend at all on the item.box_pos and item.grid_pos. You may not need those data fields at all, because they are easily calculated from the item index, grid column count, and item height and width.
Some related notes:
Don't hard code 201 and 221 in the code. Store those in variables once, only, and then use those variables when you need the item height and width.
There is a lot of duplication in your data structures. I recommend that you ruthlessly eliminate all duplicated data unless there is a specific need for it. Specifically:
selectioncoords: {
topleft: {
x: 0,
y: 0
},
topright: {
x: 0,
y: 0
},
bottomleft: {
x: 0,
y: 0
},
bottomright: {
x: 0,
y: 0
},
width: 0,
height: 0
}
More than half the data here is duplicated or can be calculated. This is all you need:
selectioncoords: {
left: 0,
right: 0,
top: 0,
bottom: 0
}
The reason I bring this up is that was a bit confusing when working on the code: "I want the left edge. Do I get that from topleft.x or bottomleft.x? Are they really the same like they seem? How do I pick?"
Also, as mentioned above, the item.box_pos and item.grid_pos may not be needed at all if the items are stored in a sequential array. If they are needed, you could store just one and calculate the other from it, since there's a direct relationship between the two:
box_pos.x === ( grid_pos.column - 1 ) * itemWidth
box_pos.y === ( grid_pos.row - 1 ) * itemHeight
You can limit the scope of your checks by indexing each item in a grid, as often as necessary and no more often. You can use the grid to give you a list of elements near an X, Y coordinate or that might be in an X1, Y2, X1, Y2 range.
To get you started ...
var Grid = function(pixelWidth, pixelHeight, boxSize) {
this.cellsIn = function(x1, y1, x2, y2) {
var rv = [];
for (var x = x1; x < x2; x += boxSize) {
for (var y = y1; y < y2; y += boxSize) {
var gx = Math.ceil(x/boxSize);
var gy = Math.ceil(y/boxSize);
rv.push(this.cells[gx][gy]);
}
}
return rv;
} // cellsIn()
this.add = function(x1, y1, x2, y2, o) {
var cells = this.cellsIn(x1, y1, x2, y2);
for (var i in cells) {
cells[i].push(o);
}
} // add()
this.get = function(x1, y1, x2, y2) {
var rv = [];
var rv_index = {};
var cells = this.cellsIn(x1, y1, x2, y2);
for (var i in cells) {
var cell = cells[i];
for (var oi in cell) {
if (!rv_index[cell[oi]]) {
rv_index[cell[oi]] = 1;
rv.push(cell[oi]);
}
}
}
return rv;
} // get()
this.cells = [];
for (var x = 0; x < Math.ceil(pixelWidth/boxSize); x++) {
this.cells[x] = [];
for (var y = 0; y < Math.ceil(pixelHeight/boxSize); y++) {
this.cells[x][y] = [];
}
}
};
So, rather than iterating through all possible objects, whatever they may be, you iterate over all the objects that are near or potentially in the given coordinates.
This requires that you maintain/re-index the grid as item coordinates change. And you'll likely want to add some functionality to the above (or similar) Grid class to modify/move existing objects. But, to the best of my knowledge, an index of this sort is the best, if not only, way to index objects "in space."
Disclaimer: The code above isn't tested. But, I have similar code that is. See the DemoGrid function class here: http://www.thepointless.com/js/ascii_monsters.js
The functionality of my DemoGrid is similar (as far as I remember, it's been awhile), but accepts x, y, radius as parameters instead. Also notable, my mouse events don't touch the grid every time the event fires. Checks are rate-limited by a game/main loop.
If the system is set up such that
self.cache.items is ordered from left to right and top to bottom
(0,0),(1,0),(2,0),(0,1),(1,1),(1,2),(0,2),(1,2),(2,2)
There is an item in each space
GOOD - (0,0),(1,0),(2,0),(0,1),(1,1),(1,2),(0,2),(1,2),(2,2)
BAD - (0,0),(2,0)(1,2),(1,3),(2,1),(2,3)
We need to know the total number of columns.
So the code to get you started.
// Some 'constants' we'll need.
number_of_columns = 4;
item_width = 201;
item_height = 221;
// First off, we are dealing with a grid system,
// so that means that if given the starting x and y of the marquee,
// we can determine which element in the cache to start where we begin.
top_left_selected_index = Math.floor(selectioncoords.topleft.x / item_width) + (Math.floor(selectioncoords.topright.y / item_height) * number_of_columns );
// Now, because the array is in order, and there are no empty cache points,
// we know that the lower bound of the selected items is `top_left_selected_index`
// so all we have to do is walk the array to grab the other selected.
number_columns_selected = (selectioncoords.bottomright.x - selectioncoords.topleft.x) / item_width;
// if it it doesn't divide exactly it means there is an extra column selected
if((selectioncoords.bottomright.x - selectioncoords.topleft.x) % item_width > 0){
number_columns_selected += 1;
}
// if it it doesn't divide exactly it means there is an extra column selected
number_rows_selected = (selectioncoords.bottomright.y - selectioncoords.topleft.y) / item_height;
if((selectioncoords.bottomright.y - selectioncoords.topleft.y) % item_height > 0){
number_rows_selected += 1;
}
// Outer loop handles the moving the pointer in terms of the row, so it
// increments by the number of columns.
// EX: Given my simple example array, To get from (1,0) to (1,1)
// requires an index increase of 3
for(i=0; i < number_rows_selected; i++){
// Inner loop marches through the the columns, so it is just one at a time.
// Added j < number_of_columns in case your marquee stretches well past your content
for(j=0; j < number_columns_selected && j < number_of_columns; j++){
// Do stuff to the selected items.
self.cache.items[top_left_selected_index + (i * number_of_columns) + j];
}
}

Categories

Resources