Найти большинство ненаселенных точек в системе координат - PullRequest
0 голосов
/ 04 февраля 2019

У меня есть система координат, которая в основном представляет экран.
И у меня есть произвольное количество позиций.Например:

population = [
    {x: 100.44, 200.54},
    {x: 123.45, 678.9},
    {x: 1300.23, 435.81},
    {x: 462.23, 468.37},
    {x: 956.58, 385.38},
];

Я ищу алгоритм, который находит наиболее населенный пункт.

Белые мини-кружки представляют население и красные точки X, которые кажутся мне безлюдными:

screenshot

Моя цель -Запустите анимацию, которая случайным образом перемещает все эти белые мини-круги в случайных направлениях, и как только круг покидает экран, он должен телепортироваться в самое незаселенное место, чтобы уменьшить количество больших пустых мест.

Я пытался добиться этого, вычисляя сумму расстояний от каждой целочисленной координаты до каждого круга, а затем выбирая координату с наибольшей суммой расстояний.Уже одно это, похоже, уже довольно сильно загружает процессор, но я заметил, что этот алгоритм заставляет круги телепортироваться к границам моей системы координат.Поэтому я также добавил суммы расстояний от каждой целочисленной координаты до каждой целочисленной координаты границы.В этот момент скрипт по существу зависает.Так что это определенно неправильный подход.

У меня заканчиваются идеи.Я думаю, мне не нужен идеальный алгоритм, а скорее алгоритм со здоровым балансом между точностью и производительностью.В конце я хочу иметь возможность запускать этот алгоритм несколько раз в секунду на холсте 1920x1080, используя около 80 таких мини-кругов.В идеале алгоритм должен иметь параметр для настройки точности и, следовательно, сколько процессорного времени он использует.

Это был мой подход, упомянутый выше.Я закомментировал строки, которые заставили скрипт просто зависнуть:

let circles = [
    {x: 60.44, y: 190.54},
    {x: 103.45, y: 18.9},
    {x: 390.23, y: 135.81},
    {x: 302.23, y: 28.37},
    {x: 56.58, y: 85.38},
]

function getDistance(p1, p2) {
    return Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2)
}
function drawCircle(ctx,x,y,r,c) {
    ctx.beginPath()
    ctx.arc(x, y, r, 0, 2 * Math.PI, false)
    ctx.fillStyle = c
    ctx.fill()
}


const canvas = document.getElementById('canvas')
const ctx = canvas.getContext("2d")

let highestDistanceSum = 0
let coordWithHighestDistanceSum
for (let x=0; x<canvas.width; x++) {
    for (let y=0; y<canvas.height; y++) {
        let canvasCoord = {x: x, y: y}
        let distanceSum = 0
        for (let circle of circles) {
            distanceSum += getDistance(canvasCoord, circle)
        }
        /*
        // Pretend as if every pixel on the border is a circle
        // Causes massive CPU usage
        for (let x2=0; x<canvas.width; x2++) {
            distanceSum += getDistance(canvasCoord, {x: x2, y: 0})
            distanceSum += getDistance(canvasCoord, {x: x2, y: canvas.height})
        }
        for (let y2=0; y<canvas.height; y2++) {
            distanceSum += getDistance(canvasCoord, {x: 0, y: y2})
            distanceSum += getDistance(canvasCoord, {x: canvas.width, y: y2})
        }
        */
            
        if (distanceSum > highestDistanceSum) {
            coordWithHighestDistanceSum = canvasCoord
            highestDistanceSum = distanceSum
        }
    }
}


for (let p of circles) {
    drawCircle(ctx, p.x, p.y, 3, 'black')
}

drawCircle(ctx, coordWithHighestDistanceSum.x, coordWithHighestDistanceSum.y, 5, 'red')
<canvas id="canvas" width="400" height="200" style="border:1px solid #d3d3d3;"></canvas>

Ответы [ 3 ]

