Центрировать карту в d3 с учетом объекта geoJSON

140

В настоящее время в d3, если у вас есть объект geoJSON, который вы собираетесь нарисовать, вам нужно масштабировать его и переводить, чтобы получить желаемый размер, и перевести его, чтобы центрировать. Это очень утомительная задача проб и ошибок, и мне было интересно, знает ли кто-нибудь лучший способ получить эти значения?

Так, например, если у меня есть этот код

var path, vis, xy;
xy = d3.geo.mercator().scale(8500).translate([0, -1200]);

path = d3.geo.path().projection(xy);

vis = d3.select("#vis").append("svg:svg").attr("width", 960).attr("height", 600);

d3.json("../../data/ireland2.geojson", function(json) {
  return vis.append("svg:g")
    .attr("class", "tracts")
    .selectAll("path")
    .data(json.features).enter()
    .append("svg:path")
    .attr("d", path)
    .attr("fill", "#85C3C0")
    .attr("stroke", "#222");
});

Как, черт возьми, мне получить .scale (8500) и .translate ([0, -1200]), не продвигаясь постепенно?

альпинистский
источник
См.
Hugolpz

Ответы:

134

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

  1. Создайте проекцию и d3.geo.path
  2. Вычислить границы текущей проекции
  3. Используйте эти границы для расчета масштаба и перевода
  4. Восстановите проекцию

В коде:

  var width  = 300;
  var height = 400;

  var vis = d3.select("#vis").append("svg")
      .attr("width", width).attr("height", height)

  d3.json("nld.json", function(json) {
      // create a first guess for the projection
      var center = d3.geo.centroid(json)
      var scale  = 150;
      var offset = [width/2, height/2];
      var projection = d3.geo.mercator().scale(scale).center(center)
          .translate(offset);

      // create the path
      var path = d3.geo.path().projection(projection);

      // using the path determine the bounds of the current map and use 
      // these to determine better values for the scale and translation
      var bounds  = path.bounds(json);
      var hscale  = scale*width  / (bounds[1][0] - bounds[0][0]);
      var vscale  = scale*height / (bounds[1][1] - bounds[0][1]);
      var scale   = (hscale < vscale) ? hscale : vscale;
      var offset  = [width - (bounds[0][0] + bounds[1][0])/2,
                        height - (bounds[0][1] + bounds[1][1])/2];

      // new projection
      projection = d3.geo.mercator().center(center)
        .scale(scale).translate(offset);
      path = path.projection(projection);

      // add a rectangle to see the bound of the svg
      vis.append("rect").attr('width', width).attr('height', height)
        .style('stroke', 'black').style('fill', 'none');

      vis.selectAll("path").data(json.features).enter().append("path")
        .attr("d", path)
        .style("fill", "red")
        .style("stroke-width", "1")
        .style("stroke", "black")
    });
Ян ван дер Лаан
источник
9
Привет, Ян ван дер Лаан, я никогда не благодарил тебя за этот ответ. Кстати, это действительно хороший ответ, если бы я мог разделить награду, которую я бы сделал. Спасибо за это!
climboid
Если я применяю это, я получаю границы = бесконечность. Есть идеи, как это можно решить?
Simke Nys
@SimkeNys Это может быть та же проблема, что упоминалась здесь stackoverflow.com/questions/23953366/… Попробуйте решение, упомянутое там.
Ян ван дер Лаан,
1
Привет, Ян, спасибо за код. Я пробовал ваш пример с некоторыми данными GeoJson, но это не сработало. Вы можете сказать мне, что я делаю не так? :) Я загрузил данные GeoJson: onedrive.live.com/…
user2644964
1
В D3 v4 подгонка проекции - это встроенный метод: projection.fitSize([width, height], geojson)( Документы API ) - см. Ответ @dnltsk ниже.
Флориан Ледерманн
173

Мой ответ близок к ответу Яна ван дер Лаана, но вы можете немного упростить его, потому что вам не нужно вычислять географический центроид; вам нужна только ограничивающая рамка. И, используя немасштабированную, непереведенную единичную проекцию, вы можете упростить математику.

Важная часть кода такова:

// Create a unit projection.
var projection = d3.geo.albers()
    .scale(1)
    .translate([0, 0]);

// Create a path generator.
var path = d3.geo.path()
    .projection(projection);

// Compute the bounds of a feature of interest, then derive scale & translate.
var b = path.bounds(state),
    s = .95 / Math.max((b[1][0] - b[0][0]) / width, (b[1][1] - b[0][1]) / height),
    t = [(width - s * (b[1][0] + b[0][0])) / 2, (height - s * (b[1][1] + b[0][1])) / 2];

