Display a zoomable Orthographic projection with countries and some locations shown. Display 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 several blocks
xxxxxxxxxx
<meta charset="utf-8">
<style>
body {
background: #fcfcfa;
height: 500px;
position: relative;
width: 960px;
}
.border {
fill: #666;
stroke: black;
}
.clip {
fill: #fcfcfa;
}
.stroke {
fill: none;
stroke: #000;
stroke-width: 3px;
}
.fill {
fill: #ccc;
}
.graticule {
fill: none;
stroke: #777;
stroke-width: .5;
stroke-opacity: .5;
vector-effect: non-scaling-stroke;
}
.sphere {
fill: #7BB5FF;
}
.gradient {
fill: url(#gradient);
}
.country {
fill: #335533;
fill-opacity: 0.7;
stroke: #fff;
stroke-width: 0.5;
vector-effect: non-scaling-stroke;
}
.equator {
stroke: red;
stroke-width: 1px;
}
.cityname {
fill-opacity: 0.8;
fill:black;
font-size:8px;
font-family: "Arial", sans-serif;
background-color: yellow;
}
.satellite {
fill: blue;
}
.beam {
fill: yellow;
fill-opacity: 0.1;
stroke: red;
stroke-width: 0.5;
vector-effect: non-scaling-stroke;
/* 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;
}
.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;
var longitude = 0;
var i = 0;
var projection = d3.geoOrthographic()
.center([0, 10])
.scale(290)
.rotate([longitude,0])
var path = d3.geoPath(projection);
var graticule = d3.geoGraticule();
var sphere = {type: "Sphere"};
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
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","white")
filter.append("feComposite")
.attr("in","SourceGraphic")
var gradient = defs.append("svg:linearGradient")
.attr("id", "gradient")
.attr("x1", "0%")
.attr("y1", "0%")
.attr("fx1", "30%")
.attr("fy1", "30%")
.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.3);
gradient.append("svg:stop") // final step setting
.attr("offset", "100%")
.attr("stop-color", "#009")
.attr("stop-opacity", 0.3);
svg.append("rect")
.attr("class","border")
.attr("x",0)
.attr("y",0)
.attr("width", 960)
.attr("height", 600)
var g = svg.append("g")
g.append("path")
.datum(sphere)
.attr("class", "sphere")
.attr("d", path)
g.append("path")
.datum(sphere)
.attr("class", "gradient")
.attr("d", path)
g.append("path")
.datum(graticule)
.attr("class", "graticule")
.attr("d", path)
// load data
queue()
.defer(d3.json, "world-110m.json")
.defer(d3.json, "beam.json")
.defer(d3.tsv, "countries.tsv")
.defer(d3.csv, "stations.csv")
.await(ready);
function ready(error, world, isolines, names, stations) {
if (error) throw error;
var countries = topojson.feature(world, world.objects.countries).features;
// this folds the TSV country name data 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);
});
g.selectAll("circle")
.data(stations)
.enter()
// only visible cities
.append("circle")
.attr("cx", function(d) {
return projection([d.lon, d.lat])[0];
})
.attr("cy", function(d) {
return projection([d.lon, d.lat])[1];
})
.attr("r", 3)
.style("fill","red")
.attr("visibility", function(d) {
var mlon = (d.lon % 360) + longitude;
if (mlon > -90 && mlon < 90) {
return 'visible';
} else {
return 'hidden';
}
})
// TODO: Add labels and make rotate
g.selectAll("text")
.data(stations)
.enter()
.append("text") // append text
.attr("x", function(d) {
return projection([d.lon, d.lat])[0];
})
.attr("y", function(d) {
return projection([d.lon, d.lat])[1];
})
.attr("dx", 5) // 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.station;
})
.attr("visibility", function(d) {
var mlon = (d.lon % 360) + longitude;
if (mlon > -90 && mlon < 90) {
return 'visible';
} else {
return 'hidden';
}
})
.attr("filter", function(d) {
var mlon = (d.lon % 360) + longitude;
if (mlon > -90 && mlon < 90) {
return "url(#solid)";
} else {
return '';
}
})
// show countries
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-opacity', 1);
// })
// .on('mouseleave', function(d, i) {
// d3.select(this).style('fill-opacity',.7);
// 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);
// })
var geojson = topojson.feature(isolines, isolines.objects.TEST).features;
g.selectAll("isoline")
.data(geojson)
.enter().append("path")
.attr("class", "beam")
.attr("d", path)
.on('mouseenter', function(d, i) {
d3.select(this).style('fill-opacity', .3);
})
.on('mouseleave', function(d, i) {
d3.select(this).style('fill-opacity', .1);
tooltip.classed("hidden", true);
})
.on("mousemove", function(d) {
label = d.properties.Name + ' dB';
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);
})
};
// zoom and pan
var zoom = d3.zoom()
.scaleExtent([1, 19])
.translateExtent([[0,0],[960,600]])
.extent([[0,0],[960,460]])
.on("zoom",function() {
g.attr("transform", d3.event.transform)
g.selectAll("circle")
.attr("d", path.projection(projection));
});
svg.call(zoom)
svg.append("rect")
.attr("class","clip")
.attr("x",0)
.attr("y",600)
.attr("width", 960)
.attr("height", 600)
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");
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 setGlobeRotation() {
projection.rotate([longitude,0]);
g.selectAll("path").attr("d", path);
g.selectAll("circle")
.attr("cx", function(d) {
return projection([d.lon, d.lat])[0];
})
.attr("cy", function(d) {
return projection([d.lon, d.lat])[1];
})
.attr("visibility", function(d) {
var mlon = (d.lon % 360) + longitude;
if (mlon > -90 && mlon < 90) {
return 'visible';
} else {
return 'hidden';
}
})
g.selectAll("text")
.attr("x", function(d) {
return projection([d.lon, d.lat])[0];
})
.attr("y", function(d) {
return projection([d.lon, d.lat])[1];
})
.attr("visibility", function(d) {
var mlon = (d.lon % 360) + longitude;
if (mlon > -90 && mlon < 90) {
return 'visible';
} else {
return 'hidden';
}
})
.attr("filter", function(d) {
var mlon = (d.lon % 360) + longitude;
if (mlon > -90 && mlon < 90) {
return "url(#solid)";
} else {
return '';
}
})
}
function slide(h) {
handle.attr("cx", x(h));
longitude = h;
setGlobeRotation()
}
var sats = svg.append("g")
.attr("class", "sats")
.attr("transform", "translate(0 660)")
d3.csv("satellites.csv", function(error, satellites) {
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){
longitude = d.longitude;
setGlobeRotation();
handle.attr("cx",x(d.longitude))
})
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