0 голосов
/ 07 февраля 2019

(так как это белая точка на черном холсте, я назову белую точку звездой для легкого различения)

Прежде всего, ваше решение не соответствует вашим критериям.Вам не нужна точка с наибольшей суммой расстояния до всей звезды.Вам нужна точка с самым дальним расстоянием до ее ближайшей звезды .

Чтобы уточнить, скажем, например, такую ​​ситуацию, когда одна звезда находится в центре, а большое количество звезд несколькорасстояние от центра:

enter image description here

Метод "сумма наибольшего расстояния", вероятно, даст точку где-то в красном круге (который слишком близко илидаже перекрывают центральную звезду), а то, что вы хотите, больше похоже на что-то в зеленом круге:

enter image description here

Имея это в виду:

  1. Прежде всего мы должны разделить экран на квадраты разумного размера, мы построим карту расстояний из этих квадратов, чтобы найти ту, которая имеет наибольшее расстояние от любой звезды.

Часть «разумного размера» очень важна с точки зрения производительности.Вы использовали разрешение 1920x1080, которое, на мой взгляд, слишком детально.Чтобы получить визуально приятный результат, разрешение 48x30 или даже 32x20 будет более чем достаточно.

Чтобы действительно построить карту расстояний, мы можем просто использовать Поиск в ширину .Мы преобразуем положение всех звезд в координаты сетки и используем их в качестве начальных позиций BFS.

Результат будет примерно таким:

enter image description here

Здесь все еще есть одна большая проблема: красный квадрат находится у нижнего края!

Получите, что края и угловые квадраты имеют «обманное» преимущество перед центральными координатами, так как нет звездыв сторону (или даже 3 стороны, для угловых квадратов).Таким образом, очень вероятно, что квадрат угла и края будет иметь наибольшее расстояние до любой звезды.

Это не очень приятно для вашего художественного произведения?Таким образом, мы можем немного обмануть, отфильтровав результаты, которые находятся в пределах определенного отступа.К счастью, результаты BFS сортируются по умолчанию, поэтому мы можем просто перебирать результаты, пока не найдем тот, который помещается в нужную область.

Ниже приведен полный код с комментарием.Даже при отображении карты расстояний весь процесс занимает 20 мс, что должно быть достаточно для фрагмента webgl (который работает при макс. 30 к / с ~ 33 мс / кадр)

Это решение также позаботится о редких случаях, когда несколько звезд выходятсвязаны в том же кадре.В этом случае просто получите несколько разных координат из результата BFS.

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>