// Update the projection to use computed scale & translate.
projection
    .scale(s)
    .translate(t);

После компоновки ограничивающей рамки объекта в единичной проекции вы можете вычислить соответствующий масштаб , сравнив соотношение сторон ограничивающей рамки ( b[1][0] - b[0][0]и b[1][1] - b[0][1]) с соотношением сторон холста ( widthи height). В этом случае я также масштабировал ограничивающую рамку до 95% холста, а не до 100%, поэтому по краям осталось немного больше места для штрихов и окружающих элементов или отступов.

Затем вы можете вычислить перевод, используя центр ограничивающей рамки ( (b[1][0] + b[0][0]) / 2и (b[1][1] + b[0][1]) / 2) и центр холста ( width / 2и height / 2). Обратите внимание, что поскольку ограничивающая рамка находится в координатах единичной проекции, ее необходимо умножить на масштаб ( s).

Например, bl.ocks.org/4707858 :

проект в ограничивающую рамку

Возникает связанный с этим вопрос: как увеличить или уменьшить масштаб до определенной функции в коллекции, не регулируя проекцию, т. Е. Комбинируя проекцию с геометрическим преобразованием для увеличения и уменьшения масштаба. Здесь используются те же принципы, что и выше, но математика немного отличается, потому что геометрическое преобразование (атрибут SVG «преобразование») сочетается с географической проекцией.

Например, bl.ocks.org/4699541 :

увеличить до ограничивающей рамки

mbostock
источник
2
Хочу отметить, что в приведенном выше коде есть несколько ошибок, особенно в индексах границ. Это должно выглядеть так: s = (0.95 / Math.max ((b [1] [0] - b [0] [0]) / width, (b [1] [1] - b [0] [0]] ) / высота)) * 500, t = [(ширина - s * (b [1] [0] + b [0] [0])) / 2, (высота - s * (b [1] [1]] + b [0] [1])) / 2];
iros
2
@iros - Похоже, * 500здесь посторонний ... тоже b[1][1] - b[0][0]должен быть b[1][1] - b[0][1]в расчете шкалы.
nrabinowitz
2
Отчет об ошибке
Пауло Скардин,
5
Итак:b.s = b[0][1]; b.n = b[1][1]; b.w = b[0][0]; b.e = b[1][0]; b.height = Math.abs(b.n - b.s); b.width = Math.abs(b.e - b.w); s = .9 / Math.max(b.width / width, (b.height / height));
Херб Кодилл
3
Именно из-за такого сообщества работать с D3 так приятно. Потрясающие!
arunkjn 08
58

С d3 v4 или v5 это становится проще!

var projection = d3.geoMercator().fitSize([width, height], geojson);
var path = d3.geoPath().projection(projection);

и наконец

g.selectAll('path')
  .data(geojson.features)
  .enter()
  .append('path')
  .attr('d', path)
  .style("fill", "red")
  .style("stroke-width", "1")
  .style("stroke", "black");

Наслаждайтесь, ура

днлцк
источник
2
Я надеюсь, что за этот ответ проголосуют больше. Некоторое время работал d3v4и только что открыл для себя этот метод.
Марк
2
Откуда gвзялось? Это контейнер SVG?
Tschallacka
1
Tschallacka gдолжна быть <g></g>бирка
giankotarola
1
Жаль, что это так далеко после 2 качественных ответов. Это легко пропустить, и это, очевидно, проще, чем другие ответы.
Курт
1
Спасибо. Работает и в v5!
Чайтанья Бангера
53

Я новичок в d3 - попробую объяснить, как я это понимаю, но не уверен, что все правильно понял.

Секрет в том, что некоторые методы работают с картографическим пространством (широта, долгота), а другие - с декартовым пространством (x, y на экране). Картографическое пространство (наша планета) (почти) сферическое, декартово пространство (экран) - плоское - чтобы наложить одно на другое, вам понадобится алгоритм, который называется проекцией . Это пространство слишком мало, чтобы углубляться в увлекательную тему проекций и то, как они искажают географические особенности, чтобы превратить сферическую в плоскость; одни предназначены для сохранения углов, другие - для сохранения расстояний и так далее - всегда есть компромисс (у Майка Бостока есть огромная коллекция примеров ).

введите описание изображения здесь

В d3 объект проекции имеет свойство / сеттер center, заданное в единицах карты:

projection.center ([местоположение])

Если указан центр, устанавливает центр проекции в указанное место, двухэлементный массив долготы и широты в градусах и возвращает проекцию. Если центр не указан, возвращает текущий центр, который по умолчанию равен 0 °, 0 °⟩.

