- PVSM.RU - https://www.pvsm.ru -
Доброго времени суток, уважаемый читатель! В прошлый раз [1] мы изучали процесс создания интерактивной карты-хороплета, теперь предлагаю немного усложнить задачу и перейти к трёхмерной модели Земли, именуемой в народе глобусом. Глобус делать будем двух видов: d3.js [2]. У каждого варианта свои преимущества. В моём исполнении Голубая планета выглядит следующим образом:

А как создать свой собственный Мир с материками и океанами можно узнать под катом.
Сперва нам нужно найти геоданные. Как и в прошлый раз мы будем использовать TopoJSON [3] для этих целей. О том как его получить можно прочитать в предыдущей статье [1] в разделе «Дела картографические». И так у нас есть TopoJSON [3] файл world-110m.json для карты с масштабом 1:110,000,000, или 1 см = 1,100 км (1″ = 1,736 миль) и файл world-110m-country-names.tsv с названиями стран вида id — название страны. Внешний файл с названиями используется для удобства, так как в этом случае можно легко перевести названия на любой язык. Всё, можно приступать непосредственно к созданию глобуса.
Нашей целью будет глобус, который можно:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Nice title</title>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/queue.v1.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
</head>
<style>
Your awesome CSS
</style>
<body>
<h1>Cool Header</h1>
<script>
Your awesome d3.js code
</script>
</body>
</html>
Определим основные переменные и добавим DOM элементы.
var width = 600,
height = 500,
sens = 0.25,
focused;
//Setting projection
var projection = d3.geo.orthographic()
.scale(245)
.rotate([0, 0])
.translate([width / 2, height / 2])
.clipAngle(90);
var path = d3.geo.path()
.projection(projection);
//SVG container
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
//Adding water
svg.append("path")
.datum({type: "Sphere"})
.attr("class", "water")
.attr("d", path);
var countryTooltip = d3.select("body").append("div").attr("class", "countryTooltip"),
countryList = d3.select("body").append("select").attr("name", "countries");
Переменная sens отвечает за точность при вращении мышкой, а focused используется как триггер для выбранной (центрированной) страны. Про используемую проекцию можно почитать на wikipedia: Orthographic projection [4]. Метод .clipAngle() [5] определяет какую часть сферы мы будем отображать (а точнее видеть), про это опять же можно почитать на wikipedia: small-circle clipping [6]. Остальное вроде в разъяснениях не нуждается.
Далее мы загружаем наши файлы при помощи библиотеки queue.js [7], которая позволяет делать нам это асинхронно.
queue()
.defer(d3.json, "data/world-110m.json")
.defer(d3.tsv, "data/world-110m-country-names.tsv")
.await(ready);
Теперь перейдём к главной функции, в нашем случае она называется ready. Вначале, мы добавляем названия стран в наш dropdown list и отрисовываем страны на глобусе.
function ready(error, world, countryData) {
var countryById = {},
countries = topojson.feature(world, world.objects.countries).features;
//Adding countries to select
countryData.forEach(function(d) {
countryById[d.id] = d.name;
option = countryList.append("option");
option.text(d.name);
option.property("value", d.id);
});
//Drawing countries on the globe
var world = svg.selectAll("path.land")
.data(countries)
.enter().append("path")
.attr("class", "land")
.attr("d", path)
Перейдём к обработке событий мыши. Здесь пояснений требует drag.origin() [8], он позволяет нам задать «оригинальные» (действительные) стартовые координаты при захвате элемента, в нашем случае широту и долготу.
//Drag event
.call(d3.behavior.drag()
.origin(function() { var r = projection.rotate(); return {x: r[0] / sens, y: -r[1] / sens}; })
.on("drag", function() {
var rotate = projection.rotate();
projection.rotate([d3.event.x * sens, -d3.event.y * sens, rotate[2]]);
svg.selectAll("path.land").attr("d", path);
svg.selectAll(".focused").classed("focused", focused = false);
}))
//Mouse events
.on("mouseover", function(d) {
countryTooltip.text(countryById[d.id])
.style("left", (d3.event.pageX + 7) + "px")
.style("top", (d3.event.pageY - 15) + "px")
.style("display", "block")
.style("opacity", 1);
})
.on("mouseout", function(d) {
countryTooltip.style("opacity", 0)
.style("display", "none");
})
.on("mousemove", function(d) {
countryTooltip.style("left", (d3.event.pageX + 7) + "px")
.style("top", (d3.event.pageY - 15) + "px");
});
Для реализации фокусировки на стране нам необходимо написать функцию, которая бы возвращала нам геоданные для страны по её id'шнику. Собственно вот она.
function country(cnt, sel) {
for(var i = 0, l = cnt.length; i < l; i++) {
if(cnt[i].id == sel.value) {return cnt[i];}
}
};
Теперь можно непосредственно перейти к реализации фокусировки (центровки) на стране, выбранной из списка.
//Country focus on option select
d3.select("select").on("change", function() {
var rotate = projection.rotate(),
focusedCountry = country(countries, this),
p = d3.geo.centroid(focusedCountry);
svg.selectAll(".focused").classed("focused", focused = false);
//Globe rotating
(function transition() {
d3.transition()
.duration(2500)
.tween("rotate", function() {
var r = d3.interpolate(projection.rotate(), [-p[0], -p[1]]);
return function(t) {
projection.rotate(r(t));
svg.selectAll("path").attr("d", path)
.classed("focused", function(d, i) { return d.id == focusedCountry.id ? focused = d : false; });
};
})
.transition();
})();
});
Здесь вся соль кроется в transition.tween() [9], который позволяет нам вызывать заданную функцию (поворот) для каждого интерполированного значения.
Всё — GitHub [10] (там же можно задать вопросы тем у кого read-only на Хабрахабре), а пощупать результат можно через сервис bl.ocks.org [11].
Давайте рассмотрим преимущества SVG:
pathС SVG реализацией вроде разобрались, давайте посмотрим как сделать что-то подобное на canvas. Создадим простую анимацию вращения Земли. Тут многое будет аналогично предыдущему примеру. Кода мало, поэтому приведу его весь сразу.
var width = 800,
height = 500;
var projection = d3.geo.orthographic()
.scale(245)
.rotate([180, 0])
.translate([width / 2, height / 2])
.clipAngle(90);
var canvas = d3.select("body").append("canvas")
.attr("width", width)
.attr("height", height);
var c = canvas.node().getContext("2d");
var path = d3.geo.path()
.projection(projection)
.context(c);
function getImage(path, callback) {
var img = new Image();
img.src = path;
img.onload = callback(null, img);
}
queue()
.defer(d3.json, "data/world-110m.json")
.defer(d3.tsv, "data/world-110m-country-names.tsv")
.defer(getImage, "data/space.jpg")
.await(ready);
//Main function
function ready(error, world, countryData, space) {
var globe = {type: "Sphere"},
land = topojson.feature(world, world.objects.land),
borders = topojson.mesh(world, world.objects.countries, function(a, b) { return a !== b; });
//Earth rotating
(function transition() {
d3.transition()
.duration(15000)
.ease("linear")
.tween("rotate", function() {
var r = d3.interpolate(projection.rotate(), [-180, 0]);
return function(t) {
projection.rotate(r(t));
c.clearRect(0, 0, width, height);
c.drawImage(space, 0, 0);
c.fillStyle = "#00006B", c.beginPath(), path(globe), c.fill();
c.fillStyle = "#29527A", c.beginPath(), path(land), c.fill();
c.strokeStyle = "#fff", c.lineWidth = .5, c.beginPath(), path(borders), c.stroke();
projection.rotate([180, 0]);
};
})
.transition().duration(30).ease("linear")
.each("end", transition);
})();
};
Вращение реализовано как поворот из точки [180, 0] в точку [-180, 0], которые совпадают. Таким образом, «интерполятор», не заметив подвоха, сделает то что нам нужно. Потом мы начинаем рисовать на canvas, предварительно очистив его. Рисуем фон, сферу, материки и границы стран. Бесконечное вращение получаем за счёт рекурсивного вызова функции transition.
Ну вот мы и создали анимацию. Исходники можно найти на GitHub [12], а полюбоваться космическими видами можно через сервис bl.ocks.org [13].
Рассмотрим преимущества Canvas:
Вот мы и рассмотрели ещё пару интересных примеров созданных с помощью замечательной библиотеки d3.js [2]. Я старался, чтобы примеры были в меру просты для понимания, наглядны и довольно-таки интересны. В борьбе d3.js [2]!
Автор: KoGor
Источник [14]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/kartografiya/38663
Ссылки в тексте:
[1] прошлый раз: http://habrahabr.ru/post/181766/
[2] d3.js: http://d3js.org
[3] TopoJSON: https://github.com/mbostock/topojson/wiki
[4] Orthographic projection: http://en.wikipedia.org/wiki/Orthographic_projection_(cartography)
[5] .clipAngle(): https://github.com/mbostock/d3/wiki/Geo-Projections#wiki-clipAngle
[6] small-circle clipping: http://en.wikipedia.org/wiki/Circle_of_a_sphere
[7] queue.js: https://github.com/mbostock/queue
[8] drag.origin(): https://github.com/mbostock/d3/wiki/Drag-Behavior#wiki-origin
[9] transition.tween(): https://github.com/mbostock/d3/wiki/Transitions#wiki-tween
[10] GitHub: https://gist.github.com/KoGor/5994804
[11] bl.ocks.org: http://bl.ocks.org/KoGor/5994804
[12] GitHub: https://gist.github.com/KoGor/5994960
[13] bl.ocks.org: http://bl.ocks.org/KoGor/5994960
[14] Источник: http://habrahabr.ru/post/186532/
Нажмите здесь для печати.