<body style="margin: 0; padding: 0;">
  <canvas id="canvas" style="display: block;"></canvas>
  <script>
    // higher GRID_WIDTH = better result, more calculation time
    // We will caculate gridHeight later based on window size
    const GRID_WIDTH = 48;
    const GRID_PADDING = 3;

    const heatMapColors = [
      '#ffffff',
      '#ffdddd',
      '#ffbbbb',
      '#ff9999',
      '#ff7777',
      '#ff5555',
      '#ff3333',
      '#ff0000'
    ]

    const init = () => {
      var circles = [];
      for (var i = 0; i < 90; i++) {
        circles.push({
          x: Math.random() * window.innerWidth,
          y: Math.random() * window.innerHeight
        });
      }

      const canvas = document.getElementById('canvas')
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;
      const ctx = canvas.getContext("2d");

      const cellSize = window.innerWidth / GRID_WIDTH;
      const gridHeight = Math.ceil(canvas.height / cellSize);

      update(ctx, circles, GRID_WIDTH, gridHeight, cellSize);
    }

    const update = (ctx, circles, gridWidth, gridHeight, cellSize) => {
      const start = new Date();

      // Perform a BFS from all stars to find distance of each rect from closest star
      // After BFS visitedCoords will be an array of all grid rect, with distance-from-star (weight) sorted in ascending order
      var bfsFrontier = getGridCoordOfStars(circles, cellSize).map(coord => ({ ...coord, weight: 0 }));
      var visitedCoords = [...bfsFrontier];

      while (bfsFrontier.length > 0) {
        const current = bfsFrontier.shift();
        const neighbors = getNeighbors(current, gridWidth, gridHeight);

        for (let neighbor of neighbors) {
          if (visitedCoords.findIndex(weightedCoord => coordsEqual(weightedCoord, neighbor)) === -1) {
            visitedCoords.push(neighbor);
            bfsFrontier.push(neighbor);
          }
        }
      }

      // Visualize heatmap
      for (let coord of visitedCoords) {
        drawRect(ctx, coord.x * cellSize, coord.y * cellSize, cellSize, cellSize, heatMapColors[Math.min(coord.weight, heatMapColors.length - 1)]);
      }

      const emptiestCoord = getLastCoordWithinPadding(visitedCoords, gridWidth, gridHeight, GRID_PADDING);
      const emptiestPosition = {
        x: (emptiestCoord.x + 0.5) * cellSize,
        y: (emptiestCoord.y + 0.5) * cellSize
      }

      drawCircle(ctx, emptiestPosition.x, emptiestPosition.y, 5, 'yellow');
      for (let p of circles) {
        drawCircle(ctx, p.x, p.y, 3, 'black')
      }

      console.log(`Processing time: ${new Date().getTime() - start.getTime()} ms`);
    }

    const drawCircle = (ctx, x, y, r, c) => {
      ctx.beginPath()
      ctx.arc(x, y, r, 0, 2 * Math.PI, false)
      ctx.fillStyle = c
      ctx.fill()
    }

    const drawRect = (ctx, x, y, width, height, c) => {
      ctx.beginPath();
      ctx.rect(x, y, width, height);
      ctx.fillStyle = c;
      ctx.fill();
    }

    // Convert star position to grid coordinate
    // Don't need to worry about duplication, BFS still work with duplicates
    const getGridCoordOfStars = (stars, cellSize) =>
      stars.map(star => ({
        x: Math.floor(star.x / cellSize),
        y: Math.floor(star.y / cellSize)
      }))

    const coordsEqual = (coord1, coord2) => coord1.x === coord2.x && coord1.y === coord2.y;

    const getNeighbors = (weightedCoord, gridWidth, gridHeight) => {
      var result = [];
      if (weightedCoord.x > 0) result.push({ x: weightedCoord.x - 1, y: weightedCoord.y, weight: weightedCoord.weight + 1 })
      if (weightedCoord.x < gridWidth - 1) result.push({ x: weightedCoord.x + 1, y: weightedCoord.y, weight: weightedCoord.weight + 1 })

      if (weightedCoord.y > 0) result.push({ x: weightedCoord.x, y: weightedCoord.y - 1, weight: weightedCoord.weight + 1 })
      if (weightedCoord.y < gridHeight - 1) result.push({ x: weightedCoord.x, y: weightedCoord.y + 1, weight: weightedCoord.weight + 1 })

      return result;
    }

    // loop through a BFS result from bottom to top and return first occurence inside padding
    const getLastCoordWithinPadding = (coords, gridWidth, gridHeight, padding) => {
      for (let i = coords.length - 1; i > 0; i--) {
        const coord = coords[i];
        if (
          coord.x >= padding
          && coord.x < gridWidth - padding - 1
          && coord.y >= padding
          && coord.y < gridHeight - padding - 1
        ) return coord;
      }

      // This does not happen with current logic, but I leave it here to catch future code changes
      return coords[coords.length - 1];
    }

    init();
  </script>
</body>

</html>

Редактировать :

Я только что прочитал ответ @ArneHugo и вижу добавление границы вместесо звездами в качестве стартовых позиций BFS также будет работать.Это немного медленнее, но дает более приятный результат.

Вот еще одна версия, которая реализует их идею:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>

