diff --git a/css/app.css b/css/app.css
index 8b9e5e6ec..2eccfbe2d 100644
--- a/css/app.css
+++ b/css/app.css
@@ -609,7 +609,7 @@ a:hover .icon.out-link { background-position: -500px -14px;}
top:0;
right:0;
height:60px;
- z-index: 1;
+ z-index: 9;
min-width: 768px;
}
@@ -655,7 +655,7 @@ a:hover .icon.out-link { background-position: -500px -14px;}
right: 0;
top: 0;
height: 59px;
- z-index: 3;
+ z-index: 50;
}
.footer {
@@ -683,7 +683,7 @@ a:hover .icon.out-link { background-position: -500px -14px;}
float: left;
height: 100%;
overflow: hidden;
- z-index: 2;
+ z-index: 10;
background: #f6f6f6;
}
@@ -2104,6 +2104,51 @@ img.wiki-image {
bottom: 0;
}
+/* Map-In-Map
+------------------------------------------------------- */
+
+.map-in-map {
+ position: absolute;
+ overflow: hidden;
+ top: 60px;
+ width: 200px;
+ height: 150px;
+ z-index: 5;
+ background: #000;
+ border: #aaa 1px solid;
+ box-shadow: 0 0 2em black;
+}
+
+.map-in-map-tiles {
+ transform-origin:0 0;
+ -ms-transform-origin:0 0;
+ -webkit-transform-origin:0 0;
+ -moz-transform-origin:0 0;
+ -o-transform-origin:0 0;
+ -moz-user-select: none;
+ -webkit-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+.map-in-map-svg {
+ position: relative;
+ overflow: hidden;
+ height: 100%;
+ width: 100%;
+}
+
+.map-in-map-bbox {
+ fill: none;
+ stroke: rgba(255, 255, 0, 0.75);
+ stroke-width: 1;
+ shape-rendering: crispEdges;
+}
+
+.map-in-map-bbox.thick {
+ stroke-width: 5;
+}
+
/* About Section
------------------------------------------------------- */
@@ -2254,7 +2299,7 @@ img.wiki-image {
left: 0;
right: 0;
margin: auto;
- z-index: 3;
+ z-index: 50;
}
.modal .loader {
@@ -2266,7 +2311,7 @@ img.wiki-image {
}
.shaded {
- z-index: 2;
+ z-index: 49;
position: absolute;
top: 0;
bottom: 0;
diff --git a/css/map.css b/css/map.css
index b3ec162e5..c3edae105 100644
--- a/css/map.css
+++ b/css/map.css
@@ -1032,6 +1032,7 @@ g.turn circle {
/* Cursors */
+.map-in-map,
#map {
cursor: auto; /* Opera */
cursor: url(img/cursor-grab.png) 9 9, auto; /* FF */
diff --git a/index.html b/index.html
index 3e71faba6..fd5e3aeba 100644
--- a/index.html
+++ b/index.html
@@ -89,6 +89,7 @@
+
diff --git a/js/id/ui.js b/js/id/ui.js
index 35ed98bbe..e61c3160d 100644
--- a/js/id/ui.js
+++ b/js/id/ui.js
@@ -32,6 +32,11 @@ iD.ui = function(context) {
.attr('id', 'map')
.call(map);
+ content.append('div')
+ .attr('class', 'map-in-map')
+ .style('display', 'none')
+ .call(iD.ui.MapInMap(context));
+
bar.append('div')
.attr('class', 'spacer col4');
diff --git a/js/id/ui/map_in_map.js b/js/id/ui/map_in_map.js
new file mode 100644
index 000000000..385807854
--- /dev/null
+++ b/js/id/ui/map_in_map.js
@@ -0,0 +1,274 @@
+iD.ui.MapInMap = function(context) {
+ var key = '/';
+
+ function map_in_map(selection) {
+ var backgroundLayer = iD.TileLayer(),
+ overlayLayer = iD.TileLayer(),
+ projection = iD.geo.RawMercator(),
+ zoom = d3.behavior.zoom()
+ .scaleExtent([ztok(0.5), ztok(24)])
+ .on('zoom', zoomPan),
+ transformed = false,
+ panning = false,
+ zDiff = 6, // by default, minimap renders at (main zoom - 6)
+ tStart, tLast, tCurr, kLast, kCurr, tiles, svg, timeoutId;
+
+ function ztok(z) { return 256 * Math.pow(2, z); }
+ function ktoz(k) { return Math.log(k) / Math.LN2 - 8; }
+
+
+ function startMouse() {
+ context.surface().on('mouseup.map-in-map-outside', endMouse);
+ context.container().on('mouseup.map-in-map-outside', endMouse);
+
+ tStart = tLast = tCurr = projection.translate();
+ panning = true;
+ }
+
+
+ function zoomPan() {
+ var e = d3.event.sourceEvent,
+ t = d3.event.translate,
+ k = d3.event.scale,
+ zMain = ktoz(context.projection.scale() * 2 * Math.PI),
+ zMini = ktoz(k);
+
+ // restrict minimap zoom to < (main zoom - 3)
+ if (zMini > zMain - 3) {
+ zMini = zMain - 3;
+ zoom.scale(kCurr).translate(tCurr); // restore last good values
+ return;
+ }
+
+ tCurr = t;
+ kCurr = k;
+ zDiff = zMain - zMini;
+
+ var scale = kCurr / kLast,
+ tX = Math.round((tCurr[0] / scale - tLast[0]) * scale),
+ tY = Math.round((tCurr[1] / scale - tLast[1]) * scale);
+
+ iD.util.setTransform(tiles, tX, tY, scale);
+ iD.util.setTransform(svg, 0, 0, scale);
+ transformed = true;
+
+ queueRedraw();
+
+ e.preventDefault();
+ e.stopPropagation();
+ }
+
+
+ function endMouse() {
+ context.surface().on('mouseup.map-in-map-outside', null);
+ context.container().on('mouseup.map-in-map-outside', null);
+
+ updateProjection();
+ panning = false;
+
+ if (tCurr[0] !== tStart[0] && tCurr[1] !== tStart[1]) {
+ var dMini = selection.dimensions(),
+ cMini = [ dMini[0] / 2, dMini[1] / 2 ];
+
+ context.map().center(projection.invert(cMini));
+ }
+ }
+
+
+ function updateProjection() {
+ var loc = context.map().center(),
+ dMini = selection.dimensions(),
+ cMini = [ dMini[0] / 2, dMini[1] / 2 ],
+ tMain = context.projection.translate(),
+ kMain = context.projection.scale(),
+ zMain = ktoz(kMain * 2 * Math.PI),
+ zMini = Math.max(zMain - zDiff, 0.5),
+ kMini = ztok(zMini);
+
+ projection
+ .translate(tMain)
+ .scale(kMini / (2 * Math.PI));
+
+ var s = projection(loc),
+ mouse = panning ? [ tCurr[0] - tStart[0], tCurr[1] - tStart[1] ] : [0, 0],
+ tMini = [
+ cMini[0] - s[0] + tMain[0] + mouse[0],
+ cMini[1] - s[1] + tMain[1] + mouse[1]
+ ];
+
+ projection
+ .translate(tMini)
+ .clipExtent([[0, 0], dMini]);
+
+ zoom
+ .center(cMini)
+ .translate(tMini)
+ .scale(kMini);
+
+ tLast = tCurr = tMini;
+ kLast = kCurr = kMini;
+
+ if (transformed) {
+ iD.util.setTransform(tiles, 0, 0);
+ iD.util.setTransform(svg, 0, 0);
+ transformed = false;
+ }
+ }
+
+
+ function redraw() {
+ if (hidden()) return;
+
+ updateProjection();
+
+ var dMini = selection.dimensions(),
+ zMini = ktoz(projection.scale() * 2 * Math.PI);
+
+ // setup tile container
+ tiles = selection
+ .selectAll('.map-in-map-tiles')
+ .data([0]);
+
+ tiles
+ .enter()
+ .append('div')
+ .attr('class', 'map-in-map-tiles');
+
+
+ // redraw background
+ backgroundLayer
+ .source(context.background().baseLayerSource())
+ .projection(projection)
+ .dimensions(dMini);
+
+ var background = tiles
+ .selectAll('.map-in-map-background')
+ .data([0]);
+
+ background.enter()
+ .append('div')
+ .attr('class', 'map-in-map-background');
+
+ background
+ .call(backgroundLayer);
+
+ // redraw overlay
+ var overlaySources = context.background().overlayLayerSources(),
+ hasOverlay = false;
+
+ for (var i = 0; i < overlaySources.length; i++) {
+ if (overlaySources[i].validZoom(zMini)) {
+ overlayLayer
+ .source(overlaySources[i])
+ .projection(projection)
+ .dimensions(dMini);
+
+ hasOverlay = true;
+ break;
+ }
+ }
+
+ var overlay = tiles
+ .selectAll('.map-in-map-overlay')
+ .data(hasOverlay ? [0] : []);
+
+ overlay.enter()
+ .append('div')
+ .attr('class', 'map-in-map-overlay');
+
+ overlay.exit()
+ .remove();
+
+ if (hasOverlay) {
+ overlay
+ .call(overlayLayer);
+ }
+
+ // redraw bounding box
+ if (!panning) {
+ var getPath = d3.geo.path().projection(projection),
+ bbox = { type: 'Polygon', coordinates: [context.map().extent().polygon()] };
+
+ svg = selection.selectAll('.map-in-map-svg')
+ .data([0]);
+
+ svg.enter()
+ .append('svg')
+ .attr('class', 'map-in-map-svg');
+
+ var path = svg.selectAll('.map-in-map-bbox')
+ .data([bbox]);
+
+ path.enter()
+ .append('path')
+ .attr('class', 'map-in-map-bbox');
+
+ path
+ .attr('d', getPath)
+ .classed('thick', function(d) { return getPath.area(d) < 30; });
+ }
+ }
+
+
+ function queueRedraw() {
+ clearTimeout(timeoutId);
+ timeoutId = setTimeout(function() { redraw(); }, 300);
+ }
+
+
+ function hidden() {
+ return selection.style('display') === 'none';
+ }
+
+
+ function toggle() {
+ if (d3.event) d3.event.preventDefault();
+
+ if (hidden()) {
+ selection
+ .style('display', 'block')
+ .style('opacity', 0)
+ .transition()
+ .duration(200)
+ .style('opacity', 1);
+
+ redraw();
+
+ } else {
+ selection
+ .style('display', 'block')
+ .style('opacity', 1)
+ .transition()
+ .duration(200)
+ .style('opacity', 0)
+ .each('end', function() {
+ d3.select(this).style('display', 'none');
+ });
+ }
+ }
+
+
+ selection
+ .on('mousedown.map-in-map', startMouse)
+ .on('mouseup.map-in-map', endMouse);
+
+ selection
+ .call(zoom)
+ .on('dblclick.zoom', null);
+
+ context.map()
+ .on('drawn.map-in-map', function(drawn) {
+ if (drawn.full === true) redraw();
+ });
+
+ redraw();
+
+ var keybinding = d3.keybinding('map-in-map')
+ .on(key, toggle);
+
+ d3.select(document)
+ .call(keybinding);
+ }
+
+ return map_in_map;
+};
diff --git a/test/index.html b/test/index.html
index 4f4f6510c..ae6159606 100644
--- a/test/index.html
+++ b/test/index.html
@@ -84,6 +84,7 @@
+