diff --git a/modules/behavior/breathe.js b/modules/behavior/breathe.js index 85a494ea7..f95caa871 100644 --- a/modules/behavior/breathe.js +++ b/modules/behavior/breathe.js @@ -152,7 +152,9 @@ export function behaviorBreathe() { breathe.off = function() { done = true; - timer.stop(); + if (timer) { + timer.stop(); + } selected .interrupt() .call(reset); diff --git a/modules/core/history.js b/modules/core/history.js index a9ced8604..212f3349e 100644 --- a/modules/core/history.js +++ b/modules/core/history.js @@ -26,6 +26,9 @@ export function coreHistory(context) { annotation = actions.pop(); } + stack[index].transform = context.projection.transform(); + stack[index].selectedIDs = context.selectedIDs(); + var graph = stack[index].graph; for (var i = 0; i < actions.length; i++) { graph = actions[i](graph); @@ -129,7 +132,7 @@ export function coreHistory(context) { if (stack[index].annotation) break; } - dispatch.call('undone'); + dispatch.call('undone', this, stack[index]); return change(previous); }, @@ -142,7 +145,7 @@ export function coreHistory(context) { if (stack[index].annotation) break; } - dispatch.call('redone'); + dispatch.call('redone', this, stack[index]); return change(previous); }, diff --git a/modules/modes/select.js b/modules/modes/select.js index fdfa54399..72212850c 100644 --- a/modules/modes/select.js +++ b/modules/modes/select.js @@ -71,6 +71,23 @@ export function modeSelect(context, selectedIDs) { } + function checkSelectedIDs() { + var ids = []; + if (Array.isArray(selectedIDs)) { + ids = selectedIDs.filter(function(id) { + return context.hasEntity(id); + }); + } + + if (ids.length) { + selectedIDs = ids; + } else { + context.enter(modeBrowse(context)); + } + return !!ids.length; + } + + // find the common parent ways for nextVertex, previousVertex function commonParents() { var graph = context.graph(), @@ -171,6 +188,8 @@ export function modeSelect(context, selectedIDs) { mode.reselect = function() { + if (!checkSelectedIDs()) return; + var surfaceNode = context.surface().node(); if (surfaceNode.focus) { // FF doesn't support it surfaceNode.focus(); @@ -206,10 +225,7 @@ export function modeSelect(context, selectedIDs) { function update() { closeMenu(); - if (_.some(selectedIDs, function(id) { return !context.hasEntity(id); })) { - // Exit mode if selected entity gets undone - context.enter(modeBrowse(context)); - } + checkSelectedIDs(); } @@ -236,6 +252,8 @@ export function modeSelect(context, selectedIDs) { function selectElements(drawn) { + if (!checkSelectedIDs()) return; + var surface = context.surface(), entity = singular(); @@ -283,7 +301,7 @@ export function modeSelect(context, selectedIDs) { if (parent) { var way = context.entity(parent); context.enter( - modeSelect(context, [way.first()]).follow(true) + modeSelect(context, [way.first()]).follow(true).suppressMenu(true) ); } } @@ -295,7 +313,7 @@ export function modeSelect(context, selectedIDs) { if (parent) { var way = context.entity(parent); context.enter( - modeSelect(context, [way.last()]).follow(true) + modeSelect(context, [way.last()]).follow(true).suppressMenu(true) ); } } @@ -319,7 +337,7 @@ export function modeSelect(context, selectedIDs) { if (index !== -1) { context.enter( - modeSelect(context, [way.nodes[index]]).follow(true) + modeSelect(context, [way.nodes[index]]).follow(true).suppressMenu(true) ); } } @@ -343,7 +361,7 @@ export function modeSelect(context, selectedIDs) { if (index !== -1) { context.enter( - modeSelect(context, [way.nodes[index]]).follow(true) + modeSelect(context, [way.nodes[index]]).follow(true).suppressMenu(true) ); } } @@ -372,6 +390,8 @@ export function modeSelect(context, selectedIDs) { } + if (!checkSelectedIDs()) return; + behaviors.forEach(function(behavior) { context.install(behavior); }); diff --git a/modules/operations/delete.js b/modules/operations/delete.js index 6b8042a22..af046916f 100644 --- a/modules/operations/delete.js +++ b/modules/operations/delete.js @@ -26,7 +26,7 @@ export function operationDelete(selectedIDs, context) { annotation = t('operations.delete.annotation.' + geometry); // Select the next closest node in the way. - if (geometry === 'vertex' && parents.length === 1 && parent.nodes.length > 2) { + if (geometry === 'vertex' && parent.nodes.length > 2) { var nodes = parent.nodes, i = nodes.indexOf(id); @@ -44,13 +44,16 @@ export function operationDelete(selectedIDs, context) { } } + context.perform(action, annotation); + if (nextSelectedID && context.hasEntity(nextSelectedID)) { - context.enter(modeSelect(context, [nextSelectedID])); + context.enter( + modeSelect(context, [nextSelectedID]).follow(true).suppressMenu(true) + ); } else { context.enter(modeBrowse(context)); } - context.perform(action, annotation); }; diff --git a/modules/renderer/map.js b/modules/renderer/map.js index d78d495cc..fdc94c952 100644 --- a/modules/renderer/map.js +++ b/modules/renderer/map.js @@ -17,6 +17,7 @@ import { } from '../svg/index'; import { geoExtent } from '../geo/index'; +import { modeSelect } from '../modes/select'; import { utilFastMouse, @@ -44,9 +45,9 @@ export function rendererMap(context) { drawAreas = svgAreas(projection, context), drawMidpoints = svgMidpoints(projection, context), drawLabels = svgLabels(projection, context), - supersurface, - wrapper, - surface, + supersurface = d3.select(null), + wrapper = d3.select(null), + surface = d3.select(null), mouse, mousemove; @@ -65,14 +66,31 @@ export function rendererMap(context) { context .on('change.map', immediateRedraw); + context.connection() .on('change.map', immediateRedraw); + context.history() - .on('change.map', immediateRedraw); + .on('change.map', immediateRedraw) + .on('undone.context redone.context', function(stack) { + var followSelected = false; + if (Array.isArray(stack.selectedIDs)) { + followSelected = (stack.selectedIDs.length === 1 && stack.selectedIDs[0][0] === 'n'); + context.enter( + modeSelect(context, stack.selectedIDs).suppressMenu(true).follow(followSelected) + ); + } + if (!followSelected && stack.transform) { + map.transformEase(stack.transform); + } + }); + context.background() .on('change.map', immediateRedraw); + context.features() .on('redraw.map', immediateRedraw); + drawLayers .on('change.map', function() { context.background().updateImagery(); @@ -300,7 +318,7 @@ export function rendererMap(context) { function redraw(difference, extent) { - if (!surface || !redrawEnabled) return; + if (surface.empty() || !redrawEnabled) return; // If we are in the middle of a zoom/pan, we can't do differenced redraws. // It would result in artifacts where differenced entities are redrawn with @@ -389,6 +407,26 @@ export function rendererMap(context) { }; + function setTransform(t2, duration, force) { + var t = projection.transform(); + if (!force && t2.k === t.k && t2.x === t.x && t2.y === t.y) { + return false; + } + + if (duration) { + _selection + .transition() + .duration(duration) + .on('start', function() { map.startEase(); }) + .call(zoom.transform, d3.zoomIdentity.translate(t2.x, t2.y).scale(t2.k)); + } else { + projection.transform(t2); + transformStart = t2; + _selection.call(zoom.transform, transformStart); + } + } + + function setZoom(z2, force, duration) { if (z2 === map.zoom() && !force) { return false; @@ -582,6 +620,13 @@ export function rendererMap(context) { }; + map.transformEase = function(t2, duration) { + duration = duration || 250; + setTransform(t2, duration, false); + return map; + }; + + map.startEase = function() { utilBindOnce(surface, 'mousedown.ease', function() { map.cancelEase(); diff --git a/modules/svg/labels.js b/modules/svg/labels.js index ab3865223..88adf4906 100644 --- a/modules/svg/labels.js +++ b/modules/svg/labels.js @@ -615,7 +615,8 @@ export function svgLabels(projection, context) { // hide labels along selected ways, or near selected vertices for (var i = 0; i < selectedIDs.length; i++) { - var entity = graph.entity(selectedIDs[i]); + var entity = graph.hasEntity(selectedIDs[i]); + if (!entity) continue; var geometry = entity.geometry(graph); if (geometry === 'line') { diff --git a/test/spec/behavior/hash.js b/test/spec/behavior/hash.js index 8c129860c..2c7e6e68a 100644 --- a/test/spec/behavior/hash.js +++ b/test/spec/behavior/hash.js @@ -5,14 +5,12 @@ describe('iD.behaviorHash', function () { beforeEach(function () { context = iD.Context(); - context.container(d3.select(document.createElement('div'))); - - // Neuter connection - context.connection().loadTiles = function () {}; + context.connection().loadTiles = function () {}; // Neuter connection + var container = d3.select(document.createElement('div')); + context.container(container); + container.call(context.map()); hash = iD.behaviorHash(context); - d3.select(document.createElement('div')) - .call(context.map()); }); afterEach(function () { diff --git a/test/spec/behavior/lasso.js b/test/spec/behavior/lasso.js index d330b0623..a4331efb7 100644 --- a/test/spec/behavior/lasso.js +++ b/test/spec/behavior/lasso.js @@ -1,17 +1,12 @@ describe('iD.behaviorLasso', function () { - var lasso, context; + var context, lasso; beforeEach(function () { context = iD.Context(); - context.container(d3.select(document.createElement('div'))); - - // Neuter connection - context.connection().loadTiles = function () {}; - - lasso = iD.behaviorLasso(context); - d3.select(document.createElement('div')) + .attr('id', 'map') .call(context.map()); + lasso = iD.behaviorLasso(context); }); afterEach(function () { diff --git a/test/spec/behavior/select.js b/test/spec/behavior/select.js index 3887c2773..5deddc548 100644 --- a/test/spec/behavior/select.js +++ b/test/spec/behavior/select.js @@ -31,6 +31,18 @@ describe('iD.behaviorSelect', function() { container.remove(); }); + specify('refuse to enter select mode with no ids', function() { + context.enter(iD.modeSelect(context, [])); + expect(context.mode().id, 'empty array').to.eql('browse'); + context.enter(iD.modeSelect(context, undefined)); + expect(context.mode().id, 'undefined').to.eql('browse'); + }); + + specify('refuse to enter select mode with nonexistent ids', function() { + context.enter(iD.modeSelect(context, ['w-1'])); + expect(context.mode().id).to.eql('browse'); + }); + specify('click on entity selects the entity', function() { happen.click(context.surface().selectAll('.' + a.id).node()); expect(context.selectedIDs()).to.eql([a.id]); diff --git a/test/spec/renderer/features.js b/test/spec/renderer/features.js index 2c09ce658..c27979efa 100644 --- a/test/spec/renderer/features.js +++ b/test/spec/renderer/features.js @@ -1,10 +1,12 @@ describe('iD.Features', function() { var dimensions = [1000, 1000], - context, - features; + context, features; beforeEach(function() { context = iD.Context(); + d3.select(document.createElement('div')) + .attr('id', 'map') + .call(context.map()); context.map().zoom(16); features = iD.Features(context); });