<body style="margin: 0; padding: 0;">
  <canvas id="canvas" style="display: block;"></canvas>
  <script>
    const GRID_WIDTH = 48; // We will caculate gridHeight based on window size

    const heatMapColors = [
      '#ffffff',
      '#ffdddd',
      '#ffbbbb',
      '#ff9999',
      '#ff7777',
      '#ff5555',
      '#ff3333',
      '#ff0000'
    ]

    const init = () => {
      var circles = [];
      for (var i = 0; i < 90; i++) {
        circles.push({
          x: Math.random() * window.innerWidth,
          y: Math.random() * window.innerHeight
        });
      }

      const canvas = document.getElementById('canvas')
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;
      const ctx = canvas.getContext("2d");

      const cellSize = window.innerWidth / GRID_WIDTH;
      const gridHeight = Math.ceil(canvas.height / cellSize); // calculate gridHeight

      // cache border coords array since it's never changed
      const borderCoords = getBorderCoords(GRID_WIDTH, gridHeight);

      update(ctx, circles, GRID_WIDTH, gridHeight, cellSize, borderCoords);
    }

    const update = (ctx, circles, gridWidth, gridHeight, cellSize, borderCoords) => {
      const start = new Date();

      // Perform a BFS from all stars to find distance of each rect from closest star
      // After BFS visitedCoords will be an array of all grid rect, with distance-from-star (weight) sorted in ascending order

      var bfsFrontier = borderCoords.concat(
        getGridCoordOfStars(circles, cellSize).map(coord => ({ ...coord, weight: 0 }))
      );

      var visitedCoords = [...bfsFrontier];

      while (bfsFrontier.length > 0) {
        const current = bfsFrontier.shift();
        const neighbors = getNeighbors(current, gridWidth, gridHeight);

        for (let neighbor of neighbors) {
          if (visitedCoords.findIndex(weightedCoord => coordsEqual(weightedCoord, neighbor)) === -1) {
            visitedCoords.push(neighbor);
            bfsFrontier.push(neighbor);
          }
        }
      }

      // Visualize heatmap
      for (let coord of visitedCoords) {
        drawRect(ctx, coord.x * cellSize, coord.y * cellSize, cellSize, cellSize, heatMapColors[Math.min(coord.weight, heatMapColors.length - 1)]);
      }

      const emptiestCoord = visitedCoords[visitedCoords.length - 1];
      const emptiestPosition = {
        x: (emptiestCoord.x + 0.5) * cellSize,
        y: (emptiestCoord.y + 0.5) * cellSize
      }

      drawCircle(ctx, emptiestPosition.x, emptiestPosition.y, 5, 'yellow');
      for (let p of circles) {
        drawCircle(ctx, p.x, p.y, 3, 'black')
      }

      console.log(`Processing time: ${new Date().getTime() - start.getTime()} ms`);
    }

    const drawCircle = (ctx, x, y, r, c) => {
      ctx.beginPath()
      ctx.arc(x, y, r, 0, 2 * Math.PI, false)
      ctx.fillStyle = c
      ctx.fill()
    }

    const drawRect = (ctx, x, y, width, height, c) => {
      ctx.beginPath();
      ctx.rect(x, y, width, height);
      ctx.fillStyle = c;
      ctx.fill();
    }

    const getBorderCoords = (gridWidth, gridHeight) => {
      var borderCoords = [];
      for (var x = 0; x < gridWidth; x++) {
        for (var y = 0; y < gridHeight; y++) {
          if (x === 0 || y === 0 || x === gridWidth - 1 || y === gridHeight - 1) borderCoords.push({ x, y, weight: 0 })
        }
      }

      return borderCoords;
    }

    // Convert star position to grid coordinate and filter out duplicates
    const getGridCoordOfStars = (stars, cellSize) => stars.map(star => ({
      x: Math.floor(star.x / cellSize),
      y: Math.floor(star.y / cellSize)
    }))

    const uniqueCoord = (arr) => arr.filter((candidate, index) => arr.findIndex(item => coordsEqual(item, candidate)) === index);

    const coordsEqual = (coord1, coord2) => coord1.x === coord2.x && coord1.y === coord2.y;

    const getNeighbors = (weightedCoord, gridWidth, gridHeight) => {
      var result = [];
      if (weightedCoord.x > 0) result.push({ x: weightedCoord.x - 1, y: weightedCoord.y, weight: weightedCoord.weight + 1 })
      if (weightedCoord.x < gridWidth - 1) result.push({ x: weightedCoord.x + 1, y: weightedCoord.y, weight: weightedCoord.weight + 1 })

      if (weightedCoord.y > 0) result.push({ x: weightedCoord.x, y: weightedCoord.y - 1, weight: weightedCoord.weight + 1 })
      if (weightedCoord.y < gridHeight - 1) result.push({ x: weightedCoord.x, y: weightedCoord.y + 1, weight: weightedCoord.weight + 1 })

      return result;
    }

    init();
  </script>
