// Zan Armstrong - May, 2015 // parts of the code adapted from Mike Bostock's Lambert Azimuthal Equal Area http://bl.ocks.org/mbostock/3757101 "use strict"; // set up variables var smallWidth = 300, smallHeight = 300, largeWidth = 600, largeHeight = 600; var margin = { top: 0 } var padding = 5; var transitionDuration = 800; // to hold land and border data var land; var borders; // land colors var colors = ["black", "lightgreen"] var comparisons = { "Sweden,Madagascar": { scale: 1120, latLon: [{ lat: 62.6, lon: 16.3 }, { lat: -19, lon: 46.7 }] }, "Australia,Antarctica": { scale: 470, latLon: [{ lat: -27, lon: 130 }, { lat: -90, lon: 0 }] }, "Europe,Brazil": { scale: 527, latLon: [{ lat: 56.44, lon: 17.1 }, { lat: -6.07833, lon: -53.2052 }] }, "US,Australia": { scale: 640, latLon: [{ lat: 35.65, lon: -97.24 }, { lat: -27, lon: 130 }] }, "South_America,Greenland": { scale: 458, latLon: [{ lat: -15.97, lon: -52.87 }, { lat: 65.88, lon: -42.21 }] }, "Brazil,US": { scale: 360, latLon: [{ lat: -16.7833, lon: -53.2052 }, { lat: 35.65, lon: -97.24 }] }, "Africa,North_America": { scale: 350, latLon: [{ lat: 6.52865, lon: 20.3586336 }, { lat: 48.2392291, lon: -98.9443219 }] }, "North_Africa,Russia": { scale: 470, latLon: [{ lat: 15.0, lon: 18.82 }, { lat: 60.65, lon: 95.995 }] }, "Saudi_Arabia,Alaska": { scale: 713, latLon: [{ lat: 22.389, lon: 46.59 }, { lat: 64.23, lon: -149.862 }] }, "Europe,Antarctica": { scale: 454, latLon: [{ lat: 56.44, lon: 17.1 }, { lat: -90, lon: 0 }] } } // define state, with default values var state = { scale: 450, latLon: [{ lat: 6.52865, lon: 20.3586336 }, { lat: 48.2392291, lon: -98.9443219 }] } // use hash fragment from URl to set state if (window.location.hash.split("&").length != 0) { var windowState = window.location.hash.split("&"); for (var i = 0; i < windowState.length; i++) { var k = windowState[i].replace('#', '').split('='); if (k[0] == "scale") { state.scale = +k[1]; } else if (k[0] == "center0") { state.latLon[0] = { lat: k[1].split(",")[0], lon: k[1].split(",")[1] }; } else if (k[0] == "center1") { state.latLon[1] = { lat: k[1].split(",")[0], lon: k[1].split(",")[1] }; } } } // define slider var zoomRange = [220, 1600] var zoomToBoxScale = d3.scale.linear().domain([470, 2000]).range([80, 20]); var slider = d3.select("#tool") .append("p") .append("input") .attr("type", "range") .style("margin-left", "650px") .style("width", 240 + "px") .attr("min", zoomRange[0]) .attr("max", zoomRange[1]) .attr("step", (zoomRange[1] - zoomRange[0]) / 400) .on("input", slided); slider.property("value", state.scale) function slided(d) { var duration = 0; //updateCenterBoxSize(zoomToBoxScale(state.scale), zoomToBoxScale(d3.select(this).property("value")), duration) state.scale = d3.select(this).property("value"); updateLargeScaleOnly(); updateHash() } // define canvas object and context for small canvases var canvasObj = [ d3.select("#small0").append("canvas") .attr("width", smallWidth) .attr("height", smallHeight), d3.select("#small1").append("canvas") .attr("width", smallWidth) .attr("height", smallHeight) ] var smallContext = [ canvasObj[0].node().getContext("2d"), canvasObj[1].node().getContext("2d") ] // set opacity smallContext[0].globalAlpha = 0.8 smallContext[1].globalAlpha = 0.8 // define canvas object and context for large canvas var largeCanvasObj = d3.select("#large").append("canvas") .attr("width", largeWidth) .attr("height", largeHeight) var largeCanvasContext = largeCanvasObj.node().getContext("2d") largeCanvasContext.globalAlpha = .9 var graticule = d3.geo.graticule()() var mapObjects = [] var largeMapObjects = [] // set up ranges/scales var zoomRange = [220, 1600] // var zoomToBoxScale = d3.scale.linear().domain([470, 2000]).range([80, 20]); // set up initial visable state for (var i = 0; i < 2; i++) { mapObjects[i] = setUpSmallMaps(state.latLon[i].lat, state.latLon[i].lon, i) largeMapObjects[i] = setUpLargeMaps(state.latLon[i].lat, state.latLon[i].lon, i) } // set up small maps function setUpSmallMaps(lat, lon, name) { var projectionSmall = d3.geo.orthographic() .translate([smallWidth / 2, smallHeight / 2]) .scale(smallWidth / 2 * .9) .center([0, 0]) .rotate([-lon, -lat]) .clipAngle(90) .precision(.7); var path = d3.geo.path() .projection(projectionSmall) .context(smallContext[name]); canvasObj[name].call(dragSetupSmall(name)) return { "projection": projectionSmall, "path": path, } } function setUpLargeMaps(lat, lon, name) { var projectionLarge = d3.geo.azimuthalEqualArea() .translate([largeWidth / 2, largeHeight / 2]) .scale(state.scale) .center([0, 0]) .clipAngle(180 - 1e-3) .clipExtent([ [2 * padding, 2 * padding], [largeWidth - 2 * padding, largeHeight - 2 * padding] ]) .rotate([-lon, -lat]) .precision(.7); var path = d3.geo.path() .projection(projectionLarge) .context(largeCanvasContext); largeCanvasObj.call(dragSetupLarge()) return { "projection": projectionLarge, "path": path, } } // what to draw on the canvas for large/small var drawCanvasLarge = function() { largeCanvasContext.clearRect(0, 0, largeWidth, largeHeight); largeCanvasContext.strokeStyle = "#333", largeCanvasContext.lineWidth = 1, largeCanvasContext.strokeRect(2 * padding, 2 * padding, largeWidth - 4 * padding, largeHeight - 4 * padding); largeCanvasContext.fillStyle = "white", largeCanvasContext.fillRect(2 * padding, 2 * padding, largeWidth - 4 * padding, largeHeight - 4 * padding); largeCanvasContext.fillStyle = colors[0], largeCanvasContext.beginPath(), largeMapObjects[0].path(land), largeCanvasContext.fill(); largeCanvasContext.strokeStyle = "#fff", largeCanvasContext.lineWidth = .5, largeCanvasContext.beginPath(), largeMapObjects[0].path(borders), largeCanvasContext.stroke(); largeCanvasContext.fillStyle = colors[1], largeCanvasContext.beginPath(), largeMapObjects[1].path(land), largeCanvasContext.fill(); largeCanvasContext.strokeStyle = "#fff", largeCanvasContext.lineWidth = .5, largeCanvasContext.beginPath(), largeMapObjects[1].path(borders), largeCanvasContext.stroke(); } var drawCanvasSmall = function(i) { smallContext[i].clearRect(0, 0, smallWidth, smallHeight); smallContext[i].fillStyle = "white", smallContext[i].strokeStyle = "#333", smallContext[i].beginPath(), smallContext[i].arc(smallWidth / 2, smallHeight / 2, smallWidth / 2 - 3 * padding, 0, 2 * Math.PI, false), smallContext[i].stroke(), smallContext[i].fill(); smallContext[i].strokeStyle = "#ccc", smallContext[i].lineWidth = .5, smallContext[i].beginPath(), mapObjects[i].path(graticule), smallContext[i].stroke(); smallContext[i].fillStyle = colors[i], smallContext[i].beginPath(), mapObjects[i].path(land), smallContext[i].fill(); } d3.json("world-110m.json", function(error, world) { land = topojson.feature(world, world.objects.land); borders = topojson.mesh(world, world.objects.countries) // use data to draw paths on small maps drawCanvasLarge() drawCanvasSmall(0) addBoxSmall(0, zoomToBoxScale(largeMapObjects[0].projection.scale())) drawCanvasSmall(1) addBoxSmall(1, zoomToBoxScale(largeMapObjects[1].projection.scale())) }) // updates on button click d3.selectAll("button").on("click", function() { // update state, view, hash state.latLon[0] = comparisons[this.name].latLon[0]; state.latLon[1] = comparisons[this.name].latLon[1]; state.scale = comparisons[this.name].scale; rotateAndScale() updateHash() }) // updating just the small context w/ rotation var rotateSmallTween = function(iter) { return function(d) { var r = d3.interpolate(mapObjects[iter].projection.rotate(), [-state.latLon[iter].lon, -state.latLon[iter].lat]); var halfSideTween = d3.interpolate(zoomToBoxScale(largeMapObjects[iter].projection.scale()), zoomToBoxScale(state.scale)) return function(t) { mapObjects[iter].projection.rotate(r(t)); drawCanvasSmall(iter) addBoxSmall(iter, halfSideTween(t)) }; } } var addBoxSmall = function(iter, halfSide) { smallContext[iter].strokeStyle = "#333"; smallContext[iter].strokeRect( smallWidth / 2 - halfSide, smallHeight / 2 - halfSide, 2 * halfSide, 2 * halfSide); } var updateSmallCanvasOverDuration = function(iter) { d3.select("#small" + iter) .select("canvas") .transition() .duration(transitionDuration) .tween("rotate", rotateSmallTween(iter)) } // update large & small smallContext, based on update function (either rotationAndScaleTween or rotationTween) function rotateAndScale() { (function transition() { updateSmallCanvasOverDuration(0) updateSmallCanvasOverDuration(1) d3.select("#large").select("canvas") .transition() .duration(transitionDuration) .tween("d", rotationAndScaleTween) })() } // update only scale, and therefore only large canvas function updateLargeScaleOnly() { (function transition() { d3.select("#large").select("canvas") .transition() .duration(0) .tween("d", scaleTween) })() } // update large & small canvases, based on update function (rotationAndScaleTween or rotationTween) function updateRotationFromSmallPan(name) { drawCanvasSmall(name) addBoxSmall(name, zoomToBoxScale(largeMapObjects[name].projection.scale())) drawCanvasLarge() } function updateRotationFromLargePan() { drawCanvasSmall(0) addBoxSmall(0, zoomToBoxScale(largeMapObjects[0].projection.scale())) drawCanvasSmall(1) addBoxSmall(1, zoomToBoxScale(largeMapObjects[1].projection.scale())) drawCanvasLarge() } function rotationTween() { var r1 = d3.interpolate(largeMapObjects[0].projection.rotate(), [-state.latLon[0].lon, -state.latLon[0].lat]); var r2 = d3.interpolate(largeMapObjects[1].projection.rotate(), [-state.latLon[1].lon, -state.latLon[1].lat]); return function(t) { // update rotation largeMapObjects[0].projection.rotate(r1(t)); largeMapObjects[1].projection.rotate(r2(t)); drawCanvasLarge() }; } function scaleTween() { var interpolateScale = d3.interpolate(largeMapObjects[0].projection.scale(), state.scale); var halfSideTween = d3.interpolate(zoomToBoxScale(largeMapObjects[0].projection.scale()), zoomToBoxScale(state.scale)) return function(t) { // update scale for large projections largeMapObjects[0].projection.scale(interpolateScale(t)); largeMapObjects[1].projection.scale(interpolateScale(t)); // and redraw large drawCanvasLarge() // redraw small canvases, with box drawCanvasSmall(0) addBoxSmall(0, halfSideTween(t)) drawCanvasSmall(1) addBoxSmall(1, halfSideTween(t)) }; } function rotationAndScaleTween() { var r1 = d3.interpolate(largeMapObjects[0].projection.rotate(), [-state.latLon[0].lon, -state.latLon[0].lat]); var r2 = d3.interpolate(largeMapObjects[1].projection.rotate(), [-state.latLon[1].lon, -state.latLon[1].lat]); var interpolateScale = d3.interpolate(largeMapObjects[0].projection.scale(), state.scale); return function(t) { // update rotation largeMapObjects[0].projection.rotate(r1(t)); largeMapObjects[1].projection.rotate(r2(t)); // update scale largeMapObjects[0].projection.scale(interpolateScale(t)); largeMapObjects[1].projection.scale(interpolateScale(t)); drawCanvasLarge() }; } // update functions var updateHash = function() { window.location.hash = "scale=" + state.scale + "¢er0=" + state.latLon[0].lat + "," + state.latLon[0].lon + "¢er1=" + state.latLon[1].lat + "," + state.latLon[1].lon; slider.property("value", state.scale) } // DRAG function dragSetupSmall(name) { function resetDrag() { dragDistance = { x: 0, y: 0 }; } var dragDistance = { x: 0, y: 0 }; return d3.behavior.drag() .on("dragstart", function() { d3.event.sourceEvent.preventDefault(); }) .on("drag", function() { dragDistance.x = dragDistance.x + d3.event.dx; dragDistance.y = dragDistance.y + d3.event.dy; updateRotateFromSmallDrag(dragDistance, name) resetDrag() }) .on("dragend", function() { updateRotateFromSmallDrag(dragDistance, name) resetDrag() }) } function dragSetupLarge() { function resetDrag() { dragDistance = { x: 0, y: 0 }; } var dragDistance = { x: 0, y: 0 }; return d3.behavior.drag() .on("dragstart", function() { d3.event.sourceEvent.preventDefault(); }) .on("drag", function() { dragDistance.x = dragDistance.x + d3.event.dx; dragDistance.y = dragDistance.y + d3.event.dy; updateRotateFromLargeDrag(dragDistance) resetDrag() }) .on("dragend", function() { updateRotateFromLargeDrag(dragDistance) resetDrag() }); } function updateRotateFromSmallDrag(pixelDifference, name) { var newRotate = pixelDiff_to_rotation_small(mapObjects[name].projection, pixelDifference) // set new rotate mapObjects[name].projection.rotate(newRotate) largeMapObjects[name].projection.rotate(newRotate) updateStateRotation(newRotate, name) updateRotationFromSmallPan(name) } function updateRotateFromLargeDrag(pixelDifference) { var newRotate0 = pixelDiff_to_rotation_large(largeMapObjects[0].projection, pixelDifference) var newRotate1 = pixelDiff_to_rotation_large(largeMapObjects[1].projection, pixelDifference) // set new rotate mapObjects[0].projection.rotate(newRotate0) mapObjects[1].projection.rotate(newRotate1) largeMapObjects[0].projection.rotate(newRotate0) largeMapObjects[1].projection.rotate(newRotate1) updateStateRotation(newRotate0, 0) updateStateRotation(newRotate1, 1) updateRotationFromLargePan(name) } function pixelDiff_to_rotation_small(projection, pxDiff) { var k = projection.rotate() return ([k[0] + pxDiff.x / 136 * 90, k[1] - pxDiff.y, k[2]]) } function pixelDiff_to_rotation_large(projection, pxDiff) { var k = projection.invert([largeWidth / 2 - pxDiff.x, largeHeight / 2 - pxDiff.y]) return [-k[0], -k[1], 0] } function updateStateRotation(rotateCoord, name) { state.latLon[name] = { lon: rotateCoord[0], lat: rotateCoord[1] } updateHash() }