Также есть перевод в пикселях, где центр проекции стоит относительно холста:

projection.translate ([точка])

Если указана точка, устанавливает смещение перемещения проекции равным указанному двухэлементному массиву [x, y] и возвращает проекцию. Если точка не указана, возвращает текущее смещение перемещения, которое по умолчанию равно [480, 250]. Смещение перемещения определяет пиксельные координаты центра проекции. Смещение сдвига по умолчанию помещает ⟨0 °, 0 °⟩ в центр области 960 × 500.

Когда я хочу центрировать объект на холсте, мне нравится устанавливать центр проекции в центр ограничивающего прямоугольника объекта - это работает для меня при использовании меркатора. (WGS 84, используется в картах Google) для моей страны (Бразилия), никогда не тестировался с использованием других проекций и полушарий. Возможно, вам придется внести коррективы в другие ситуации, но если вы усвоите эти основные принципы, все будет в порядке.

Например, с учетом проекции и пути:

var projection = d3.geo.mercator()
    .scale(1);

var path = d3.geo.path()
    .projection(projection);

boundsМетод из pathвозвращает ограничивающий прямоугольник в пикселях . Используйте его, чтобы найти правильный масштаб, сравнивая размер в пикселях с размером в единицах карты (0,95 дает вам запас 5% по сравнению с наилучшим подходом по ширине или высоте). Базовая геометрия здесь, вычисление ширины / высоты прямоугольника с учетом диагонально противоположных углов:

var b = path.bounds(feature),
    s = 0.9 / Math.max(
                   (b[1][0] - b[0][0]) / width, 
                   (b[1][1] - b[0][1]) / height
               );
projection.scale(s); 

введите описание изображения здесь

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

b = d3.geo.bounds(feature);

Установите центр проекции в центр ограничивающей рамки:

projection.center([(b[1][0]+b[0][0])/2, (b[1][1]+b[0][1])/2]);

Используйте этот translateметод, чтобы переместить центр карты в центр холста:

projection.translate([width/2, height/2]);

К настоящему времени у вас должен быть объект в центре карты, увеличенный с запасом 5%.

Пауло Скардин
источник
Есть где-нибудь бл.окс?
Hugolpz
Извините, нет ничего плохого, что вы пытаетесь сделать? Это что-то вроде масштабирования при нажатии? Опубликуйте его, и я смогу взглянуть на ваш код.
Пауло Скардин,
Ответ и изображения Bostock содержат ссылки на примеры bl.ocks.org, которые позволяют мне скопировать весь код. Работа сделана. +1 и спасибо за отличные иллюстрации!
Hugolpz 01
4

Есть центр () метод , который вы можете использовать , который принимает широчайшие пару / долготы.

Насколько я понимаю, translate () используется только для буквального перемещения пикселей карты. Не знаю, как определить масштаб.

Дэйв Фостер
источник
8
Если вы используете TopoJSON и хотите центрировать всю карту, вы можете запустить topojson с --bbox, чтобы включить атрибут bbox в объект JSON. Координаты широты и долготы для центра должны быть [(b [0] + b [2]) / 2, (b [1] + b [3]) / 2] (где b - значение bbox).
Пауло Скардин
3

Помимо Центр карта в d3 данного объекта GeoJSON , обратите внимание , что вы можете предпочесть fitExtent()более , fitSize()если вы хотите задать отступы вокруг границ объекта. fitSize()автоматически устанавливает для этого отступа значение 0.

Сильвен Лесаж
источник
2

Я искал в Интернете простой способ центрировать свою карту и меня вдохновил ответ Яна ван дер Лаана и mbostock. Вот более простой способ использования jQuery, если вы используете контейнер для svg. Я создал границу 95% для отступов / границ и т. Д.

var width = $("#container").width() * 0.95,
    height = $("#container").width() * 0.95 / 1.9 //using height() doesn't work since there's nothing inside

var projection = d3.geo.mercator().translate([width / 2, height / 2]).scale(width);
var path = d3.geo.path().projection(projection);

var svg = d3.select("#container").append("svg").attr("width", width).attr("height", height);

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

Вэй Хао
источник
1

Чтобы панорамировать / масштабировать карту, вы должны посмотреть на наложение SVG на Leaflet. Это будет намного проще, чем преобразовать SVG. См. Этот пример http://bost.ocks.org/mike/leaflet/, а затем Как изменить центр карты в буклете