</body>

</html>
0 голосов
/ 10 февраля 2019

Вы можете представить себе холст как матрицу, имеющую столбцы и строки 1080 × 1920, инициализированные единицами, представляющими пустую область, и нулями в столбце x th , строка y th , представляющаяуказать на эту координату.Теперь вам нужно найти максимально пустой прямоугольник в двоичной матрице.

This Dr.Статья Добба содержит один из самых быстрых алгоритмов решения задачи.Вы можете найти реализацию JavaScript в Интернете или реализовать ее самостоятельно.

Вы также можете рассмотреть вопрос о поиске максимально пустого квадрата.

var canvas = document.querySelector("#canvas-1");
var rctx = canvas.getContext("2d");
var ncols = canvas.width;
var nrows = canvas.height;
var npoints = +canvas.dataset.points;
var matrix = Array(nrows).fill(0).map(function() {
  return Array(ncols).fill(1);
});
var i, x, y, t0, t1, maxrect, maxsquare;

/*
 * For consistency with algorithms, the matrix is initialized with 1s 
 * representing the blank area and the points are represented with 0s
 */
for (i = 0; i < npoints; i++) {
  x = Math.floor(Math.random() * ncols);
  y = Math.floor(Math.random() * nrows);
  matrix[y][x] = 0;
}

t0 = new Date();
maxrect = maximalRectangle(matrix);
t1 = new Date();
console.log("Rectangle found in %dms", t1 - t0);

t0 = new Date();
maxsquare = maximalSquare(matrix);
t1 = new Date();
console.log("Square found in %dms", t1 - t0);

/*
 * Render the results
 */
rctx.fillStyle = "rgba(255,0,0,.5)";
rctx.fillRect(maxrect.left, maxrect.top, maxrect.right - maxrect.left + 1, maxrect.bottom - maxrect.top + 1);

rctx.fillStyle = "rgba(0,0,255,.5)";
rctx.fillRect(maxsquare.left, maxsquare.top, maxsquare.right - maxsquare.left + 1, maxsquare.bottom - maxsquare.top + 1);

rctx.fillStyle = "rgb(255,255,255)";
for (y = 0; y < nrows; y++) {
  for (x = 0; x < ncols; x++) {
    if (matrix[y][x] === 0) {
      rctx.fillRect(x, y, 1, 1);
    }
  }
}

/*
 * implementation of this answer:
 * https://stackoverflow.com/a/20039017/87015
 */
