Статьи

HTML5: визуализация роста городского населения на глобусе 3D-мира с Three.js и Canvas

В этой статье я еще раз рассмотрю визуализацию данных / гео с Three.js. На этот раз я покажу вам, как можно изобразить рост городского населения за период с 1950 по 2050 год на трехмерном глобусе, используя Three.js. Получающаяся визуализация оживляет рост крупнейших городов мира во вращающемся трехмерном мире. Результат, к которому мы стремимся, выглядит следующим образом (рабочий пример смотрите здесь ):

Рост населения мира 1950-2050.jpg

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

Для этой визуализации я хотел иметь следующие функции:

  1. Нарисуйте круг на поверхности сферы, чтобы визуализировать размер города
  2. Показать обзор пяти крупнейших городов
  3. Перейдите через годы с 1950 по 2050 год и обновите анимацию
  4. Управляйте вращением мира с помощью простых клавиш управления

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

Нарисуйте круг на поверхности сферы, чтобы визуализировать размер города

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

"Sofia";"";"Bulgaria";42.70;23.33;"BGR";520.00;620.00;710.00;810.00;890.00;980.00;10
70.00;1180.00;1190.00;1170.00;1130.00;1170.00;1180.00;1210.00;1230.00;1240.00;1236
 
"Mandalay";"";"Myanmar";21.97;96.08;"MMR";170.00;200.00;250.00;310.00;370.00;440.00;
500.00;560.00;640.00;720.00;810.00;920.00;960.00;1030.00;1170.00;1310.00;1446

Где первая запись — это название города, третья и четвертая — это местоположение, а из седьмой записи мы видим численность населения за определенный год. Начиная с 1950 года и увеличиваясь с шагом 5 лет до 2025 года с окончательной записью на 2050 год. Итак, чтобы нарисовать круги, нам просто нужно прочитать в файле CSV и для каждой строки определить точку на сфере и исходя из года, который мы хотим рисуем, создаем круг с определенным радиусом.

Рисование круга на сфере звучит легко, но довольно сложно, если вы хотите сделать это полностью с помощью Three.js. Вам необходимо создать собственные геометрии, которые точно соответствуют кривой сферы и разместить их в правильном положении. К счастью, есть легкая альтернатива. С Three.js мы можем указать содержимое холста в качестве текстуры для объекта. Мы используем это следующим образом:

  1. Создайте холст с таким же соотношением сторон, как у карты, которую мы используем для нашего земного шара.
  2. Нарисуйте наши круги прямо на холсте
  3. Используйте холст как текстуру для прозрачной сферы, чуть больше, чем шар
  4. Сообщите Three.js, когда необходимо обновить текстуру.

Для этого я определил простой скрытый холст прямо в html.

<canvas id="canvas" width="1024" height="512" style="display: none;"></canvas>

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

    function addCities(data, year) {
 
        var yearIndex = getYearIndex(year);
 
        var ctx = $('#canvas')[0].getContext("2d");
        ctx.clearRect(0,0,1024,512);
 
 
        var renderedValues = [];
        for (var i = 0 ; i < data.length-1 ; i++) {
 
            //get the data, and set the offset, we need to do this since the x,y coordinates
            //from the data aren't in the correct format
            var x = parseInt(data[i][3]);
            var y = parseInt(data[i][4]);
 
            var x2 =   ((1024/360.0) * (180 + y));
            var y2 =   ((512/180.0) * (90 - x));
 
            //draw a circle
 
            var yearValue = parseInt(data[i][yearIndex]);
            var nextValue = parseInt(data[i][yearIndex+1]);
 
            var yearSize = 5;
	    // we need to handle the period from 2025 to 2050 different
            if (yearIndex == 21) {
                yearSize = 25;
            }
            var step = (nextValue-yearValue)/yearSize;
            var valueToSet = yearValue + ((year%yearSize)*step);
 
 
            // for each city draw it at the correct position and size
            ctx.fillStyle = "#cc3333";
            ctx.globalAlpha = 0.5;
            ctx.beginPath();
            ctx.arc(x2,y2,valueToSet/1000.0,0,2*Math.PI,false);
            ctx.fill();
 
        }
    }

 

Эта функция принимает многомерный массив, представляющий файл CSV (см. Источник или эту статью о том, как читать в файлах CSV). Год — это текущий год, за который нам нужно представить данные. Затем мы получаем позицию и конвертируем ее в координаты x, y на нашем холсте. Теперь нам нужно где рисовать, теперь нам нужно знать, что рисовать. Для этого я возьму значения двух ближайших лет. Поэтому для 1978 года я использую значения 1975 и 1980 годов, чтобы определить приблизительный размер города в конкретном году. На основании этих параметров я рисую простую полупрозрачную дугу. Это приводит к следующему холсту (который обычно вы не увидите, потому что он скрыт).

