Easiest way to find intersecting elements is to iterate over them and check intersection one by one.
But that's not optimal, as each iteration will have to read and parse DOM attributes again and again.
Since you know that the map
is static and will not change, you can gather info beforehand and prepare data for quick lookups.
If we assume that all rects on the map
are of the same size, we can make quick calculation to get positions of rects intersecting with area of a circle.
Since your SVG is too big for including in code snippets, code samples below are JavaScript-only, with additional links to fiddles.
Easy, sub-optimal implementation
/**
* @typedef Area
* @property {number} x1 X position of top-left
* @property {number} y1 Y position of top-left
* @property {number} x2 X position of bottom-right
* @property {number} y2 Y position of bottom-right
*/
/**
* Based on https://stackoverflow.com/a/2752387/6352710
* @param {SVGElement} $rect
* @param {Area} area
* @return {boolean}
*/
function areIntersecting ($rect, area) {
const x1 = parseFloat($rect.getAttribute('x'));
const y1 = parseFloat($rect.getAttribute('y'));
const x2 = x1 + parseFloat($rect.getAttribute('width')) + parseFloat($rect.getAttribute('stroke-width'));
const y2 = y1 + parseFloat($rect.getAttribute('height'));
return !(x1 > area.x2 ||
x2 < area.x1 ||
y1 > area.y2 ||
y2 < area.y1);
}
/**
* @param {SVGElement[]} rects
* @param {SVGElement} $circle
* @return {SVGElement[]}
*/
function findIntersectingRects (rects, $circle) {
let x = parseFloat($circle.getAttribute('cx'));
let y = parseFloat($circle.getAttribute('cy'));
let r = parseFloat($circle.getAttribute('r'));
let box = {
x1: x - r,
y1: y - r,
x2: x + r,
y2: y + r
};
return rects.filter($rect => areIntersecting($rect, box));
}
/*
* Following code is just for the example.
*/
// Get array of `RECT` elements
const $map = document.getElementById('map');
const rects = Array.from($map.querySelectorAll('rect'));
// Get array of `CIRCLE` elements
const $edit = document.getElementById('edit');
const circles = Array.from($edit.querySelectorAll('circle'));
// Change opacity of `RECT` elements that are
// intersecting with `CIRCLE` elements.
circles.forEach($circle => {
findIntersectingRects(rects, $circle).forEach($rect => $rect.setAttribute('style', 'fill-opacity: 0.3'))
});
Test it at https://jsfiddle.net/subw6reL/.
A bit faster implementation
/**
* @typedef Area
* @property {number} x1 X position of top-left
* @property {number} y1 Y position of top-left
* @property {number} x2 X position of bottom-right
* @property {number} y2 Y position of bottom-right
* @property {SVGElement} [$e] optional reference to SVG element
*/
/**
* Besides properties defined below, grid may contain multiple
* objects named after X value of area, and those object may contain
* multiple Areas, named after Y value of those areas.
*
* @typedef Grid
* @property {number} x X position of top-left
* @property {number} y Y position of top-left
* @property {number} w Width of each rect in grid
* @property {number} h Height of each rect in grid
*/
/**
* @param {Grid} grid
* @param {SVGElement} $circle
* @return {SVGElement[]}
*/
function findIntersectingRects (grid, $circle) {
let r = parseFloat($circle.getAttribute('r'));
let x1 = parseFloat($circle.getAttribute('cx')) - r;
let y1 = parseFloat($circle.getAttribute('cy')) - r;
let x2 = x1 + r + r;
let y2 = y1 + r + r;
let gX = x1 - ((x1 - grid.x) % grid.w);
let gY = y1 - ((y1 - grid.y) % grid.h);
var result = [];
while (gX <= x2) {
let y = gY;
let row = grid[gX];
while (row && y <= y2) {
if (row[y]) {
result.push(row[y].$e);
}
y += grid.h;
}
gX += grid.w;
}
return result;
}
/**
* @param {SVGElement[]} rects
* @return {Grid}
*/
function loadGrid (rects) {
const grid = {
x: Infinity,
y: Infinity,
w: Infinity,
h: Infinity
};
rects.forEach($rect => {
let x = parseFloat($rect.getAttribute('x'));
let y = parseFloat($rect.getAttribute('y'));
let w = parseFloat($rect.getAttribute('width')) + parseFloat($rect.getAttribute('stroke-width'));
let h = parseFloat($rect.getAttribute('height'));
grid[x] = grid[x] || {};
grid[x][y] = grid[x][y] || {
x1: x,
y1: y,
x2: x + w,
y2: y + h,
$e: $rect
};
if (grid.w === Infinity) {
grid.w = w;
}
else if (grid.w !== w) {
console.error($rect, 'has different width');
}
if (grid.h === Infinity) {
grid.h = h;
}
else if (grid.h !== h) {
console.error($rect, 'has different height');
}
if (x < grid.x) {
grid.x = x;
}
if (y < grid.y) {
grid.y = y;
}
});
return grid;
}
/*
* Following code is just for the example.
*/
// Get array of `RECT` elements
const $map = document.getElementById('map');
const grid = loadGrid(Array.from($map.querySelectorAll('rect')));
// Get array of `CIRCLE` elements
const $edit = document.getElementById('edit');
const circles = Array.from($edit.querySelectorAll('circle'));
// Change opacity of `RECT` elements that are
// intersecting with `CIRCLE` elements.
circles.forEach($circle => {
findIntersectingRects(grid, $circle).forEach($rect => $rect.setAttribute('style', 'fill-opacity: 0.3'))
});
Test it at https://jsfiddle.net/f2xLq3ka/.
More optimizations possible
Instead of using regular Object
for a grid
, one could use Array
by calculating x and y somewhat like: arrayGrid[rect.x / grid.w][rect.y / grid.h]
.
Example code above does not make sure that values are rounded, so Math.floor
and Math.ceil
should be used on calculated values.
If you don't know if map
elements will always be of the same size, you could check that at initialization and then prepare findIntersectingRects
function optimized for given situation.
Tricks
There's also a trick to draw the grid on canvas, each rectangle with different color (based on rectangle's x
and y
), and then get the color of pixel at the circle's position/area ;). I doubt that would be faster, but it can be useful in a bit more complicated situations (multi-layered map, with irregular shapes, for example).