function maximalRectangle(matrix) {
  var best_area = 0;
  var best_rect = {};
  var M = matrix[0].length;
  var N = matrix.length;
  var c = Array(M + 1).fill(0);
  var s = [];
  var m, n, open_width, area, prev;
  for (n = 0; n < N; n++) {
    for (m = 0; m < M; m++) {
      if (matrix[n][m] === 0) {
        c[m] = 0;
      } else {
        c[m]++;
      }
    }
    open_width = 0;
    for (m = 0; m < M + 1; m++) {
      if (c[m] > open_width) {
        s.push({
          m: m,
          w: open_width
        });
        open_width = c[m];
      } else if (c[m] < open_width) {
        do {
          prev = s.pop();
          area = open_width * (m - prev.m);
          if (area > best_area) {
            best_area = area;
            best_rect.left = prev.m;
            best_rect.right = m - 1;
            best_rect.top = n - open_width + 1;
            best_rect.bottom = n;
          }
          open_width = prev.w;
        } while (c[m] < open_width);
        open_width = c[m];
        if (open_width != 0) {
          s.push(prev);
        }
      }
    }
  }
  return {
    area: best_area,
    left: best_rect.left,
    top: best_rect.top,
    right: best_rect.right,
    bottom: best_rect.bottom
  };
}

/*
 * (possibly buggy) implementation of this answer:
 * https://stackoverflow.com/a/1726667/87015
 */
function maximalSquare(matrix) {
  var best_length = 0;
  var best_square = {};
  var M = matrix[0].length;
  var N = matrix.length;
  var c = Array(M + 1).fill(0);
  var n, m, temp, prev = 0;
  for (n = 1; n <= N; n++) {
    for (m = 1; m <= M; m++) {
      temp = c[m];
      if (matrix[n - 1][m - 1] === 1) {
        c[m] = Math.min(Math.min(c[m - 1], prev), c[m]) + 1;
        if (best_length < c[m]) {
          best_length = c[m];
          best_square.left = m - best_length;
          best_square.right = m - 1;
          best_square.top = n - best_length;
          best_square.bottom = n - 1;
        }
      } else {
        c[m] = 0;
      }
      prev = temp;
    }
  }
  return {
    area: best_length * best_length,
    left: best_square.left,
    top: best_square.top,
    right: best_square.right,
    bottom: best_square.bottom
  };
}
<canvas id="canvas-1" width="1920" height="1080" data-points="80" style="background-color: #000;"></canvas>
0 голосов
/ 04 февраля 2019

Исходное решение (с правками)

Вот пример, хотя я думаю, что было бы более интересно, если у вас больше кругов.

  • Я считаю только расстояние донесколько ближайших окружностей
  • Согласно вашему запросу, я заставил границы холста отталкивать новые круги, поэтому у вас гораздо меньше шансов получить новые круги на границе.Это делается путем подсчета расстояний до краев ([canvas.width - coord.x, coord.x - 0, canvas.height - coord.y, coord.y -0]), а также расстояний до каждого из существующих кругов.

(я также перешел на более функциональный стиль, потому чтоЯ обнаружил, что это проще, хотя в этом нет необходимости.)

const numberOfCirclesToGetDistanceTo = 2

let circles = [
    {x: 60.44, y: 190.54},
    {x: 103.45, y: 18.9},
    {x: 390.23, y: 135.81},
    {x: 302.23, y: 28.37},
    {x: 56.58, y: 85.38},
]

function getDistance(p1, p2) {
    return Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2)
}
function drawCircle(ctx,x,y,r,c) {
    ctx.beginPath()
    ctx.arc(x, y, r, 0, 2 * Math.PI, false)
    ctx.fillStyle = c
    ctx.fill()
}


const canvas = document.getElementById('canvas')
const ctx = canvas.getContext("2d")

function getCoordWithHighestDistanceSum() {
    const xList = Array(canvas.width).fill().map((_, index) => index)
    const yList = Array(canvas.height).fill().map((_, index) => index)
    const coords = xList.flatMap(x => yList.reduce((coords, y) => coords.concat({ x, y }), []))

    const ascending = (a, b) => a - b
    const sumTotal = (sum, next) => sum + next

    const coordsWithDistance = coords.map(coord => {
        const distances = [
            ...circles.map(circle => getDistance(coord, circle)),
            ...[canvas.width - coord.x, coord.x - 0, canvas.height - coord.y, coord.y -0],
        ]

        return {
            coord,
            dist: distances
                .sort(ascending)
                .slice(0, numberOfCirclesToGetDistanceTo)
                .reduce(sumTotal, 0)
        }
    })

    return coordsWithDistance
        .sort((a, b) => b.dist - a.dist)
        [0].coord
}

