Визуализация данных Python / Flask и интерактивные карты

Вы когда-нибудь хотели создать интерактивную карту визуализации данных? В своем последнем побочном проекте я создал довольно интересную визуализацию того, как вирус может распространяться по Соединенным Штатам. Если вы хотите проверить готовый сайт, вы можете нажать здесь:

Http://ethanszombies.herokuapp.com

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

1 - Что мы хотим создать? - - - - - - - - - - - - - - - - - -

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

1. Вирус зомби быстрее распространяется в густонаселенных районах, таких как внутренние города.

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

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

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

2. - О чем нам нужно подумать? - - - - - - - - - - - - - - - - - -

Допустим, у нас есть карта США, и каждая область на карте окрашена в белый цвет. Мы могли бы представить зараженного человека, изменив цвет этого пикселя или div на другой цвет.

а. Временные интервалы

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

б. Решение в каждом интервале

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

Мы могли бы использовать данные о каждой области для принятия этих решений.

Инфекция

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

Скорость

  1. Высота может быть фактором скорости. В каждом интервале зараженный блок перемещается на определенное расстояние, но если он выше, это расстояние будет меньше.

Направление

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

3. - Как нам нанести нужные данные на карту? - - - - - - - - - - - - - - - - - -

Первым делом нужно закодировать карту с нужными нам данными. Мне кажется, что один из способов сделать это - создать карту определенного измерения. Для моих целей я думаю, что размер 1150px X 768px должен подойти.

Затем нам нужно определить, сколько места на карте займет один зомби. После некоторого тестирования я обнаружил, что достаточно области приблизительно 5px X 5px. Более того, и это казалось неуклюжим. Тем более, что нужно было обрабатывать столько данных, что это постоянно потребляло ресурсы.

После того, как я определился с подходящим размером карты, мой следующий шаг - создать наложение поверх карты. Этот оверлей будет содержать кнопки нужного мне размера (5 пикселей на 5 пикселей каждая).

Для плотности населения, для начала, я собираюсь создать максимальную плотность населения 10 и минимальную 0. Эти числа являются произвольными и предназначены только для представления изменений в плотности, но не обязательно соответствуют фактическим. численность населения.

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

Я считаю, что вы можете нажимать каждую кнопку, и при каждом нажатии количество (плотность) увеличится на 1. Если у нас есть 40 000 кнопок, нажатие каждой из них кажется неразумным, поэтому вместо этого я могу привязать это событие к наведению указателя мыши. чтобы ускорить ввод данных. Также может быть хорошей идеей изменить цвет при наведении курсора мыши, чтобы было легче отслеживать, какие блоки и какие значения содержат. Теперь, когда мы подумали о псевдокоде, самое время начать настраивать наш файл и писать реальный код, поскольку у нас есть более четкое представление о том, что мы хотим создать, и как нам нужно создавать Это. Во многих проектах я трачу много времени на рисование, прежде чем писать какой-либо код, что в конечном итоге делает код более организованным.

Настройка файла - - - - - - - - - - - - - - - - - -

Файловая структура, которая у нас в конечном итоге будет, вероятно, будет выглядеть примерно так:

Для начала, мы можем создать большую часть этого с помощью командной строки. Некоторые файлы, такие как density.txt и densitydata.json, будут созданы в первой части проекта.

в командной строке сделайте следующее:

mkdir <project folder name>
cd <project folder name>

Теперь мы создадим все исходные файлы в командной строке:

touch app.py .gitignore README.md requirements.txt runtime.txt
mkdir templates
mkdir static
cd templates
touch base.html
cd ..
cd static
touch main.css

Затем, если мы хотим создать репозиторий git и развернуть его на heroku, это хорошее место для этого.

git init

Затем, если мы настроим учетную запись heroku, мы можем создать новый проект heroku, запустив:

heroku create <project name>

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

pyvenv-3.6 env (substitute your python version here)

Затем мы активируем эту среду следующим образом:

