From 05949608aa09005877f86fbf0e38231b813e3541 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 24 Apr 2019 01:52:34 -0400 Subject: [PATCH] Support straightening of points (closes #6217) - Split `actionStraighten` into `actionStraightenWay` and `actionStraightenNodes` - Now `operationStraighten` chooses the correct action depending on selected entities - Also move `getSmallestSurroundingRectangle` from `actionReflect` to `geo.js` --- data/core.yaml | 12 +- dist/locales/en.json | 14 ++- modules/actions/index.js | 3 +- modules/actions/reflect.js | 53 ++------- modules/actions/straighten_nodes.js | 61 ++++++++++ .../{straighten.js => straighten_way.js} | 2 +- modules/geo/geom.js | 52 +++++++-- modules/geo/index.js | 1 + modules/operations/straighten.js | 110 ++++++++++-------- test/index.html | 3 +- test/spec/actions/straighten_nodes.js | 88 ++++++++++++++ .../{straighten.js => straighten_way.js} | 26 ++--- test/spec/geo/geom.js | 13 +++ 13 files changed, 314 insertions(+), 124 deletions(-) create mode 100644 modules/actions/straighten_nodes.js rename modules/actions/{straighten.js => straighten_way.js} (98%) create mode 100644 test/spec/actions/straighten_nodes.js rename test/spec/actions/{straighten.js => straighten_way.js} (88%) diff --git a/data/core.yaml b/data/core.yaml index d4e3aed3b..0522b3d21 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -119,12 +119,16 @@ en: not_downloaded: This can't be made square because parts of it have not yet been downloaded. straighten: title: Straighten - description: Straighten this line. + description: + points: Straighten these points. + line: Straighten this line. key: S - annotation: Straightened a line. + annotation: + points: Straightened several points. + line: Straightened a line. too_bendy: This can't be straightened because it bends too much. - connected_to_hidden: This line can't be straightened because it is connected to a hidden feature. - not_downloaded: This line can't be straightened because parts of it have not yet been downloaded. + connected_to_hidden: This can't be straightened because it is connected to a hidden feature. + not_downloaded: This can't be straightened because parts of it have not yet been downloaded. delete: title: Delete description: diff --git a/dist/locales/en.json b/dist/locales/en.json index ee0177ffb..5ed75e435 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -152,12 +152,18 @@ }, "straighten": { "title": "Straighten", - "description": "Straighten this line.", + "description": { + "points": "Straighten these points.", + "line": "Straighten this line." + }, "key": "S", - "annotation": "Straightened a line.", + "annotation": { + "points": "Straightened several points.", + "line": "Straightened a line." + }, "too_bendy": "This can't be straightened because it bends too much.", - "connected_to_hidden": "This line can't be straightened because it is connected to a hidden feature.", - "not_downloaded": "This line can't be straightened because parts of it have not yet been downloaded." + "connected_to_hidden": "This can't be straightened because it is connected to a hidden feature.", + "not_downloaded": "This can't be straightened because parts of it have not yet been downloaded." }, "delete": { "title": "Delete", diff --git a/modules/actions/index.js b/modules/actions/index.js index b56fbbb09..dfdd1c7e6 100644 --- a/modules/actions/index.js +++ b/modules/actions/index.js @@ -31,7 +31,8 @@ export { actionReverse } from './reverse'; export { actionRevert } from './revert'; export { actionRotate } from './rotate'; export { actionSplit } from './split'; -export { actionStraighten } from './straighten'; +export { actionStraightenNodes } from './straighten_nodes'; +export { actionStraightenWay } from './straighten_way'; export { actionUnrestrictTurn } from './unrestrict_turn'; export { actionReflect } from './reflect.js'; export { actionUpgradeTags } from './upgrade_tags'; diff --git a/modules/actions/reflect.js b/modules/actions/reflect.js index 5df28d557..16c35669b 100644 --- a/modules/actions/reflect.js +++ b/modules/actions/reflect.js @@ -1,50 +1,10 @@ -import { - polygonHull as d3_polygonHull, - polygonCentroid as d3_polygonCentroid -} from 'd3-polygon'; - -import { geoExtent, geoRotate, geoVecInterp, geoVecLength } from '../geo'; +import { geoGetSmallestSurroundingRectangle, geoVecInterp, geoVecLength } from '../geo'; import { utilGetAllNodes } from '../util'; /* Reflect the given area around its axis of symmetry */ export function actionReflect(reflectIds, projection) { - var useLongAxis = true; - - - // http://gis.stackexchange.com/questions/22895/finding-minimum-area-rectangle-for-given-points - // http://gis.stackexchange.com/questions/3739/generalisation-strategies-for-building-outlines/3756#3756 - function getSmallestSurroundingRectangle(graph, nodes) { - var points = nodes.map(function(n) { return projection(n.loc); }); - var hull = d3_polygonHull(points); - var centroid = d3_polygonCentroid(hull); - var minArea = Infinity; - var ssrExtent = []; - var ssrAngle = 0; - var c1 = hull[0]; - - for (var i = 0; i <= hull.length - 1; i++) { - var c2 = (i === hull.length - 1) ? hull[0] : hull[i + 1]; - var angle = Math.atan2(c2[1] - c1[1], c2[0] - c1[0]); - var poly = geoRotate(hull, -angle, centroid); - var extent = poly.reduce(function(extent, point) { - return extent.extend(geoExtent(point)); - }, geoExtent()); - - var area = extent.area(); - if (area < minArea) { - minArea = area; - ssrExtent = extent; - ssrAngle = angle; - } - c1 = c2; - } - - return { - poly: geoRotate(ssrExtent.polygon(), ssrAngle, centroid), - angle: ssrAngle - }; - } + var _useLongAxis = true; var action = function(graph, t) { @@ -52,7 +12,8 @@ export function actionReflect(reflectIds, projection) { t = Math.min(Math.max(+t, 0), 1); var nodes = utilGetAllNodes(reflectIds, graph); - var ssr = getSmallestSurroundingRectangle(graph, nodes); + var points = nodes.map(function(n) { return projection(n.loc); }); + var ssr = geoGetSmallestSurroundingRectangle(points); // Choose line pq = axis of symmetry. // The shape's surrounding rectangle has 2 axes of symmetry. @@ -64,7 +25,7 @@ export function actionReflect(reflectIds, projection) { var p, q; var isLong = (geoVecLength(p1, q1) > geoVecLength(p2, q2)); - if ((useLongAxis && isLong) || (!useLongAxis && !isLong)) { + if ((_useLongAxis && isLong) || (!_useLongAxis && !isLong)) { p = p1; q = q1; } else { @@ -95,8 +56,8 @@ export function actionReflect(reflectIds, projection) { action.useLongAxis = function(val) { - if (!arguments.length) return useLongAxis; - useLongAxis = val; + if (!arguments.length) return _useLongAxis; + _useLongAxis = val; return action; }; diff --git a/modules/actions/straighten_nodes.js b/modules/actions/straighten_nodes.js new file mode 100644 index 000000000..45e4c5615 --- /dev/null +++ b/modules/actions/straighten_nodes.js @@ -0,0 +1,61 @@ +import { geoGetSmallestSurroundingRectangle, geoVecDot, geoVecLength, geoVecInterp } from '../geo'; + + +/* Align nodes along their common axis */ +export function actionStraightenNodes(nodeIDs, projection) { + + function positionAlongWay(a, o, b) { + return geoVecDot(a, b, o) / geoVecDot(b, b, o); + } + + + var action = function(graph, t) { + if (t === null || !isFinite(t)) t = 1; + t = Math.min(Math.max(+t, 0), 1); + + var nodes = nodeIDs.map(function(id) { return graph.entity(id); }); + var points = nodes.map(function(n) { return projection(n.loc); }); + var ssr = geoGetSmallestSurroundingRectangle(points); + + // Choose line pq = axis of symmetry. + // The shape's surrounding rectangle has 2 axes of symmetry. + // Snap points to the long axis + var p1 = [(ssr.poly[0][0] + ssr.poly[1][0]) / 2, (ssr.poly[0][1] + ssr.poly[1][1]) / 2 ]; + var q1 = [(ssr.poly[2][0] + ssr.poly[3][0]) / 2, (ssr.poly[2][1] + ssr.poly[3][1]) / 2 ]; + var p2 = [(ssr.poly[3][0] + ssr.poly[4][0]) / 2, (ssr.poly[3][1] + ssr.poly[4][1]) / 2 ]; + var q2 = [(ssr.poly[1][0] + ssr.poly[2][0]) / 2, (ssr.poly[1][1] + ssr.poly[2][1]) / 2 ]; + var p, q; + + var isLong = (geoVecLength(p1, q1) > geoVecLength(p2, q2)); + if (isLong) { + p = p1; + q = q1; + } else { + p = p2; + q = q2; + } + + // Move points onto line pq + for (var i = 0; i < points.length; i++) { + var node = nodes[i]; + var point = points[i]; + var u = positionAlongWay(point, p, q); + var point2 = geoVecInterp(p, q, u); + var loc2 = projection.invert(point2); + graph = graph.replace(node.move(geoVecInterp(node.loc, loc2, t))); + } + + return graph; + }; + + + action.disabled = function() { + return false; + }; + + + action.transitionable = true; + + + return action; +} diff --git a/modules/actions/straighten.js b/modules/actions/straighten_way.js similarity index 98% rename from modules/actions/straighten.js rename to modules/actions/straighten_way.js index f4237d213..b317d8606 100644 --- a/modules/actions/straighten.js +++ b/modules/actions/straighten_way.js @@ -6,7 +6,7 @@ import { utilArrayDifference } from '../util'; /* * Based on https://github.com/openstreetmap/potlatch2/net/systemeD/potlatch2/tools/Straighten.as */ -export function actionStraighten(selectedIDs, projection) { +export function actionStraightenWay(selectedIDs, projection) { function positionAlongWay(a, o, b) { return geoVecDot(a, b, o) / geoVecDot(b, b, o); diff --git a/modules/geo/geom.js b/modules/geo/geom.js index 7c5fb789f..ec5ccb576 100644 --- a/modules/geo/geom.js +++ b/modules/geo/geom.js @@ -1,11 +1,13 @@ import { - geoVecAngle, - geoVecCross, - geoVecDot, - geoVecEqual, - geoVecInterp, - geoVecLength, - geoVecSubtract + polygonHull as d3_polygonHull, + polygonCentroid as d3_polygonCentroid +} from 'd3-polygon'; + +import { geoExtent } from './extent.js'; + +import { + geoVecAngle, geoVecCross, geoVecDot, geoVecEqual, + geoVecInterp, geoVecLength, geoVecSubtract } from './vector.js'; @@ -15,11 +17,13 @@ export function geoAngle(a, b, projection) { return geoVecAngle(projection(a.loc), projection(b.loc)); } + export function geoEdgeEqual(a, b) { return (a[0] === b[0] && a[1] === b[1]) || (a[0] === b[1] && a[1] === b[0]); } + // Rotate all points counterclockwise around a pivot point by given angle export function geoRotate(points, angle, around) { return points.map(function(point) { @@ -272,6 +276,40 @@ export function geoPolygonIntersectsPolygon(outer, inner, checkSegments) { } +// http://gis.stackexchange.com/questions/22895/finding-minimum-area-rectangle-for-given-points +// http://gis.stackexchange.com/questions/3739/generalisation-strategies-for-building-outlines/3756#3756 +export function geoGetSmallestSurroundingRectangle(points) { + var hull = d3_polygonHull(points); + var centroid = d3_polygonCentroid(hull); + var minArea = Infinity; + var ssrExtent = []; + var ssrAngle = 0; + var c1 = hull[0]; + + for (var i = 0; i <= hull.length - 1; i++) { + var c2 = (i === hull.length - 1) ? hull[0] : hull[i + 1]; + var angle = Math.atan2(c2[1] - c1[1], c2[0] - c1[0]); + var poly = geoRotate(hull, -angle, centroid); + var extent = poly.reduce(function(extent, point) { + return extent.extend(geoExtent(point)); + }, geoExtent()); + + var area = extent.area(); + if (area < minArea) { + minArea = area; + ssrExtent = extent; + ssrAngle = angle; + } + c1 = c2; + } + + return { + poly: geoRotate(ssrExtent.polygon(), ssrAngle, centroid), + angle: ssrAngle + }; +} + + export function geoPathLength(path) { var length = 0; for (var i = 0; i < path.length - 1; i++) { diff --git a/modules/geo/index.js b/modules/geo/index.js index 90c0b99ae..4d62211aa 100644 --- a/modules/geo/index.js +++ b/modules/geo/index.js @@ -14,6 +14,7 @@ export { geoZoomToScale } from './geo.js'; export { geoAngle } from './geom.js'; export { geoChooseEdge } from './geom.js'; export { geoEdgeEqual } from './geom.js'; +export { geoGetSmallestSurroundingRectangle } from './geom.js'; export { geoHasLineIntersections } from './geom.js'; export { geoHasSelfIntersections } from './geom.js'; export { geoRotate } from './geom.js'; diff --git a/modules/operations/straighten.js b/modules/operations/straighten.js index 046e93a28..3aa0ea772 100644 --- a/modules/operations/straighten.js +++ b/modules/operations/straighten.js @@ -1,65 +1,81 @@ import { t } from '../util/locale'; -import { actionStraighten } from '../actions/index'; +import { actionStraightenNodes, actionStraightenWay } from '../actions/index'; import { behaviorOperation } from '../behavior/index'; import { utilArrayDifference, utilGetAllNodes } from '../util/index'; export function operationStraighten(selectedIDs, context) { - var action = actionStraighten(selectedIDs, context.projection); var wayIDs = selectedIDs.filter(function(id) { return id.charAt(0) === 'w'; }); - var nodes = utilGetAllNodes(wayIDs, context.graph()); + var nodeIDs = selectedIDs.filter(function(id) { return id.charAt(0) === 'n'; }); + + var nodes = utilGetAllNodes(selectedIDs, context.graph()); var coords = nodes.map(function(n) { return n.loc; }); + var action = chooseAction(); + var geometry; var _disabled; + function chooseAction() { + // straighten selected nodes + if (wayIDs.length === 0 && nodeIDs.length > 2) { + geometry = 'points'; + return actionStraightenNodes(nodeIDs, context.projection); + + // straighten selected ways (possibly between range of 2 selected nodes) + } else if (wayIDs.length > 0 && (nodeIDs.length === 0 || nodeIDs.length === 2)) { + var startNodeIDs = []; + var endNodeIDs = []; + + for (var i = 0; i < selectedIDs.length; i++) { + var entity = context.entity(selectedIDs[i]); + if (entity.type === 'node') { + continue; + } else if (entity.type !== 'way' || entity.isClosed()) { + return false; // exit early, can't straighten these + } + + startNodeIDs.push(entity.first()); + endNodeIDs.push(entity.last()); + } + + // Remove duplicate end/startNodeIDs (duplicate nodes cannot be at the line end) + startNodeIDs = startNodeIDs.filter(function(n) { + return startNodeIDs.indexOf(n) === startNodeIDs.lastIndexOf(n); + }); + endNodeIDs = endNodeIDs.filter(function(n) { + return endNodeIDs.indexOf(n) === endNodeIDs.lastIndexOf(n); + }); + + // Ensure all ways are connected (i.e. only 2 unique endpoints/startpoints) + if (utilArrayDifference(startNodeIDs, endNodeIDs).length + + utilArrayDifference(endNodeIDs, startNodeIDs).length !== 2) return false; + + // Ensure path contains at least 3 unique nodes + var wayNodeIDs = utilGetAllNodes(wayIDs, context.graph()) + .map(function(node) { return node.id; }); + if (wayNodeIDs.length <= 2) return false; + + // If range of 2 selected nodes is supplied, ensure nodes lie on the selected path + if (nodeIDs.length === 2 && ( + wayNodeIDs.indexOf(nodeIDs[0]) === -1 || wayNodeIDs.indexOf(nodeIDs[1]) === -1 + )) return false; + + geometry = 'line'; + return actionStraightenWay(selectedIDs, context.projection); + } + + return false; + } + + function operation() { + if (!action) return; context.perform(action, operation.annotation()); } operation.available = function() { - var nodeIDs = nodes.map(function(node) { return node.id; }); - var startNodeIDs = []; - var endNodeIDs = []; - var selectedNodeIDs = []; - - for (var i = 0; i < selectedIDs.length; i++) { - var entity = context.entity(selectedIDs[i]); - if (entity.type === 'node') { - selectedNodeIDs.push(entity.id); - continue; - } else if (entity.type !== 'way' || entity.isClosed()) { - return false; // exit early, can't straighten these - } - - startNodeIDs.push(entity.first()); - endNodeIDs.push(entity.last()); - } - - // Remove duplicate end/startNodeIDs (duplicate nodes cannot be at the line end) - startNodeIDs = startNodeIDs.filter(function(n) { - return startNodeIDs.indexOf(n) === startNodeIDs.lastIndexOf(n); - }); - endNodeIDs = endNodeIDs.filter(function(n) { - return endNodeIDs.indexOf(n) === endNodeIDs.lastIndexOf(n); - }); - - // Return false if line is only 2 nodes long - if (nodeIDs.length <= 2) return false; - - // Return false unless exactly 0 or 2 specific start/end nodes are selected - if (!(selectedNodeIDs.length === 0 || selectedNodeIDs.length === 2)) return false; - - // Ensure all ways are connected (i.e. only 2 unique endpoints/startpoints) - if (utilArrayDifference(startNodeIDs, endNodeIDs).length + - utilArrayDifference(endNodeIDs, startNodeIDs).length !== 2) return false; - - // Ensure both start/end selected nodes lie on the selected path - if (selectedNodeIDs.length === 2 && ( - nodeIDs.indexOf(selectedNodeIDs[0]) === -1 || nodeIDs.indexOf(selectedNodeIDs[1]) === -1 - )) return false; - - return true; + return Boolean(action); }; @@ -96,12 +112,12 @@ export function operationStraighten(selectedIDs, context) { var disable = operation.disabled(); return disable ? t('operations.straighten.' + disable) : - t('operations.straighten.description'); + t('operations.straighten.description.' + geometry); }; operation.annotation = function() { - return t('operations.straighten.annotation'); + return t('operations.straighten.annotation.' + geometry); }; diff --git a/test/index.html b/test/index.html index 4dbafa952..827d7cea7 100644 --- a/test/index.html +++ b/test/index.html @@ -59,7 +59,8 @@ - + + diff --git a/test/spec/actions/straighten_nodes.js b/test/spec/actions/straighten_nodes.js new file mode 100644 index 000000000..96a2412a0 --- /dev/null +++ b/test/spec/actions/straighten_nodes.js @@ -0,0 +1,88 @@ +describe('iD.actionStraightenNodes', function () { + var projection = function (l) { return l; }; + projection.invert = projection; + + it('straightens points', function() { + var graph = iD.coreGraph([ + iD.osmNode({ id: 'a', loc: [0, -1] }), + iD.osmNode({ id: 'b', loc: [5, 1], tags: { foo: 'bar' } }), + iD.osmNode({ id: 'c', loc: [10, -1] }), // untagged + iD.osmNode({ id: 'd', loc: [15, 1] }) + ]); + + graph = iD.actionStraightenNodes(['a','b','c','d'], projection)(graph); + 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(5, 1e-6); + expect(graph.entity('b').loc[1]).to.be.closeTo(0, 1e-6); + expect(graph.entity('c').loc[0]).to.be.closeTo(10, 1e-6); // doesn't delete untagged + expect(graph.entity('c').loc[1]).to.be.closeTo(0, 1e-6); // doesn't delete untagged + expect(graph.entity('d').loc[0]).to.be.closeTo(15, 1e-6); + expect(graph.entity('d').loc[1]).to.be.closeTo(0, 1e-6); + }); + + + describe('transitions', function () { + it('is transitionable', function() { + expect(iD.actionStraightenNodes().transitionable).to.be.true; + }); + + it('straighten at t = 0', function() { + var graph = iD.coreGraph([ + iD.osmNode({ id: 'a', loc: [0, -1] }), + iD.osmNode({ id: 'b', loc: [5, 1], tags: { foo: 'bar' } }), + iD.osmNode({ id: 'c', loc: [10, -1] }), // untagged + iD.osmNode({ id: 'd', loc: [15, 1] }) + ]); + + graph = iD.actionStraightenNodes(['a','b','c','d'], projection)(graph, 0); + 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(5, 1e-6); + expect(graph.entity('b').loc[1]).to.be.closeTo(1, 1e-6); + expect(graph.entity('c').loc[0]).to.be.closeTo(10, 1e-6); // doesn't delete untagged + expect(graph.entity('c').loc[1]).to.be.closeTo(-1, 1e-6); // doesn't delete untagged + expect(graph.entity('d').loc[0]).to.be.closeTo(15, 1e-6); + expect(graph.entity('d').loc[1]).to.be.closeTo(1, 1e-6); + }); + + it('straighten at t = 0.5', function() { + var graph = iD.coreGraph([ + iD.osmNode({ id: 'a', loc: [0, -1] }), + iD.osmNode({ id: 'b', loc: [5, 1], tags: { foo: 'bar' } }), + iD.osmNode({ id: 'c', loc: [10, -1] }), // untagged + iD.osmNode({ id: 'd', loc: [15, 1] }) + ]); + + graph = iD.actionStraightenNodes(['a','b','c','d'], 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(-0.5, 1e-6); + expect(graph.entity('b').loc[0]).to.be.closeTo(5, 1e-6); + expect(graph.entity('b').loc[1]).to.be.closeTo(0.5, 1e-6); + expect(graph.entity('c').loc[0]).to.be.closeTo(10, 1e-6); // doesn't delete untagged + expect(graph.entity('c').loc[1]).to.be.closeTo(-0.5, 1e-6); // doesn't delete untagged + expect(graph.entity('d').loc[0]).to.be.closeTo(15, 1e-6); + expect(graph.entity('d').loc[1]).to.be.closeTo(0.5, 1e-6); + }); + + it('straighten at t = 1', function() { + var graph = iD.coreGraph([ + iD.osmNode({ id: 'a', loc: [0, -1] }), + iD.osmNode({ id: 'b', loc: [5, 1], tags: { foo: 'bar' } }), + iD.osmNode({ id: 'c', loc: [10, -1] }), // untagged + iD.osmNode({ id: 'd', loc: [15, 1] }) + ]); + + graph = iD.actionStraightenNodes(['a','b','c','d'], projection)(graph, 1); + 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(5, 1e-6); + expect(graph.entity('b').loc[1]).to.be.closeTo(0, 1e-6); + expect(graph.entity('c').loc[0]).to.be.closeTo(10, 1e-6); // doesn't delete untagged + expect(graph.entity('c').loc[1]).to.be.closeTo(0, 1e-6); // doesn't delete untagged + expect(graph.entity('d').loc[0]).to.be.closeTo(15, 1e-6); + expect(graph.entity('d').loc[1]).to.be.closeTo(0, 1e-6); + }); + }); + +}); diff --git a/test/spec/actions/straighten.js b/test/spec/actions/straighten_way.js similarity index 88% rename from test/spec/actions/straighten.js rename to test/spec/actions/straighten_way.js index 17bb0dbff..544121530 100644 --- a/test/spec/actions/straighten.js +++ b/test/spec/actions/straighten_way.js @@ -1,4 +1,4 @@ -describe('iD.actionStraighten', function () { +describe('iD.actionStraightenWay', function () { var projection = d3.geoMercator(); describe('#disabled', function () { @@ -10,7 +10,7 @@ describe('iD.actionStraighten', function () { iD.osmNode({id: 'd', loc: [3, 0]}), iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'd']}) ]); - expect(iD.actionStraighten(['-'], projection).disabled(graph)).not.to.be.ok; + expect(iD.actionStraightenWay(['-'], projection).disabled(graph)).not.to.be.ok; }); it('returns \'too_bendy\' for ways with internal nodes far off centerline', function () { @@ -21,7 +21,7 @@ describe('iD.actionStraighten', function () { iD.osmNode({id: 'd', loc: [3, 0]}), iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'd']}) ]); - expect(iD.actionStraighten(['-'], projection).disabled(graph)).to.equal('too_bendy'); + expect(iD.actionStraightenWay(['-'], projection).disabled(graph)).to.equal('too_bendy'); }); it('returns \'too_bendy\' for ways with coincident start/end nodes', function () { @@ -32,7 +32,7 @@ describe('iD.actionStraighten', function () { iD.osmNode({id: 'd', loc: [0, 0]}), iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'd']}) ]); - expect(iD.actionStraighten(['-'], projection).disabled(graph)).to.equal('too_bendy'); + expect(iD.actionStraightenWay(['-'], projection).disabled(graph)).to.equal('too_bendy'); }); }); @@ -45,7 +45,7 @@ describe('iD.actionStraighten', function () { iD.osmWay({id: '-', nodes: ['a', 'b', 'c']}) ]); - graph = iD.actionStraighten(['-'], projection)(graph); + graph = iD.actionStraightenWay(['-'], projection)(graph); expect(graph.entity('-').nodes).to.eql(['a', 'c']); expect(graph.hasEntity('b')).to.eq(undefined); }); @@ -58,7 +58,7 @@ describe('iD.actionStraighten', function () { iD.osmWay({id: '-', nodes: ['a', 'b', 'c']}) ]); - graph = iD.actionStraighten(['-'], projection)(graph); + graph = iD.actionStraightenWay(['-'], 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); @@ -73,7 +73,7 @@ describe('iD.actionStraighten', function () { iD.osmWay({id: '=', nodes: ['b']}) ]); - graph = iD.actionStraighten(['-'], projection)(graph); + graph = iD.actionStraightenWay(['-'], 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); @@ -94,7 +94,7 @@ describe('iD.actionStraighten', function () { iD.Way({id: '--', nodes: ['d', 'e', 'f', 'g', 'h']}) ]); - graph = iD.actionStraighten(['-', '--'], projection)(graph); + graph = iD.actionStraightenWay(['-', '--'], projection)(graph); expect(graph.entity('-').nodes).to.eql(['a', 'b', 'd']); expect(graph.entity('--').nodes).to.eql(['d', 'f', 'h']); expect(graph.entity('f').loc[0]).to.be.closeTo(5, 1e-6); @@ -117,7 +117,7 @@ describe('iD.actionStraighten', function () { iD.Way({id: '--', nodes: ['h', 'g', 'f', 'e', 'd']}) ]); - graph = iD.actionStraighten(['-', '--'], projection)(graph); + graph = iD.actionStraightenWay(['-', '--'], projection)(graph); expect(graph.entity('-').nodes).to.eql(['a', 'b', 'd']); expect(graph.entity('--').nodes).to.eql(['h', 'f', 'd']); expect(graph.entity('f').loc[0]).to.be.closeTo(5, 1e-6); @@ -127,7 +127,7 @@ describe('iD.actionStraighten', function () { describe('transitions', function () { it('is transitionable', function() { - expect(iD.actionStraighten().transitionable).to.be.true; + expect(iD.actionStraightenWay().transitionable).to.be.true; }); it('straighten at t = 0', function() { @@ -139,7 +139,7 @@ describe('iD.actionStraighten', function () { iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'd']}) ]); - graph = iD.actionStraighten(['-'], projection)(graph, 0); + graph = iD.actionStraightenWay(['-'], 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); @@ -156,7 +156,7 @@ describe('iD.actionStraighten', function () { iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'd']}) ]); - graph = iD.actionStraighten(['-'], projection)(graph, 0.5); + graph = iD.actionStraightenWay(['-'], 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); @@ -173,7 +173,7 @@ describe('iD.actionStraighten', function () { iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'd']}) ]); - graph = iD.actionStraighten(['-'], projection)(graph, 1); + graph = iD.actionStraightenWay(['-'], 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); diff --git a/test/spec/geo/geom.js b/test/spec/geo/geom.js index e93a3b53b..dabe3e852 100644 --- a/test/spec/geo/geom.js +++ b/test/spec/geo/geom.js @@ -446,6 +446,19 @@ describe('iD.geo - geometry', function() { }); }); + describe('geoGetSmallestSurroundingRectangle', function() { + it('calculates a smallest surrounding rectangle', function() { + // +----b---------d + // | | + // a---------c----+ + var points = [[0, -1], [5, 1], [10, -1], [15, 1]]; + var ssr = iD.geoGetSmallestSurroundingRectangle(points); + expect(ssr.poly).to.eql([[0, -1], [0, 1], [15, 1], [15, -1], [0, -1]]); + expect(ssr.angle).to.eql(0); + }); + + }); + describe('geoPathLength', function() { it('calculates a simple path length', function() { var path = [[0, 0], [0, 1], [3, 5]];