Кристофор Карл
источник
При добавлении другую зависимость вызывает беспокойство, PAN и ZOOM можно легко сделать в чистом d3 см stackoverflow.com/questions/17093614/...
Пауло Scardine
Этот ответ не имеет отношения к d3. Вы также можете панорамировать / масштабировать карту в d3, листовка не нужна. (Только что понял, что это старый пост, просто просматривал ответы)
JARRRRG
0

С ответом mbostocks и комментарием Херба Кодилла я начал сталкиваться с проблемами с Аляской, так как использовал проекцию меркатора. Должен отметить, что для своих целей я пытаюсь спроецировать и центрировать штаты США. Я обнаружил, что мне пришлось объединить два ответа с ответом Яна ван дер Лаана со следующим исключением для многоугольников, которые перекрывают полушария (многоугольники, которые имеют абсолютное значение для Востока и Запада больше 1):

  1. настроить простую проекцию в меркаторе:

    проекция = d3.geo.mercator (). scale (1) .translate ([0,0]);

  2. создать путь:

    path = d3.geo.path (). projection (проекция);

3. установить мои границы:

var bounds = path.bounds(topoJson),
  dx = Math.abs(bounds[1][0] - bounds[0][0]),
  dy = Math.abs(bounds[1][1] - bounds[0][1]),
  x = (bounds[1][0] + bounds[0][0]),
  y = (bounds[1][1] + bounds[0][1]);

4. Добавьте исключение для Аляски и укажите, что полушария перекрывают друг друга:

if(dx > 1){
var center = d3.geo.centroid(topojson.feature(json, json.objects[topoObj]));
scale = height / dy * 0.85;
console.log(scale);
projection = projection
    .scale(scale)
    .center(center)
    .translate([ width/2, height/2]);
}else{
scale = 0.85 / Math.max( dx / width, dy / height );
offset = [ (width - scale * x)/2 , (height - scale * y)/2];

// new projection
projection = projection                     
    .scale(scale)
    .translate(offset);
}

Надеюсь, это поможет.

DimeZilla
источник
0

Для людей, которые хотят отрегулировать вертикальное и горизонтальное положение, вот решение:

  var width  = 300;
  var height = 400;

  var vis = d3.select("#vis").append("svg")
      .attr("width", width).attr("height", height)

  d3.json("nld.json", function(json) {
      // create a first guess for the projection
      var center = d3.geo.centroid(json)
      var scale  = 150;
      var offset = [width/2, height/2];
      var projection = d3.geo.mercator().scale(scale).center(center)
          .translate(offset);

      // create the path
      var path = d3.geo.path().projection(projection);

      // using the path determine the bounds of the current map and use 
      // these to determine better values for the scale and translation
      var bounds  = path.bounds(json);
      var hscale  = scale*width  / (bounds[1][0] - bounds[0][0]);
      var vscale  = scale*height / (bounds[1][1] - bounds[0][1]);
      var scale   = (hscale < vscale) ? hscale : vscale;
      var offset  = [width - (bounds[0][0] + bounds[1][0])/2,
                        height - (bounds[0][1] + bounds[1][1])/2];

      // new projection
      projection = d3.geo.mercator().center(center)
        .scale(scale).translate(offset);
      path = path.projection(projection);

      // adjust projection
      var bounds  = path.bounds(json);
      offset[0] = offset[0] + (width - bounds[1][0] - bounds[0][0]) / 2;
      offset[1] = offset[1] + (height - bounds[1][1] - bounds[0][1]) / 2;

      projection = d3.geo.mercator().center(center)
        .scale(scale).translate(offset);
      path = path.projection(projection);

      // add a rectangle to see the bound of the svg
      vis.append("rect").attr('width', width).attr('height', height)
        .style('stroke', 'black').style('fill', 'none');

      vis.selectAll("path").data(json.features).enter().append("path")
        .attr("d", path)
        .style("fill", "red")
        .style("stroke-width", "1")
        .style("stroke", "black")
    });
компьютерщик
источник
0

Как я центрировал Topojson, где мне нужно было вытащить функцию:

      var projection = d3.geo.albersUsa();

      var path = d3.geo.path()
        .projection(projection);


      var tracts = topojson.feature(mapdata, mapdata.objects.tx_counties);

      projection
          .scale(1)
          .translate([0, 0]);

      var b = path.bounds(tracts),
          s = .95 / Math.max((b[1][0] - b[0][0]) / width, (b[1][1] - b[0][1]) / height),
          t = [(width - s * (b[1][0] + b[0][0])) / 2, (height - s * (b[1][1] + b[0][1])) / 2];

      projection
          .scale(s)
          .translate(t);

        svg.append("path")
            .datum(topojson.feature(mapdata, mapdata.objects.tx_counties))
            .attr("d", path)
Изучение статистики на примере
источник