This part is responsible for processing the Data in the val array so i everything is an integer and saves it in the output array. (The val array sometimes contains floats and i cant work with those)
Lets say the next element that gets processed has a x and y pair that is already in the output array with any other color.
How do i replace the old object with the new.
val.forEach(element => {
output.push({
x: Math.round(element.x / element.width),
y: Math.round(element.y / element.height),
color: mapColorToBlock(element.color)
})
});
/* val array...
[
{"x":0,"y":0,"color":"blue","width":256,"height":256},
{"x":0,"y":256,"color":"blue","width":256,"height":256},
{"x":256,"y":256,"color":"blue","width":256,"height":256},
{"x":256,"y":0,"color":"blue","width":256,"height":256},
{"x":0,"y":256,"color":"lime","width":256,"height":256}
]
*/
/*output array after the processing(notice how there are 2 objects with x = 0 and y = 1 (the second and last entry in output))
[
{ x: 0, y: 0, color: 12 },
{ x: 0, y: 1, color: 12 },
{ x: 1, y: 1, color: 12 },
{ x: 1, y: 0, color: 12 },
{ x: 0, y: 1, color: 6 }
]
*/
It is important that the new one replaces the old object.(The new entry doesnt have to be in the same position in the array as the old one)
In this scenario the output array would look like this.
[
{ x: 0, y: 0, color: 12 },
{ x: 1, y: 1, color: 12 },
{ x: 1, y: 0, color: 12 },
{ x: 0, y: 1, color: 6 }
]
Note:
It is important that the new one replaces the old object
val.forEach(element => {
const x = Math.round(element.x / element.width);
const y = Math.round(element.y / element.height);
const isDuplicate = output.some(item => item.x === x && item.y === y);
if (!isDuplicate) {
output.push({
x: x,
y: y,
color: mapColorToBlock(element.color)
})
}
});
reduce over the array to create a new object with keys based on the x and y coordinates, and then use Object.values to create a new array of objects from that object's values.
const data=[{x:0,y:0,color:"blue",width:256,height:256},{x:0,y:256,color:"blue",width:256,height:256},{x:256,y:256,color:"blue",width:256,height:256},{x:256,y:0,color:"blue",width:256,height:256},{x:0,y:256,color:"lime",width:256,height:256}];
const out = data.reduce((acc, c) => {
// Extract the values and create a key
// from the x and y values
const { x, y, color, width, height } = c;
const key = `${x}|${y}`;
// Do your x/y calculations
const newX = Math.round(x / width);
const newY = Math.round(y / height);
// Add the current object to the property
// defined by the key *replacing the data
// if it already exists*
acc[key] = { x: newX, y: newY, color };
// Return the accumulator for the next iteration
return acc;
}, {});
// Now extract the values from the new object
console.log(Object.values(out));
A modified version of Shuvo's answer.
This one is only sensible if mapColorToBlock can return different results in successive calls for the same value passed, otherwise Shuvo's answer which skips duplicates is better.
"It is important that the new one replaces the old object"...
let val = [
{"x":0,"y":0,"color":"blue","width":256,"height":256},
{"x":0,"y":256,"color":"blue","width":256,"height":256},
{"x":256,"y":256,"color":"blue","width":256,"height":256},
{"x":256,"y":0,"color":"blue","width":256,"height":256},
{"x":0,"y":256,"color":"lime","width":256,"height":256}
];
let output = [];
val.forEach(element => {
const x = Math.round(element.x / element.width);
const y = Math.round(element.y / element.height);
const found = output.find(item => item.x === x && item.y === y);
if (found) {
found.color = mapColorToBlock(element.color);
} else {
output.push({
x: x,
y: y,
color: mapColorToBlock(element.color)
})
}
});
Here you have to replace the previous element with same x and y
val.forEach(element => {
output = [...output.filter(out => !( out.x === Math.round(element.x / element.width) && out.y === Math.round(element.y / element.height)), {
x: Math.round(element.x / element.width),
y: Math.round(element.y / element.height),
color: mapColorToBlock(element.color)
}]
});
If I have 4 points
var x1;
var y1;
var x2;
var y2;
var x3;
var y3;
var x4;
var y4;
that make up a box. So
(x1,y1) is top left
(x2,y2) is top right
(x3,y3) is bottom left
(x4,y4) is bottom right
And then each point has a weight ranging from 0-522. How can I calculate a coordinate (tx,ty) that lies inside the box, where the point is closer to the the place that has the least weight (but taking all weights into account). So for example. if (x3,y3) has weight 0, and the others have weight 522, the (tx,ty) should be (x3,y3). If then (x2,y2) had weight like 400, then (tx,ty) should be move a little closer towards (x2,y2) from (x3,y3).
Does anyone know if there is a formula for this?
Thanks
Creating a minimum, complete, verifiable exmample
You have a little bit of a tricky problem here, but it's really quite fun. There might be better ways to solve it, but I found it most reliable to use Point and Vector data abstractions to model the problem better
I'll start with a really simple data set – the data below can be read (eg) Point D is at cartesian coordinates (1,1) with a weight of 100.
|
|
| B(0,1) #10 D(1,1) #100
|
|
| ? solve weighted average
|
|
| A(0,0) #20 C(1,0) #40
+----------------------------------
Here's how we'll do it
find the unweighted midpoint, m
convert each Point to a Vector of Vector(degrees, magnitude) using m as the origin
add all the Vectors together, vectorSum
divide vectorSum's magnitude by the total magnitude
convert the vector to a point, p
offset p by unweighted midpoint m
Possible JavaScript implementation
I'll go thru the pieces one at a time then there will be a complete runnable example at the bottom.
The Math.atan2, Math.cos, and Math.sin functions we'll be using return answers in radians. That's kind of a bother, so there's a couple helpers in place to work in degrees.
// math
const pythag = (a,b) => Math.sqrt(a * a + b * b)
const rad2deg = rad => rad * 180 / Math.PI
const deg2rad = deg => deg * Math.PI / 180
const atan2 = (y,x) => rad2deg(Math.atan2(y,x))
const cos = x => Math.cos(deg2rad(x))
const sin = x => Math.sin(deg2rad(x))
Now we'll need a way to represent our Point and Point-related functions
// Point
const Point = (x,y) => ({
x,
y,
add: ({x: x2, y: y2}) =>
Point(x + x2, y + y2),
sub: ({x: x2, y: y2}) =>
Point(x - x2, y - y2),
bind: f =>
f(x,y),
inspect: () =>
`Point(${x}, ${y})`
})
Point.origin = Point(0,0)
Point.fromVector = ({a,m}) => Point(m * cos(a), m * sin(a))
And of course the same goes for Vector – strangely enough adding Vectors together is actually easier when you convert them back to their x and y cartesian coordinates. other than that, this code is pretty straightforward
// Vector
const Vector = (a,m) => ({
a,
m,
scale: x =>
Vector(a, m*x),
add: v =>
Vector.fromPoint(Point.fromVector(Vector(a,m)).add(Point.fromVector(v))),
inspect: () =>
`Vector(${a}, ${m})`
})
Vector.zero = Vector(0,0)
Vector.fromPoint = ({x,y}) => Vector(atan2(y,x), pythag(x,y))
Lastly we'll need to represent our data above in JavaScript and create a function which calculates the weighted point. With Point and Vector by our side, this will be a piece of cake
// data
const data = [
[Point(0,0), 20],
[Point(0,1), 10],
[Point(1,1), 100],
[Point(1,0), 40],
]
// calc weighted point
const calcWeightedMidpoint = points => {
let midpoint = calcMidpoint(points)
let totalWeight = points.reduce((acc, [_, weight]) => acc + weight, 0)
let vectorSum = points.reduce((acc, [point, weight]) =>
acc.add(Vector.fromPoint(point.sub(midpoint)).scale(weight/totalWeight)), Vector.zero)
return Point.fromVector(vectorSum).add(midpoint)
}
console.log(calcWeightedMidpoint(data))
// Point(0.9575396819442366, 0.7079725827019256)
Runnable script
// math
const pythag = (a,b) => Math.sqrt(a * a + b * b)
const rad2deg = rad => rad * 180 / Math.PI
const deg2rad = deg => deg * Math.PI / 180
const atan2 = (y,x) => rad2deg(Math.atan2(y,x))
const cos = x => Math.cos(deg2rad(x))
const sin = x => Math.sin(deg2rad(x))
// Point
const Point = (x,y) => ({
x,
y,
add: ({x: x2, y: y2}) =>
Point(x + x2, y + y2),
sub: ({x: x2, y: y2}) =>
Point(x - x2, y - y2),
bind: f =>
f(x,y),
inspect: () =>
`Point(${x}, ${y})`
})
Point.origin = Point(0,0)
Point.fromVector = ({a,m}) => Point(m * cos(a), m * sin(a))
// Vector
const Vector = (a,m) => ({
a,
m,
scale: x =>
Vector(a, m*x),
add: v =>
Vector.fromPoint(Point.fromVector(Vector(a,m)).add(Point.fromVector(v))),
inspect: () =>
`Vector(${a}, ${m})`
})
Vector.zero = Vector(0,0)
Vector.unitFromPoint = ({x,y}) => Vector(atan2(y,x), 1)
Vector.fromPoint = ({x,y}) => Vector(atan2(y,x), pythag(x,y))
// data
const data = [
[Point(0,0), 20],
[Point(0,1), 10],
[Point(1,1), 100],
[Point(1,0), 40],
]
// calc unweighted midpoint
const calcMidpoint = points => {
let count = points.length;
let midpoint = points.reduce((acc, [point, _]) => acc.add(point), Point.origin)
return midpoint.bind((x,y) => Point(x/count, y/count))
}
// calc weighted point
const calcWeightedMidpoint = points => {
let midpoint = calcMidpoint(points)
let totalWeight = points.reduce((acc, [_, weight]) => acc + weight, 0)
let vectorSum = points.reduce((acc, [point, weight]) =>
acc.add(Vector.fromPoint(point.sub(midpoint)).scale(weight/totalWeight)), Vector.zero)
return Point.fromVector(vectorSum).add(midpoint)
}
console.log(calcWeightedMidpoint(data))
// Point(0.9575396819442366, 0.7079725827019256)
Going back to our original visualization, everything looks right!
|
|
| B(0,1) #10 D(1,1) #100
|
|
| * <-- about right here
|
|
|
| A(0,0) #20 C(1,0) #40
+----------------------------------
Checking our work
Using a set of points with equal weighting, we know what the weighted midpoint should be. Let's verify that our two primary functions calcMidpoint and calcWeightedMidpoint are working correctly
const data = [
[Point(0,0), 5],
[Point(0,1), 5],
[Point(1,1), 5],
[Point(1,0), 5],
]
calcMidpoint(data)
// => Point(0.5, 0.5)
calcWeightedMidpoint(data)
// => Point(0.5, 0.5)
Great! Now we'll test to see how some other weights work too. First let's just try all the points but one with a zero weight
const data = [
[Point(0,0), 0],
[Point(0,1), 0],
[Point(1,1), 0],
[Point(1,0), 1],
]
calcWeightedMidpoint(data)
// => Point(1, 0)
Notice if we change that weight to some ridiculous number, it won't matter. Scaling of the vector is based on the point's percentage of weight. If it gets 100% of the weight, it (the point) will not pull the weighted midpoint past (the point) itself
const data = [
[Point(0,0), 0],
[Point(0,1), 0],
[Point(1,1), 0],
[Point(1,0), 1000],
]
calcWeightedMidpoint(data)
// => Point(1, 0)
Lastly, we'll verify one more set to ensure weighting is working correctly – this time we'll have two pairs of points that are equally weighted. The output is exactly what we're expecting
const data = [
[Point(0,0), 0],
[Point(0,1), 0],
[Point(1,1), 500],
[Point(1,0), 500],
]
calcWeightedMidpoint(data)
// => Point(1, 0.5)
Millions of points
Here we will create a huge point cloud of random coordinates with random weights. If points are random and things are working correctly with our function, the answer should be pretty close to Point(0,0)
const RandomWeightedPoint = () => [
Point(Math.random() * 1000 - 500, Math.random() * 1000 - 500),
Math.random() * 1000
]
let data = []
for (let i = 0; i < 1e6; i++)
data[i] = RandomWeightedPoint()
calcWeightedMidpoint(data)
// => Point(0.008690554978970092, -0.08307212085822799)
A++
Assume w1, w2, w3, w4 are the weights.
You can start with this (pseudocode):
M = 522
a = 1
b = 1 / ( (1 - w1/M)^a + (1 - w2/M)^a + (1 - w3/M)^a + (1 - w4/M)^a )
tx = b * (x1*(1-w1/M)^a + x2*(1-w2/M)^a + x3*(1-w3/M)^a + x4*(1-w4/M)^a)
ty = b * (y1*(1-w1/M)^a + y2*(1-w2/M)^a + y3*(1-w3/M)^a + y4*(1-w4/M)^a)
This should approximate the behavior you want to accomplish. For the simplest case set a=1 and your formula will be simpler. You can adjust behavior by changing a.
Make sure you use Math.pow instead of ^ if you use Javascript.
A very simple approach is this:
Convert each point's weight to 522 minus the actual weight.
Multiply each x/y co-ordinate by its adjusted weight.
Sum all multiplied x/y co-ordinates together, and --
Divide by the total adjusted weight of all points to get your adjusted average position.
That should produce a point with a position that is biased proportionally towards the "lightest" points, as described. Assuming that weights are prefixed w, a quick snippet (followed by JSFiddle example) is:
var tx = ((522-w1)*x1 + (522-w2)*x2 + (522-w3)*x3 + (522-w4)*x4) / (2088-(w1+w2+w3+w4));
var ty = ((522-w1)*y1 + (522-w2)*y2 + (522-w3)*y3 + (522-w4)*y4) / (2088-(w1+w2+w3+w4));
JSFiddle example of this
Even though this has already been answered, I feel the one, short code snippet that shows the simplicity of calculating a weighted-average is missing:
function weightedAverage(v1, w1, v2, w2) {
if (w1 === 0) return v2;
if (w2 === 0) return v1;
return ((v1 * w1) + (v2 * w2)) / (w1 + w2);
}
Now, to make this specific to your problem, you have to apply this to your points via a reducer. The reducer makes it a moving average: the value it returns represents the weights of the points it merged.
// point: { x: xCoordinate, y: yCoordinate, w: weight }
function avgPoint(p1, p2) {
return {
x: weightedAverage(p1.x, p1.w, p2.x, p2.w),
x: weightedAverage(p1.x, p1.w, p2.x, p2.w),
w: p1.w + pw.2,
}
}
Now, you can reduce any list of points to get an average coordinate and the weight it represents:
[ /* points */ ].reduce(avgPoint, { x: 0, y: 0, w: 0 })
I hope user naomik doesn't mind, but I used some of their test cases in this runnable example:
function weightedAverage(v1, w1, v2, w2) {
if (w1 === 0) return v2;
if (w2 === 0) return v1;
return ((v1 * w1) + (v2 * w2)) / (w1 + w2);
}
function avgPoint(p1, p2) {
return {
x: weightedAverage(p1.x, p1.w, p2.x, p2.w),
y: weightedAverage(p1.y, p1.w, p2.y, p2.w),
w: p1.w + p2.w,
}
}
function getAvgPoint(arr) {
return arr.reduce(avgPoint, {
x: 0,
y: 0,
w: 0
});
}
const testCases = [
{
data: [
{ x: 0, y: 0, w: 1 },
{ x: 0, y: 1, w: 1 },
{ x: 1, y: 1, w: 1 },
{ x: 1, y: 0, w: 1 },
],
result: { x: 0.5, y: 0.5 }
},
{
data: [
{ x: 0, y: 0, w: 0 },
{ x: 0, y: 1, w: 0 },
{ x: 1, y: 1, w: 500 },
{ x: 1, y: 0, w: 500 },
],
result: { x: 1, y: 0.5 }
}
];
testCases.forEach(c => {
var expected = c.result;
var outcome = getAvgPoint(c.data);
console.log("Expected:", expected.x, ",", expected.y);
console.log("Returned:", outcome.x, ",", outcome.y);
console.log("----");
});
const rndTest = (function() {
const randomWeightedPoint = function() {
return {
x: Math.random() * 1000 - 500,
y: Math.random() * 1000 - 500,
w: Math.random() * 1000
};
};
let data = []
for (let i = 0; i < 1e6; i++)
data[i] = randomWeightedPoint()
return getAvgPoint(data);
}());
console.log("Expected: ~0 , ~0, 500000000")
console.log("Returned:", rndTest.x, ",", rndTest.y, ",", rndTest.w);
.as-console-wrapper {
min-height: 100%;
}
I have a grid with items inside of it with x and y co-orditantes. I am trying to write a function (with lodash) to determine where is the first empty spot where the top most left most spot is the first position.
I am trying to do this by iterating over each spot until I find the first empty spot. It is only a 2 column layout so I work through them in a pattern like so - x: 0, y:0 -> x:1, y:0 -> x:0, y:1 -> x:1, y:1 ... and then checking all the items along the way to see if there is not a match, so I then know if there is an opening. My attempt looks like so :
function fillEmptySpace(isFilled, startX, startY) {
if (!isFilled) {
_.forEach(items, function(item, i) {
if (!_.isMatch(item, {
'x': startX
}) && !_.isMatch(item, {
'y': startY
})
) {
console.log("empty spot at", startX, startY);
isFilled = true;
} else if (!_.isMatch(item, {
'x': startX + 1
}) && !_.isMatch(item, {
'y': startY
})) {
console.log("empty spot at", startX + 1, startY);
isFilled = true;
}
});
startY += 1;
fillEmptySpace(isFilled, startX, startY);
}
}
fillEmptySpace(false, 0, 0);
The data looks like so :
var items = [{
i: 'a',
x: 0,
y: 0,
w: 1,
h: 1,
maxW: 2
}, {
i: 'b',
x: 1,
y: 4,
w: 1,
h: 1,
maxW: 2
}, {
i: 'c',
x: 0,
y: 1,
w: 1,
h: 1,
maxW: 2
}, {
i: 'd',
x: 0,
y: 2,
w: 1,
h: 1,
maxW: 2
}];
And here is the fiddle I have been fooling around in : https://jsfiddle.net/alexjm/ugpy13xd/38/
I can't seem to get this logic quite right, I am not sure a this point where I am getting it wrong. Any input would be greatly appreciated!
Just as a note : with the provided data it should identify the first empty space as x:1, y:0, however right now it is saying empty spot at 0 0, which cannot be correct. Thanks!
When it comes to 2D arrays, the 1D index can be calculated with x + y * width. If we then sort the 1D indexes, we can create an O(nlogn) solution:
function findEmptySpace(grid, width) {
var index = _(grid)
.map(function(p) { return p.x + p.y * width })
.sortBy()
.findIndex(_.negate(_.eq));
if (index < 0) index = grid.length;
return {
x: index % width,
y: index / width >> 0 // ">> 0" has the same result as "Math.floor"
};
}
var items = [{x:0,y:0},{x:0,y:4},{x:0,y:1},{x:0,y:2}];
function findEmptySpace(grid, width) {
var index = _(grid)
.map(function(p) { return p.x + p.y * width; })
.sortBy()
.findIndex(_.negate(_.eq));
if (index < 0) index = grid.length;
return {
x: index % width,
y: index / width >> 0 // ">> 0" has the same result as "Math.floor"
};
}
document.getElementById('btn').onclick = function() {
var space = findEmptySpace(items, 2);
items.push(space);
console.log(space);
};
#btn { font-size: 14pt }
<script src="https://cdn.jsdelivr.net/lodash/4.13.1/lodash.min.js"></script>
<button id="btn">Fill the Empty Space</button>
If you pre-sort the array, the solution would be worst-case O(n).
May I suggest checking to see if the point exists vs checking to see if it doesn't. Iterate over each item in the list to see if it exists if it does set a flag, then increment positions through your grid. Keep in mind this will not account for coords less than your intial value of "startY". Consider the following code:
function findEmptySpace(startX, startY) {
var isFilled = false;
_.forEach(items, function(item, i) {
if (_.isMatch(item, { 'x': startX }) && _.isMatch(item, { 'y': startY }) {
// this spot is filled check next x
isFilled = true;
continue;
}
}
if (isFilled == true) {
// we need to recursively call our function but I don't know the value of x
(startX == 0) ? findEmptySpace(1, startY): findEmptySpace(0, startY + 1);
} else {
console.log("Congrats, we found a spot", startX, startY);
}
}
It looks like you're always going to find a match at 0,0 since your logic is finding if there is any item in the list that is not on 0,0, instead of if there is an item in the list on 0,0
What you really want to do is stop checking once you've found an item in the current x,y (and, additionally, check both x and y in your isMatch). You can use your existing routine and your existing isFilled check:
function fillEmptySpace(isFilled, startX, startY) {
_.forEach(items, function(item, i) {
if (!isFilled) {
if (!_.isMatch(item, {'x': startX, 'y': startY})) {
console.log("empty spot at", startX, startY);
isFilled = true;
} else if (!_.isMatch(item, {'x': startX + 1, 'y': startY})) {
console.log("empty spot at", startX + 1, startY);
isFilled = true;
}
}
});
if (!isFilled) {
startY += 1;
fillEmptySpace(isFilled, startX, startY);
}
}