Центрирование карты в d3 по объекту geoJSON

В настоящее время в 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]), не переходя понемногу?

11 ответов

Решение

Следующее, кажется, делает примерно то, что вы хотите. Масштабирование, кажется, в порядке. При применении к моей карте есть небольшое смещение. Это небольшое смещение, вероятно, вызвано тем, что я использую команду 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")
    });

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

Важной частью кода является следующее:

// 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).

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

проецировать на ограничительную рамку

С этим связан вопрос: как увеличить масштаб до определенной функции в коллекции, не корректируя проекцию, т. Е. Объединить проекцию с геометрическим преобразованием для увеличения или уменьшения масштаба. При этом используются те же принципы, что и выше, но математика немного отличается, поскольку геометрическое преобразование (атрибут SVG "transform") объединяется с географической проекцией.

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

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

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

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");

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

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

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

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

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

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

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

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

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

Когда я хочу центрировать объект на холсте, я хочу установить центр проекции в центр ограничивающего прямоугольника объекта - это работает для меня при использовании mercator (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% запасом.

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

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

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

Я искал в интернете беспроблемный способ центрировать свою карту, и меня вдохновил ответ Яна ван дер Лаана и 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);

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

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

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

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

    projection = d3.geo.mercator (). scale (1).translate ([0,0]);

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

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

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);
}

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

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

  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")
    });

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

      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)
Другие вопросы по тегам