// Zan Armstrong - May, 2015 // parts of the code adapted from Mike Bostock's Lambert Azimuthal Equal Area http://bl.ocks.org/mbostock/3757101 // set up variables var width = 300, height = 300, largeWidth = 600, largeHeight = 600; var margin = { top: 0 } 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 }] } } // use/manage hash fragment and state var state = { scale: 450, latLon: [{ lat: 6.52865, lon: 20.3586336 }, { lat: 48.2392291, lon: -98.9443219 }] } 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] }; } } } var mobile = false; if (document.getElementById("testMobile").offsetWidth <= 0) { mobile = true; } // set upvariables var padding = 5; var transitionDuration = 1000; var mainSVG = d3.select("#tool").append("svg") .attr("width", 905) .attr("height", 600) .attr("class", "mainSVG"); var mapObjects = [] // set up ranges/scales var zoomRange = [220, 1600] var zoomToBoxScale = d3.scale.linear().domain([470, 2000]).range([80, 20]); // slider setup for scale var slider = d3.select("#tool") .append("p") .append("input") .attr("type", "range") .style("margin-left", "750px") .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"); updateLargeScale(largeMapObjects, state.scale, duration); updateHash() } // create small maps for (var i = 0; i < 2; i++) { mapObjects[i] = setUpSmallMaps(state.latLon[i].lat, state.latLon[i].lon, i) } // set up small maps function setUpSmallMaps(lat, lon, name) { var center = [-lon, -lat] var projectionSmall = d3.geo.orthographic() .translate([width / 2, height / 2]) .scale(width / 2 * .9) .center([0, 0]) .rotate(center) .clipAngle(90) .precision(.7); var path = d3.geo.path() .projection(projectionSmall); var graticule = d3.geo.graticule(); var svg = mainSVG.append("g") .attr('transform', 'translate(' + padding + ',' + (height * name + margin.top) + ')') .classed("g_" + name, true); svg.append("defs").append("path") .datum({ type: "Sphere" }) .attr("id", "sphere") .attr("d", path); svg.append("use") .attr("class", "stroke") .attr("xlink:href", "#sphere"); svg.append("use") .attr("class", "fill") .attr("xlink:href", "#sphere"); svg.append("path") .datum(graticule) .attr("class", "graticule graticule_" + name) .attr("d", path); svg.call(dragSetupSmall(name)); // end drag behavior setup return { "svg": svg, "projection": projectionSmall, "path": path, "graticule": graticule, "lat": lat, "lon": lon } } // set up large maps var largeSvg = mainSVG.append("g").attr("class", "largeSvg") .attr('transform', 'translate(' + (2 * padding + width) + ',' + (margin.top) + ')') var largeMapObjects = [] for (var i = 0; i < 2; i++) { largeMapObjects[i] = setUpLargeMaps(state.latLon[i].lat, state.latLon[i].lon, i, largeSvg) } function setUpLargeMaps(lat, lon, name, svg) { var center = [-lon, -lat] 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(center) .precision(.7); var path = d3.geo.path() .projection(projectionLarge); return { "svg": svg, "projection": projectionLarge, "path": path, "lat": lat, "lon": lon } } d3.json("world-110m.json", function(error, world) { largeMapObjects.data = world; // use world data to set up small maps for (var i = 0; i < mapObjects.length; i++) { svg = mapObjects[i].svg; svg .insert("path", ".graticule" + i) .datum(topojson.feature(world, world.objects.land)) .attr("class", "land land_" + i) .attr("d", mapObjects[i].path); addCenterBox(svg, [mapObjects[i].lon, mapObjects[i].lat], mapObjects[i].projection) } // use world data to set up large maps for (var i = 0; i < 2; i++) { svg = largeMapObjects[i].svg.append('g').attr("class", "large_" + i); svg.insert("path", ".large_graticule_" + i) .datum(topojson.feature(world, world.objects.land)) .attr("class", "large_land_" + i) .attr("d", largeMapObjects[i].path); svg.insert("path", ".large_graticule" + i) .datum(topojson.mesh(world, world.objects.countries, function(a, b) { return a !== b; })) .attr("class", "boundary boundary_" + i) .attr("d", largeMapObjects[i].path); } // append rectangle for large box largeSvg.append('rect') .attr("height", largeHeight - 4 * padding) .attr("x", 2 * padding) .attr("y", 2 * padding) .attr("width", largeWidth - 4 * padding) .attr("class", "largeBackgroundRect") if (!mobile) { d3.select(".largeSvg").call(dragSetupLarge()) } // do an immediate update (in case safari) - not best practice, need to fix updateLargeRotation(largeMapObjects, 0, largeMapObjects[0].projection.rotate(), 0) updateLargeRotation(largeMapObjects, 1, largeMapObjects[1].projection.rotate(), 0) }); // adjustable box sizes on small worlds function addCenterBox(svg) { var distance = zoomToBoxScale(state.scale); svg.append("rect").attr("x", width / 2 - distance) .attr("y", height / 2 - distance) .attr("width", distance * 2) .attr("height", distance * 2) .attr("stroke", "black") .attr("fill", "none") .attr("class", "littleBox") } // might be nice to combine these two drag functions in the future, but ok for now 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; updateRotateBasedOnSmallMapPan(dragDistance, name, 'drag') resetDrag() }) .on("dragend", function() { updateRotateBasedOnSmallMapPan(dragDistance, name, 'dragend') resetDrag() }) } var count = 0; function dragSetupLarge() { function resetDrag() { dragDistance = { x: 0, y: 0 }; } var dragDistance = { x: 0, y: 0 }; return d3.behavior.drag() .on("drag", function() { dragDistance.x = dragDistance.x + d3.event.dx; dragDistance.y = dragDistance.y + d3.event.dy; updateRotateBasedOnLargeMapPan(dragDistance, 0, 'na') updateRotateBasedOnLargeMapPan(dragDistance, 1, 'na') resetDrag() }) .on("dragend", function() { updateRotateBasedOnLargeMapPan(dragDistance, 0, 'na') updateRotateBasedOnLargeMapPan(dragDistance, 1, 'na') resetDrag() }); } // 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) } 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 pixelDiff_to_rotation_small(projection, pxDiff) { var k = projection.rotate() return ([k[0] + pxDiff.x / 136 * 90, k[1] - pxDiff.y, k[2]]) } function updateStateRotation(rotateCoord, name) { state.latLon[name] = { lon: rotateCoord[0], lat: rotateCoord[1] } } // currently not used function updateLargeRotationDuringDrag(map, name, newRotate, transitionDuration) { d3.selectAll(".large_land_" + name) .transition().duration([transitionDuration]) .attrTween("d", rotationTween(map[name].path, map[name].projection, newRotate)); d3.selectAll(".boundary_" + name).transition().duration([transitionDuration]).remove(); } // currently not used function updateLargeRotationEndDrag(map, name, newRotate, transitionDuration) { map[name].projection.rotate(newRotate) d3.select(".large_" + name).insert("path", ".large_graticule" + name) .datum(topojson.mesh(map.data, map.data.objects.countries, function(a, b) { return a !== b; })) .attr("class", "boundary boundary_" + name) .transition().duration([500]) .attr("d", map[name].path); } function updateLargeRotation(map, name, newRotate, transitionDuration) { d3.selectAll(".large_land_" + name) .transition().duration([transitionDuration]) .attrTween("d", rotationTween(map[name].path, map[name].projection, newRotate)); d3.selectAll(".boundary_" + name) .transition().duration([transitionDuration]) .attrTween("d", rotationTween(map[name].path, map[name].projection, newRotate)); } function updateLargeRotationAndScale(map, name, newRotate, newScale, transitionDuration) { d3.selectAll(".large_land_" + name) .transition().duration([transitionDuration]) .attrTween("d", rotationAndScaleTween(map[name].path, map[name].projection, newRotate, newScale)) d3.selectAll(".boundary_" + name) .transition().duration([transitionDuration]) .attrTween("d", rotationAndScaleTween(map[name].path, map[name].projection, newRotate, newScale)) } function updateSmallRotation(mapObject, name, newRotate, transitionDuration) { if (transitionDuration == 0) { mapObject[name].projection.rotate(newRotate) d3.selectAll(".graticule_" + name).attr("d", mapObject[name].path); d3.selectAll(".land_" + name).attr("d", mapObject[name].path); } else { d3.selectAll(".graticule_" + name) .transition() .duration([transitionDuration]) .attrTween("d", rotationTween(mapObject[name].path, mapObject[name].projection, newRotate)); d3.selectAll(".land_" + name) .transition() .duration([transitionDuration]) .attrTween("d", rotationTween(mapObject[name].path, mapObject[name].projection, newRotate)); } } function updateLargeScale(maps, newScale, duration) { if (duration > 0) { for (var i = 0; i < 2; i++) { d3.selectAll(".large_land_" + i) .transition().duration([duration]) .attrTween("d", scaleTween(maps[i].path, maps[i].projection, newScale)); d3.selectAll(".boundary_" + i) .transition().duration([duration]) .attrTween("d", scaleTween(maps[i].path, maps[i].projection, newScale)); } } else { for (var i = 0; i < 2; i++) { maps[i].projection.scale(newScale) d3.selectAll(".large_land_" + i).attr("d", maps[i].path); d3.selectAll(".boundary_" + i).attr("d", maps[i].path); } } } function updateCenterBoxSize(oldDistance, newDistance, duration) { if (duration == 0) { d3.selectAll(".littleBox") .attr("x", width / 2 - newDistance) .attr("y", height / 2 - newDistance) .attr("width", newDistance * 2) .attr("height", newDistance * 2) } else { d3.selectAll(".littleBox") .transition().duration([duration]) .attrTween("x", distanceTween(width / 2 - oldDistance, width / 2 - newDistance)) .attrTween("y", distanceTween(height / 2 - oldDistance, height / 2 - newDistance)) .attrTween("width", distanceTween(2 * oldDistance, 2 * newDistance)) .attrTween("height", distanceTween(2 * oldDistance, 2 * newDistance)) } } // updates on drag function updateRotateBasedOnSmallMapPan(pixelDifference, name, stage) { var projection = mapObjects[name].projection; projection.rotate(pixelDiff_to_rotation_small(projection, pixelDifference)) updateView(projection.rotate(), "na", name, 0, 'na') } function updateRotateBasedOnLargeMapPan(pixelDifference, name, stage) { var projection = largeMapObjects[name].projection; var newRotate = pixelDiff_to_rotation_large(projection, pixelDifference) updateView(newRotate, "na", name, 0, stage) } function updateView(newRotate, newScale, name, duration, stage) { updateStateRotation(newRotate, name) updateSmallRotation(mapObjects, name, newRotate, duration) if (newScale == 'na') { if (stage == 'drag') { updateLargeRotationDuringDrag(largeMapObjects, name, newRotate, duration) } else if (stage == 'dragend') { updateLargeRotationEndDrag(largeMapObjects, name, newRotate, duration) } else { updateLargeRotation(largeMapObjects, name, newRotate, duration) } } else { updateLargeRotationAndScale(largeMapObjects, name, newRotate, newScale, duration) if (name == 0) { updateCenterBoxSize(zoomToBoxScale(state.scale), zoomToBoxScale(newScale), duration) state.scale = newScale } } updateHash() } // updates on button click d3.selectAll("button").on("click", function() { state.latLon[0] = comparisons[this.name].latLon[0]; state.latLon[1] = comparisons[this.name].latLon[1]; updateView([-state.latLon[0].lon, -state.latLon[0].lat, 0], comparisons[this.name].scale, 0, transitionDuration) updateView([-state.latLon[1].lon, -state.latLon[1].lat, 0], comparisons[this.name].scale, 1, transitionDuration) }) // transition function rotationTween(path, projection, new3Rotation) { return function(d, i, a) { var interpolate = d3.interpolate(projection.rotate(), new3Rotation); return function(t) { projection.rotate(interpolate(t)); return path(d); } } } function scaleTween(path, projection, newScale) { return function(d, i, a) { var interpolate = d3.interpolate(projection.scale(), newScale); return function(t) { projection.scale(interpolate(t)); return path(d); } } } function distanceTween(oldDist, newDist) { return function(d, i, a) { var interpolate = d3.interpolate(oldDist, newDist); return function(t) { return interpolate(t); } } } function rotationAndScaleTween(path, projection, new3Rotation, newScale) { return function(d, i, a) { var interpolateScale = d3.interpolate(projection.scale(), newScale); var interpolateRotate = d3.interpolate(projection.rotate(), new3Rotation); return function(t) { projection.scale(interpolateScale(t)); projection.rotate(interpolateRotate(t)); return path(d); } } }