source env/bin/activate

В командной строке вы увидите (env) перед папкой проекта, чтобы вы знали, что вы используете виртуальную среду. Если вы когда-нибудь получите сообщение об ошибке, что python не может найти модуль, который вы установили, проверьте, используете ли вы еще свою виртуальную среду. Если нет, то снова запустите последнюю команду.

Затем, поскольку flask является основной зависимостью, которая нам понадобится, мы можем ее установить.

pip install flask

Если мы решим запустить это на heroku, нам нужно будет сообщить ему, какую версию python мы запускаем, и мы можем добавить это в наш файл runtime.txt:

echo "python-3.6.4" >> runtime.tx (or whatever your version is)

Мы также захотим установить Gunicorn, если мы используем heroku:

pip install gunicorn

А затем добавьте следующий текст в наш Procfile:

echo "web: gunicorn app:app" >> Procfile

Наконец, мы запустим pip freeze, чтобы обновить наш файл requirements.txt:

pip freeze > requirements.txt

Отлично, теперь, когда это все в порядке, большинство файлов в нашем дереве файлов должно быть там, и мы готовы к работе.

Возвращаясь к нашей блок-схеме ранее, вот область, над которой мы будем работать в первую очередь:

CSS - - - - - - - - - - - - - - - - - -

Исходя из рабочего процесса, о котором мы говорили ранее, первое, что нам понадобится, это изображение карты, на которое мы можем наложить сетку кнопок поверх. Для этого мы просто сохраним карту, которая нам подходит, и будем использовать ее в качестве фонового изображения для div или элемента. Мы также установим для него соответствующие размеры. Это войдет в файл main.css, который мы создали ранее:

#body {
  background-image: url('/static/states.png');
  background-size: contain;
  background-repeat: no-repeat;
  background-size: 1150px 768px;
  margin: 0px;
  padding: 0px;
  display: flex;
  flex-flow: row wrap;
  justify-content: flex-start;
  align-items: flex-start;
  width:1150px;
  height:768px;
  border:black solid 2px;
}

Мы также собираемся создать здесь два класса кнопок. Когда пользователь щелкает мышью или активный блок зомби заражает другой блок, мы меняем блок с одного класса CSS на другой. Вот созданные мной классы. Класс .btn - это класс кнопок по умолчанию, а .active - активированный класс:

