Display a zoomable Orthographic projection with countries and some locations shown. Displays a longitude slider that allows the longitude to be set dynamically. The aim is to show which earth stations are visible from a satellite in geostationary orbit at the selected longitude.
Bits have been copied from many blocks
forked from rob4acre's block: Beam Coverage
xxxxxxxxxx
<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>
https://d3js.org/d3.v4.min.js
https://d3js.org/queue.v1.min.js
https://d3js.org/d3-geo-projection.v1.min.js
https://d3js.org/topojson.v2.min.js