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 @@ +