D3 geoPath может использоваться для создания траекторий, которые следуют за большим круговым расстоянием: они не изогнуты по стилю, они изогнуты по мере необходимости, в зависимости от проекции, для представления кратчайшего пути на земле для соединения двух точек.D3 geoPaths динамически повторно выбираются, чтобы позволить это.
Это необычное поведение в библиотеках веб-географического картографирования, большинство из которых рассматривает широту и долготу как декартовы, а не трехмерные данные: где широта и долгота являются точками насфера.При обработке данных как декартовых линий прямые соединяют две точки.В d3 это может быть выполнено с помощью таких методов, как these .
Если вы хотите получить согласованную кривую для всех отрезков, мы будем рассматривать данные как декартовы и интерполировать кривую.Поскольку мы не будем использовать d3.geoPath
для этого, нет необходимости конвертировать ваши пункты назначения и источники в Geojson LineStrings, мы можем просто использовать точки напрямую.
Мы можем использовать для этого кривой-интерполятор,но интерполяторы по умолчанию не будут работать без добавления контрольных точек между конечным и начальным пунктами назначения.Вместо этого давайте попробуем пользовательскую кривую - посмотрите эти ответы ( a , b ) для получения дополнительной информации о пользовательских кривых.
Наша пользовательская кривая может взять любую точку после первой, чтобы найти среднюю точку между ней и точкой перед ней, и сместить точку, чтобы создать контрольную точку, образующую треугольник между предыдущей точкой и текущей точкой, затеммы просто рисуем между ними квадратную кривую:
var curve = function(context) {
var custom = d3.curveLinear(context);
custom._context = context;
custom.point = function(x,y) {
x = +x, y = +y;
switch (this._point) {
case 0: this._point = 1;
this._line ? this._context.lineTo(x, y) : this._context.moveTo(x, y);
this.x0 = x; this.y0 = y;
break;
case 1: this._point = 2;
default:
var x1 = this.x0 * 0.5 + x * 0.5;
var y1 = this.y0 * 0.5 + y * 0.5;
var m = 1/(y1 - y)/(x1 - x);
var r = -100; // offset of mid point.
var k = r / Math.sqrt(1 + (m*m) );
if (m == Infinity) {
y1 += r;
}
else {
y1 += k;
x1 += m*k;
}
this._context.quadraticCurveTo(x1,y1,x,y);
this.x0 = x; this.y0 = y;
break;
}
}
return custom;
}
Имея это в виду, мы можем просто нарисовать линии с чем-то вроде:
d3.line()
.curve(curve)
.x(function(d) { return d.lon; })
.y(function(d) { return d.lat; })
Как видно ниже:
let data = [{
"source": {
"lat": 40.712776,
"lon": -74.005974
},
"destination": {
"lat": 21.05,
"lon": 105.55
}
},
{
"source": {
"lat": 40.712776,
"lon": -74.005974
},
"destination": {
"lat": -35.15,
"lon": 149.08
}
}]
var curve = function(context) {
var custom = d3.curveLinear(context);
custom._context = context;
custom.point = function(x,y) {
x = +x, y = +y;
switch (this._point) {
case 0: this._point = 1;
this._line ? this._context.lineTo(x, y) : this._context.moveTo(x, y);
this.x0 = x; this.y0 = y;
break;
case 1: this._point = 2;
default:
var x1 = this.x0 * 0.5 + x * 0.5;
var y1 = this.y0 * 0.5 + y * 0.5;
var m = 1/(y1 - y)/(x1 - x);
var r = -100; // offset of mid point.
var k = r / Math.sqrt(1 + (m*m) );
if (m == Infinity) {
y1 += r;
}
else {
y1 += k;
x1 += m*k;
}
this._context.quadraticCurveTo(x1,y1,x,y);
this.x0 = x; this.y0 = y;
break;
}
}
return custom;
}
var projection = d3.geoEquirectangular().translate([250,150]).scale(500/Math.PI/2);
var path = d3.geoPath(projection);
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 300);
d3.json("https://unpkg.com/world-atlas@1/world/110m.json").then(function(world) {
var worldOutline = svg.append("path")
.datum(topojson.mesh(world))
.attr("d", path );
var line = d3.line()
.x(function(d) {
return projection([d.lon,d.lat])[0];
})
.y(function(d) {
return projection([d.lon,d.lat])[1];
})
.curve(curve);
svg.selectAll(null)
.data(data)
.enter()
.append("path")
.datum(function(d) {
return [d.source,d.destination]; // d3.line expects an array where each item represnts a vertex.
})
.attr("d",line)
.style("stroke","black")
.style("stroke-width",1.5);
});
path {
fill: none;
stroke: #ccc;
stroke-width: 1px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://unpkg.com/topojson-client@3"></script>
Ниже, просто для удовольствия, я сравниваю прямые линии, используя d3.line
, кривые линии, используя d3.line
с пользовательским интерполятором кривой, и обычнуюстарый d3.geoPath
с некоторой анимацией:
let data = [{
"source": {
"lat": 40.712776,
"lon": -74.005974
},
"destination": {
"lat": 21.05,
"lon": 105.55
}
},
{
"source": {
"lat": 40.712776,
"lon": -74.005974
},
"destination": {
"lat": -35.15,
"lon": 149.08
}
}]
var curve = function(context) {
var custom = d3.curveLinear(context);
custom._context = context;
custom.point = function(x,y) {
x = +x, y = +y;
switch (this._point) {
case 0: this._point = 1;
this._line ? this._context.lineTo(x, y) : this._context.moveTo(x, y);
this.x0 = x; this.y0 = y;
break;
case 1: this._point = 2;
default:
var x1 = this.x0 * 0.5 + x * 0.5;
var y1 = this.y0 * 0.5 + y * 0.5;
var m = 1/(y1 - y)/(x1 - x);
var r = -100; // offset of mid point.
var k = r / Math.sqrt(1 + (m*m) );
if (m == Infinity) {
y1 += r;
}
else {
y1 += k;
x1 += m*k;
}
this._context.quadraticCurveTo(x1,y1,x,y);
this.x0 = x; this.y0 = y;
break;
}
}
return custom;
}
var projection = d3.geoEquirectangular().translate([250,150]).scale(500/Math.PI/2);
var path = d3.geoPath(projection);
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 300);
d3.json("https://unpkg.com/world-atlas@1/world/110m.json").then(function(world) {
var worldOutline = svg.append("path")
.datum(topojson.mesh(world))
.attr("d", path );
var line = d3.line()
.x(function(d) {
return projection([d.lon,d.lat])[0];
})
.y(function(d) {
return projection([d.lon,d.lat])[1];
})
.curve(curve);
var fauxArcPaths = svg.selectAll(null)
.data(data)
.enter()
.append("path")
.datum(function(d) {
return [d.source,d.destination];
})
.attr("d",line)
.style("stroke","black")
.style("stroke-width",1.5);
var greatCirclePaths = svg.selectAll(null)
.data(data)
.enter()
.append("path")
.datum(function(d) {
return {type:"LineString",coordinates:
[[d.source.lon,d.source.lat],[d.destination.lon,d.destination.lat]] }
})
.attr("d",path)
.style("stroke","steelblue")
.style("stroke-width",1.5);
var straightline = d3.line()
.x(function(d) {
return projection([d.lon,d.lat])[0];
})
.y(function(d) {
return projection([d.lon,d.lat])[1];
});
var straightPaths = svg.selectAll(null)
.data(data)
.enter()
.append("path")
.datum(function(d) {
return [d.source,d.destination];
})
.attr("d",straightline)
.style("stroke-width",1.5)
.style("stroke","orange");
// animate:
d3.interval(function(elapsed) {
projection.rotate([ -elapsed / 150, elapsed/300 ]);
straightPaths.attr("d",straightline);
greatCirclePaths.attr("d",path);
fauxArcPaths.attr("d",line);
worldOutline.attr("d",path);
}, 50);
});
path {
fill: none;
stroke: #aaa;
stroke-width: 1px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://unpkg.com/topojson-client@3"></script>