|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
<style> |
|
body { |
|
background: #fcfcfa; |
|
height: 500px; |
|
position: relative; |
|
width: 960px; |
|
} |
|
.border { |
|
fill: black; |
|
stroke: black; |
|
|
|
} |
|
.clip { |
|
fill: #fcfcfa; |
|
} |
|
|
|
.stroke { |
|
fill: none; |
|
stroke: #000; |
|
stroke-width: 3px; |
|
} |
|
.fill { |
|
fill: #ccc; |
|
} |
|
.graticule { |
|
fill: none; |
|
stroke: #777; |
|
stroke-width: .5px; |
|
stroke-opacity: .5; |
|
} |
|
.sphere { |
|
fill: #7BB5FF; |
|
} |
|
.gradient { |
|
fill: url(#gradient); |
|
} |
|
.country { |
|
fill: #336633; |
|
/*fill-opacity: 0.7;*/ |
|
stroke: #fff; |
|
stroke-width: 0.5px; |
|
} |
|
.equator { |
|
stroke: black; |
|
stroke-width: 0.5px; |
|
} |
|
.cityname { |
|
fill-opacity: 1; |
|
fill: black; |
|
font-size:10px; |
|
font-family: "Arial", sans-serif; |
|
} |
|
.city { |
|
fill: red; |
|
stroke: none; |
|
} |
|
.satellite { |
|
fill: blue; |
|
} |
|
.beam { |
|
fill: red; |
|
fill-opacity: 0.15; |
|
stroke: #B10000; |
|
stroke-linejoin: round; |
|
stroke-width: 1; |
|
pointer-events: none; /* ensures that mouseover countries works thru the beam */ |
|
} |
|
.ticks { |
|
font: 10px sans-serif; |
|
} |
|
|
|
.track, |
|
.track-inset, |
|
.track-overlay { |
|
stroke-linecap: round; |
|
} |
|
|
|
.track { |
|
stroke: #000; |
|
stroke-opacity: 0.3; |
|
stroke-width: 10px; |
|
} |
|
|
|
.track-inset { |
|
stroke: #ddd; |
|
stroke-width: 8px; |
|
} |
|
|
|
.track-overlay { |
|
pointer-events: stroke; |
|
stroke-width: 50px; |
|
stroke: transparent; |
|
cursor: crosshair; |
|
} |
|
|
|
.handle { |
|
fill: #fff; |
|
stroke: #000; |
|
stroke-opacity: 0.5; |
|
stroke-width: 1.25px; |
|
} |
|
|
|
div.tooltip { |
|
color: #222; |
|
background: #fff; |
|
border-radius: 3px; |
|
box-shadow: 0px 0px 2px 0px #a6a6a6; |
|
padding: .2em; |
|
text-shadow: #f5f5f5 0 1px 0; |
|
opacity: 0.9; |
|
position: absolute; |
|
font-size:12px; |
|
font-family: "Arial", sans-serif; |
|
} |
|
.hidden { |
|
display: none; |
|
} |
|
</style> |
|
<div id="map"></div> |
|
<span id="projection-menu"></span> |
|
<script src="//d3js.org/d3.v4.min.js"></script> |
|
<script src="//d3js.org/queue.v1.min.js"></script> |
|
<script src="//d3js.org/d3-geo-projection.v1.min.js"></script> |
|
<script src="//d3js.org/topojson.v2.min.js"></script> |
|
<script> |
|
var width = 960, |
|
height = 800, |
|
longitude = 0; // initial longitude |
|
var stationpoints; //track states |
|
var stationnames; |
|
|
|
// the projection - the globe in this case |
|
var projection = d3.geoOrthographic() |
|
.center([0, 10]) |
|
.scale(290) |
|
.rotate([longitude,0]) |
|
|
|
var path = d3.geoPath(projection).pointRadius(4); |
|
; // projection needs a path |
|
var graticule = d3.geoGraticule(); // the long/lat lines |
|
var sphere = {type: "Sphere"}; // a sphere for the nice blue sea |
|
|
|
var svg = d3.select("body").append("svg") |
|
.attr("width", width) |
|
.attr("height", height) |
|
|
|
// setup the filter for the country labels |
|
var defs = svg.append("defs") |
|
var filter = defs.append("filter") |
|
.attr("x","0") |
|
.attr("y","0") |
|
.attr("width","1") |
|
.attr("height","1") |
|
.attr("id","solid") |
|
filter.append("feFlood") |
|
.attr("flood-color","#ffff99") |
|
.attr("flood-opacity","0.5") |
|
filter.append("feComposite") |
|
.attr("in","SourceGraphic") |
|
// end filter for country labels |
|
|
|
// setup the gradient to make the earth look brighter at top left |
|
var gradient = svg.append("svg:defs") |
|
.append("svg:linearGradient") |
|
.attr("id", "gradient") |
|
.attr("x1", "0%") |
|
.attr("y1", "0%") |
|
.attr("fx1", "50%") |
|
.attr("fy1", "50%") |
|
.attr("x2", "100%") |
|
.attr("y2", "100%") |
|
.attr("spreadMethod", "pad"); |
|
gradient.append("svg:stop") // middle step setting |
|
.attr("offset", "50%") |
|
.attr("stop-color", "#fff") |
|
.attr("stop-opacity", 0.6); |
|
gradient.append("svg:stop") // final step setting |
|
.attr("offset", "100%") |
|
.attr("stop-color", "#006") |
|
.attr("stop-opacity", 0.6); |
|
// end setup gradient |
|
|
|
// the frame around the earth |
|
svg.append("rect") |
|
.attr("class","border") |
|
.attr("x",0) |
|
.attr("y",0) |
|
.attr("width", 960) |
|
.attr("height", 600) |
|
|
|
var g = svg.append("g") |
|
|
|
// Draw the sphere for the earth |
|
g.append("path") |
|
.datum(sphere) |
|
.attr("class", "sphere") |
|
.attr("d", path) |
|
|
|
// draw a gradient sphere because it looks cool |
|
g.append("path") |
|
.datum(sphere) |
|
.attr("class", "gradient") |
|
.attr("d", path) |
|
|
|
// TODO: use d3-tip - add ground station data and satellite data? |
|
// Some code for the country label tooltip |
|
var offsetL = document.getElementById('map').offsetLeft+10; |
|
var offsetT = document.getElementById('map').offsetTop+10; |
|
|
|
var tooltip = d3.select("#map") |
|
.append("div") |
|
.attr("class", "tooltip hidden"); |
|
|
|
// TODO: use d3-queue ?? |
|
// load data for drawing the world, countries and earth stations |
|
queue() |
|
.defer(d3.json, "world-110m.json") // world map topojson |
|
.defer(d3.tsv, "countries.tsv") // country name data |
|
.defer(d3.csv, "stations.csv") // the earth station locations |
|
.await(ready); |
|
|
|
// draw the world, countries and stations |
|
function ready(error, world, names, stations) { |
|
if (error) throw error; |
|
|
|
// load country data features from the topojson |
|
var countries = topojson.feature(world, world.objects.countries).features; |
|
|
|
// folds the TSV country name data (separate file) into the |
|
// countries object |
|
countries = countries.filter(function(d) { |
|
return names.some(function(n) { |
|
if (d.id == n.id) return d.name = n.name; |
|
}); |
|
}).sort(function(a, b) { |
|
return a.name.localeCompare(b.name); |
|
}); |
|
|
|
// draw the earth stations - these are appended as topojson points |
|
// therefore they rotate with the earth |
|
g.selectAll(".station") |
|
.data(stations) |
|
.enter() |
|
.append("path") |
|
.attr("class", function(d) { |
|
if (d.zl == 0) { |
|
return "city" |
|
} else { |
|
return "city hidden" |
|
} |
|
}) |
|
.datum(function(d){ |
|
return {type: "Point", coordinates:[d.lon,d.lat] } |
|
}) |
|
.attr("d", path) |
|
|
|
|
|
// the station labels, these are positioned by a functions, which means |
|
// we can refresh them as the globe is rotated |
|
g.selectAll("text") |
|
.data(stations) |
|
.enter() |
|
.append("text") |
|
.attr("class", function(d) { |
|
if (d.zl == 0) { |
|
return "cityname" |
|
} else { |
|
return "cityname hidden" |
|
} |
|
}) |
|
.attr("dy", 10) // set y position relative |
|
.attr("text-anchor", "middle") // set anchor y justification |
|
.attr("dominant-baseline", "middle") // set x justification |
|
.text(function(d) { return d.station }) |
|
|
|
// show countries with some sexy mouseover effects |
|
g.selectAll(".country") |
|
.data(countries) |
|
.enter().insert("path", ".graticule") |
|
.attr("class", "country") |
|
.attr("d", path) |
|
.on('mouseenter', function(d, i) { |
|
d3.select(this).style('fill','#339933'); |
|
}) |
|
.on('mouseleave', function(d, i) { |
|
d3.select(this).style('fill','#336633'); |
|
tooltip.classed("hidden", true); |
|
}) |
|
.on("mousemove", function(d) { |
|
label = d.name; |
|
var mouse = d3.mouse(svg.node()) |
|
.map( function(d) { return parseInt(d); } ); |
|
tooltip.classed("hidden", false) |
|
.attr("style", "left:"+(mouse[0]+offsetL)+"px;top:"+(mouse[1]+offsetT)+"px") |
|
.html(label); |
|
}) |
|
|
|
stationpoints = d3.selectAll(".city"); |
|
stationnames = d3.selectAll(".cityname"); |
|
|
|
positionCities() // draw the city dots and labels using current longitude |
|
|
|
}; |
|
|
|
// draw the graticule on top of the sphere and countries |
|
g.append("path") |
|
.datum(graticule) |
|
.attr("class", "graticule") |
|
.attr("d", path) |
|
|
|
// This function redrawa the earth station labels in the |
|
// correct position as the globe rotates and hides them |
|
// if they are behind the earth |
|
function positionCities() { |
|
projection.rotate([longitude,0]); |
|
g.selectAll(".cityname") |
|
.attr("x", function(d) { |
|
return projection([d.lon, d.lat])[0]; |
|
}) |
|
.attr("y", function(d) { |
|
return projection([d.lon, d.lat])[1]; |
|
}) |
|
// determine visibility and hide label if behind earth |
|
.attr("visibility", function(d) { |
|
var diff = (Math.abs(longitude + parseFloat(d.lon)) + 90) % 360; |
|
if (diff < 180) { |
|
return 'visible'; |
|
} else { |
|
return 'hidden'; |
|
} |
|
}) |
|
// this is the text background - again need to hide when it is |
|
// behind the earth |
|
.attr("filter", function(d) { |
|
var diff = (Math.abs(longitude + parseFloat(d.lon)) + 90) % 360; |
|
if (diff < 180) { |
|
return 'url(#solid)'; |
|
} else { |
|
return ''; |
|
} |
|
}) |
|
} |
|
|
|
// Draw the Equator |
|
g.append("path") |
|
.datum({type: "LineString", coordinates: |
|
[[-180, 0], [-90, 0], [0, 0], [90, 0], [180, 0]]}) |
|
.attr("class", "equator") |
|
.attr("d", path); |
|
|
|
// Draw the beams isobands |
|
// TODO: load from a file |
|
g.append("path") |
|
.datum({type: "Polygon", coordinates: |
|
[[[0, 55],[40,45],[50,0],[0, -33],[-37, 17],[0, 55]]]}) |
|
.attr("class", "beam") |
|
.attr("d", path); |
|
|
|
g.append("path") |
|
.datum({type: "Polygon", coordinates: |
|
[[[0, 45],[40,25],[40,0],[0, -3],[-27, 20],[0, 55]]]}) |
|
.attr("class", "beam") |
|
.attr("d", path); |
|
|
|
g.append("path") |
|
.datum({type: "Polygon", coordinates: |
|
[[[0, 35],[30,25],[20,10],[0, 10],[-7, 20],[0, 55]]]}) |
|
.attr("class", "beam") |
|
.attr("d", path); |
|
|
|
|
|
// zoom and pan stuff |
|
var zoom = d3.zoom() |
|
.scaleExtent([1, 10]) |
|
.translateExtent([[0,0],[960,600]]) |
|
.extent([[0,0],[960,460]]) |
|
.on("zoom",function() { |
|
g.attr("transform", d3.event.transform); |
|
var k = d3.event.transform["k"] // get the current zoom level |
|
// change cityname font-size / dy as zoom in |
|
g.selectAll(".cityname") |
|
.attr("style", function(d) { |
|
var fs = 10 - (0.7*k); |
|
return "font-size:"+fs+"px"; |
|
}) |
|
.attr("dy", function(d) { |
|
var fs = 10 - (0.7*k); |
|
return fs; |
|
}) |
|
// change city label size as zoom in |
|
g.selectAll(".city") |
|
.attr("d", path.pointRadius(4 - (0.3 * k))); |
|
|
|
// show selected stations only when zoomed in |
|
stationnames.classed('hidden', function(d) { |
|
if (k > 6) { |
|
return false; |
|
} else { |
|
if (parseInt(d.zl) > 1) { |
|
return true; |
|
} else { |
|
return false; |
|
} |
|
} |
|
}) |
|
|
|
// show selected stations only when zoomed in |
|
stationpoints.classed('hidden', function(d) { |
|
if (k > 6) { |
|
return false; |
|
} else { |
|
if (parseInt(d.zl) > 1) { |
|
return true; |
|
} else { |
|
return false; |
|
} |
|
} |
|
}) |
|
}); |
|
|
|
svg.call(zoom) |
|
|
|
// this is just to provide a background for the slider and the satellites |
|
// so that when the earth is zoomed it is clipped |
|
svg.append("rect") |
|
.attr("class","clip") |
|
.attr("x",0) |
|
.attr("y",600) |
|
.attr("width", 960) |
|
.attr("height", 600) |
|
|
|
// linear scale for the slider |
|
var x = d3.scaleLinear() |
|
.domain([180, -180]) |
|
.range([0, width]) |
|
.clamp(true); |
|
|
|
var slider = svg.append("g") |
|
.attr("class", "slider") |
|
.attr("transform", "translate(0 620)") |
|
|
|
slider.append("line") |
|
.attr("class", "track") |
|
.attr("x1", x.range()[0]) |
|
.attr("x2", x.range()[1]) |
|
.select(function() { |
|
return this.parentNode.appendChild(this.cloneNode(true)); |
|
}) |
|
.attr("class", "track-inset") |
|
.select(function() { |
|
return this.parentNode.appendChild(this.cloneNode(true)); |
|
}) |
|
.attr("class", "track-overlay") |
|
.call(d3.drag() |
|
.on("start.interrupt", function() { slider.interrupt(); }) |
|
.on("start drag", function() { slide(x.invert(d3.event.x)); })); |
|
|
|
slider.insert("g", ".track-overlay") |
|
.attr("class", "ticks") |
|
.attr("transform", "translate(0," + 18 + ")") |
|
.selectAll("text") |
|
.data(x.ticks(10)) |
|
.enter().append("text") |
|
.attr("x", x) |
|
.attr("text-anchor", "middle") |
|
.text(function(d) { return d + "°"; }); |
|
|
|
var handle = slider.insert("circle", ".track-overlay") |
|
.attr("class", "handle") |
|
.attr("cx", 480) |
|
.attr("id", "longitude") |
|
.attr("r", 9); |
|
|
|
var feature = g.selectAll("path"); |
|
|
|
function slide(h) { |
|
handle.attr("cx", x(h)); |
|
longitude = h; |
|
setGlobeRotation() |
|
} |
|
|
|
// function to rotate the globe to current longitude |
|
function setGlobeRotation() { |
|
positionCities() |
|
g.selectAll("path").attr("d", path); |
|
} |
|
|
|
// the satellites g element |
|
var sats = svg.append("g") |
|
.attr("class", "sats") |
|
.attr("transform", "translate(0 660)") |
|
|
|
// read the satellites file, draw the sats |
|
// TODO: handle when multiple satellites have the same longitude |
|
d3.csv("satellites.csv", function(error, satellites) { |
|
// satellite marker |
|
// TODO: make it sexier |
|
sats.selectAll("rect") |
|
.data(satellites) |
|
.enter() |
|
.append("rect") |
|
.attr("x", function(d) { |
|
return x(d.longitude) - 5; |
|
}) |
|
.attr("y", -10) |
|
.attr("height", 20) |
|
.attr("width", 10) |
|
.attr("class","satellite") |
|
.on("click", function(d){ |
|
// move slider - this updates the globe rotation and labels |
|
slide(x.invert(x(d.longitude))) |
|
}) |
|
|
|
// satellite names |
|
sats.selectAll("text") |
|
.data(satellites) |
|
.enter() |
|
.append("text") // append text |
|
.attr("x", function(d) { |
|
return x(d.longitude); |
|
}) |
|
.attr("dx", 15) // set y position of bottom of text |
|
.attr("class", "cityname") |
|
.attr("text-anchor", "start") // set anchor y justification |
|
.attr("alignment-baseline", "middle") // set anchor y justification |
|
.text(function(d) { |
|
return d.name; |
|
}) |
|
}); |
|
|
|
|
|
</script> |