Рост населения мира 1950-2050-1.jpg

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

  var texture;
    function addOverlay() {
        var spGeo = new THREE.SphereGeometry(604,50,50);
        texture = new THREE.Texture($('#canvas')[0]);
 
        var material = new THREE.MeshBasicMaterial({
            map : texture,
            transparent : true,
            opacity: 0.7,
            blending: THREE.AdditiveAlphaBlending
 
        });
 
        var meshOverlay = new THREE.Mesh(spGeo,material);
        scene.add(meshOverlay);
    }

 

Это создает немного большую сферу и определяет текстуру на основе холста, к которому мы нарисовали наши круги. Используя прозрачный материал и используя этот особый режим смешивания, мы создаем новую сферу, которая показывает все наши круги в правильных положениях. Чтобы убедиться, что Three.js обновляет текстуру, нам нужно добавить «texture.needsUpdate» в наш цикл рендеринга.

  // render the scene
    var timer = 0;
    var rotateSpeed = 0.004;
    function render() {
        texture.needsUpdate = true;
        timer+=rotateSpeed;
        camera.position.x = (Math.cos( timer ) *  1800);
        camera.position.z = (Math.sin( timer ) *  1800);
        camera.lookAt( scene.position );
 
        light.position = camera.position;
        light.lookAt(scene.position);
 
        renderer.render( scene, camera );
        requestAnimationFrame( render );
    }

Это была самая важная часть. Теперь мы можем рисовать и обновлять круги прямо на сфере.

Показать обзор пяти крупнейших городов

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

function addCities(data, year) {
 
        var yearIndex = getYearIndex(year);
 
	...
 
        var renderedValues = [];
        for (var i = 0 ; i < data.length-1 ; i++) {
 
	     ...
            // push interesting values into array to be sorted
            renderedValues.push({value: valueToSet, city: data[i][0]});
        }
 
        // sort according to size
        renderedValues.sort(function(a,b){return b.value-a.value});
 
        // draw the overview of largest cities
        var ctx2 = $('#largest')[0].getContext("2d");
        ctx2.globalAlpha = 0.5;
        ctx2.clearRect(0,0,400,600);
        for (var j = 0; j < 6 ; j++) {
            var x = 70;
            var y = (j*80)+100;
 
            // draw circles
            ctx2.beginPath();
            ctx2.fillStyle = "#cc3333";
            ctx2.arc(x,y,renderedValues[j].value/1000.0,0,2*Math.PI,false);
            ctx2.fill();
 
            // output text
            ctx2.fillStyle = "#aaaaaa";
            ctx2.font = "16px Arial";
            ctx2.fillText(renderedValues[j].city,x+50,y-10);
            ctx2.fillText(Math.round(renderedValues[j].value)+'000',x+50,y+10);
 
 
        }
    }

 

Не так уж сложно Каждый город, нарисованный в виде круга, помещается в массив. Этот массив отсортирован по размеру, и 6 лучших городов нарисованы на холсте. Этот холст показан справа от трехмерного шара.

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

Перейдите через годы с 1950 по 2050 год и обновите анимацию

   function stepYears(elements) {
        var input = {
          year: 1950
        };
 
        var currentYear = 0;
        $(input).animate(
                {year: 2050},
                {step: function(now)
                    {
                        var newYear = Math.round(input.year);
                        if (newYear != currentYear) {
                            $("#currentYear").text("Year: " + newYear);
 
                            currentYear = newYear;
                        }
                        addCities(elements, input.year);
                    },
                 duration: 45000,
                 easing: 'linear'
                }
        );
    }

Этот фрагмент JQuery обновляет входную переменную с 1950 до 2050 в течение 45 секунд. Поскольку мы хотим линейного перехода, мы явно определяем замедление. Для каждого изменения мы вызываем функцию addCities, которую мы видели ранее, чтобы обновить холст, который мы используем в качестве текстуры.

Последней функцией, которую я хотел добавить, было простое управление вращением сферы.

Управляйте вращением мира с помощью простых клавиш управления

В этом примере я не вращаю сферу, а вращаю камеры. Как мы уже видели, наш цикл рендеринга выглядит так:

     // render the scene
    var timer = 0;
    var rotateSpeed = 0.004;
    function render() {
        texture.needsUpdate = true;
        timer+=rotateSpeed;
        camera.position.x = (Math.cos( timer ) *  1800);
        camera.position.z = (Math.sin( timer ) *  1800);
        camera.lookAt( scene.position );
 
        light.position = camera.position;
        light.lookAt(scene.position);
 
        renderer.render( scene, camera );
        requestAnimationFrame( render );
    }

Скорость и направление вращения управляются переменной rotateSpeed. Если мы регистрируем нажатие клавиш для обновления этого значения, мы можем легко контролировать вращение. Для регистрации нажатий клавиш я использую JQuery.

            // read all the elements
            $(document).keypress(function(e) {
 
 
                switch (e.which) {
                    case 97 : {
                        // rotate globe step to left
                        rotateSpeed+=0.001;
                        break;
                    };
 
                    case 115 : {
                        // rotate globe step to right
                        rotateSpeed-=0.001;
                        break;
                    };
 
                    case 112 : {
                        rotateSpeed=0;
                        break;
                    };
 
 
                }
            });

Вот и все. Рабочий пример этой демонстрации можно найти здесь .