Zoomable/rotatable world globe that uses orthographic projection. Drag behavior is enhanced as described here: https://www.jasondavies.com/maps/rotate/
Performance is not good due to redrawing whole world upon zoom/drag.
Zoomable/rotatable world globe that uses orthographic projection. Drag behavior is enhanced as described here: https://www.jasondavies.com/maps/rotate/
Performance is not good due to redrawing whole world upon zoom/drag.
| // Copyright (c) 2013, Jason Davies, http://www.jasondavies.com | |
| // See LICENSE.txt for details. | |
| (function() { | |
| var radians = Math.PI / 180, | |
| degrees = 180 / Math.PI; | |
| // TODO make incremental rotate optional | |
| d3.geo.zoom = function() { | |
| var projection, | |
| zoomPoint, | |
| event = d3.dispatch("zoomstart", "zoom", "zoomend"), | |
| zoom = d3.behavior.zoom() | |
| .on("zoomstart", function() { | |
| var mouse0 = d3.mouse(this), | |
| rotate = quaternionFromEuler(projection.rotate()), | |
| point = position(projection, mouse0); | |
| if (point) zoomPoint = point; | |
| zoomOn.call(zoom, "zoom", function() { | |
| projection.scale(d3.event.scale); | |
| var mouse1 = d3.mouse(this), | |
| between = rotateBetween(zoomPoint, position(projection, mouse1)); | |
| projection.rotate(eulerFromQuaternion(rotate = between | |
| ? multiply(rotate, between) | |
| : multiply(bank(projection, mouse0, mouse1), rotate))); | |
| mouse0 = mouse1; | |
| event.zoom.apply(this, arguments); | |
| }); | |
| event.zoomstart.apply(this, arguments); | |
| }) | |
| .on("zoomend", function() { | |
| zoomOn.call(zoom, "zoom", null); | |
| event.zoomend.apply(this, arguments); | |
| }), | |
| zoomOn = zoom.on; | |
| zoom.projection = function(_) { | |
| return arguments.length ? zoom.scale((projection = _).scale()) : projection; | |
| }; | |
| return d3.rebind(zoom, event, "on"); | |
| }; | |
| function bank(projection, p0, p1) { | |
| var t = projection.translate(), | |
| angle = Math.atan2(p0[1] - t[1], p0[0] - t[0]) - Math.atan2(p1[1] - t[1], p1[0] - t[0]); | |
| return [Math.cos(angle / 2), 0, 0, Math.sin(angle / 2)]; | |
| } | |
| function position(projection, point) { | |
| var t = projection.translate(), | |
| spherical = projection.invert(point); | |
| return spherical && isFinite(spherical[0]) && isFinite(spherical[1]) && cartesian(spherical); | |
| } | |
| function quaternionFromEuler(euler) { | |
| var λ = .5 * euler[0] * radians, | |
| φ = .5 * euler[1] * radians, | |
| γ = .5 * euler[2] * radians, | |
| sinλ = Math.sin(λ), cosλ = Math.cos(λ), | |
| sinφ = Math.sin(φ), cosφ = Math.cos(φ), | |
| sinγ = Math.sin(γ), cosγ = Math.cos(γ); | |
| return [ | |
| cosλ * cosφ * cosγ + sinλ * sinφ * sinγ, | |
| sinλ * cosφ * cosγ - cosλ * sinφ * sinγ, | |
| cosλ * sinφ * cosγ + sinλ * cosφ * sinγ, | |
| cosλ * cosφ * sinγ - sinλ * sinφ * cosγ | |
| ]; | |
| } | |
| function multiply(a, b) { | |
| var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], | |
| b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3]; | |
| return [ | |
| a0 * b0 - a1 * b1 - a2 * b2 - a3 * b3, | |
| a0 * b1 + a1 * b0 + a2 * b3 - a3 * b2, | |
| a0 * b2 - a1 * b3 + a2 * b0 + a3 * b1, | |
| a0 * b3 + a1 * b2 - a2 * b1 + a3 * b0 | |
| ]; | |
| } | |
| function rotateBetween(a, b) { | |
| if (!a || !b) return; | |
| var axis = cross(a, b), | |
| norm = Math.sqrt(dot(axis, axis)), | |
| halfγ = .5 * Math.acos(Math.max(-1, Math.min(1, dot(a, b)))), | |
| k = Math.sin(halfγ) / norm; | |
| return norm && [Math.cos(halfγ), axis[2] * k, -axis[1] * k, axis[0] * k]; | |
| } | |
| function eulerFromQuaternion(q) { | |
| return [ | |
| Math.atan2(2 * (q[0] * q[1] + q[2] * q[3]), 1 - 2 * (q[1] * q[1] + q[2] * q[2])) * degrees, | |
| Math.asin(Math.max(-1, Math.min(1, 2 * (q[0] * q[2] - q[3] * q[1])))) * degrees, | |
| Math.atan2(2 * (q[0] * q[3] + q[1] * q[2]), 1 - 2 * (q[2] * q[2] + q[3] * q[3])) * degrees | |
| ]; | |
| } | |
| function cartesian(spherical) { | |
| var λ = spherical[0] * radians, | |
| φ = spherical[1] * radians, | |
| cosφ = Math.cos(φ); | |
| return [ | |
| cosφ * Math.cos(λ), | |
| cosφ * Math.sin(λ), | |
| Math.sin(φ) | |
| ]; | |
| } | |
| function dot(a, b) { | |
| for (var i = 0, n = a.length, s = 0; i < n; ++i) s += a[i] * b[i]; | |
| return s; | |
| } | |
| function cross(a, b) { | |
| return [ | |
| a[1] * b[2] - a[2] * b[1], | |
| a[2] * b[0] - a[0] * b[2], | |
| a[0] * b[1] - a[1] * b[0] | |
| ]; | |
| } | |
| })(); |
| <!DOCTYPE html> | |
| <meta charset="utf-8"> | |
| <style> | |
| #globe{ | |
| background: #fcfcfa; | |
| width: 900px; | |
| height: 500px; | |
| margin-left: 50px; | |
| } | |
| .stroke { | |
| fill: none; | |
| stroke: #000; | |
| stroke-width: 3px; | |
| } | |
| .fill { | |
| fill: #fff; | |
| } | |
| .graticule { | |
| fill: none; | |
| stroke: #777; | |
| stroke-width: .5px; | |
| stroke-opacity: .5; | |
| } | |
| .land { | |
| fill: #222; | |
| } | |
| .boundary { | |
| fill: none; | |
| stroke: #fff; | |
| stroke-width: .5px; | |
| } | |
| .overlay { | |
| fill: none; | |
| pointer-events: all; | |
| } | |
| </style> | |
| <body> | |
| <script src="http://d3js.org/d3.v3.min.js"></script> | |
| <script src="http://d3js.org/topojson.v1.min.js"></script> | |
| <script src="d3.geo.zoom.js"></script> | |
| <div id="globe"></div> | |
| <script> | |
| var width = 680, | |
| height = 680; | |
| var projection = d3.geo.orthographic() | |
| .scale(270) | |
| .translate([width / 2, height / 2]) | |
| .clipAngle(90) | |
| .precision(.1); | |
| var zoom = d3.behavior.zoom() | |
| .scaleExtent([1,6]) | |
| .on("zoom",zoomed); | |
| var zoomEnhanced = d3.geo.zoom().projection(projection) | |
| .on("zoom",zoomedEnhanced); | |
| var drag = d3.behavior.drag() | |
| .origin(function() { var r = projection.rotate(); return {x: r[0], y: -r[1]}; }) | |
| .on("drag", dragged) | |
| .on("dragstart", dragstarted) | |
| .on("dragend", dragended); | |
| var path = d3.geo.path() | |
| .projection(projection); | |
| var graticule = d3.geo.graticule(); | |
| var svg = d3.select("#globe").append("svg") | |
| .attr("width", width) | |
| .attr("height", height); | |
| var pathG = svg.append("g"); | |
| svg.append("rect") | |
| .attr("class", "overlay") | |
| .attr("width", width) | |
| .attr("height", height) | |
| .call(zoomEnhanced) | |
| pathG.append("defs").append("path") | |
| .datum({type: "Sphere"}) | |
| .attr("id", "sphere") | |
| .attr("d", path); | |
| pathG.append("use") | |
| .attr("class", "stroke") | |
| .attr("xlink:href", "#sphere"); | |
| pathG.append("use") | |
| .attr("class", "fill") | |
| .attr("xlink:href", "#sphere"); | |
| pathG.append("path") | |
| .datum(graticule) | |
| .attr("class", "graticule") | |
| .attr("d", path); | |
| d3.json("worldTopo.json", function(error, world) { | |
| // to render meridians/graticules on top of lands, use insert which adds new path before graticule in the selection | |
| pathG.insert("path", ".graticule") | |
| .datum(topojson.feature(world, world.objects.land)) | |
| .attr("class", "land") | |
| .attr("d", path) | |
| pathG.insert("path", ".graticule") | |
| .datum(topojson.mesh(world, world.objects.countries, function(a, b) { return a !== b; })) | |
| .attr("class", "boundary") | |
| .attr("d", path); | |
| }); | |
| // apply transformations to map and all elements on it | |
| function zoomed() | |
| { | |
| pathG.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")"); | |
| //grids.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")"); | |
| //geofeatures.select("path.graticule").style("stroke-width", 0.5 / d3.event.scale); | |
| pathG.selectAll("path.boundary").style("stroke-width", 0.5 / d3.event.scale); | |
| } | |
| function zoomedEnhanced() | |
| { | |
| pathG.selectAll("path").attr("d",path); | |
| } | |
| function dragstarted(d) | |
| { | |
| //stopPropagation prevents dragging to "bubble up" which triggers same event for all elements below this object | |
| d3.event.sourceEvent.stopPropagation(); | |
| d3.select(this).classed("dragging", true); | |
| } | |
| function dragged() { | |
| projection.rotate([d3.event.x, -d3.event.y]); | |
| pathG.selectAll("path").attr("d", path); | |
| } | |
| function dragended(d) | |
| { | |
| d3.select(this).classed("dragging", false); | |
| } | |
| d3.select(self.frameElement).style("height", height + "px"); | |
| </script> |