From 1685e4c2b46a979fa215e527d5621f7617b21e73 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 23 Dec 2016 00:56:44 -0500 Subject: [PATCH 01/13] Support transitioned reflect actions --- modules/actions/reflect.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/modules/actions/reflect.js b/modules/actions/reflect.js index 6382f83c8..a28ea7d2d 100644 --- a/modules/actions/reflect.js +++ b/modules/actions/reflect.js @@ -6,6 +6,7 @@ import { import { geoEuclideanDistance, geoExtent, + geoInterp, geoRotate } from '../geo'; @@ -52,7 +53,10 @@ export function actionReflect(reflectIds, projection) { } - var action = function(graph) { + var action = function(graph, t) { + if (t === null || !isFinite(t)) t = 1; + t = Math.min(Math.max(+t, 0), 1); + var nodes = utilGetAllNodes(reflectIds, graph), ssr = getSmallestSurroundingRectangle(graph, nodes); @@ -87,7 +91,8 @@ export function actionReflect(reflectIds, projection) { a * (c[0] - p[0]) + b * (c[1] - p[1]) + p[0], b * (c[0] - p[0]) - a * (c[1] - p[1]) + p[1] ]; - node = node.move(projection.invert(c2)); + var loc2 = projection.invert(c2); + node = node.move(geoInterp(node.loc, loc2, t)); graph = graph.replace(node); } @@ -102,5 +107,8 @@ export function actionReflect(reflectIds, projection) { }; + action.transitionable = true; + + return action; } From d917424fc45b31b624ae22ee85c1de48c032241e Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 23 Dec 2016 00:57:05 -0500 Subject: [PATCH 02/13] Support for performing transitionable actions --- modules/core/history.js | 59 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 7 deletions(-) diff --git a/modules/core/history.js b/modules/core/history.js index 212f3349e..3e858956a 100644 --- a/modules/core/history.js +++ b/modules/core/history.js @@ -17,7 +17,7 @@ export function coreHistory(context) { lock = utilSessionMutex('lock'); - function perform(actions) { + function perform(actions, t) { actions = Array.prototype.slice.call(actions); var annotation; @@ -31,7 +31,7 @@ export function coreHistory(context) { var graph = stack[index].graph; for (var i = 0; i < actions.length; i++) { - graph = actions[i](graph); + graph = actions[i](graph, t); } return { @@ -76,13 +76,58 @@ export function coreHistory(context) { perform: function() { - var previous = stack[index].graph; + // complete any transition already in progress + d3.select(document) + .interrupt('history.perform'); - stack = stack.slice(0, index + 1); - stack.push(perform(arguments)); - index++; + var transitionable = false; + if (arguments.length === 1 || + arguments.length === 2 && !_.isFunction(arguments[1])) { + transitionable = !!arguments[0].transitionable; + } - return change(previous); + if (transitionable) { + var origArguments = arguments; + d3.select(document) + .transition('history.perform') + .duration(150) + .ease(d3.easeLinear) + .tween('history.tween', function() { + return function(t) { + if (t < 1) _doOverwrite([origArguments[0]], t); + }; + }) + .on('start', function() { + _doPerform([origArguments[0]], 0); + }) + .on('end interrupt', function() { + _doOverwrite(origArguments, 1); + }); + + } else { + return _doPerform(arguments); + } + + + function _doPerform(args, t) { + var previous = stack[index].graph; + stack = stack.slice(0, index + 1); + stack.push(perform(args, t)); + index++; + return change(previous); + } + + function _doOverwrite(args, t) { + var previous = stack[index].graph; + if (index > 0) { + index--; + stack.pop(); + } + stack = stack.slice(0, index + 1); + stack.push(perform(args, t)); + index++; + return change(previous); + } }, From fdc37287ec410da68f83690c2de5f13a4eb7ee99 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 23 Dec 2016 10:52:00 -0500 Subject: [PATCH 03/13] Support transitioned circularize action --- modules/actions/circularize.js | 63 ++++++++++++++++++++++++++-------- 1 file changed, 49 insertions(+), 14 deletions(-) diff --git a/modules/actions/circularize.js b/modules/actions/circularize.js index 283cd829e..f57c10f68 100644 --- a/modules/actions/circularize.js +++ b/modules/actions/circularize.js @@ -14,8 +14,16 @@ export function actionCircularize(wayId, projection, maxAngle) { maxAngle = (maxAngle || 20) * Math.PI / 180; - var action = function(graph) { - var way = graph.entity(wayId); + var action = function(graph, t) { + if (t === null || !isFinite(t)) t = 1; + t = Math.min(Math.max(+t, 0), 1); + + var way = graph.entity(wayId), + origNodes = {}; + + graph.childNodes(way).forEach(function(node) { + if (!origNodes[node.id]) origNodes[node.id] = node; + }); if (!way.isConvex(graph)) { graph = action.makeConvex(graph); @@ -56,21 +64,27 @@ export function actionCircularize(wayId, projection, maxAngle) { endNodeIndex = nodes.indexOf(endNode), numberNewPoints = -1, indexRange = endNodeIndex - startNodeIndex, - distance, totalAngle, eachAngle, startAngle, endAngle, - angle, loc, node, j, - inBetweenNodes = []; + nearNodes = {}, + inBetweenNodes = [], + startAngle, endAngle, totalAngle, eachAngle, + angle, loc, node, origNode, j, k; if (indexRange < 0) { indexRange += nodes.length; } // position this key node - distance = geoEuclideanDistance(centroid, keyPoints[i]); + var distance = geoEuclideanDistance(centroid, keyPoints[i]); if (distance === 0) { distance = 1e-4; } keyPoints[i] = [ centroid[0] + (keyPoints[i][0] - centroid[0]) / distance * radius, - centroid[1] + (keyPoints[i][1] - centroid[1]) / distance * radius]; - graph = graph.replace(keyNodes[i].move(projection.invert(keyPoints[i]))); + centroid[1] + (keyPoints[i][1] - centroid[1]) / distance * radius + ]; + loc = projection.invert(keyPoints[i]); + node = keyNodes[i]; + origNode = origNodes[node.id]; + node = node.move(geoInterp(origNode.loc, loc, t)); + graph = graph.replace(node); // figure out the between delta angle we want to match to startAngle = Math.atan2(keyPoints[i][1] - centroid[1], keyPoints[i][0] - centroid[0]); @@ -87,14 +101,20 @@ export function actionCircularize(wayId, projection, maxAngle) { eachAngle = totalAngle / (indexRange + numberNewPoints); } while (Math.abs(eachAngle) > maxAngle); - // move existing points + + // move existing nodes for (j = 1; j < indexRange; j++) { angle = startAngle + j * eachAngle; loc = projection.invert([ - centroid[0] + Math.cos(angle)*radius, - centroid[1] + Math.sin(angle)*radius]); + centroid[0] + Math.cos(angle) * radius, + centroid[1] + Math.sin(angle) * radius + ]); - node = nodes[(j + startNodeIndex) % nodes.length].move(loc); + node = nodes[(j + startNodeIndex) % nodes.length]; + origNode = origNodes[node.id]; + nearNodes[node.id] = angle; + + node = node.move(geoInterp(origNode.loc, loc, t)); graph = graph.replace(node); } @@ -103,9 +123,21 @@ export function actionCircularize(wayId, projection, maxAngle) { angle = startAngle + (indexRange + j) * eachAngle; loc = projection.invert([ centroid[0] + Math.cos(angle) * radius, - centroid[1] + Math.sin(angle) * radius]); + centroid[1] + Math.sin(angle) * radius + ]); - node = osmNode({loc: loc}); + // choose a nearnode to use as the original + var min = Infinity; + for (var nodeId in nearNodes) { + var nearAngle = nearNodes[nodeId], + dist = Math.abs(nearAngle - angle); + if (dist < min) { + dist = min; + origNode = origNodes[nodeId]; + } + } + + node = osmNode({ loc: geoInterp(origNode.loc, loc, t) }); graph = graph.replace(node); nodes.splice(endNodeIndex + j, 0, node); @@ -195,5 +227,8 @@ export function actionCircularize(wayId, projection, maxAngle) { }; + action.transitionable = true; + + return action; } From 3e82a7352c59d4e7f3f933546d9c0a0a7262e6a8 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 23 Dec 2016 11:09:17 -0500 Subject: [PATCH 04/13] Support transitioned straighten action --- modules/actions/straighten.js | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/modules/actions/straighten.js b/modules/actions/straighten.js index 60a49416e..43765865f 100644 --- a/modules/actions/straighten.js +++ b/modules/actions/straighten.js @@ -1,5 +1,5 @@ import { actionDeleteNode } from './delete_node'; -import { geoEuclideanDistance } from '../geo/index'; +import { geoEuclideanDistance, geoInterp } from '../geo/index'; /* @@ -8,12 +8,15 @@ import { geoEuclideanDistance } from '../geo/index'; export function actionStraighten(wayId, projection) { function positionAlongWay(n, s, e) { - return ((n[0] - s[0]) * (e[0] - s[0]) + (n[1] - s[1]) * (e[1] - s[1]))/ + return ((n[0] - s[0]) * (e[0] - s[0]) + (n[1] - s[1]) * (e[1] - s[1])) / (Math.pow(e[0] - s[0], 2) + Math.pow(e[1] - s[1], 2)); } - var action = function(graph) { + var action = function(graph, t) { + if (t === null || !isFinite(t)) t = 1; + t = Math.min(Math.max(+t, 0), 1); + var way = graph.entity(wayId), nodes = graph.childNodes(way), points = nodes.map(function(n) { return projection(n.loc); }), @@ -26,17 +29,18 @@ export function actionStraighten(wayId, projection) { var node = nodes[i], point = points[i]; - if (graph.parentWays(node).length > 1 || + if (t < 1 || graph.parentWays(node).length > 1 || graph.parentRelations(node).length || node.hasInterestingTags()) { var u = positionAlongWay(point, startPoint, endPoint), - p0 = startPoint[0] + u * (endPoint[0] - startPoint[0]), - p1 = startPoint[1] + u * (endPoint[1] - startPoint[1]); + p = [ + startPoint[0] + u * (endPoint[0] - startPoint[0]), + startPoint[1] + u * (endPoint[1] - startPoint[1]) + ], + loc2 = projection.invert(p); - graph = graph - .replace(graph.entity(node.id) - .move(projection.invert([p0, p1]))); + graph = graph.replace(node.move(geoInterp(node.loc, loc2, t))); } else { // safe to delete @@ -83,5 +87,8 @@ export function actionStraighten(wayId, projection) { }; + action.transitionable = true; + + return action; } From cc5f23804211cd58b9e4d589f2e789483d90a04c Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 23 Dec 2016 12:04:53 -0500 Subject: [PATCH 05/13] Support transitioned orthogonalize action --- modules/actions/orthogonalize.js | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/modules/actions/orthogonalize.js b/modules/actions/orthogonalize.js index b3c311b33..5426e04bd 100644 --- a/modules/actions/orthogonalize.js +++ b/modules/actions/orthogonalize.js @@ -1,6 +1,6 @@ import _ from 'lodash'; import { actionDeleteNode } from './delete_node'; -import { geoEuclideanDistance } from '../geo/index'; +import { geoEuclideanDistance, geoInterp } from '../geo/index'; /* * Based on https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/potlatch2/tools/Quadrilateralise.as @@ -11,26 +11,30 @@ export function actionOrthogonalize(wayId, projection) { upperThreshold = Math.cos(threshold * Math.PI / 180); - var action = function(graph) { + var action = function(graph, t) { + if (t === null || !isFinite(t)) t = 1; + t = Math.min(Math.max(+t, 0), 1); + var way = graph.entity(wayId), nodes = graph.childNodes(way), points = _.uniq(nodes).map(function(n) { return projection(n.loc); }), corner = {i: 0, dotp: 1}, epsilon = 1e-4, - i, j, score, motions; + node, loc, score, motions, i, j; if (nodes.length === 4) { for (i = 0; i < 1000; i++) { motions = points.map(calcMotion); - points[corner.i] = addPoints(points[corner.i],motions[corner.i]); + points[corner.i] = addPoints(points[corner.i], motions[corner.i]); score = corner.dotp; if (score < epsilon) { break; } } - graph = graph.replace(graph.entity(nodes[corner.i].id) - .move(projection.invert(points[corner.i]))); + node = graph.entity(nodes[corner.i].id); + loc = projection.invert(points[corner.i]); + graph = graph.replace(node.move(geoInterp(node.loc, loc, t))); } else { var best, @@ -57,25 +61,25 @@ export function actionOrthogonalize(wayId, projection) { for (i = 0; i < points.length; i++) { // only move the points that actually moved if (originalPoints[i][0] !== points[i][0] || originalPoints[i][1] !== points[i][1]) { - graph = graph.replace(graph.entity(nodes[i].id) - .move(projection.invert(points[i]))); + loc = projection.invert(points[i]); + node = graph.entity(nodes[i].id); + graph = graph.replace(node.move(geoInterp(node.loc, loc, t))); } } // remove empty nodes on straight sections - for (i = 0; i < points.length; i++) { - var node = nodes[i]; + for (i = 0; t === 1 && i < points.length; i++) { + node = graph.entity(nodes[i].id); if (graph.parentWays(node).length > 1 || graph.parentRelations(node).length || node.hasInterestingTags()) { - continue; } var dotp = normalizedDotProduct(i, points); if (dotp < -1 + epsilon) { - graph = actionDeleteNode(nodes[i].id)(graph); + graph = actionDeleteNode(node.id)(graph); } } } @@ -182,5 +186,8 @@ export function actionOrthogonalize(wayId, projection) { }; + action.transitionable = true; + + return action; } From c48cdc7c70dbeb509b1c85d5d6b84f69e5f7a43a Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 23 Dec 2016 12:22:48 -0500 Subject: [PATCH 06/13] Clearer intent for right triangle code --- modules/actions/orthogonalize.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/actions/orthogonalize.js b/modules/actions/orthogonalize.js index 5426e04bd..31c532d00 100644 --- a/modules/actions/orthogonalize.js +++ b/modules/actions/orthogonalize.js @@ -22,7 +22,7 @@ export function actionOrthogonalize(wayId, projection) { epsilon = 1e-4, node, loc, score, motions, i, j; - if (nodes.length === 4) { + if (points.length === 3) { // move only one vertex for right triangle for (i = 0; i < 1000; i++) { motions = points.map(calcMotion); points[corner.i] = addPoints(points[corner.i], motions[corner.i]); From cb70b8028f19903ea1063f0cc8f3adc3d28d1a29 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 23 Dec 2016 12:26:00 -0500 Subject: [PATCH 07/13] Pacify eslint --- modules/actions/circularize.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/actions/circularize.js b/modules/actions/circularize.js index f57c10f68..6a14124fc 100644 --- a/modules/actions/circularize.js +++ b/modules/actions/circularize.js @@ -67,7 +67,7 @@ export function actionCircularize(wayId, projection, maxAngle) { nearNodes = {}, inBetweenNodes = [], startAngle, endAngle, totalAngle, eachAngle, - angle, loc, node, origNode, j, k; + angle, loc, node, origNode, j; if (indexRange < 0) { indexRange += nodes.length; From 723f0ca43a0ee1891ced6a641449c1d8dc47025a Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 23 Dec 2016 14:58:18 -0500 Subject: [PATCH 08/13] Add tests for transitioned straighten action --- test/spec/actions/straighten.js | 89 +++++++++++++++++++++++++++------ 1 file changed, 73 insertions(+), 16 deletions(-) diff --git a/test/spec/actions/straighten.js b/test/spec/actions/straighten.js index 93097f44d..e9c63ed4f 100644 --- a/test/spec/actions/straighten.js +++ b/test/spec/actions/straighten.js @@ -5,12 +5,11 @@ describe('iD.actionStraighten', function () { it('returns falsy for ways with internal nodes near centerline', function () { var graph = iD.Graph([ iD.Node({id: 'a', loc: [0, 0]}), - iD.Node({id: 'b', loc: [1, 0.1]}), + iD.Node({id: 'b', loc: [1, 0.01]}), iD.Node({id: 'c', loc: [2, 0]}), iD.Node({id: 'd', loc: [3, 0]}), iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd']}) ]); - expect(iD.actionStraighten('-', projection).disabled(graph)).not.to.be.ok; }); @@ -22,7 +21,6 @@ describe('iD.actionStraighten', function () { iD.Node({id: 'd', loc: [3, 0]}), iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd']}) ]); - expect(iD.actionStraighten('-', projection).disabled(graph)).to.equal('too_bendy'); }); @@ -34,49 +32,108 @@ describe('iD.actionStraighten', function () { iD.Node({id: 'd', loc: [0, 0]}), iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd']}) ]); - expect(iD.actionStraighten('-', projection).disabled(graph)).to.equal('too_bendy'); }); }); + it('deletes empty nodes', function() { var graph = iD.Graph([ iD.Node({id: 'a', loc: [0, 0]}), - iD.Node({id: 'b', loc: [2, 0], tags: {}}), - iD.Node({id: 'c', loc: [2, 2]}), + iD.Node({id: 'b', loc: [1, 0.01], tags: {}}), + iD.Node({id: 'c', loc: [2, 0]}), iD.Way({id: '-', nodes: ['a', 'b', 'c']}) ]); graph = iD.actionStraighten('-', projection)(graph); - + expect(graph.entity('-').nodes).to.eql(['a', 'c']); expect(graph.hasEntity('b')).to.eq(undefined); }); it('does not delete tagged nodes', function() { var graph = iD.Graph([ iD.Node({id: 'a', loc: [0, 0]}), - iD.Node({id: 'b', loc: [2, 0], tags: {foo: 'bar'}}), - iD.Node({id: 'c', loc: [2, 2]}), + iD.Node({id: 'b', loc: [1, 0.01], tags: {foo: 'bar'}}), + iD.Node({id: 'c', loc: [2, 0]}), iD.Way({id: '-', nodes: ['a', 'b', 'c']}) ]); graph = iD.actionStraighten('-', projection)(graph); - expect(graph.entity('-').nodes).to.eql(['a', 'b', 'c']); + expect(graph.entity('b').loc[0]).to.be.closeTo(1, 1e-6); + expect(graph.entity('b').loc[1]).to.be.closeTo(0, 1e-6); }); it('does not delete nodes connected to other ways', function() { var graph = iD.Graph([ iD.Node({id: 'a', loc: [0, 0]}), - iD.Node({id: 'b', loc: [2, 0]}), - iD.Node({id: 'c', loc: [2, 2]}), - iD.Node({id: 'd', loc: [0, 2]}), - iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd']}), + iD.Node({id: 'b', loc: [1, 0.01]}), + iD.Node({id: 'c', loc: [2, 0]}), + iD.Way({id: '-', nodes: ['a', 'b', 'c']}), iD.Way({id: '=', nodes: ['b']}) ]); graph = iD.actionStraighten('-', projection)(graph); - - expect(graph.entity('-').nodes).to.have.length(3); + expect(graph.entity('-').nodes).to.eql(['a', 'b', 'c']); + expect(graph.entity('b').loc[0]).to.be.closeTo(1, 1e-6); + expect(graph.entity('b').loc[1]).to.be.closeTo(0, 1e-6); }); + + + describe('transitions', function () { + it('is transitionable', function() { + expect(iD.actionStraighten().transitionable).to.be.true; + }); + + it('straighten at t = 0', function() { + var graph = iD.Graph([ + iD.Node({id: 'a', loc: [0, 0]}), + iD.Node({id: 'b', loc: [1, 0.01], tags: {foo: 'bar'}}), + iD.Node({id: 'c', loc: [2, -0.01]}), + iD.Node({id: 'd', loc: [3, 0]}), + iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd']}) + ]); + + graph = iD.actionStraighten('-', projection)(graph, 0); + expect(graph.entity('-').nodes).to.eql(['a', 'b', 'c', 'd']); + expect(graph.entity('b').loc[0]).to.be.closeTo(1, 1e-6); + expect(graph.entity('b').loc[1]).to.be.closeTo(0.01, 1e-6); + expect(graph.entity('c').loc[0]).to.be.closeTo(2, 1e-6); + expect(graph.entity('c').loc[1]).to.be.closeTo(-0.01, 1e-6); + }); + + it('straighten at t = 0.5', function() { + var graph = iD.Graph([ + iD.Node({id: 'a', loc: [0, 0]}), + iD.Node({id: 'b', loc: [1, 0.01], tags: {foo: 'bar'}}), + iD.Node({id: 'c', loc: [2, -0.01]}), + iD.Node({id: 'd', loc: [3, 0]}), + iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd']}) + ]); + + graph = iD.actionStraighten('-', projection)(graph, 0.5); + expect(graph.entity('-').nodes).to.eql(['a', 'b', 'c', 'd']); + expect(graph.entity('b').loc[0]).to.be.closeTo(1, 1e-6); + expect(graph.entity('b').loc[1]).to.be.closeTo(0.005, 1e-6); + expect(graph.entity('c').loc[0]).to.be.closeTo(2, 1e-6); + expect(graph.entity('c').loc[1]).to.be.closeTo(-0.005, 1e-6); + }); + + it('straighten at t = 1', function() { + var graph = iD.Graph([ + iD.Node({id: 'a', loc: [0, 0]}), + iD.Node({id: 'b', loc: [1, 0.01], tags: {foo: 'bar'}}), + iD.Node({id: 'c', loc: [2, -0.01]}), + iD.Node({id: 'd', loc: [3, 0]}), + iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd']}) + ]); + + graph = iD.actionStraighten('-', projection)(graph, 1); + expect(graph.entity('-').nodes).to.eql(['a', 'b', 'd']); + expect(graph.entity('b').loc[0]).to.be.closeTo(1, 1e-6); + expect(graph.entity('b').loc[1]).to.be.closeTo(0, 1e-6); + expect(graph.hasEntity('c')).to.eq(undefined); + }); + }); + }); From f9c09c0648c21a607393d6c81fa99ad1839f92a7 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 23 Dec 2016 15:05:51 -0500 Subject: [PATCH 09/13] Add tests for transitioned reflect action --- test/spec/actions/reflect.js | 121 +++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/test/spec/actions/reflect.js b/test/spec/actions/reflect.js index 1a8b22061..c06737129 100644 --- a/test/spec/actions/reflect.js +++ b/test/spec/actions/reflect.js @@ -63,4 +63,125 @@ describe('iD.actionReflect', function() { expect(graph.entity('d').loc[1]).to.be.closeTo(2, 1e-6); }); + + describe('transitions', function () { + it('is transitionable', function() { + expect(iD.actionReflect().transitionable).to.be.true; + }); + + it('reflect long at t = 0', function() { + var graph = iD.Graph([ + iD.Node({id: 'a', loc: [0, 0]}), + iD.Node({id: 'b', loc: [4, 0]}), + iD.Node({id: 'c', loc: [4, 2]}), + iD.Node({id: 'd', loc: [1, 2]}), + iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd', 'a']}) + ]); + graph = iD.actionReflect(['-'], projection)(graph, 0); + expect(graph.entity('a').loc[0]).to.be.closeTo(0, 1e-6); + expect(graph.entity('a').loc[1]).to.be.closeTo(0, 1e-6); + expect(graph.entity('b').loc[0]).to.be.closeTo(4, 1e-6); + expect(graph.entity('b').loc[1]).to.be.closeTo(0, 1e-6); + expect(graph.entity('c').loc[0]).to.be.closeTo(4, 1e-6); + expect(graph.entity('c').loc[1]).to.be.closeTo(2, 1e-6); + expect(graph.entity('d').loc[0]).to.be.closeTo(1, 1e-6); + expect(graph.entity('d').loc[1]).to.be.closeTo(2, 1e-6); + }); + + it('reflect long at t = 0.5', function() { + var graph = iD.Graph([ + iD.Node({id: 'a', loc: [0, 0]}), + iD.Node({id: 'b', loc: [4, 0]}), + iD.Node({id: 'c', loc: [4, 2]}), + iD.Node({id: 'd', loc: [1, 2]}), + iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd', 'a']}) + ]); + graph = iD.actionReflect(['-'], projection)(graph, 0.5); + expect(graph.entity('a').loc[0]).to.be.closeTo(0, 1e-6); + expect(graph.entity('a').loc[1]).to.be.closeTo(1, 1e-6); + expect(graph.entity('b').loc[0]).to.be.closeTo(4, 1e-6); + expect(graph.entity('b').loc[1]).to.be.closeTo(1, 1e-6); + expect(graph.entity('c').loc[0]).to.be.closeTo(4, 1e-6); + expect(graph.entity('c').loc[1]).to.be.closeTo(1, 1e-6); + expect(graph.entity('d').loc[0]).to.be.closeTo(1, 1e-6); + expect(graph.entity('d').loc[1]).to.be.closeTo(1, 1e-6); + }); + + it('reflect long at t = 1', function() { + var graph = iD.Graph([ + iD.Node({id: 'a', loc: [0, 0]}), + iD.Node({id: 'b', loc: [4, 0]}), + iD.Node({id: 'c', loc: [4, 2]}), + iD.Node({id: 'd', loc: [1, 2]}), + iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd', 'a']}) + ]); + graph = iD.actionReflect(['-'], projection)(graph, 1); + expect(graph.entity('a').loc[0]).to.be.closeTo(0, 1e-6); + expect(graph.entity('a').loc[1]).to.be.closeTo(2, 1e-6); + expect(graph.entity('b').loc[0]).to.be.closeTo(4, 1e-6); + expect(graph.entity('b').loc[1]).to.be.closeTo(2, 1e-6); + expect(graph.entity('c').loc[0]).to.be.closeTo(4, 1e-6); + expect(graph.entity('c').loc[1]).to.be.closeTo(0, 1e-6); + expect(graph.entity('d').loc[0]).to.be.closeTo(1, 1e-6); + expect(graph.entity('d').loc[1]).to.be.closeTo(0, 1e-6); + }); + + it('reflect short at t = 0', function() { + var graph = iD.Graph([ + iD.Node({id: 'a', loc: [0, 0]}), + iD.Node({id: 'b', loc: [4, 0]}), + iD.Node({id: 'c', loc: [4, 2]}), + iD.Node({id: 'd', loc: [1, 2]}), + iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd', 'a']}) + ]); + graph = iD.actionReflect(['-'], projection).useLongAxis(false)(graph, 0); + expect(graph.entity('a').loc[0]).to.be.closeTo(0, 1e-6); + expect(graph.entity('a').loc[1]).to.be.closeTo(0, 1e-6); + expect(graph.entity('b').loc[0]).to.be.closeTo(4, 1e-6); + expect(graph.entity('b').loc[1]).to.be.closeTo(0, 1e-6); + expect(graph.entity('c').loc[0]).to.be.closeTo(4, 1e-6); + expect(graph.entity('c').loc[1]).to.be.closeTo(2, 1e-6); + expect(graph.entity('d').loc[0]).to.be.closeTo(1, 1e-6); + expect(graph.entity('d').loc[1]).to.be.closeTo(2, 1e-6); + }); + + it('reflect short at t = 0.5', function() { + var graph = iD.Graph([ + iD.Node({id: 'a', loc: [0, 0]}), + iD.Node({id: 'b', loc: [4, 0]}), + iD.Node({id: 'c', loc: [4, 2]}), + iD.Node({id: 'd', loc: [1, 2]}), + iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd', 'a']}) + ]); + graph = iD.actionReflect(['-'], projection).useLongAxis(false)(graph, 0.5); + expect(graph.entity('a').loc[0]).to.be.closeTo(2, 1e-6); + expect(graph.entity('a').loc[1]).to.be.closeTo(0, 1e-6); + expect(graph.entity('b').loc[0]).to.be.closeTo(2, 1e-6); + expect(graph.entity('b').loc[1]).to.be.closeTo(0, 1e-6); + expect(graph.entity('c').loc[0]).to.be.closeTo(2, 1e-6); + expect(graph.entity('c').loc[1]).to.be.closeTo(2, 1e-6); + expect(graph.entity('d').loc[0]).to.be.closeTo(2, 1e-6); + expect(graph.entity('d').loc[1]).to.be.closeTo(2, 1e-6); + }); + + it('reflect short at t = 1', function() { + var graph = iD.Graph([ + iD.Node({id: 'a', loc: [0, 0]}), + iD.Node({id: 'b', loc: [4, 0]}), + iD.Node({id: 'c', loc: [4, 2]}), + iD.Node({id: 'd', loc: [1, 2]}), + iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd', 'a']}) + ]); + graph = iD.actionReflect(['-'], projection).useLongAxis(false)(graph, 1); + expect(graph.entity('a').loc[0]).to.be.closeTo(4, 1e-6); + expect(graph.entity('a').loc[1]).to.be.closeTo(0, 1e-6); + expect(graph.entity('b').loc[0]).to.be.closeTo(0, 1e-6); + expect(graph.entity('b').loc[1]).to.be.closeTo(0, 1e-6); + expect(graph.entity('c').loc[0]).to.be.closeTo(0, 1e-6); + expect(graph.entity('c').loc[1]).to.be.closeTo(2, 1e-6); + expect(graph.entity('d').loc[0]).to.be.closeTo(3, 1e-6); + expect(graph.entity('d').loc[1]).to.be.closeTo(2, 1e-6); + }); + + }); }); From 5dde5aededca96995ed8efaddb4319c13afa596f Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 23 Dec 2016 16:14:49 -0500 Subject: [PATCH 10/13] Add tests for transitioned circularize action --- test/spec/actions/circularize.js | 49 ++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/test/spec/actions/circularize.js b/test/spec/actions/circularize.js index 7d598f9ed..4c70bc4e2 100644 --- a/test/spec/actions/circularize.js +++ b/test/spec/actions/circularize.js @@ -271,4 +271,53 @@ describe('iD.actionCircularize', function () { expect(isCircular('-', graph)).to.be.ok; }); + + describe('transitions', function () { + it('is transitionable', function() { + expect(iD.actionCircularize().transitionable).to.be.true; + }); + + it('circularize at t = 0', function() { + var graph = iD.Graph([ + iD.Node({id: 'a', loc: [0, 0]}), + iD.Node({id: 'b', loc: [2, 0]}), + iD.Node({id: 'c', loc: [2, 2]}), + iD.Node({id: 'd', loc: [0, 2]}), + iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd', 'a']}) + ]); + graph = iD.actionCircularize('-', projection)(graph, 0); + expect(isCircular('-', graph)).to.be.not.ok; + expect(graph.entity('-').nodes).to.have.length(20); + expect(area('-', graph)).to.be.closeTo(-4, 1e-2); + }); + + it('circularize at t = 0.5', function() { + var graph = iD.Graph([ + iD.Node({id: 'a', loc: [0, 0]}), + iD.Node({id: 'b', loc: [2, 0]}), + iD.Node({id: 'c', loc: [2, 2]}), + iD.Node({id: 'd', loc: [0, 2]}), + iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd', 'a']}) + ]); + graph = iD.actionCircularize('-', projection)(graph, 0.5); + expect(isCircular('-', graph)).to.be.not.ok; + expect(graph.entity('-').nodes).to.have.length(20); + expect(area('-', graph)).to.be.closeTo(-4.812, 1e-2); + }); + + it('circularize at t = 1', function() { + var graph = iD.Graph([ + iD.Node({id: 'a', loc: [0, 0]}), + iD.Node({id: 'b', loc: [2, 0]}), + iD.Node({id: 'c', loc: [2, 2]}), + iD.Node({id: 'd', loc: [0, 2]}), + iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd', 'a']}) + ]); + graph = iD.actionCircularize('-', projection)(graph, 1); + expect(isCircular('-', graph)).to.be.ok; + expect(graph.entity('-').nodes).to.have.length(20); + expect(area('-', graph)).to.be.closeTo(-6.168, 1e-2); + }); + }); + }); From ef96fde38a5abbd77a38e405c3421bf6496d3293 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 23 Dec 2016 16:37:17 -0500 Subject: [PATCH 11/13] Add tests for transitioned orthogonalize action --- test/spec/actions/orthogonalize.js | 70 +++++++++++++++++++++++++++--- 1 file changed, 65 insertions(+), 5 deletions(-) diff --git a/test/spec/actions/orthogonalize.js b/test/spec/actions/orthogonalize.js index 2c8138977..d3fb8946d 100644 --- a/test/spec/actions/orthogonalize.js +++ b/test/spec/actions/orthogonalize.js @@ -11,7 +11,6 @@ describe('iD.actionOrthogonalize', function () { ]); graph = iD.actionOrthogonalize('-', projection)(graph); - expect(graph.entity('-').nodes).to.have.length(5); }); @@ -25,7 +24,6 @@ describe('iD.actionOrthogonalize', function () { ]); graph = iD.actionOrthogonalize('-', projection)(graph); - expect(graph.entity('-').nodes).to.have.length(5); }); @@ -38,7 +36,6 @@ describe('iD.actionOrthogonalize', function () { ]); graph = iD.actionOrthogonalize('-', projection)(graph); - expect(graph.entity('-').nodes).to.have.length(4); }); @@ -53,7 +50,6 @@ describe('iD.actionOrthogonalize', function () { ]); graph = iD.actionOrthogonalize('-', projection)(graph); - expect(graph.hasEntity('d')).to.eq(undefined); }); @@ -68,7 +64,6 @@ describe('iD.actionOrthogonalize', function () { ]); graph = iD.actionOrthogonalize('-', projection)(graph); - expect(graph.entity('-').nodes).to.have.length(6); expect(graph.hasEntity('d')).to.not.eq(undefined); }); @@ -121,4 +116,69 @@ describe('iD.actionOrthogonalize', function () { expect(Object.keys(diff.changes()).sort()).to.eql(['a', 'b', 'c', 'f']); }); + + + describe('transitions', function () { + it('is transitionable', function() { + expect(iD.actionOrthogonalize().transitionable).to.be.true; + }); + + it('orthogonalize at t = 0', function() { + var graph = iD.Graph([ + iD.Node({id: 'a', loc: [0, 0]}), + iD.Node({id: 'b', loc: [1, 0.01], tags: {foo: 'bar'}}), + iD.Node({id: 'c', loc: [2, -0.01]}), + iD.Node({id: 'd', loc: [3, 0]}), + iD.Node({id: 'e', loc: [3, 1]}), + iD.Node({id: 'f', loc: [0, 1]}), + iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd', 'e', 'f', 'a']}) + ]); + + graph = iD.actionOrthogonalize('-', projection)(graph, 0); + expect(graph.entity('-').nodes).to.eql(['a', 'b', 'c', 'd', 'e', 'f', 'a']); + expect(graph.entity('b').loc[0]).to.be.closeTo(1, 1e-6); + expect(graph.entity('b').loc[1]).to.be.closeTo(0.01, 1e-6); + expect(graph.entity('c').loc[0]).to.be.closeTo(2, 1e-6); + expect(graph.entity('c').loc[1]).to.be.closeTo(-0.01, 1e-6); + + }); + + it('orthogonalize at t = 0.5', function() { + var graph = iD.Graph([ + iD.Node({id: 'a', loc: [0, 0]}), + iD.Node({id: 'b', loc: [1, 0.01], tags: {foo: 'bar'}}), + iD.Node({id: 'c', loc: [2, -0.01]}), + iD.Node({id: 'd', loc: [3, 0]}), + iD.Node({id: 'e', loc: [3, 1]}), + iD.Node({id: 'f', loc: [0, 1]}), + iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd', 'e', 'f', 'a']}) + ]); + + graph = iD.actionOrthogonalize('-', projection)(graph, 0.5); + expect(graph.entity('-').nodes).to.eql(['a', 'b', 'c', 'd', 'e', 'f', 'a']); + expect(graph.entity('b').loc[0]).to.be.closeTo(1, 1e-3); + expect(graph.entity('b').loc[1]).to.be.closeTo(0.005, 1e-3); + expect(graph.entity('c').loc[0]).to.be.closeTo(2, 1e-3); + expect(graph.entity('c').loc[1]).to.be.closeTo(-0.005, 1e-3); + }); + + it('orthogonalize at t = 1', function() { + var graph = iD.Graph([ + iD.Node({id: 'a', loc: [0, 0]}), + iD.Node({id: 'b', loc: [1, 0.01], tags: {foo: 'bar'}}), + iD.Node({id: 'c', loc: [2, -0.01]}), + iD.Node({id: 'd', loc: [3, 0]}), + iD.Node({id: 'e', loc: [3, 1]}), + iD.Node({id: 'f', loc: [0, 1]}), + iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd', 'e', 'f', 'a']}) + ]); + + graph = iD.actionOrthogonalize('-', projection)(graph, 1); + expect(graph.entity('-').nodes).to.eql(['a', 'b', 'd', 'e', 'f', 'a']); + expect(graph.entity('b').loc[0]).to.be.closeTo(1, 2e-3); + expect(graph.entity('b').loc[1]).to.be.closeTo(0, 2e-3); + expect(graph.hasEntity('c')).to.eq(undefined); + }); + }); + }); From eb0cdd5e8c4c49c9c4370aaadcc677ab55bfcbae Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 23 Dec 2016 17:18:28 -0500 Subject: [PATCH 12/13] History transition code cleanup --- modules/core/history.js | 106 ++++++++++++++++++++-------------------- 1 file changed, 54 insertions(+), 52 deletions(-) diff --git a/modules/core/history.js b/modules/core/history.js index 3e858956a..3f4e03388 100644 --- a/modules/core/history.js +++ b/modules/core/history.js @@ -11,13 +11,15 @@ import { utilRebind } from '../util/rebind'; export function coreHistory(context) { - var stack, index, tree, - imageryUsed = ['Bing'], + var imageryUsed = ['Bing'], dispatch = d3.dispatch('change', 'undone', 'redone'), - lock = utilSessionMutex('lock'); + lock = utilSessionMutex('lock'), + duration = 150, + stack, index, tree; - function perform(actions, t) { + // internal _act, accepts list of actions and eased time + function _act(actions, t) { actions = Array.prototype.slice.call(actions); var annotation; @@ -42,6 +44,40 @@ export function coreHistory(context) { } + // internal _perform with eased time + function _perform(args, t) { + var previous = stack[index].graph; + stack = stack.slice(0, index + 1); + stack.push(_act(args, t)); + index++; + return change(previous); + } + + + // internal _replace with eased time + function _replace(args, t) { + var previous = stack[index].graph; + // assert(index == stack.length - 1) + stack[index] = _act(args, t); + return change(previous); + } + + + // internal _overwrite with eased time + function _overwrite(args, t) { + var previous = stack[index].graph; + if (index > 0) { + index--; + stack.pop(); + } + stack = stack.slice(0, index + 1); + stack.push(_act(args, t)); + index++; + return change(previous); + } + + + // determine diffrence and dispatch a change event function change(previous) { var difference = coreDifference(previous, history.graph()); dispatch.call('change', this, difference); @@ -80,64 +116,46 @@ export function coreHistory(context) { d3.select(document) .interrupt('history.perform'); - var transitionable = false; + var transitionable = false, + action0 = arguments[0]; + if (arguments.length === 1 || arguments.length === 2 && !_.isFunction(arguments[1])) { - transitionable = !!arguments[0].transitionable; + transitionable = !!action0.transitionable; } if (transitionable) { var origArguments = arguments; d3.select(document) .transition('history.perform') - .duration(150) + .duration(duration) .ease(d3.easeLinear) .tween('history.tween', function() { return function(t) { - if (t < 1) _doOverwrite([origArguments[0]], t); + if (t < 1) _overwrite([action0], t); }; }) .on('start', function() { - _doPerform([origArguments[0]], 0); + _perform([action0], 0); }) .on('end interrupt', function() { - _doOverwrite(origArguments, 1); + _overwrite(origArguments, 1); }); } else { - return _doPerform(arguments); - } - - - function _doPerform(args, t) { - var previous = stack[index].graph; - stack = stack.slice(0, index + 1); - stack.push(perform(args, t)); - index++; - return change(previous); - } - - function _doOverwrite(args, t) { - var previous = stack[index].graph; - if (index > 0) { - index--; - stack.pop(); - } - stack = stack.slice(0, index + 1); - stack.push(perform(args, t)); - index++; - return change(previous); + return _perform(arguments); } }, replace: function() { - var previous = stack[index].graph; + return _replace(arguments, 1); + }, - // assert(index == stack.length - 1) - stack[index] = perform(arguments); - return change(previous); + // Same as calling pop and then perform + overwrite: function() { + return _overwrite(arguments, 1); }, @@ -152,22 +170,6 @@ export function coreHistory(context) { }, - // Same as calling pop and then perform - overwrite: function() { - var previous = stack[index].graph; - - if (index > 0) { - index--; - stack.pop(); - } - stack = stack.slice(0, index + 1); - stack.push(perform(arguments)); - index++; - - return change(previous); - }, - - undo: function() { var previous = stack[index].graph; From 9ba610a9775497ac73277de0b914b1547d44c945 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 4 Jan 2017 16:06:09 -0500 Subject: [PATCH 13/13] Add test for transitioned history.perform --- test/spec/core/history.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/spec/core/history.js b/test/spec/core/history.js index 723474153..10693c726 100644 --- a/test/spec/core/history.js +++ b/test/spec/core/history.js @@ -51,6 +51,7 @@ describe('iD.History', function () { history.on('change', spy); var difference = history.perform(action); expect(spy).to.have.been.calledWith(difference); + expect(spy.callCount).to.eql(1); }); it('performs multiple actions', function () { @@ -61,6 +62,17 @@ describe('iD.History', function () { expect(action2).to.have.been.called; expect(history.undoAnnotation()).to.equal('annotation'); }); + + it('performs transitionable actions in a transition', function (done) { + var action1 = function() { return iD.Graph(); }; + action1.transitionable = true; + history.on('change', spy); + history.perform(action1); + window.setTimeout(function() { + expect(spy.callCount).to.be.above(2); + done(); + }, 300); + }); }); describe('#replace', function () {