.btn {
  width: 4.5px;
  height:4.8px;
  background-color: transparent;
  margin: 0px;
  padding: 0px;
  font-size: 1px;
}
.active {
  width: 4.5px;
  height:4.8px;
  /* Permalink - use to edit and share this gradient: https://colorzilla.com/gradient-editor/#136325+0,305937+100&1+15,0+88 */
background: -moz-radial-gradient(center, ellipse cover, rgba(19,99,37,1) 0%, rgba(23,97,40,1) 15%, rgba(44,90,53,0) 88%, rgba(48,89,55,0) 100%); /* FF3.6-15 */
background: -webkit-radial-gradient(center, ellipse cover, rgba(19,99,37,1) 0%,rgba(23,97,40,1) 15%,rgba(44,90,53,0) 88%,rgba(48,89,55,0) 100%); /* Chrome10-25,Safari5.1-6 */
background: radial-gradient(ellipse at center, rgba(19,99,37,1) 0%,rgba(23,97,40,1) 15%,rgba(44,90,53,0) 88%,rgba(48,89,55,0) 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#136325', endColorstr='#00305937',GradientType=1 ); /* IE6-9 fallback on horizontal gradient */
  margin: 0px;
  padding: 0px;
  font-size: 1px;
  border-radius: 25%;
  
}
.btn:hover {
  cursor: pointer;
}

HTML - - - - - - - - - - - - - - - - - -

Затем мы можем продолжить и создать фактический HTML, указанный в нашем CSS. Это войдет в файл base.html, который мы создали ранее:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title></title>
  </head>
  <link type="text/css" rel="stylesheet" href="{{ url_for('static', filename='main.css') }}" />
<body>
  <div id="body">
  </div>
<button id="submitform" type="submit">SUBMIT</button>
  </body>
  <div id="formGoesHere"></div>
</html>

JavaScript - - - - - - - - - - - - - - - - - -

Затем мы добавим немного JavaScript. Мы будем использовать петлю для создания кнопок нужной длины. Если вы использовали те же размеры, что и я, то 40 000 кнопок кажутся правильными. Обычно у вас может быть отдельный файл JavaScript. Хотя это, вероятно, лучшая практика, я считаю, что если я пишу менее 300 строк кода, то обычно проще включить JavaScript в тег скрипта. Если похоже, что у нас будет больше, мы всегда сможем изменить это позже.

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
  <script type='text/javascript'>
        $(document).ready(function(){
$(document).ready(function(){
  for (var i = 0; i < 40000; i++) {
            var inputBtn = `
            <div type="button" class="btn">0</div>
            `;
            $('#body').append(inputBtn);
          }
});
  </script>

Затем я создам событие наведения курсора для всех только что созданных тегов:

var color1 = '#fff600';
          var color2 = '#ffc32b';
          var color3 = '#e8a600';
          var color4 = '#e88300';
          var color5 = '#ce7502';
          var color6 = '#ce3b01';
          var color7 = '#ad3201';
          var color8 = '#7a2402';
          var color9 = '#561a02';
          var color10 = '#1e0800';
        
        $('body').on('mouseover', '.btn', function () {
          var currentVal = $(this).html();
          console.log(currentVal);
          var newVal = parseInt(currentVal) + 1;
          if (newVal > 10){
            newVal = 10 }
          $(this).html(newVal);
          if ($(this).html() == 1){
            $(this).css('background-color', color1);
          }
          else if ($(this).html() == 2){
            $(this).css('background-color', color2);
          }
          else if ($(this).html() == 3){
            $(this).css('background-color', color3);
          }
          else if ($(this).html() == 4){
            $(this).css('background-color', color4);
          }
          else if ($(this).html() == 5){
            $(this).css('background-color', color5);
          }
          else if ($(this).html() == 6){
            $(this).css('background-color', color6);
          }
          else if ($(this).html() == 7){
            $(this).css('background-color', color7);
          }
          else if ($(this).html() == 8){
            $(this).css('background-color', color8);
          }
          else if ($(this).html() == 9){
            $(this).css('background-color', color9);
          }
          else if ($(this).html() == 10){
            $(this).css('background-color', color10);
          }
        });

В этом предыдущем примере каждый блок или элемент массива будет иметь значение от 0 до 10. Если мы хотим вернуться и отредактировать эту форму и данные, чтобы мы могли хранить данные о населении и высоте в каждом блоке, мы можем захотеть чтобы увеличить значение по-другому или вместо этого сохранить значение истина / ложь. Мы всегда можем изменить ввод здесь, но записать его в новый файл на сервере, в зависимости от того, какие данные мы хотим сохранить. Мы поговорим об этой идее позже, но просто кое-что, чтобы рассмотреть.

И, наконец, я создам форму, которая позволит мне отправлять эти данные из внешнего интерфейса моего фляги на серверную часть Python. Затем я могу манипулировать данными и создавать хеш.

var inputArr = ''
        $('#submitform').click(function(){
          $(".btn").each(function(){
            var input = $(this).html();
            inputArr += ("," + input);
          });
          var form2 = `
          <form action="form1" id="myForm" method="post">
            <input value="${inputArr}" name="t1"/>
          </form>
          `;
          console.log(inputArr);
          $('#formGoesHere').append(form2);
          $('#myForm').submit();
        });
});

Несмотря на то, что мы создали форму и все остальное в интерфейсе, на самом деле она еще ни с чем не связана. Чтобы связать эти данные с python, нам нужно будет изменить наш файл app.py и использовать запросы Flask для получения данных из нашего файла base.html. На этом этапе мы находимся на нашей блок-схеме:

Python - - - - - - - - - - - - - - - - - -

В app.py давайте продолжим и импортируем вещи, которые нам понадобятся для работы:

from flask import Flask, request, render_template

Затем мы определим приложение:

app = Flask(__name__)

Наконец, мы определим маршрут, который будет связан с нашим файлом base.html, который будет просто основным маршрутом («/»):

@app.route('/', methods=['GET', 'POST'])
def searchTopic():
    return render_template('base.html')

Этот маршрут будет отображать страницу, но нам также необходимо создать маршрут для формы. Если вы вернетесь к написанному нами html, действие для этой формы было «form1», и это будет маршрут, который мы будем использовать для данных нашей формы.

@app.route('/form1', methods=['GET', 'POST'])
def getFormData():
    data = request.form['t1']
    f = open('file.txt', 'w')
    f.write(data)
    f.close()
    return render_template('base.html')

Здесь наш маршрут в app.py, «/ form1» связан с действием формы в нашем html. И имя ввода в этой форме «t1» связано с request.form [«t1»] в app.py.

Наконец, мы добавим следующую строку в конец нашего файла app.py:

if __name__ == '__main__':
    app.run()

Теперь, если мы отправим форму, эти данные будут сохранены как переменная data. Затем мы откроем файл с именем file.txt и запишем нашу переменную data в этот файл. Теперь мы можем перебирать данные в файле и преобразовывать их в JSON вместе с другими данными, которые мы можем собрать позже.

Вернуться к HTML / JavaScript - - - - - - - - - - - - - - - - - - -

Если вы настроите свой таким образом, вы увидите, что на указателе мыши мы меняем значение html в каждом div, а также цвет. Цвет позволяет легко увидеть, что было изменено, а что нет. Когда я отправляю форму, я могу отправить значение div в виде массива на python. Процесс ввода может выглядеть так:

В журнале консоли записывается значение каждого div, и вы можете видеть карту области, как мы ее создаем. Когда у нас есть все необходимые данные карты, мы можем прокомментировать этот код из нашего файла JavaScript, поскольку он нам не понадобится для фактического запуска моделирования. Но это удобно, если мы хотим создать новую карту любого нового типа данных. В моем случае я отправил одну карту, которая содержала только данные о населении… другую карту, которая содержала данные о высоте и так далее… а затем объединила каждый список в хэш на Python.

Данные, которые мы вводим через интерфейс, интерпретируются как массив или значения, разделенные запятыми. Данные карты будут в основном массивом из 40000 элементов в диапазоне от 0 до 10, например:

[0,0,0,0,0,0,1,1,2,3,4,0,0,0,0,0,0,0,5,6,6,7,7,7,4,1,0……..]

4 - Сборка необработанных данных в JSON - - - - - - - - - - - - - - - - - - -

В python каждый список будет иметь одинаковое количество индексов, то есть в списке 1 массив с индексом 552 будет соответствовать 552-му div в нашей сетке. Таким образом, я буду перебирать один список, но одновременно проверять другой список, без необходимости перебирать оба списка. В приведенном ниже примере у меня есть один список, содержащий данные о населении, и второй список, который содержит данные о том, является ли текущий блок водой или нет.

import json
with open('population.txt') as f:
    file1 = [line.rstrip() for line in f]
with open('file.txt') as f:
    file2 = [line.rstrip() for line in f]
array = []
for idx, i in enumerate(file1):
    arr = i.split(",")
    waterArr = file2[idx].split(",")
    x = 0
    for index, a in enumerate(arr):
        myObj = {
            "population": 0,
            "water": 0,
        }
        if int(a) > 9:
            myObj['population'] = 8
        elif int(a) is 9:
            myObj['population'] = 7
        elif int(a) is 8:
            myObj['population'] = 6
        elif int(a) is 7:
            myObj['population'] = 5
        elif int(a) is 6:
            myObj['population'] = 4
        elif int(a) is 5:
            myObj['population'] = 3
        elif int(a) is 4:
            myObj['population'] = 2
        elif int(a) is 3:
            myObj['population'] = 1
        elif int(a) < 3:
            myObj['population'] = 0
        if waterArr[index] == '1':
            myObj['water'] = 1
        array.append(myObj)
with open('density2.json', 'w') as outfile:
    json.dump(array, outfile)

Затем я конвертирую данные в json и сохраняю их как файл json. Обновленный список данных json выглядит так:

...{"water": 1, "population": 0}, {"water": 0, "population": 0}, {"water": 1, "population": 0}, {"water": 0, "population": 0}, {"water": 0, "population": 0}, {"water": 0, "population": 0}, {"water": 0, "population": 0}, {"water": 0, "population": 0}, {"water": 0, "population": 1}, {"water": 0, "population": 1}, {"water": 0, "population": 0}, {"water": 0, "population": 0}, {"water": 0, "population": 1}, {"water": 0, "population": 1}, {"water": 0, "population": 5}, {"water": 0, "population": 0}, {"water": 0, "population": 0}, {"water": 0, "population": 0},...

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

КОНЕЦ ЧАСТИ 1 - - - - - - - - - - - - - - - - - -

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

Прежде чем двигаться дальше, следует учесть одну вещь: поскольку это довольно небольшой проект, я не собираюсь создавать дополнительные файлы для различных функций. Я просто закомментирую ненужные разделы. Поскольку мы переходим от первой части ко второй части этого проекта, мы можем закомментировать весь код, связанный со сбором, организацией или сбором данных. Сюда входят предыдущие строки, в которых мы превращаем текстовые файлы в хэш, а также код JavaScript, в котором мы создаем и отправляем данные формы. Однако, если вам проще отслеживать отдельные файлы, вы можете это сделать. Просто имейте в виду, что ваша файловая структура будет отличаться от моей.

ЧАСТЬ 2 - - - - - - - - - - - - - - - - - -

Теперь, когда у нас есть эти данные, мы хотим отправить их во внешний интерфейс как json. Мы можем сделать это с помощью форм для колб.

В app.py на основном маршруте мы просто включим функцию, которая получает эти данные из Python. Как я упоминал ранее, мне кажется, что проще просто закомментировать предыдущий код, который у нас был в этом файле, который собирает наш текстовый файл в JSON, и включить эти строки под закомментированным разделом. Но если вы предпочитаете использовать новый файл, это тоже нормально:

------- IN TEST.PY -------------------
with open('density2.json') as f:
    popDensity = json.load(f)
def getData():
    return popDensity

Когда вызывается наш основной маршрут (‘/’), мы вызываем функцию getData из модуля test.py и возвращаем ее как результат, который будет использоваться в нашем файле JavaScript. Мы также изменим маршрут app.py, который мы создали ранее, чтобы принимать и отправлять эти данные во внешний интерфейс. Мы также включим вверху оператор импорта, чтобы импортировать модуль test.py, содержащий функцию, которую мы хотим использовать:

from test import getData

И мы добавим эту функцию к основному маршруту для получения данных:

@app.route('/', methods=['GET', 'POST'])
def searchTopic():
    output = getData()
    return render_template('base.html', result=output)

5 - Использование данных JSON во внешнем интерфейсе для воздействия на симуляцию. - - - - - - - - - - - - - - - - - -

В нашем файле JavaScript мы можем получить данные, используя flask:

$(document).ready(function(){
          var data = {{ result|tojson }};

Наши переменные данные теперь содержат результат, который мы передали в нашей функции маршрута «/».

Моя мысль здесь состоит в том, чтобы закодировать каждый div с данными JSON в виде атрибутов элемента. Я начну с атрибутов «вода», «население» и «направление». Атрибуты Water и Population передаются из python, а направление Direction будет полностью контролироваться из JavaScript. Мы начнем с создания этих атрибутов, чтобы мы могли использовать их в наших элементах:

var population = document.createAttribute("population");
          var direction = document.createAttribute("direction");
          var isWater = document.createAttribute("isWater");

Наши элементы могут выглядеть примерно так:

<div id="2554" population="5" direction="northeast" isWater="notWater" type="button" class="btn"></div>

Теперь мы не хотим жестко кодировать каждый div, поэтому сделаем это в цикле.

var directionArray = ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw'];
          for (var i = 0; i < data.length; i++) {
                population.value = data[i].population;  
                direction1 = directionArray[Math.floor(Math.random() * directionArray.length)];
                var inputBtn = `
                <div id="${i}" population="${data[i].population}" direction="${direction1}" isWater="${data[i].water}" type="button" class="btn"></div>
                `;
                $('#body').append(inputBtn);
          }

Опять же, надеюсь, мы закомментировали JavaScript, который мы использовали для создания карты данных, но этот код будет выглядеть очень похоже на то, что мы делали ранее. Изначально, когда мы были в первой части этого проекта и использовали JavaScript для создания наших HTML-блоков, каждый div был пустым. Теперь в нашем цикле for мы также назначаем атрибуты каждому элементу перед добавлением его в #body.

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

Реализация интервалов - - - - - - - - - - - - - - - - - - - -

Если конкретный блок заражен, как показано выше, и это блок div # 456, то есть 8 возможных направлений, в которых он может двигаться из текущего положения: n, ne, e, se, s, sw, w, nw. Если мы думаем об этих div как о массиве, то направление на восток будет текущей позицией +1. Запад будет текущей позицией -1. север и юг зависят от количества элементов в каждой строке. В моем случае это -255 для севера, + 255 для юга и так далее. Если в любой заданный интервал текущий зараженный блок не находится в густонаселенной области, он переместится в новый блок. Теперь, если на этом этапе мы больше ничего не сделали, направление зомби уже установлено для каждого блока. В каждом интервале мы могли проверять направление данного блока, а затем перемещать его в новое соответствующее пространство. Однако давайте представим, что мы это сделали, и текущий блок имеет направление «ne». Вот как это будет выглядеть на каждом интервале:

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

Итак, теперь, когда у нас есть псевдокод, вот реальный код. Мы начнем с события onClick, которое будет первым событием, которое вызовет все остальные:

$('body').on('click', '.btn', function () {
            let color9 = colorArr[Math.floor(Math.random() * colorArr.length)];
            $(this).attr('isActive', 1);
            $(this).removeClass('btn');
            $(this).addClass('active');
            $('.btn').click(false);
      
          });

При щелчке мы удаляем класс btn, отключаем любые другие нажатия кнопок и добавляем класс active. Теперь, когда мы получим нашу функцию setInterval, она подберет этот блок. Далее я собираюсь создать массив из двух элементов, 1 и 0:

var changeArr = [0,1];

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

setInterval(function(){ 
            var buttons = $( ".active" ).toArray();

Внутри функции setInterval первое, что мы сделаем, это получим массив всех активных кнопок. Сначала будет только один.

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

var currentID = $(buttons[i]).attr('id');
                var id1 = parseInt(currentID)+1;
                var id2 = parseInt(currentID)+254;
                var id3 = parseInt(currentID)+255;
                var id4 = parseInt(currentID)+256;
                var id5 = currentID-1;
                var id6 = currentID-254;
                var id7 = currentID-255;
                var id8 = currentID-256;
                var adjacentbuttons = [$('#'+id1+''),$('#'+id5+''),$('#'+id7+''),$('#'+id8+''),$('#'+id6+''),$('#'+id3+''),$('#'+id4+''),$('#'+id2+'')];

Далее мы что-то сделаем, если плотность населения больше 10:

if($(buttons[i]).attr('population') >= 10){
                  $(adjacentbuttons[2]).attr('isActive', 1);
                  $(adjacentbuttons[2]).removeClass('btn');
                  $(adjacentbuttons[2]).addClass('active');
                  $(adjacentbuttons[4]).attr('isActive', 1);
                  $(adjacentbuttons[4]).removeClass('btn');
                  $(adjacentbuttons[4]).addClass('active');
                  $(adjacentbuttons[0]).attr('isActive', 1);
                  $(adjacentbuttons[0]).removeClass('btn');
                  $(adjacentbuttons[0]).addClass('active');
                  $(adjacentbuttons[6]).attr('isActive', 1);
                  $(adjacentbuttons[6]).removeClass('btn');
                  $(adjacentbuttons[6]).addClass('active');
                  $(adjacentbuttons[5]).attr('isActive', 1);
                  $(adjacentbuttons[5]).removeClass('btn');
                  $(adjacentbuttons[5]).addClass('active');
                  $(adjacentbuttons[1]).attr('isActive', 1);
                  $(adjacentbuttons[1]).removeClass('btn');
                  $(adjacentbuttons[1]).addClass('active');
                  $(adjacentbuttons[3]).attr('isActive', 1);
                  $(adjacentbuttons[3]).removeClass('btn');
                  $(adjacentbuttons[3]).addClass('active');
      
                }

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

else if($(buttons[i]).attr('population') == 3){
                  $(adjacentbuttons[7]).attr('isActive', 1);
                  $(adjacentbuttons[7]).removeClass('btn');
                  $(adjacentbuttons[7]).addClass('active');
                }

Если плотность всего 3, мы заражаем только один соседний блок.

Направление - - - - - - - - - - - - - -

Вот логика, которую я использую для каждого направления:

if($(buttons[i]).attr('direction') == 'n'){
                  let change = changeArr[Math.floor(Math.random() * changeArr.length)];
                  if (change == 0){
                    $(buttons[i]).attr('isActive', 0);
                    $(adjacentbuttons[2]).attr('isActive', 1);
                    $(adjacentbuttons[2]).attr('direction', 'n');
                    $(adjacentbuttons[2]).removeClass('btn');
                    $(adjacentbuttons[2]).addClass('active');
                    $(buttons[i]).removeClass('active');
                    $(buttons[i]).addClass('btn');
                  }
                  else if (change == 1 && $(buttons[i]).attr('isWater') != 1){
                  let newDirection = directionArray[Math.floor(Math.random() * directionArray.length)];
                    $(buttons[i]).attr('isActive', 0);
                    $(adjacentbuttons[2]).attr('isActive', 1);
                    $(adjacentbuttons[2]).attr('direction', newDirection);
                    $(adjacentbuttons[2]).removeClass('btn');
                    $(adjacentbuttons[2]).addClass('active');
                    $(buttons[i]).removeClass('active');
                    $(buttons[i]).addClass('btn');
                  }
                  else if ($(buttons[i]).attr('isWater') == 1){
                    $(buttons[i]).removeClass('active');
                    $(buttons[i]).addClass('btn');
                  }
      
                }

В основном здесь я ищу по три вещи на каждом интервале.

Я выбираю случайный выбор из массива выбора.

2 - Так направление меняется? Если нет, то отправляю в путь.

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

4 - Если это не вода ... тогда я снова устанавливаю кнопку на «btn» вместо «active».

В заключение:

Честно говоря, это довольно просто и довольно просто. Однако с этим можно сделать гораздо больше. Даже в этом примере мы могли бы включить данные о высоте, чего мы еще не сделали. Мы могли бы включать все другие типы данных. Если вы ищете увлекательный способ быстрого создания интерактивной карты, я надеюсь, это поможет. Меня больше всего беспокоит то, что setInterval - действительно ресурсоемкий способ решить эту проблему, и я уверен, что есть гораздо лучшее решение. Если у вас есть идеи или другие отзывы, не стесняйтесь делиться. Спасибо!