index.html
<!DOCTYPE html>
<meta charset='utf-8'>
<style>
.baseMap{
stroke-width:0.8px;
stroke:white;
fill:#E4E5E6;
opacity:0.9;
}
.cities_start{
fill:rgba(199,70,70,.8);
}
.cities_end{
fill:rgba(29, 168, 183, 1);
}
.line{
stroke:rgba(199,70,70,.7);
stroke-width:3px;
fill:none;
stroke-dasharray:3, 3;
}
.geo-globe {
fill: rgba(236,249,255,0.8);
}
</style>
<body>
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/queue-async/1.0.7/queue.min.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/topojson/1.6.19/topojson.min.js'></script>
<script src='arc.js'></script>
<div id='map'>
<script>
var margin = {top: 30, right: 30, bottom: 30, left: 30},
width = 800 - margin.left - margin.right,
height = 600 - margin.top - margin.bottom;
var projection = d3.geo.orthographic().scale(280).translate([400,300]).clipAngle(90).precision(0.5);
var path = d3.geo.path()
.projection(projection);
var voronoi = d3.geom.voronoi()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.clipExtent([[0, 0], [width, height]]);
var svg = d3.select('#map').append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.attr('class', 'graph-svg-component')
.call(responsivefy)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
var backgroundCircle = svg.append("circle")
.attr('cx', width / 1.85)
.attr('cy', height / 1.84099)
.attr('r', 0)
.attr('class', 'geo-globe');
backgroundCircle.attr('r', projection.scale());
queue()
.defer(d3.json, 'world_countries.json')
.defer(d3.csv, 'data.csv')
.await(ready);
function ready(error, world, data) {
if (error) throw error;
data.forEach(
function(d){
d.end_lat = +d.end_lat;
d.end_long = +d.end_long;
d.start_lat = +d.start_lat;
d.start_long = +d.start_long;
d.greatcircle = new arc.GreatCircle({x:d.start_long, y:d.start_lat}, {x:d.end_long, y:d.end_lat});
d.line = d.greatcircle.Arc(100, {offset:10});
d.arc = d.line.json();
}
);
svg.selectAll('path')
.data(world.features)
.enter()
.append('path')
.attr('d', path)
.attr('class','baseMap');
svg.selectAll('.cities_start')
.data(data)
.enter()
.append('circle')
.attr('cx', function(d){ return projection([d.start_long, d.start_lat])[0]})
.attr('cy', function(d){ return projection([d.start_long, d.start_lat])[1]})
.attr('r', '3')
.attr('class','cities_start');
svg.selectAll('.cities_end')
.data(data)
.enter()
.append('circle')
.attr('cx', function(d){ return projection([d.end_long, d.end_lat])[0]})
.attr('cy', function(d){ return projection([d.end_long, d.end_lat])[1]})
.attr('r', '3')
.attr('class','cities_end');
svg.append("g")
.attr("class", "line")
.selectAll(".arcs")
.data(data.map(function(d){ return d.arc; }))
.enter()
.append("path")
.attr("d", path);
}
d3.select("svg").call(
d3.behavior.drag()
.origin(function() {
var r = projection.rotate();
return {x: r[0], y: -r[1]};
})
.on("drag", function() {
var r = projection.rotate();
projection.rotate([d3.event.x, -d3.event.y, r[2]]);
svg.selectAll("path").attr("d",path);
svg.selectAll(".cities_start")
.attr('cx', function(d){ return projection([d.start_long, d.start_lat])[0]})
.attr('cy', function(d){ return projection([d.start_long, d.start_lat])[1]})
svg.selectAll(".cities_end")
.attr('cx', function(d){ return projection([d.end_long, d.end_lat])[0]})
.attr('cy', function(d){ return projection([d.end_long, d.end_lat])[1]})
})
);
function responsivefy(svg) {
var container = d3.select(svg.node().parentNode),
width = parseInt(svg.style('width')),
height = parseInt(svg.style('height')),
aspect = width / height;
svg.attr('viewBox', '0 0 ' + width + ' ' + height)
.attr('perserveAspectRatio', 'xMinYMid')
.call(resize);
d3.select(window).on('resize', resize);
function resize() {
var targetWidth = parseInt(container.style('width'));
svg.attr('width', targetWidth);
svg.attr('height', Math.round(targetWidth / aspect));
}
}
</script>
arc.js
'use strict';
var D2R = Math.PI / 180;
var R2D = 180 / Math.PI;
var Coord = function(lon,lat) {
this.lon = lon;
this.lat = lat;
this.x = D2R * lon;
this.y = D2R * lat;
};
Coord.prototype.view = function() {
return String(this.lon).slice(0, 4) + ',' + String(this.lat).slice(0, 4);
};
Coord.prototype.antipode = function() {
var anti_lat = -1 * this.lat;
var anti_lon = (this.lon < 0) ? 180 + this.lon : (180 - this.lon) * -1;
return new Coord(anti_lon, anti_lat);
};
var LineString = function() {
this.coords = [];
this.length = 0;
};
LineString.prototype.move_to = function(coord) {
this.length++;
this.coords.push(coord);
};
var Arc = function(properties) {
this.properties = properties || {};
this.geometries = [];
};
Arc.prototype.json = function() {
if (this.geometries.length <= 0) {
return {'geometry': { 'type': 'LineString', 'coordinates': null },
'type': 'Feature', 'properties': this.properties
};
} else if (this.geometries.length == 1) {
return {'geometry': { 'type': 'LineString', 'coordinates': this.geometries[0].coords },
'type': 'Feature', 'properties': this.properties
};
} else {
var multiline = [];
for (var i = 0; i < this.geometries.length; i++) {
multiline.push(this.geometries[i].coords);
}
return {'geometry': { 'type': 'MultiLineString', 'coordinates': multiline },
'type': 'Feature', 'properties': this.properties
};
}
};
Arc.prototype.wkt = function() {
var wkt_string = '';
var wkt = 'LINESTRING(';
var collect = function(c) { wkt += c[0] + ' ' + c[1] + ','; };
for (var i = 0; i < this.geometries.length; i++) {
if (this.geometries[i].coords.length === 0) {
return 'LINESTRING(empty)';
} else {
var coords = this.geometries[i].coords;
coords.forEach(collect);
wkt_string += wkt.substring(0, wkt.length - 1) + ')';
}
}
return wkt_string;
};
var GreatCircle = function(start,end,properties) {
if (!start || start.x === undefined || start.y === undefined) {
throw new Error("GreatCircle constructor expects two args: start and end objects with x and y properties");
}
if (!end || end.x === undefined || end.y === undefined) {
throw new Error("GreatCircle constructor expects two args: start and end objects with x and y properties");
}
this.start = new Coord(start.x,start.y);
this.end = new Coord(end.x,end.y);
this.properties = properties || {};
var w = this.start.x - this.end.x;
var h = this.start.y - this.end.y;
var z = Math.pow(Math.sin(h / 2.0), 2) +
Math.cos(this.start.y) *
Math.cos(this.end.y) *
Math.pow(Math.sin(w / 2.0), 2);
this.g = 2.0 * Math.asin(Math.sqrt(z));
if (this.g == Math.PI) {
throw new Error('it appears ' + start.view() + ' and ' + end.view() + " are 'antipodal', e.g diametrically opposite, thus there is no single route but rather infinite");
} else if (isNaN(this.g)) {
throw new Error('could not calculate great circle between ' + start + ' and ' + end);
}
};
GreatCircle.prototype.interpolate = function(f) {
var A = Math.sin((1 - f) * this.g) / Math.sin(this.g);
var B = Math.sin(f * this.g) / Math.sin(this.g);
var x = A * Math.cos(this.start.y) * Math.cos(this.start.x) + B * Math.cos(this.end.y) * Math.cos(this.end.x);
var y = A * Math.cos(this.start.y) * Math.sin(this.start.x) + B * Math.cos(this.end.y) * Math.sin(this.end.x);
var z = A * Math.sin(this.start.y) + B * Math.sin(this.end.y);
var lat = R2D * Math.atan2(z, Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)));
var lon = R2D * Math.atan2(y, x);
return [lon, lat];
};
GreatCircle.prototype.Arc = function(npoints,options) {
var first_pass = [];
if (!npoints || npoints <= 2) {
first_pass.push([this.start.lon, this.start.lat]);
first_pass.push([this.end.lon, this.end.lat]);
} else {
var delta = 1.0 / (npoints - 1);
for (var i = 0; i < npoints; ++i) {
var step = delta * i;
var pair = this.interpolate(step);
first_pass.push(pair);
}
}
var bHasBigDiff = false;
var dfMaxSmallDiffLong = 0;
var dfDateLineOffset = options && options.offset ? options.offset : 10;
var dfLeftBorderX = 180 - dfDateLineOffset;
var dfRightBorderX = -180 + dfDateLineOffset;
var dfDiffSpace = 360 - dfDateLineOffset;
for (var j = 1; j < first_pass.length; ++j) {
var dfPrevX = first_pass[j-1][0];
var dfX = first_pass[j][0];
var dfDiffLong = Math.abs(dfX - dfPrevX);
if (dfDiffLong > dfDiffSpace &&
((dfX > dfLeftBorderX && dfPrevX < dfRightBorderX) || (dfPrevX > dfLeftBorderX && dfX < dfRightBorderX))) {
bHasBigDiff = true;
} else if (dfDiffLong > dfMaxSmallDiffLong) {
dfMaxSmallDiffLong = dfDiffLong;
}
}
var poMulti = [];
if (bHasBigDiff && dfMaxSmallDiffLong < dfDateLineOffset) {
var poNewLS = [];
poMulti.push(poNewLS);
for (var k = 0; k < first_pass.length; ++k) {
var dfX0 = parseFloat(first_pass[k][0]);
if (k > 0 && Math.abs(dfX0 - first_pass[k-1][0]) > dfDiffSpace) {
var dfX1 = parseFloat(first_pass[k-1][0]);
var dfY1 = parseFloat(first_pass[k-1][1]);
var dfX2 = parseFloat(first_pass[k][0]);
var dfY2 = parseFloat(first_pass[k][1]);
if (dfX1 > -180 && dfX1 < dfRightBorderX && dfX2 == 180 &&
k+1 < first_pass.length &&
first_pass[k-1][0] > -180 && first_pass[k-1][0] < dfRightBorderX)
{
poNewLS.push([-180, first_pass[k][1]]);
k++;
poNewLS.push([first_pass[k][0], first_pass[k][1]]);
continue;
} else if (dfX1 > dfLeftBorderX && dfX1 < 180 && dfX2 == -180 &&
k+1 < first_pass.length &&
first_pass[k-1][0] > dfLeftBorderX && first_pass[k-1][0] < 180)
{
poNewLS.push([180, first_pass[k][1]]);
k++;
poNewLS.push([first_pass[k][0], first_pass[k][1]]);
continue;
}
if (dfX1 < dfRightBorderX && dfX2 > dfLeftBorderX)
{
var tmpX = dfX1;
dfX1 = dfX2;
dfX2 = tmpX;
var tmpY = dfY1;
dfY1 = dfY2;
dfY2 = tmpY;
}
if (dfX1 > dfLeftBorderX && dfX2 < dfRightBorderX) {
dfX2 += 360;
}
if (dfX1 <= 180 && dfX2 >= 180 && dfX1 < dfX2)
{
var dfRatio = (180 - dfX1) / (dfX2 - dfX1);
var dfY = dfRatio * dfY2 + (1 - dfRatio) * dfY1;
poNewLS.push([first_pass[k-1][0] > dfLeftBorderX ? 180 : -180, dfY]);
poNewLS = [];
poNewLS.push([first_pass[k-1][0] > dfLeftBorderX ? -180 : 180, dfY]);
poMulti.push(poNewLS);
}
else
{
poNewLS = [];
poMulti.push(poNewLS);
}
poNewLS.push([dfX0, first_pass[k][1]]);
} else {
poNewLS.push([first_pass[k][0], first_pass[k][1]]);
}
}
} else {
var poNewLS0 = [];
poMulti.push(poNewLS0);
for (var l = 0; l < first_pass.length; ++l) {
poNewLS0.push([first_pass[l][0],first_pass[l][1]]);
}
}
var arc = new Arc(this.properties);
for (var m = 0; m < poMulti.length; ++m) {
var line = new LineString();
arc.geometries.push(line);
var points = poMulti[m];
for (var j0 = 0; j0 < points.length; ++j0) {
line.move_to(points[j0]);
}
}
return arc;
};
if (typeof window === 'undefined') {
module.exports.Coord = Coord;
module.exports.Arc = Arc;
module.exports.GreatCircle = GreatCircle;
} else {
var arc = {};
arc.Coord = Coord;
arc.Arc = Arc;
arc.GreatCircle = GreatCircle;
}
data.csv
end_lat,end_long,start_lat,start_long,end_country,start_country
35.8592948,104.1361117,23.5,121,大陸地區,台灣
34.7857324,134.3756902,23.5,121,日本,台灣
37.6,-95.665,23.5,121,美國,台灣
35.8615124,127.096405,23.5,121,大韓民國(南韓),台灣
51.1719674,10.4541194,23.5,121,德意志聯邦共和國,台灣
46.2157467,2.2088258,23.5,121,法國,台灣
55.3632592,-3.4433238,23.5,121,英國,台灣
52.2129919,5.2793703,23.5,121,荷蘭王國,台灣
49.8037633,15.4749126,23.5,121,捷克共和國,台灣
56,-96,23.5,121,加拿大,台灣
62.1983366,17.5652566,23.5,121,瑞典王國,台灣
-27.9210555,133.247866,23.5,121,澳大利亞,台灣
22.3576782,114.1210181,23.5,121,香港,台灣
40.2085,-3.713,23.5,121,西班牙王國,台灣
47.696472,13.3457348,23.5,121,奧地利共和國,台灣
13.03887,101.490104,23.5,121,泰王國(泰國),台灣
1.3147308,103.8470128,23.5,121,新加坡共和國,台灣
4.140634,109.6181485,23.5,121,馬來西亞,台灣
64.9146659,26.0672553,23.5,121,芬蘭共和國,台灣
11.6978351,122.6217542,23.5,121,菲律賓共和國,台灣