const coordWithHighestDistanceSum = getCoordWithHighestDistanceSum()

for (let p of circles) {
    drawCircle(ctx, p.x, p.y, 3, 'black')
}

drawCircle(ctx, coordWithHighestDistanceSum.x, coordWithHighestDistanceSum.y, 5, 'red')
<canvas id="canvas" width="400" height="200" style="border:1px solid #d3d3d3;"></canvas>

Интерактивная версия

Вот более интерактивная версия, чтобы вы могли проверить, как она работает.Как вы увидите, в большинстве случаев наименее густонаселенная область располагается вдоль края, когда другие круги генерируются случайным образом.Кроме того, чем больше кругов вы учитываете при проверке расстояния, тем больше вы склоняетесь к краям и углам при выборе наименее густонаселенной области.

Логика поиска наименее населенной области такая же, какв оригинальном решении.

let circles = []
let coordWithHighestDistanceSum = void 0

const canvas = document.getElementById('canvas')
const ctx = canvas.getContext("2d")

const xList = Array(canvas.width).fill().map((_, index) => index)
const yList = Array(canvas.height).fill().map((_, index) => index)
const coords = xList.flatMap(x => yList.reduce((coords, y) => coords.concat({ x, y }), []))

function render() {
    ctx.clearRect(0, 0, canvas.width, canvas.height)

    function drawCircle(ctx,x,y,r,c) {
        ctx.beginPath()
        ctx.arc(x, y, r, 0, 2 * Math.PI, false)
        ctx.fillStyle = c
        ctx.fill()
    }

    circles.forEach(circle => drawCircle(ctx, circle.x, circle.y, 3, 'black'))

    if (coordWithHighestDistanceSum) {
        drawCircle(ctx, coordWithHighestDistanceSum.x, coordWithHighestDistanceSum.y, 5, 'red')
    }
}

function generateCircles() {
    const nofCircles = Number(document.getElementById('nofCircles').value)

    const randomCoord = () => coords[Math.floor(Math.random() * coords.length)]

    circles = Array(nofCircles).fill().map(randomCoord)
    findLeastPopulatedCoordinate()

    render()
}

function findLeastPopulatedCoordinate() {
    const nofCirclesToSumDistanceTo = Number(document.getElementById('nofCirclesToSumDistanceTo').value)

    const ascending = (a, b) => a - b
    const sumTotal = (sum, next) => sum + next

    function getDistance(p1, p2) {
        return Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2)
    }

    coordWithHighestDistanceSum = coords
        .map(coord => ({
            coord,
            dist: []
                .concat(circles.map(circle => getDistance(coord, circle)))
                .concat([canvas.width - coord.x, coord.x - 0, canvas.height - coord.y, coord.y -0])
                .sort(ascending)
                .slice(0, nofCirclesToSumDistanceTo)
                .reduce(sumTotal, 0)
        }))
        .sort((a, b) => b.dist - a.dist)
        [0].coord

    render()
}

generateCircles()
findLeastPopulatedCoordinate()
<div>
    <label>Number of circles</label>
    <input type="number" id="nofCircles" value="30" />
</div>
<div>
    <label>Number of circles to sum distance to</label>
    <input type="number" id="nofCirclesToSumDistanceTo" value="1" />
</div>
<button onclick="generateCircles()">Generate circles</button>
<button onclick="findLeastPopulatedCoordinate()">Find least populated coordinate</button>
<canvas id="canvas" width="400" height="200" style="border:1px solid #d3d3d3;"></canvas>
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...