diff --git a/data/core.yaml b/data/core.yaml index b16152a63..722410a62 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -85,12 +85,16 @@ en: orthogonalize: title: Square description: + vertex: Square this corner. line: Square the corners of this line. area: Square the corners of this area. - key: S + key: Q annotation: + vertex: Squared a single corner. line: Squared the corners of a line. area: Squared the corners of an area. + end_vertex: This can't be squared because it is an end node. + square_enough: This can't be made more square than it already is. not_squarish: This can't be made square because it is not squarish. too_large: This can't be made square because not enough of it is currently visible. connected_to_hidden: This can't be made square because it is connected to a hidden feature. diff --git a/data/presets.yaml b/data/presets.yaml index 283371dca..10167ae48 100644 --- a/data/presets.yaml +++ b/data/presets.yaml @@ -4020,7 +4020,7 @@ en: highway/footway: # highway=footway name: Foot Path - # 'terms: hike,hiking,trackway,trail,walk' + # 'terms: hike,hiking,promenade,trackway,trail,walk' terms: '' highway/footway/conveying: # 'highway=footway, conveying=*' @@ -4187,7 +4187,7 @@ en: highway/steps: # highway=steps name: Steps - # 'terms: stairs,staircase' + # 'terms: stairs,staircase,stairway' terms: '' highway/steps/conveying: # 'highway=steps, conveying=*' diff --git a/data/presets/presets.json b/data/presets/presets.json index e08c7eb0f..2624a4dd6 100644 --- a/data/presets/presets.json +++ b/data/presets/presets.json @@ -426,7 +426,7 @@ "highway/crossing/unmarked": {"fields": ["crossing", "kerb", "tactile_paving"], "geometry": ["vertex"], "addTags": {"highway": "crossing", "crossing": "unmarked"}, "removeTags": {"highway": "crossing", "crossing": "unmarked"}, "tags": {"highway": "crossing"}, "reference": {"key": "highway", "value": "crossing"}, "terms": [], "name": "Unmarked Crossing"}, "highway/cycleway": {"icon": "maki-bicycle", "fields": ["name", "oneway", "surface", "width", "structure", "access", "incline"], "moreFields": ["wheelchair", "lit", "smoothness", "maxspeed", "covered", "dog"], "geometry": ["line"], "tags": {"highway": "cycleway"}, "terms": ["bike"], "name": "Cycle Path"}, "highway/elevator": {"icon": "temaki-elevator", "fields": ["access_simple", "opening_hours", "maxweight", "ref", "wheelchair"], "moreFields": ["maxheight"], "geometry": ["vertex"], "tags": {"highway": "elevator"}, "terms": ["lift"], "name": "Elevator"}, - "highway/footway": {"icon": "temaki-pedestrian", "fields": ["name", "surface", "width", "structure", "access", "incline"], "moreFields": ["wheelchair", "lit", "smoothness", "covered", "dog"], "geometry": ["line"], "terms": ["hike", "hiking", "trackway", "trail", "walk"], "tags": {"highway": "footway"}, "name": "Foot Path"}, + "highway/footway": {"icon": "temaki-pedestrian", "fields": ["name", "surface", "width", "structure", "access", "incline"], "moreFields": ["wheelchair", "lit", "smoothness", "covered", "dog"], "geometry": ["line"], "terms": ["hike", "hiking", "promenade", "trackway", "trail", "walk"], "tags": {"highway": "footway"}, "name": "Foot Path"}, "highway/footway/zebra-raised": {"icon": "temaki-pedestrian", "fields": ["crossing", "access", "surface", "kerb", "tactile_paving"], "geometry": ["line"], "tags": {"highway": "footway", "footway": "crossing", "crossing": "zebra", "traffic_calming": "table"}, "reference": {"key": "traffic_calming", "value": "table"}, "terms": ["zebra crossing", "marked crossing", "crosswalk", "flat top", "hump", "speed", "slow"], "name": "Marked Crosswalk (Raised)", "searchable": false}, "highway/footway/zebra": {"icon": "temaki-pedestrian", "fields": ["crossing", "access", "surface", "kerb", "tactile_paving"], "geometry": ["line"], "tags": {"highway": "footway", "footway": "crossing", "crossing": "zebra"}, "reference": {"key": "footway", "value": "crossing"}, "terms": ["zebra crossing", "marked crossing", "crosswalk"], "name": "Marked Crosswalk", "searchable": false}, "highway/footway/conveying": {"icon": "temaki-pedestrian", "fields": ["name", "conveying", "access_simple", "lit", "width", "wheelchair"], "geometry": ["line"], "terms": ["moving sidewalk", "autwalk", "skywalk", "travolator", "travelator", "travellator", "conveyor"], "tags": {"highway": "footway", "conveying": "*"}, "name": "Moving Walkway"}, @@ -462,7 +462,7 @@ "highway/service/parking_aisle": {"icon": "iD-highway-service", "geometry": ["line"], "tags": {"highway": "service", "service": "parking_aisle"}, "reference": {"key": "service", "value": "parking_aisle"}, "name": "Parking Aisle"}, "highway/services": {"icon": "maki-car", "fields": ["{highway/rest_area}"], "moreFields": ["{highway/rest_area}"], "geometry": ["point", "vertex", "area"], "tags": {"highway": "services"}, "terms": ["services", "travel plaza", "service station"], "name": "Service Area"}, "highway/speed_camera": {"icon": "maki-attraction", "geometry": ["point", "vertex"], "fields": ["direction", "ref", "maxspeed"], "tags": {"highway": "speed_camera"}, "terms": [], "name": "Speed Camera"}, - "highway/steps": {"icon": "iD-highway-steps", "fields": ["surface", "lit", "width", "incline_steps", "handrail", "step_count"], "geometry": ["line"], "tags": {"highway": "steps"}, "terms": ["stairs", "staircase"], "name": "Steps"}, + "highway/steps": {"icon": "iD-highway-steps", "fields": ["incline_steps", "handrail", "step_count", "surface", "lit", "width"], "moreFields": ["covered", "dog"], "geometry": ["line"], "tags": {"highway": "steps"}, "terms": ["stairs", "staircase", "stairway"], "name": "Steps"}, "highway/steps/conveying": {"icon": "maki-entrance", "fields": ["name", "incline_steps", "conveying", "access_simple", "lit", "width", "handrail", "step_count"], "geometry": ["line"], "terms": ["moving staircase", "moving stairway", "people mover"], "tags": {"highway": "steps", "conveying": "*"}, "name": "Escalator"}, "highway/stop": {"icon": "temaki-stop", "fields": ["stop", "direction_vertex"], "geometry": ["vertex"], "tags": {"highway": "stop"}, "terms": ["stop", "halt", "sign"], "name": "Stop Sign"}, "highway/street_lamp": {"icon": "temaki-bulb", "geometry": ["point", "vertex"], "tags": {"highway": "street_lamp"}, "fields": ["lamp_type", "direction", "ref"], "terms": ["streetlight", "street light", "lamp", "light", "gaslight"], "name": "Street Lamp"}, diff --git a/data/presets/presets/highway/footway.json b/data/presets/presets/highway/footway.json index f43435d30..ca4b89ba4 100644 --- a/data/presets/presets/highway/footway.json +++ b/data/presets/presets/highway/footway.json @@ -21,6 +21,7 @@ "terms": [ "hike", "hiking", + "promenade", "trackway", "trail", "walk" diff --git a/data/presets/presets/highway/steps.json b/data/presets/presets/highway/steps.json index 03f3a61fe..f8ab6b936 100644 --- a/data/presets/presets/highway/steps.json +++ b/data/presets/presets/highway/steps.json @@ -1,12 +1,16 @@ { "icon": "iD-highway-steps", "fields": [ - "surface", - "lit", - "width", "incline_steps", "handrail", - "step_count" + "step_count", + "surface", + "lit", + "width" + ], + "moreFields": [ + "covered", + "dog" ], "geometry": [ "line" @@ -16,7 +20,8 @@ }, "terms": [ "stairs", - "staircase" + "staircase", + "stairway" ], "name": "Steps" } diff --git a/dist/locales/en.json b/dist/locales/en.json index 2b037d142..50f0bea75 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -113,14 +113,18 @@ "orthogonalize": { "title": "Square", "description": { + "vertex": "Square this corner.", "line": "Square the corners of this line.", "area": "Square the corners of this area." }, - "key": "S", + "key": "Q", "annotation": { + "vertex": "Squared a single corner.", "line": "Squared the corners of a line.", "area": "Squared the corners of an area." }, + "end_vertex": "This can't be squared because it is an end node.", + "square_enough": "This can't be made more square than it already is.", "not_squarish": "This can't be made square because it is not squarish.", "too_large": "This can't be made square because not enough of it is currently visible.", "connected_to_hidden": "This can't be made square because it is connected to a hidden feature." @@ -5600,7 +5604,7 @@ }, "highway/footway": { "name": "Foot Path", - "terms": "hike,hiking,trackway,trail,walk" + "terms": "hike,hiking,promenade,trackway,trail,walk" }, "highway/footway/zebra-raised": { "name": "Marked Crosswalk (Raised)", @@ -5744,7 +5748,7 @@ }, "highway/steps": { "name": "Steps", - "terms": "stairs,staircase" + "terms": "stairs,staircase,stairway" }, "highway/steps/conveying": { "name": "Escalator", diff --git a/modules/actions/orthogonalize.js b/modules/actions/orthogonalize.js index b9d64b205..15f904a83 100644 --- a/modules/actions/orthogonalize.js +++ b/modules/actions/orthogonalize.js @@ -1,34 +1,65 @@ import _clone from 'lodash-es/clone'; -import _uniq from 'lodash-es/uniq'; +import _cloneDeep from 'lodash-es/cloneDeep'; import { actionDeleteNode } from './delete_node'; -import { geoVecInterp, geoVecLength } from '../geo'; +import { + geoVecAdd, + geoVecEqual, + geoVecInterp, + geoVecLength, + geoVecNormalize, + geoVecNormalizedDot, + geoVecProject, + geoVecScale, + geoVecSubtract +} from '../geo'; -/* - * Based on https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/potlatch2/tools/Quadrilateralise.as - */ -export function actionOrthogonalize(wayId, projection) { - var threshold = 12, // degrees within right or straight to alter - lowerThreshold = Math.cos((90 - threshold) * Math.PI / 180), - upperThreshold = Math.cos(threshold * Math.PI / 180); +export function actionOrthogonalize(wayID, projection, vertexID) { + var epsilon = 1e-4; + var threshold = 13; // degrees within right or straight to alter + + // We test normalized dot products so we can compare as cos(angle) + var lowerThreshold = Math.cos((90 - threshold) * Math.PI / 180); + var upperThreshold = Math.cos(threshold * Math.PI / 180); 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, - node, loc, score, motions, i, j; + var way = graph.entity(wayID); + way = way.removeNode(''); // sanity check - remove any consecutive duplicates + graph = graph.replace(way); + + var isClosed = way.isClosed(); + var nodes = _clone(graph.childNodes(way)); + if (isClosed) nodes.pop(); + + if (vertexID !== undefined) { + nodes = nodeSubset(nodes, vertexID, isClosed); + if (nodes.length !== 3) return graph; + } + + // note: all geometry functions here use the unclosed node/point/coord list + + var nodeCount = {}; + var points = []; + var corner = { i: 0, dotp: 1 }; + var node, point, loc, score, motions, i, j; + + for (i = 0; i < nodes.length; i++) { + node = nodes[i]; + nodeCount[node.id] = (nodeCount[node.id] || 0) + 1; + points.push({ id: node.id, coord: projection(node.loc) }); + } + 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]); + + points[corner.i].coord = geoVecAdd(points[corner.i].coord, motions[corner.i]); score = corner.dotp; if (score < epsilon) { break; @@ -36,22 +67,45 @@ export function actionOrthogonalize(wayId, projection) { } node = graph.entity(nodes[corner.i].id); - loc = projection.invert(points[corner.i]); + loc = projection.invert(points[corner.i].coord); graph = graph.replace(node.move(geoVecInterp(node.loc, loc, t))); } else { - var best, - originalPoints = _clone(points); + var straights = []; + var simplified = []; + + // Remove points from nearly straight sections.. + // This produces a simplified shape to orthogonalize + for (i = 0; i < points.length; i++) { + point = points[i]; + var dotp = 0; + if (isClosed || (i > 0 && i < points.length - 1)) { + var a = points[(i - 1 + points.length) % points.length]; + var b = points[(i + 1) % points.length]; + dotp = Math.abs(normalizedDotProduct(a.coord, b.coord, point.coord)); + } + + if (dotp > upperThreshold) { + straights.push(point); + } else { + simplified.push(point); + } + } + + // Orthogonalize the simplified shape + var bestPoints = _cloneDeep(simplified); + var originalPoints = _cloneDeep(simplified); score = Infinity; for (i = 0; i < 1000; i++) { - motions = points.map(calcMotion); + motions = simplified.map(calcMotion); + for (j = 0; j < motions.length; j++) { - points[j] = addPoints(points[j],motions[j]); + simplified[j].coord = geoVecAdd(simplified[j].coord, motions[j]); } - var newScore = squareness(points); + var newScore = calcScore(simplified, isClosed); if (newScore < score) { - best = _clone(points); + bestPoints = _cloneDeep(simplified); score = newScore; } if (score < epsilon) { @@ -59,30 +113,41 @@ export function actionOrthogonalize(wayId, projection) { } } - points = best; + var bestCoords = bestPoints.map(function(p) { return p.coord; }); + if (isClosed) bestCoords.push(bestCoords[0]); - 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]) { - loc = projection.invert(points[i]); - node = graph.entity(nodes[i].id); + // move the nodes that should move + for (i = 0; i < bestPoints.length; i++) { + point = bestPoints[i]; + if (!geoVecEqual(originalPoints[i].coord, point.coord)) { + node = graph.entity(point.id); + loc = projection.invert(point.coord); graph = graph.replace(node.move(geoVecInterp(node.loc, loc, t))); } } - // remove empty nodes on straight sections - for (i = 0; t === 1 && i < points.length; i++) { - node = graph.entity(nodes[i].id); + // move the nodes along straight segments + for (i = 0; i < straights.length; i++) { + point = straights[i]; + if (nodeCount[point.id] > 1) continue; // skip self-intersections - if (graph.parentWays(node).length > 1 || - graph.parentRelations(node).length || - node.hasInterestingTags()) { - continue; - } + node = graph.entity(point.id); - var dotp = normalizedDotProduct(i, points); - if (dotp < -1 + epsilon) { + if (t === 1 && + graph.parentWays(node).length === 1 && + graph.parentRelations(node).length === 0 && + !node.hasInterestingTags() + ) { + // remove uninteresting points.. graph = actionDeleteNode(node.id)(graph); + + } else { + // move interesting points to the nearest edge.. + var choice = geoVecProject(point.coord, bestCoords); + if (choice) { + loc = projection.invert(choice.target); + graph = graph.replace(node.move(geoVecInterp(node.loc, loc, t))); + } } } } @@ -90,107 +155,146 @@ export function actionOrthogonalize(wayId, projection) { return graph; - function calcMotion(b, i, array) { - var a = array[(i - 1 + array.length) % array.length], - c = array[(i + 1) % array.length], - p = subtractPoints(a, b), - q = subtractPoints(c, b), - scale, dotp; + function calcMotion(point, i, array) { + // don't try to move the endpoints of a non-closed way. + if (!isClosed && (i === 0 || i === array.length - 1)) return [0, 0]; + // don't try to move a node that appears more than once (self intersection) + if (nodeCount[array[i].id] > 1) return [0, 0]; - scale = 2 * Math.min(geoVecLength(p, [0, 0]), geoVecLength(q, [0, 0])); - p = normalizePoint(p, 1.0); - q = normalizePoint(q, 1.0); + var a = array[(i - 1 + array.length) % array.length].coord; + var origin = point.coord; + var b = array[(i + 1) % array.length].coord; + var p = geoVecSubtract(a, origin); + var q = geoVecSubtract(b, origin); - dotp = filterDotProduct(p[0] * q[0] + p[1] * q[1]); + var scale = 2 * Math.min(geoVecLength(p), geoVecLength(q)); + p = geoVecNormalize(p); + q = geoVecNormalize(q); - // nasty hack to deal with almost-straight segments (angle is closer to 180 than to 90/270). - if (array.length > 3) { - if (dotp < -0.707106781186547) { - dotp += 1.0; - } - } else if (dotp && Math.abs(dotp) < corner.dotp) { + var dotp = (p[0] * q[0] + p[1] * q[1]); + var val = Math.abs(dotp); + + if (val < lowerThreshold) { // nearly orthogonal corner.i = i; - corner.dotp = Math.abs(dotp); + corner.dotp = val; + var vec = geoVecNormalize(geoVecAdd(p, q)); + return geoVecScale(vec, 0.1 * dotp * scale); } - return normalizePoint(addPoints(p, q), 0.1 * dotp * scale); + return [0, 0]; // do nothing } }; - function squareness(points) { - return points.reduce(function(sum, val, i, array) { - var dotp = normalizedDotProduct(i, array); - - dotp = filterDotProduct(dotp); - return sum + 2.0 * Math.min(Math.abs(dotp - 1.0), Math.min(Math.abs(dotp), Math.abs(dotp + 1))); - }, 0); - } - - - function normalizedDotProduct(i, points) { - var a = points[(i - 1 + points.length) % points.length], - b = points[i], - c = points[(i + 1) % points.length], - p = subtractPoints(a, b), - q = subtractPoints(c, b); - - p = normalizePoint(p, 1.0); - q = normalizePoint(q, 1.0); - - return p[0] * q[0] + p[1] * q[1]; - } - - - function subtractPoints(a, b) { - return [a[0] - b[0], a[1] - b[1]]; - } - - - function addPoints(a, b) { - return [a[0] + b[0], a[1] + b[1]]; - } - - - function normalizePoint(point, scale) { - var vector = [0, 0]; - var length = Math.sqrt(point[0] * point[0] + point[1] * point[1]); - if (length !== 0) { - vector[0] = point[0] / length; - vector[1] = point[1] / length; + function normalizedDotProduct(a, b, origin) { + if (geoVecEqual(origin, a) || geoVecEqual(origin, b)) { + return 1; // coincident points, treat as straight and try to remove } - - vector[0] *= scale; - vector[1] *= scale; - - return vector; + return geoVecNormalizedDot(a, b, origin); } function filterDotProduct(dotp) { - if (lowerThreshold > Math.abs(dotp) || Math.abs(dotp) > upperThreshold) { - return dotp; + var val = Math.abs(dotp); + if (val < epsilon) { + return 0; // already orthogonal + } else if (val < lowerThreshold || val > upperThreshold) { + return dotp; // can be adjusted + } else { + return null; // ignore vertex + } + } + + + function calcScore(points, isClosed) { + var score = 0; + var first = isClosed ? 0 : 1; + var last = isClosed ? points.length : points.length - 1; + var coords = points.map(function(p) { return p.coord; }); + + for (var i = first; i < last; i++) { + var a = coords[(i - 1 + coords.length) % coords.length]; + var origin = coords[i]; + var b = coords[(i + 1) % coords.length]; + + var dotp = filterDotProduct(normalizedDotProduct(a, b, origin)); + if (dotp === null) continue; // ignore vertex + score = score + 2.0 * Math.min(Math.abs(dotp - 1.0), Math.min(Math.abs(dotp), Math.abs(dotp + 1))); } - return 0; + return score; + } + + + // similar to calcScore, but returns quickly if there is something to do + function canOrthogonalize(coords, isClosed) { + var score = null; + var first = isClosed ? 0 : 1; + var last = isClosed ? coords.length : coords.length - 1; + + for (var i = first; i < last; i++) { + var a = coords[(i - 1 + coords.length) % coords.length]; + var origin = coords[i]; + var b = coords[(i + 1) % coords.length]; + + var dotp = filterDotProduct(normalizedDotProduct(a, b, origin)); + if (dotp === null) continue; // ignore vertex + if (Math.abs(dotp) > 0) return 1; // something to do + score = 0; // already square + } + + return score; + } + + + // if we are only orthogonalizing one vertex, + // get that vertex and the previous and next + function nodeSubset(nodes, vertexID, isClosed) { + var first = isClosed ? 0 : 1; + var last = isClosed ? nodes.length : nodes.length - 1; + + for (var i = first; i < last; i++) { + if (nodes[i].id === vertexID) { + return [ + nodes[(i - 1 + nodes.length) % nodes.length], + nodes[i], + nodes[(i + 1) % nodes.length] + ]; + } + } + + return []; } action.disabled = function(graph) { - var way = graph.entity(wayId), - nodes = graph.childNodes(way), - points = _uniq(nodes).map(function(n) { return projection(n.loc); }); + var way = graph.entity(wayID); + way = way.removeNode(''); // sanity check - remove any consecutive duplicates + graph = graph.replace(way); - if (squareness(points)) { - return false; + var isClosed = way.isClosed(); + var nodes = _clone(graph.childNodes(way)); + if (isClosed) nodes.pop(); + + if (vertexID !== undefined) { + nodes = nodeSubset(nodes, vertexID, isClosed); + if (nodes.length !== 3) return 'end_vertex'; } - return 'not_squarish'; + var coords = nodes.map(function(n) { return projection(n.loc); }); + var score = canOrthogonalize(coords, isClosed); + + if (score === null) { + return 'not_squarish'; + } else if (score === 0) { + return 'square_enough'; + } else { + return false; + } }; action.transitionable = true; - return action; } diff --git a/modules/core/difference.js b/modules/core/difference.js index a00f0f0ce..e897fb983 100644 --- a/modules/core/difference.js +++ b/modules/core/difference.js @@ -5,7 +5,7 @@ import _values from 'lodash-es/values'; /* - iD.Difference represents the difference between two graphs. + iD.coreDifference represents the difference between two graphs. It knows how to calculate the set of entities that were created, modified, or deleted, and also contains the logic for recursively extending a difference to the complete set diff --git a/modules/geo/index.js b/modules/geo/index.js index e27f00cfc..4ef9e250d 100644 --- a/modules/geo/index.js +++ b/modules/geo/index.js @@ -36,5 +36,8 @@ export { geoVecEqual } from './vector.js'; export { geoVecFloor } from './vector.js'; export { geoVecInterp } from './vector.js'; export { geoVecLength } from './vector.js'; +export { geoVecNormalize } from './vector.js'; +export { geoVecNormalizedDot } from './vector.js'; +export { geoVecProject } from './vector.js'; export { geoVecSubtract } from './vector.js'; export { geoVecScale } from './vector.js'; diff --git a/modules/geo/vector.js b/modules/geo/vector.js index e67a6d176..97318607d 100644 --- a/modules/geo/vector.js +++ b/modules/geo/vector.js @@ -37,11 +37,21 @@ export function geoVecInterp(a, b, t) { // http://jsperf.com/id-dist-optimization export function geoVecLength(a, b) { + b = b || [0, 0]; var x = a[0] - b[0]; var y = a[1] - b[1]; return Math.sqrt((x * x) + (y * y)); } +// get a unit vector +export function geoVecNormalize(a) { + var length = Math.sqrt((a[0] * a[0]) + (a[1] * a[1])); + if (length !== 0) { + return geoVecScale(a, 1 / length); + } + return [0, 0]; +} + // Return the counterclockwise angle in the range (-pi, pi) // between the positive X axis and the line intersecting a and b. export function geoVecAngle(a, b) { @@ -51,8 +61,17 @@ export function geoVecAngle(a, b) { // dot product export function geoVecDot(a, b, origin) { origin = origin || [0, 0]; - return (a[0] - origin[0]) * (b[0] - origin[0]) + - (a[1] - origin[1]) * (b[1] - origin[1]); + var p = geoVecSubtract(a, origin); + var q = geoVecSubtract(b, origin); + return (p[0]) * (q[0]) + (p[1]) * (q[1]); +} + +// normalized dot product +export function geoVecNormalizedDot(a, b, origin) { + origin = origin || [0, 0]; + var p = geoVecNormalize(geoVecSubtract(a, origin)); + var q = geoVecNormalize(geoVecSubtract(b, origin)); + return geoVecDot(p, q); } // 2D cross product of OA and OB vectors, returns magnitude of Z vector @@ -60,7 +79,45 @@ export function geoVecDot(a, b, origin) { // negative for clockwise turn, and zero if the points are collinear. export function geoVecCross(a, b, origin) { origin = origin || [0, 0]; - return (a[0] - origin[0]) * (b[1] - origin[1]) - - (a[1] - origin[1]) * (b[0] - origin[0]); + var p = geoVecSubtract(a, origin); + var q = geoVecSubtract(b, origin); + return (p[0]) * (q[1]) - (p[1]) * (q[0]); +} + + +// find closest orthogonal projection of point onto points array +export function geoVecProject(a, points) { + var min = Infinity; + var idx; + var target; + + for (var i = 0; i < points.length - 1; i++) { + var o = points[i]; + var s = geoVecSubtract(points[i + 1], o); + var v = geoVecSubtract(a, o); + var proj = geoVecDot(v, s) / geoVecDot(s, s); + var p; + + if (proj < 0) { + p = o; + } else if (proj > 1) { + p = points[i + 1]; + } else { + p = [o[0] + proj * s[0], o[1] + proj * s[1]]; + } + + var dist = geoVecLength(p, a); + if (dist < min) { + min = dist; + idx = i + 1; + target = p; + } + } + + if (idx !== undefined) { + return { index: idx, distance: min, target: target }; + } else { + return null; + } } diff --git a/modules/operations/orthogonalize.js b/modules/operations/orthogonalize.js index d4d7a6bf3..908ca6071 100644 --- a/modules/operations/orthogonalize.js +++ b/modules/operations/orthogonalize.js @@ -6,31 +6,59 @@ import { behaviorOperation } from '../behavior/index'; export function operationOrthogonalize(selectedIDs, context) { - var entityId = selectedIDs[0], - entity = context.entity(entityId), - extent = entity.extent(context.graph()), - geometry = context.geometry(entityId), - action = actionOrthogonalize(entityId, context.projection); + var _entityID; + var _entity; + var _geometry; + var action = chooseAction(); + + + function chooseAction() { + if (selectedIDs.length !== 1) return null; + + _entityID = selectedIDs[0]; + _entity = context.entity(_entityID); + _geometry = context.geometry(_entityID); + + // square a line/area + if (_entity.type === 'way' && _uniq(_entity.nodes).length > 2 ) { + return actionOrthogonalize(_entityID, context.projection); + + // square a single vertex + } else if (_geometry === 'vertex') { + var graph = context.graph(); + var parents = graph.parentWays(_entity); + if (parents.length === 1) { + var way = parents[0]; + if (way.nodes.indexOf(_entityID) !== -1) { + return actionOrthogonalize(way.id, context.projection, _entityID); + } + } + } + + return null; + } var operation = function() { + if (!action) return; context.perform(action, operation.annotation()); }; operation.available = function() { - return selectedIDs.length === 1 && - entity.type === 'way' && - entity.isClosed() && - _uniq(entity.nodes).length > 2; + return Boolean(action); }; operation.disabled = function() { + if (!action) return ''; + + var extent = _entity.extent(context.graph()); var reason; - if (extent.percentContainedIn(context.extent()) < 0.8) { + + if (_geometry !== 'vertex' && extent.percentContainedIn(context.extent()) < 0.8) { reason = 'too_large'; - } else if (context.hasHiddenConnections(entityId)) { + } else if (context.hasHiddenConnections(_entityID)) { reason = 'connected_to_hidden'; } return action.disabled(context.graph()) || reason; @@ -41,12 +69,12 @@ export function operationOrthogonalize(selectedIDs, context) { var disable = operation.disabled(); return disable ? t('operations.orthogonalize.' + disable) : - t('operations.orthogonalize.description.' + geometry); + t('operations.orthogonalize.description.' + _geometry); }; operation.annotation = function() { - return t('operations.orthogonalize.annotation.' + geometry); + return t('operations.orthogonalize.annotation.' + _geometry); }; diff --git a/modules/osm/way.js b/modules/osm/way.js index 82fdfd3c5..753f7acd4 100644 --- a/modules/osm/way.js +++ b/modules/osm/way.js @@ -195,7 +195,6 @@ _extend(osmWay.prototype, { // returns an object with the tag that implies this is an area, if any tagSuggestingArea: function() { - if (this.tags.area === 'yes') return { area: 'yes' }; if (this.tags.area === 'no') return null; @@ -230,7 +229,6 @@ _extend(osmWay.prototype, { }, isArea: function() { - if (this.tags.area === 'yes') return true; if (!this.isClosed() || this.tags.area === 'no') diff --git a/modules/ui/intro/helper.js b/modules/ui/intro/helper.js index de261ea1a..cb40f4932 100644 --- a/modules/ui/intro/helper.js +++ b/modules/ui/intro/helper.js @@ -1,7 +1,7 @@ import { select as d3_select } from 'd3-selection'; import { t } from '../../util/locale'; -import { geoSphericalDistance } from '../../geo'; +import { geoSphericalDistance, geoVecNormalizedDot } from '../../geo'; export function pointBox(loc, context) { @@ -117,45 +117,20 @@ export function isMostlySquare(points) { var threshold = 15; // degrees within right or straight var lowerBound = Math.cos((90 - threshold) * Math.PI / 180); // near right var upperBound = Math.cos(threshold * Math.PI / 180); // near straight - var mag; for (var i = 0; i < points.length; i++) { - mag = Math.abs(normalizedDotProduct(i, points)); + var a = points[(i - 1 + points.length) % points.length]; + var origin = points[i]; + var b = points[(i + 1) % points.length]; + + var dotp = geoVecNormalizedDot(a, b, origin); + var mag = Math.abs(dotp); if (mag > lowerBound && mag < upperBound) { return false; } } return true; - - - function normalizedDotProduct(i, points) { - var a = points[(i - 1 + points.length) % points.length]; - var b = points[i]; - var c = points[(i + 1) % points.length]; - var p = subtractPoints(a, b); - var q = subtractPoints(c, b); - - p = normalizePoint(p); - q = normalizePoint(q); - - return p[0] * q[0] + p[1] * q[1]; - - - function subtractPoints(a, b) { - return [a[0] - b[0], a[1] - b[1]]; - } - - function normalizePoint(point) { - var vector = [0, 0]; - var length = Math.sqrt(point[0] * point[0] + point[1] * point[1]); - if (length !== 0) { - vector[0] = point[0] / length; - vector[1] = point[1] / length; - } - return vector; - } - } } diff --git a/package.json b/package.json index 21cdb170b..ce35d305b 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "js-yaml": "^3.9.0", "json-stringify-pretty-compact": "^1.1.0", "jsonschema": "^1.1.0", - "mapillary-js": "2.16.0", + "mapillary-js": "2.17.0", "mapillary_sprite_source": "^1.7.0", "minimist": "^1.2.0", "mocha": "^6.0.0", diff --git a/test/spec/actions/copy_entities.js b/test/spec/actions/copy_entities.js index f6ac256f3..87508b438 100644 --- a/test/spec/actions/copy_entities.js +++ b/test/spec/actions/copy_entities.js @@ -1,24 +1,24 @@ describe('iD.actionCopyEntities', function () { it('copies a node', function () { - var a = iD.osmNode({id: 'a'}), - base = iD.coreGraph([a]), - head = iD.actionCopyEntities(['a'], base)(base), - diff = iD.Difference(base, head), - created = diff.created(); + var a = iD.osmNode({id: 'a'}); + var base = iD.coreGraph([a]); + var head = iD.actionCopyEntities(['a'], base)(base); + var diff = iD.coreDifference(base, head); + var created = diff.created(); expect(head.hasEntity('a')).to.be.ok; expect(created).to.have.length(1); }); it('copies a way', function () { - var a = iD.osmNode({id: 'a'}), - b = iD.osmNode({id: 'b'}), - w = iD.osmWay({id: 'w', nodes: ['a', 'b']}), - base = iD.coreGraph([a, b, w]), - action = iD.actionCopyEntities(['w'], base), - head = action(base), - diff = iD.Difference(base, head), - created = diff.created(); + var a = iD.osmNode({id: 'a'}); + var b = iD.osmNode({id: 'b'}); + var w = iD.osmWay({id: 'w', nodes: ['a', 'b']}); + var base = iD.coreGraph([a, b, w]); + var action = iD.actionCopyEntities(['w'], base); + var head = action(base); + var diff = iD.coreDifference(base, head); + var created = diff.created(); expect(head.hasEntity('w')).to.be.ok; expect(created).to.have.length(3); @@ -26,13 +26,13 @@ describe('iD.actionCopyEntities', function () { it('copies multiple nodes', function () { var base = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}) - ]), - action = iD.actionCopyEntities(['a', 'b'], base), - head = action(base), - diff = iD.Difference(base, head), - created = diff.created(); + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}) + ]); + var action = iD.actionCopyEntities(['a', 'b'], base); + var head = action(base); + var diff = iD.coreDifference(base, head); + var created = diff.created(); expect(head.hasEntity('a')).to.be.ok; expect(head.hasEntity('b')).to.be.ok; @@ -41,29 +41,27 @@ describe('iD.actionCopyEntities', function () { it('copies multiple ways, keeping the same connections', function () { var base = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), - iD.osmWay({id: 'w1', nodes: ['a', 'b']}), - iD.osmWay({id: 'w2', nodes: ['b', 'c']}) - ]), - action = iD.actionCopyEntities(['w1', 'w2'], base), - head = action(base), - diff = iD.Difference(base, head), - created = diff.created(); + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmWay({id: 'w1', nodes: ['a', 'b']}), + iD.osmWay({id: 'w2', nodes: ['b', 'c']}) + ]); + var action = iD.actionCopyEntities(['w1', 'w2'], base); + var head = action(base); + var diff = iD.coreDifference(base, head); + var created = diff.created(); expect(created).to.have.length(5); expect(action.copies().w1.nodes[1]).to.eql(action.copies().w2.nodes[0]); }); it('obtains source entities from an alternate graph', function () { - var a = iD.osmNode({id: 'a'}), - old = iD.coreGraph([a]), - base = iD.coreGraph(), - action = iD.actionCopyEntities(['a'], old), - head = action(base), - diff = iD.Difference(base, head); - diff.created(); + var a = iD.osmNode({id: 'a'}); + var old = iD.coreGraph([a]); + var base = iD.coreGraph(); + var action = iD.actionCopyEntities(['a'], old); + var head = action(base); expect(head.hasEntity('a')).not.to.be.ok; expect(Object.keys(action.copies())).to.have.length(1); diff --git a/test/spec/actions/orthogonalize.js b/test/spec/actions/orthogonalize.js index 5da57ef8a..1b487a3ed 100644 --- a/test/spec/actions/orthogonalize.js +++ b/test/spec/actions/orthogonalize.js @@ -1,8 +1,13 @@ describe('iD.actionOrthogonalize', function () { - var projection = d3.geoMercator(); + var projection = function (l) { return l; }; + projection.invert = projection; - it('orthogonalizes a perfect quad', function () { - var graph = iD.coreGraph([ + describe('closed paths', function () { + it('orthogonalizes a perfect quad', function () { + // d --- c + // | | + // a --- b + var graph = iD.coreGraph([ iD.osmNode({id: 'a', loc: [0, 0]}), iD.osmNode({id: 'b', loc: [2, 0]}), iD.osmNode({id: 'c', loc: [2, 2]}), @@ -10,37 +15,47 @@ describe('iD.actionOrthogonalize', function () { iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'd', 'a']}) ]); - graph = iD.actionOrthogonalize('-', projection)(graph); - expect(graph.entity('-').nodes).to.have.length(5); - }); + graph = iD.actionOrthogonalize('-', projection)(graph); + expect(graph.entity('-').nodes).to.have.length(5); + }); - it('orthogonalizes a quad', function () { - var graph = iD.coreGraph([ + it('orthogonalizes a quad', function () { + // d --- c + // | | + // a --- b + var graph = iD.coreGraph([ iD.osmNode({id: 'a', loc: [0, 0]}), - iD.osmNode({id: 'b', loc: [4, 0]}), - iD.osmNode({id: 'c', loc: [3, 2]}), + iD.osmNode({id: 'b', loc: [2.1, 0]}), + iD.osmNode({id: 'c', loc: [2, 2]}), iD.osmNode({id: 'd', loc: [0, 2]}), iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'd', 'a']}) ]); - graph = iD.actionOrthogonalize('-', projection)(graph); - expect(graph.entity('-').nodes).to.have.length(5); - }); + graph = iD.actionOrthogonalize('-', projection)(graph); + expect(graph.entity('-').nodes).to.have.length(5); + }); - it('orthogonalizes a triangle', function () { - var graph = iD.coreGraph([ - iD.osmNode({id: 'a', loc: [0, 0]}), - iD.osmNode({id: 'b', loc: [3, 0]}), - iD.osmNode({id: 'c', loc: [2, 2]}), + it('orthogonalizes a triangle', function () { + // a + // | \ + // | \ + // b - c + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 3]}), + iD.osmNode({id: 'b', loc: [0.1, 0]}), + iD.osmNode({id: 'c', loc: [3, 0]}), iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'a']}) ]); - graph = iD.actionOrthogonalize('-', projection)(graph); - expect(graph.entity('-').nodes).to.have.length(4); - }); + graph = iD.actionOrthogonalize('-', projection)(graph); + expect(graph.entity('-').nodes).to.have.length(4); + }); - it('deletes empty redundant nodes', function() { - var graph = iD.coreGraph([ + it('deletes empty redundant nodes', function() { + // e - d - c + // | | + // a ----- b + var graph = iD.coreGraph([ iD.osmNode({id: 'a', loc: [0, 0]}), iD.osmNode({id: 'b', loc: [2, 0]}), iD.osmNode({id: 'c', loc: [2, 2]}), @@ -49,12 +64,15 @@ describe('iD.actionOrthogonalize', function () { iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'd', 'e', 'a']}) ]); - graph = iD.actionOrthogonalize('-', projection)(graph); - expect(graph.hasEntity('d')).to.eq(undefined); - }); + graph = iD.actionOrthogonalize('-', projection)(graph); + expect(graph.hasEntity('d')).to.eq(undefined); + }); - it('preserves non empty redundant nodes', function() { - var graph = iD.coreGraph([ + it('preserves non empty redundant nodes', function() { + // e - d - c + // | | + // a ----- b + var graph = iD.coreGraph([ iD.osmNode({id: 'a', loc: [0, 0]}), iD.osmNode({id: 'b', loc: [2, 0]}), iD.osmNode({id: 'c', loc: [2, 2]}), @@ -63,76 +81,597 @@ describe('iD.actionOrthogonalize', function () { iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'd', 'e', 'a']}) ]); - graph = iD.actionOrthogonalize('-', projection)(graph); - expect(graph.entity('-').nodes).to.have.length(6); - expect(graph.hasEntity('d')).to.not.eq(undefined); - }); - - it('preserves the shape of skinny quads', function () { - var tests = [ - [ - [-77.0339864831478, 38.8616391227204], - [-77.0209775298677, 38.8613609264884], - [-77.0210405781065, 38.8607390721519], - [-77.0339024188294, 38.8610663645859] - ], - [ - [-89.4706683, 40.6261177], - [-89.4706664, 40.6260574], - [-89.4693973, 40.6260830], - [-89.4694012, 40.6261355] - ] - ]; - - for (var i = 0; i < tests.length; i++) { - var graph = iD.coreGraph([ - iD.osmNode({id: 'a', loc: tests[i][0]}), - iD.osmNode({id: 'b', loc: tests[i][1]}), - iD.osmNode({id: 'c', loc: tests[i][2]}), - iD.osmNode({id: 'd', loc: tests[i][3]}), - iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'd', 'a']}) - ]), - initialWidth = iD.geoSphericalDistance(graph.entity('a').loc, graph.entity('b').loc), - finalWidth; - graph = iD.actionOrthogonalize('-', projection)(graph); + expect(graph.entity('-').nodes).to.have.length(6); + expect(graph.hasEntity('d')).to.not.eq(undefined); + }); - finalWidth = iD.geoSphericalDistance(graph.entity('a').loc, graph.entity('b').loc); - expect(finalWidth / initialWidth).within(0.90, 1.10); - } - }); - - it('only moves nodes which are near right or near straight', function() { - var graph = iD.coreGraph([ + it('only moves nodes which are near right or near straight', function() { + // f - e + // | \ + // | d - c + // | | + // a -------- b + var graph = iD.coreGraph([ iD.osmNode({id: 'a', loc: [0, 0]}), - iD.osmNode({id: 'b', loc: [3, 0.001]}), + iD.osmNode({id: 'b', loc: [3.1, 0]}), iD.osmNode({id: 'c', loc: [3, 1]}), iD.osmNode({id: 'd', loc: [2, 1]}), iD.osmNode({id: 'e', loc: [1, 2]}), iD.osmNode({id: 'f', loc: [0, 2]}), iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'd', 'e', 'f', 'a']}) - ]), - diff = iD.Difference(graph, iD.actionOrthogonalize('-', projection)(graph)); + ]); - expect(Object.keys(diff.changes()).sort()).to.eql(['a', 'b', 'c', 'f']); + var diff = iD.coreDifference(graph, iD.actionOrthogonalize('-', projection)(graph)); + expect(Object.keys(diff.changes()).sort()).to.eql(['a', 'b', 'c', 'f']); + }); + + it('does not move or remove self-intersecting nodes', function() { + // f -- g + // | | + // e --- d - c + // | | + // a -- b + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [ 0, -1]}), + iD.osmNode({id: 'b', loc: [ 1, -1]}), + iD.osmNode({id: 'c', loc: [ 0, 1]}), + iD.osmNode({id: 'd', loc: [ 0.1, 0]}), + iD.osmNode({id: 'e', loc: [-1, 0]}), + iD.osmNode({id: 'f', loc: [-1, 1]}), + iD.osmNode({id: 'g', loc: [ 0, 1]}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'd', 'a']}) + ]); + + var diff = iD.coreDifference(graph, iD.actionOrthogonalize('-', projection)(graph)); + expect(diff.changes().d).to.be.undefined; + expect(graph.hasEntity('d')).to.be.ok; + }); + + it('preserves the shape of skinny quads', function () { + var projection = iD.d3.geoMercator(); + var tests = [[ + [-77.0339864831478, 38.8616391227204], + [-77.0209775298677, 38.8613609264884], + [-77.0210405781065, 38.8607390721519], + [-77.0339024188294, 38.8610663645859] + ], [ + [-89.4706683, 40.6261177], + [-89.4706664, 40.6260574], + [-89.4693973, 40.6260830], + [-89.4694012, 40.6261355] + ]]; + + for (var i = 0; i < tests.length; i++) { + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: tests[i][0]}), + iD.osmNode({id: 'b', loc: tests[i][1]}), + iD.osmNode({id: 'c', loc: tests[i][2]}), + iD.osmNode({id: 'd', loc: tests[i][3]}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'd', 'a']}) + ]); + var initialWidth = iD.geoSphericalDistance(graph.entity('a').loc, graph.entity('b').loc); + graph = iD.actionOrthogonalize('-', projection)(graph); + var finalWidth = iD.geoSphericalDistance(graph.entity('a').loc, graph.entity('b').loc); + expect(finalWidth / initialWidth).within(0.90, 1.10); + } + }); }); + describe('open paths', function () { + it('orthogonalizes a perfect quad path', function () { + // d --- c + // | + // a --- b + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [2, 0]}), + iD.osmNode({id: 'c', loc: [2, 2]}), + iD.osmNode({id: 'd', loc: [0, 2]}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'd']}) + ]); + + graph = iD.actionOrthogonalize('-', projection)(graph); + expect(graph.entity('-').nodes).to.have.length(4); + }); + + it('orthogonalizes a quad path', function () { + // d --- c + // | + // a --- b + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [2.1, 0]}), + iD.osmNode({id: 'c', loc: [2, 2]}), + iD.osmNode({id: 'd', loc: [0, 2]}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'd']}) + ]); + + graph = iD.actionOrthogonalize('-', projection)(graph); + expect(graph.entity('-').nodes).to.have.length(4); + }); + + it('orthogonalizes a 3-point path', function () { + // a + // | + // | + // b - c + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 3]}), + iD.osmNode({id: 'b', loc: [0.1, 0]}), + iD.osmNode({id: 'c', loc: [3, 0]}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c']}) + ]); + + graph = iD.actionOrthogonalize('-', projection)(graph); + expect(graph.entity('-').nodes).to.have.length(3); + }); + + it('deletes empty redundant nodes', function() { + // e - d - c + // | + // a ----- b + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [2, 0]}), + iD.osmNode({id: 'c', loc: [2, 2]}), + iD.osmNode({id: 'd', loc: [1, 2]}), + iD.osmNode({id: 'e', loc: [0, 2]}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'd', 'e']}) + ]); + + graph = iD.actionOrthogonalize('-', projection)(graph); + expect(graph.hasEntity('d')).to.be.undefined; + }); + + it('preserves non empty redundant nodes', function() { + // e - d - c + // | + // a ----- b + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [2, 0]}), + iD.osmNode({id: 'c', loc: [2, 2]}), + iD.osmNode({id: 'd', loc: [1, 2], tags: {foo: 'bar'}}), + iD.osmNode({id: 'e', loc: [0, 2]}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'd', 'e']}) + ]); + + graph = iD.actionOrthogonalize('-', projection)(graph); + expect(graph.entity('-').nodes).to.have.length(5); + expect(graph.hasEntity('d')).to.be.ok; + }); + + it('only moves non-endpoint nodes which are near right or near straight', function() { + // f - e + // \ + // d - c + // | + // a -------- b + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [3.1, 0]}), + iD.osmNode({id: 'c', loc: [3, 1]}), + iD.osmNode({id: 'd', loc: [2, 1]}), + iD.osmNode({id: 'e', loc: [1, 2]}), + iD.osmNode({id: 'f', loc: [0, 2]}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'd', 'e', 'f']}) + ]); + + var diff = iD.coreDifference(graph, iD.actionOrthogonalize('-', projection)(graph)); + expect(Object.keys(diff.changes()).sort()).to.eql(['b', 'c']); + }); + + it('does not move or remove self-intersecting nodes', function() { + // f -- g + // | | + // e --- d - c + var graph = iD.coreGraph([ + iD.osmNode({id: 'c', loc: [ 0, 1]}), + iD.osmNode({id: 'd', loc: [ 0.1, 0]}), + iD.osmNode({id: 'e', loc: [-1, 0]}), + iD.osmNode({id: 'f', loc: [-1, 1]}), + iD.osmNode({id: 'g', loc: [ 0, 1]}), + iD.osmWay({id: '-', nodes: ['c', 'd', 'e', 'f', 'g', 'd']}) + ]); + + var diff = iD.coreDifference(graph, iD.actionOrthogonalize('-', projection)(graph)); + expect(diff.changes().d).to.be.undefined; + expect(graph.hasEntity('d')).to.be.ok; + }); + }); + + + describe('vertices', function () { + it('orthogonalizes a single vertex in a quad', function () { + // d --- c + // | | + // a --- b + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [2.1, 0]}), + iD.osmNode({id: 'c', loc: [2, 2]}), + iD.osmNode({id: 'd', loc: [0, 2]}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'd', 'a']}) + ]); + + var diff = iD.coreDifference(graph, iD.actionOrthogonalize('-', projection, 'b')(graph)); + expect(diff.changes().a).to.be.undefined; + expect(diff.changes().b).to.be.not.undefined; + expect(diff.changes().c).to.be.undefined; + expect(diff.changes().d).to.be.undefined; + }); + + it('orthogonalizes a single vertex in a triangle', function () { + // a + // | \ + // | \ + // b - c + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 3]}), + iD.osmNode({id: 'b', loc: [0.1, 0]}), + iD.osmNode({id: 'c', loc: [3, 0]}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'a']}) + ]); + + var diff = iD.coreDifference(graph, iD.actionOrthogonalize('-', projection, 'b')(graph)); + expect(diff.changes().a).to.be.undefined; + expect(diff.changes().b).to.be.not.undefined; + expect(diff.changes().c).to.be.undefined; + }); + + it('orthogonalizes a single vertex in a quad path', function () { + // d --- c + // | + // a --- b + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [2.1, 0]}), + iD.osmNode({id: 'c', loc: [2, 2]}), + iD.osmNode({id: 'd', loc: [0, 2]}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'd']}) + ]); + + var diff = iD.coreDifference(graph, iD.actionOrthogonalize('-', projection, 'b')(graph)); + expect(diff.changes().a).to.be.undefined; + expect(diff.changes().b).to.be.not.undefined; + expect(diff.changes().c).to.be.undefined; + expect(diff.changes().d).to.be.undefined; + }); + + it('orthogonalizes a single vertex in a 3-point path', function () { + // a + // | + // | + // b - c + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 3]}), + iD.osmNode({id: 'b', loc: [0.1, 0]}), + iD.osmNode({id: 'c', loc: [3, 0]}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c']}) + ]); + + var diff = iD.coreDifference(graph, iD.actionOrthogonalize('-', projection, 'b')(graph)); + expect(diff.changes().a).to.be.undefined; + expect(diff.changes().b).to.be.not.undefined; + expect(diff.changes().c).to.be.undefined; + }); + }); + + + describe('#disabled', function () { + + describe('closed paths', function () { + + it('returns "square_enough" for a perfect quad', function () { + // d ---- c + // | | + // a ---- b + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [2, 0]}), + iD.osmNode({id: 'c', loc: [2, 2]}), + iD.osmNode({id: 'd', loc: [0, 2]}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'd', 'a']}) + ]); + + var result = iD.actionOrthogonalize('-', projection).disabled(graph); + expect(result).to.eql('square_enough'); + }); + + it('returns false for unsquared quad', function () { + // d --- c + // | | + // a ---- b + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [2.1, 0]}), + iD.osmNode({id: 'c', loc: [2, 2]}), + iD.osmNode({id: 'd', loc: [0, 2]}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'd', 'a']}) + ]); + + var result = iD.actionOrthogonalize('-', projection).disabled(graph); + expect(result).to.be.false; + }); + + it('returns false for unsquared triangle', function () { + // a + // | \ + // | \ + // b - c + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 3]}), + iD.osmNode({id: 'b', loc: [0.1, 0]}), + iD.osmNode({id: 'c', loc: [3, 0]}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'a']}) + ]); + + var result = iD.actionOrthogonalize('-', projection).disabled(graph); + expect(result).to.be.false; + }); + + it('returns false for perfectly square shape with redundant nodes', function () { + // e - d - c + // | | + // a ----- b + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [2, 0]}), + iD.osmNode({id: 'c', loc: [2, 2]}), + iD.osmNode({id: 'd', loc: [1, 2]}), + iD.osmNode({id: 'e', loc: [0, 2]}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'd', 'e', 'a']}) + ]); + + var result = iD.actionOrthogonalize('-', projection).disabled(graph); + expect(result).to.be.false; + }); + + it('returns "not_squarish" for shape that can not be squared', function () { + // e -- d + // / \ + // f c + // \ / + // a -- b + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [1, 0]}), + iD.osmNode({id: 'b', loc: [3, 0]}), + iD.osmNode({id: 'c', loc: [4, 2]}), + iD.osmNode({id: 'd', loc: [3, 4]}), + iD.osmNode({id: 'e', loc: [1, 4]}), + iD.osmNode({id: 'f', loc: [0, 2]}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'd', 'e', 'f', 'a']}) + ]); + + var result = iD.actionOrthogonalize('-', projection).disabled(graph); + expect(result).to.eql('not_squarish'); + }); + + it('returns false for non-square self-intersecting shapes', function() { + // f -- g + // | | + // e --- d - c + // | | + // a -- b + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [ 0, -1]}), + iD.osmNode({id: 'b', loc: [ 1, -1]}), + iD.osmNode({id: 'c', loc: [ 0, 1]}), + iD.osmNode({id: 'd', loc: [ 0.1, 0]}), + iD.osmNode({id: 'e', loc: [-1, 0]}), + iD.osmNode({id: 'f', loc: [-1, 1]}), + iD.osmNode({id: 'g', loc: [ 0, 1]}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'd', 'a']}) + ]); + + var result = iD.actionOrthogonalize('-', projection).disabled(graph); + expect(result).to.be.false; + }); + + }); + + + describe('open paths', function () { + + it('returns "square_enough" for a perfect quad', function () { + // d ---- c + // | + // a ---- b + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [2, 0]}), + iD.osmNode({id: 'c', loc: [2, 2]}), + iD.osmNode({id: 'd', loc: [0, 2]}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'd']}) + ]); + + var result = iD.actionOrthogonalize('-', projection).disabled(graph); + expect(result).to.eql('square_enough'); + }); + + it('returns false for unsquared quad', function () { + // d --- c + // | + // a --- b + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [2.1, 0]}), + iD.osmNode({id: 'c', loc: [2, 2]}), + iD.osmNode({id: 'd', loc: [0, 2]}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'd']}) + ]); + + var result = iD.actionOrthogonalize('-', projection).disabled(graph); + expect(result).to.be.false; + }); + + it('returns false for unsquared 3-point path', function () { + // a + // | + // | + // b - c + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 3]}), + iD.osmNode({id: 'b', loc: [0, 0.1]}), + iD.osmNode({id: 'c', loc: [3, 0]}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c']}) + ]); + + var result = iD.actionOrthogonalize('-', projection).disabled(graph); + expect(result).to.be.false; + }); + + it('returns false for perfectly square shape with redundant nodes', function () { + // e - d - c + // | + // a ----- b + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [2, 0]}), + iD.osmNode({id: 'c', loc: [2, 2]}), + iD.osmNode({id: 'd', loc: [1, 2]}), + iD.osmNode({id: 'e', loc: [0, 2]}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'd', 'e']}) + ]); + + var result = iD.actionOrthogonalize('-', projection).disabled(graph); + expect(result).to.be.false; + }); + + it('returns "not_squarish" for path that can not be squared', function () { + // e -- d + // / \ + // f c + // / + // a -- b + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [1, 0]}), + iD.osmNode({id: 'b', loc: [3, 0]}), + iD.osmNode({id: 'c', loc: [4, 2]}), + iD.osmNode({id: 'd', loc: [3, 4]}), + iD.osmNode({id: 'e', loc: [1, 4]}), + iD.osmNode({id: 'f', loc: [0, 2]}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'd', 'e', 'f']}) + ]); + + var result = iD.actionOrthogonalize('-', projection).disabled(graph); + expect(result).to.eql('not_squarish'); + }); + + it('returns false for non-square self-intersecting paths', function() { + // f -- g + // | | + // e --- d - c + var graph = iD.coreGraph([ + iD.osmNode({id: 'c', loc: [ 0, 1]}), + iD.osmNode({id: 'd', loc: [ 0.1, 0]}), + iD.osmNode({id: 'e', loc: [-1, 0]}), + iD.osmNode({id: 'f', loc: [-1, 1]}), + iD.osmNode({id: 'g', loc: [ 0, 1]}), + iD.osmWay({id: '-', nodes: ['c', 'd', 'e', 'f', 'g', 'd']}) + ]); + + var result = iD.actionOrthogonalize('-', projection).disabled(graph); + expect(result).to.be.false; + }); + }); + + describe('vertex-only', function () { + + it('returns "square_enough" for a vertex in a perfect quad', function () { + // d ---- c + // | + // a ---- b + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [2, 0]}), + iD.osmNode({id: 'c', loc: [2, 2]}), + iD.osmNode({id: 'd', loc: [0, 2]}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'd']}) + ]); + + var result = iD.actionOrthogonalize('-', projection, 'b').disabled(graph); + expect(result).to.eql('square_enough'); + }); + + it('returns false for a vertex in an unsquared quad', function () { + // d --- c + // | + // a --- b + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [2.1, 0]}), + iD.osmNode({id: 'c', loc: [2, 2]}), + iD.osmNode({id: 'd', loc: [0, 2]}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'd']}) + ]); + + var result = iD.actionOrthogonalize('-', projection, 'b').disabled(graph); + expect(result).to.be.false; + }); + + it('returns false for a vertex in an unsquared 3-point path', function () { + // a + // | + // | + // b - c + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 3]}), + iD.osmNode({id: 'b', loc: [0, 0.1]}), + iD.osmNode({id: 'c', loc: [3, 0]}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c']}) + ]); + + var result = iD.actionOrthogonalize('-', projection, 'b').disabled(graph); + expect(result).to.be.false; + }); + + it('returns "not_squarish" for vertex that can not be squared', function () { + // e -- d + // / \ + // f c + // / + // a -- b + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [1, 0]}), + iD.osmNode({id: 'b', loc: [3, 0]}), + iD.osmNode({id: 'c', loc: [4, 2]}), + iD.osmNode({id: 'd', loc: [3, 4]}), + iD.osmNode({id: 'e', loc: [1, 4]}), + iD.osmNode({id: 'f', loc: [0, 2]}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'd', 'e', 'f']}) + ]); + + var result = iD.actionOrthogonalize('-', projection, 'b').disabled(graph); + expect(result).to.eql('not_squarish'); + }); + + }); + }); + describe('transitions', function () { it('is transitionable', function() { expect(iD.actionOrthogonalize().transitionable).to.be.true; }); + // for all of these: + // + // f ------------ e + // | | + // a -- b -- c -- d + it('orthogonalize at t = 0', function() { var graph = iD.coreGraph([ - iD.osmNode({id: 'a', loc: [0, 0]}), - iD.osmNode({id: 'b', loc: [1, 0.01], tags: {foo: 'bar'}}), - iD.osmNode({id: 'c', loc: [2, -0.01]}), - iD.osmNode({id: 'd', loc: [3, 0]}), - iD.osmNode({id: 'e', loc: [3, 1]}), - iD.osmNode({id: 'f', loc: [0, 1]}), - iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'd', 'e', 'f', 'a']}) - ]); + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [1, 0.01], tags: {foo: 'bar'}}), + iD.osmNode({id: 'c', loc: [2, -0.01]}), + iD.osmNode({id: 'd', loc: [3, 0]}), + iD.osmNode({id: 'e', loc: [3, 1]}), + iD.osmNode({id: 'f', loc: [0, 1]}), + iD.osmWay({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']); @@ -145,14 +684,14 @@ describe('iD.actionOrthogonalize', function () { it('orthogonalize at t = 0.5', function() { var graph = iD.coreGraph([ - iD.osmNode({id: 'a', loc: [0, 0]}), - iD.osmNode({id: 'b', loc: [1, 0.01], tags: {foo: 'bar'}}), - iD.osmNode({id: 'c', loc: [2, -0.01]}), - iD.osmNode({id: 'd', loc: [3, 0]}), - iD.osmNode({id: 'e', loc: [3, 1]}), - iD.osmNode({id: 'f', loc: [0, 1]}), - iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'd', 'e', 'f', 'a']}) - ]); + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [1, 0.01], tags: {foo: 'bar'}}), + iD.osmNode({id: 'c', loc: [2, -0.01]}), + iD.osmNode({id: 'd', loc: [3, 0]}), + iD.osmNode({id: 'e', loc: [3, 1]}), + iD.osmNode({id: 'f', loc: [0, 1]}), + iD.osmWay({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']); @@ -164,14 +703,14 @@ describe('iD.actionOrthogonalize', function () { it('orthogonalize at t = 1', function() { var graph = iD.coreGraph([ - iD.osmNode({id: 'a', loc: [0, 0]}), - iD.osmNode({id: 'b', loc: [1, 0.01], tags: {foo: 'bar'}}), - iD.osmNode({id: 'c', loc: [2, -0.01]}), - iD.osmNode({id: 'd', loc: [3, 0]}), - iD.osmNode({id: 'e', loc: [3, 1]}), - iD.osmNode({id: 'f', loc: [0, 1]}), - iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'd', 'e', 'f', 'a']}) - ]); + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [1, 0.01], tags: {foo: 'bar'}}), + iD.osmNode({id: 'c', loc: [2, -0.01]}), + iD.osmNode({id: 'd', loc: [3, 0]}), + iD.osmNode({id: 'e', loc: [3, 1]}), + iD.osmNode({id: 'f', loc: [0, 1]}), + iD.osmWay({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']); diff --git a/test/spec/core/difference.js b/test/spec/core/difference.js index 97ca0eaaf..8c27eb5e3 100644 --- a/test/spec/core/difference.js +++ b/test/spec/core/difference.js @@ -1,152 +1,152 @@ -describe('iD.Difference', function () { +describe('iD.coreDifference', function () { describe('#changes', function () { it('includes created entities', function () { - var node = iD.osmNode({id: 'n'}), - base = iD.coreGraph(), - head = base.replace(node), - diff = iD.Difference(base, head); + var node = iD.osmNode({id: 'n'}); + var base = iD.coreGraph(); + var head = base.replace(node); + var diff = iD.coreDifference(base, head); expect(diff.changes()).to.eql({n: {base: undefined, head: node}}); }); it('includes undone created entities', function () { - var node = iD.osmNode({id: 'n'}), - base = iD.coreGraph(), - head = base.replace(node), - diff = iD.Difference(head, base); + var node = iD.osmNode({id: 'n'}); + var base = iD.coreGraph(); + var head = base.replace(node); + var diff = iD.coreDifference(head, base); expect(diff.changes()).to.eql({n: {base: node, head: undefined}}); }); it('includes modified entities', function () { - var n1 = iD.osmNode({id: 'n'}), - n2 = n1.update({ tags: { yes: 'no' } }), - base = iD.coreGraph([n1]), - head = base.replace(n2), - diff = iD.Difference(base, head); + var n1 = iD.osmNode({id: 'n'}); + var n2 = n1.update({ tags: { yes: 'no' } }); + var base = iD.coreGraph([n1]); + var head = base.replace(n2); + var diff = iD.coreDifference(base, head); expect(diff.changes()).to.eql({n: {base: n1, head: n2}}); }); it('includes undone modified entities', function () { - var n1 = iD.osmNode({id: 'n'}), - n2 = n1.update({ tags: { yes: 'no' } }), - base = iD.coreGraph([n1]), - head = base.replace(n2), - diff = iD.Difference(head, base); + var n1 = iD.osmNode({id: 'n'}); + var n2 = n1.update({ tags: { yes: 'no' } }); + var base = iD.coreGraph([n1]); + var head = base.replace(n2); + var diff = iD.coreDifference(head, base); expect(diff.changes()).to.eql({n: {base: n2, head: n1}}); }); it('doesn\'t include updated but identical entities', function () { - var n1 = iD.osmNode({id: 'n'}), - n2 = n1.update(), - base = iD.coreGraph([n1]), - head = base.replace(n2), - diff = iD.Difference(base, head); + var n1 = iD.osmNode({id: 'n'}); + var n2 = n1.update(); + var base = iD.coreGraph([n1]); + var head = base.replace(n2); + var diff = iD.coreDifference(base, head); expect(diff.changes()).to.eql({}); }); it('includes deleted entities', function () { - var node = iD.osmNode({id: 'n'}), - base = iD.coreGraph([node]), - head = base.remove(node), - diff = iD.Difference(base, head); + var node = iD.osmNode({id: 'n'}); + var base = iD.coreGraph([node]); + var head = base.remove(node); + var diff = iD.coreDifference(base, head); expect(diff.changes()).to.eql({n: {base: node, head: undefined}}); }); it('includes undone deleted entities', function () { - var node = iD.osmNode({id: 'n'}), - base = iD.coreGraph([node]), - head = base.remove(node), - diff = iD.Difference(head, base); + var node = iD.osmNode({id: 'n'}); + var base = iD.coreGraph([node]); + var head = base.remove(node); + var diff = iD.coreDifference(head, base); expect(diff.changes()).to.eql({n: {base: undefined, head: node}}); }); it('doesn\'t include created entities that were subsequently deleted', function () { - var node = iD.osmNode(), - base = iD.coreGraph(), - head = base.replace(node).remove(node), - diff = iD.Difference(base, head); + var node = iD.osmNode(); + var base = iD.coreGraph(); + var head = base.replace(node).remove(node); + var diff = iD.coreDifference(base, head); expect(diff.changes()).to.eql({}); }); it('doesn\'t include created entities that were subsequently reverted', function () { - var node = iD.osmNode({id: 'n-1'}), - base = iD.coreGraph(), - head = base.replace(node).revert('n-1'), - diff = iD.Difference(base, head); + var node = iD.osmNode({id: 'n-1'}); + var base = iD.coreGraph(); + var head = base.replace(node).revert('n-1'); + var diff = iD.coreDifference(base, head); expect(diff.changes()).to.eql({}); }); it('doesn\'t include modified entities that were subsequently reverted', function () { - var n1 = iD.osmNode({id: 'n'}), - n2 = n1.update({ tags: { yes: 'no' } }), - base = iD.coreGraph([n1]), - head = base.replace(n2).revert('n'), - diff = iD.Difference(base, head); + var n1 = iD.osmNode({id: 'n'}); + var n2 = n1.update({ tags: { yes: 'no' } }); + var base = iD.coreGraph([n1]); + var head = base.replace(n2).revert('n'); + var diff = iD.coreDifference(base, head); expect(diff.changes()).to.eql({}); }); it('doesn\'t include deleted entities that were subsequently reverted', function () { - var node = iD.osmNode({id: 'n'}), - base = iD.coreGraph([node]), - head = base.remove(node).revert('n'), - diff = iD.Difference(base, head); + var node = iD.osmNode({id: 'n'}); + var base = iD.coreGraph([node]); + var head = base.remove(node).revert('n'); + var diff = iD.coreDifference(base, head); expect(diff.changes()).to.eql({}); }); }); describe('#extantIDs', function () { it('includes the ids of created entities', function () { - var node = iD.osmNode({id: 'n'}), - base = iD.coreGraph(), - head = base.replace(node), - diff = iD.Difference(base, head); + var node = iD.osmNode({id: 'n'}); + var base = iD.coreGraph(); + var head = base.replace(node); + var diff = iD.coreDifference(base, head); expect(diff.extantIDs()).to.eql(['n']); }); it('includes the ids of modified entities', function () { - var n1 = iD.osmNode({id: 'n'}), - n2 = n1.move([1, 2]), - base = iD.coreGraph([n1]), - head = base.replace(n2), - diff = iD.Difference(base, head); + var n1 = iD.osmNode({id: 'n'}); + var n2 = n1.move([1, 2]); + var base = iD.coreGraph([n1]); + var head = base.replace(n2); + var diff = iD.coreDifference(base, head); expect(diff.extantIDs()).to.eql(['n']); }); it('omits the ids of deleted entities', function () { - var node = iD.osmNode({id: 'n'}), - base = iD.coreGraph([node]), - head = base.remove(node), - diff = iD.Difference(base, head); + var node = iD.osmNode({id: 'n'}); + var base = iD.coreGraph([node]); + var head = base.remove(node); + var diff = iD.coreDifference(base, head); expect(diff.extantIDs()).to.eql([]); }); }); describe('#created', function () { it('returns an array of created entities', function () { - var node = iD.osmNode({id: 'n'}), - base = iD.coreGraph(), - head = base.replace(node), - diff = iD.Difference(base, head); + var node = iD.osmNode({id: 'n'}); + var base = iD.coreGraph(); + var head = base.replace(node); + var diff = iD.coreDifference(base, head); expect(diff.created()).to.eql([node]); }); }); describe('#modified', function () { it('returns an array of modified entities', function () { - var n1 = iD.osmNode({id: 'n'}), - n2 = n1.move([1, 2]), - base = iD.coreGraph([n1]), - head = base.replace(n2), - diff = iD.Difference(base, head); + var n1 = iD.osmNode({id: 'n'}); + var n2 = n1.move([1, 2]); + var base = iD.coreGraph([n1]); + var head = base.replace(n2); + var diff = iD.coreDifference(base, head); expect(diff.modified()).to.eql([n2]); }); }); describe('#deleted', function () { it('returns an array of deleted entities', function () { - var node = iD.osmNode({id: 'n'}), - base = iD.coreGraph([node]), - head = base.remove(node), - diff = iD.Difference(base, head); + var node = iD.osmNode({id: 'n'}); + var base = iD.coreGraph([node]); + var head = base.remove(node); + var diff = iD.coreDifference(base, head); expect(diff.deleted()).to.eql([node]); }); }); @@ -160,9 +160,9 @@ describe('iD.Difference', function () { ]); it('reports a created way as created', function() { - var way = iD.osmWay({id: '+'}), - head = base.replace(way), - diff = iD.Difference(base, head); + var way = iD.osmWay({id: '+'}); + var head = base.replace(way); + var diff = iD.coreDifference(base, head); expect(diff.summary()).to.eql([{ changeType: 'created', @@ -172,9 +172,9 @@ describe('iD.Difference', function () { }); it('reports a deleted way as deleted', function() { - var way = base.entity('-'), - head = base.remove(way), - diff = iD.Difference(base, head); + var way = base.entity('-'); + var head = base.remove(way); + var diff = iD.coreDifference(base, head); expect(diff.summary()).to.eql([{ changeType: 'deleted', @@ -184,9 +184,9 @@ describe('iD.Difference', function () { }); it('reports a modified way as modified', function() { - var way = base.entity('-').mergeTags({highway: 'primary'}), - head = base.replace(way), - diff = iD.Difference(base, head); + var way = base.entity('-').mergeTags({highway: 'primary'}); + var head = base.replace(way); + var diff = iD.coreDifference(base, head); expect(diff.summary()).to.eql([{ changeType: 'modified', @@ -196,9 +196,9 @@ describe('iD.Difference', function () { }); it('reports a way as modified when a member vertex is moved', function() { - var vertex = base.entity('b').move([0,3]), - head = base.replace(vertex), - diff = iD.Difference(base, head); + var vertex = base.entity('b').move([0,3]); + var head = base.replace(vertex); + var diff = iD.coreDifference(base, head); expect(diff.summary()).to.eql([{ changeType: 'modified', @@ -208,10 +208,10 @@ describe('iD.Difference', function () { }); it('reports a way as modified when a member vertex is added', function() { - var vertex = iD.osmNode({id: 'c'}), - way = base.entity('-').addNode('c'), - head = base.replace(vertex).replace(way), - diff = iD.Difference(base, head); + var vertex = iD.osmNode({id: 'c'}); + var way = base.entity('-').addNode('c'); + var head = base.replace(vertex).replace(way); + var diff = iD.coreDifference(base, head); expect(diff.summary()).to.eql([{ changeType: 'modified', @@ -221,9 +221,9 @@ describe('iD.Difference', function () { }); it('reports a way as modified when a member vertex is removed', function() { - var way = base.entity('-').removeNode('b'), - head = base.replace(way), - diff = iD.Difference(base, head); + var way = base.entity('-').removeNode('b'); + var head = base.replace(way); + var diff = iD.coreDifference(base, head); expect(diff.summary()).to.eql([{ changeType: 'modified', @@ -233,10 +233,10 @@ describe('iD.Difference', function () { }); it('reports a created way containing a moved vertex as being created', function() { - var vertex = base.entity('b').move([0,3]), - way = iD.osmWay({id: '+', nodes: ['b']}), - head = base.replace(way).replace(vertex), - diff = iD.Difference(base, head); + var vertex = base.entity('b').move([0,3]); + var way = iD.osmWay({id: '+', nodes: ['b']}); + var head = base.replace(way).replace(vertex); + var diff = iD.coreDifference(base, head); expect(diff.summary()).to.eql([{ changeType: 'created', @@ -250,10 +250,10 @@ describe('iD.Difference', function () { }); it('reports a created way with a created vertex as being created', function() { - var vertex = iD.osmNode({id: 'c'}), - way = iD.osmWay({id: '+', nodes: ['c']}), - head = base.replace(vertex).replace(way), - diff = iD.Difference(base, head); + var vertex = iD.osmNode({id: 'c'}); + var way = iD.osmWay({id: '+', nodes: ['c']}); + var head = base.replace(vertex).replace(way); + var diff = iD.coreDifference(base, head); expect(diff.summary()).to.eql([{ changeType: 'created', @@ -263,9 +263,9 @@ describe('iD.Difference', function () { }); it('reports a vertex as modified when it has tags and they are changed', function() { - var vertex = base.entity('a').mergeTags({highway: 'traffic_signals'}), - head = base.replace(vertex), - diff = iD.Difference(base, head); + var vertex = base.entity('a').mergeTags({highway: 'traffic_signals'}); + var head = base.replace(vertex); + var diff = iD.coreDifference(base, head); expect(diff.summary()).to.eql([{ changeType: 'modified', @@ -275,9 +275,9 @@ describe('iD.Difference', function () { }); it('reports a vertex as modified when it has tags and is moved', function() { - var vertex = base.entity('a').move([1, 2]), - head = base.replace(vertex), - diff = iD.Difference(base, head); + var vertex = base.entity('a').move([1, 2]); + var head = base.replace(vertex); + var diff = iD.coreDifference(base, head); expect(diff.summary()).to.eql([{ changeType: 'modified', @@ -291,9 +291,9 @@ describe('iD.Difference', function () { }); it('does not report a vertex as modified when it is moved and has no-op tag changes', function() { - var vertex = base.entity('b').update({tags: {}, loc: [1, 2]}), - head = base.replace(vertex), - diff = iD.Difference(base, head); + var vertex = base.entity('b').update({tags: {}, loc: [1, 2]}); + var head = base.replace(vertex); + var diff = iD.coreDifference(base, head); expect(diff.summary()).to.eql([{ changeType: 'modified', @@ -303,9 +303,9 @@ describe('iD.Difference', function () { }); it('reports a vertex as deleted when it had tags', function() { - var vertex = base.entity('v'), - head = base.remove(vertex), - diff = iD.Difference(base, head); + var vertex = base.entity('v'); + var head = base.remove(vertex); + var diff = iD.coreDifference(base, head); expect(diff.summary()).to.eql([{ changeType: 'deleted', @@ -315,10 +315,10 @@ describe('iD.Difference', function () { }); it('reports a vertex as created when it has tags', function() { - var vertex = iD.osmNode({id: 'c', tags: {crossing: 'marked'}}), - way = base.entity('-').addNode('c'), - head = base.replace(way).replace(vertex), - diff = iD.Difference(base, head); + var vertex = iD.osmNode({id: 'c', tags: {crossing: 'marked'}}); + var way = base.entity('-').addNode('c'); + var head = base.replace(way).replace(vertex); + var diff = iD.coreDifference(base, head); expect(diff.summary()).to.eql([{ changeType: 'modified', @@ -334,107 +334,107 @@ describe('iD.Difference', function () { describe('#complete', function () { it('includes created entities', function () { - var node = iD.osmNode({id: 'n'}), - base = iD.coreGraph(), - head = base.replace(node), - diff = iD.Difference(base, head); + var node = iD.osmNode({id: 'n'}); + var base = iD.coreGraph(); + var head = base.replace(node); + var diff = iD.coreDifference(base, head); expect(diff.complete().n).to.equal(node); }); it('includes modified entities', function () { - var n1 = iD.osmNode({id: 'n'}), - n2 = n1.move([1, 2]), - base = iD.coreGraph([n1]), - head = base.replace(n2), - diff = iD.Difference(base, head); + var n1 = iD.osmNode({id: 'n'}); + var n2 = n1.move([1, 2]); + var base = iD.coreGraph([n1]); + var head = base.replace(n2); + var diff = iD.coreDifference(base, head); expect(diff.complete().n).to.equal(n2); }); it('includes deleted entities', function () { - var node = iD.osmNode({id: 'n'}), - base = iD.coreGraph([node]), - head = base.remove(node), - diff = iD.Difference(base, head); + var node = iD.osmNode({id: 'n'}); + var base = iD.coreGraph([node]); + var head = base.remove(node); + var diff = iD.coreDifference(base, head); expect(diff.complete()).to.eql({n: undefined}); }); it('includes nodes added to a way', function () { - var n1 = iD.osmNode({id: 'n1'}), - n2 = iD.osmNode({id: 'n2'}), - w1 = iD.osmWay({id: 'w', nodes: ['n1']}), - w2 = w1.addNode('n2'), - base = iD.coreGraph([n1, n2, w1]), - head = base.replace(w2), - diff = iD.Difference(base, head); + var n1 = iD.osmNode({id: 'n1'}); + var n2 = iD.osmNode({id: 'n2'}); + var w1 = iD.osmWay({id: 'w', nodes: ['n1']}); + var w2 = w1.addNode('n2'); + var base = iD.coreGraph([n1, n2, w1]); + var head = base.replace(w2); + var diff = iD.coreDifference(base, head); expect(diff.complete().n2).to.equal(n2); }); it('includes nodes removed from a way', function () { - var n1 = iD.osmNode({id: 'n1'}), - n2 = iD.osmNode({id: 'n2'}), - w1 = iD.osmWay({id: 'w', nodes: ['n1', 'n2']}), - w2 = w1.removeNode('n2'), - base = iD.coreGraph([n1, n2, w1]), - head = base.replace(w2), - diff = iD.Difference(base, head); + var n1 = iD.osmNode({id: 'n1'}); + var n2 = iD.osmNode({id: 'n2'}); + var w1 = iD.osmWay({id: 'w', nodes: ['n1', 'n2']}); + var w2 = w1.removeNode('n2'); + var base = iD.coreGraph([n1, n2, w1]); + var head = base.replace(w2); + var diff = iD.coreDifference(base, head); expect(diff.complete().n2).to.equal(n2); }); it('includes parent ways of modified nodes', function () { - var n1 = iD.osmNode({id: 'n'}), - n2 = n1.move([1, 2]), - way = iD.osmWay({id: 'w', nodes: ['n']}), - base = iD.coreGraph([n1, way]), - head = base.replace(n2), - diff = iD.Difference(base, head); + var n1 = iD.osmNode({id: 'n'}); + var n2 = n1.move([1, 2]); + var way = iD.osmWay({id: 'w', nodes: ['n']}); + var base = iD.coreGraph([n1, way]); + var head = base.replace(n2); + var diff = iD.coreDifference(base, head); expect(diff.complete().w).to.equal(way); }); it('includes parent relations of modified entities', function () { - var n1 = iD.osmNode({id: 'n'}), - n2 = n1.move([1, 2]), - rel = iD.osmRelation({id: 'r', members: [{id: 'n'}]}), - base = iD.coreGraph([n1, rel]), - head = base.replace(n2), - diff = iD.Difference(base, head); + var n1 = iD.osmNode({id: 'n'}); + var n2 = n1.move([1, 2]); + var rel = iD.osmRelation({id: 'r', members: [{id: 'n'}]}); + var base = iD.coreGraph([n1, rel]); + var head = base.replace(n2); + var diff = iD.coreDifference(base, head); expect(diff.complete().r).to.equal(rel); }); it('includes parent relations of modified entities, recursively', function () { - var n1 = iD.osmNode({id: 'n'}), - n2 = n1.move([1, 2]), - rel1 = iD.osmRelation({id: 'r1', members: [{id: 'n'}]}), - rel2 = iD.osmRelation({id: 'r2', members: [{id: 'r1'}]}), - base = iD.coreGraph([n1, rel1, rel2]), - head = base.replace(n2), - diff = iD.Difference(base, head); + var n1 = iD.osmNode({id: 'n'}); + var n2 = n1.move([1, 2]); + var rel1 = iD.osmRelation({id: 'r1', members: [{id: 'n'}]}); + var rel2 = iD.osmRelation({id: 'r2', members: [{id: 'r1'}]}); + var base = iD.coreGraph([n1, rel1, rel2]); + var head = base.replace(n2); + var diff = iD.coreDifference(base, head); expect(diff.complete().r2).to.equal(rel2); }); it('includes parent relations of parent ways of modified nodes', function () { - var n1 = iD.osmNode({id: 'n'}), - n2 = n1.move([1, 2]), - way = iD.osmWay({id: 'w', nodes: ['n']}), - rel = iD.osmRelation({id: 'r', members: [{id: 'w'}]}), - base = iD.coreGraph([n1, way, rel]), - head = base.replace(n2), - diff = iD.Difference(base, head); + var n1 = iD.osmNode({id: 'n'}); + var n2 = n1.move([1, 2]); + var way = iD.osmWay({id: 'w', nodes: ['n']}); + var rel = iD.osmRelation({id: 'r', members: [{id: 'w'}]}); + var base = iD.coreGraph([n1, way, rel]); + var head = base.replace(n2); + var diff = iD.coreDifference(base, head); expect(diff.complete().r).to.equal(rel); }); it('copes with recursive relations', function () { - var node = iD.osmNode({id: 'n'}), - rel1 = iD.osmRelation({id: 'r1', members: [{id: 'n'}, {id: 'r2'}]}), - rel2 = iD.osmRelation({id: 'r2', members: [{id: 'r1'}]}), - base = iD.coreGraph([node, rel1, rel2]), - head = base.replace(node.move([1, 2])), - diff = iD.Difference(base, head); + var node = iD.osmNode({id: 'n'}); + var rel1 = iD.osmRelation({id: 'r1', members: [{id: 'n'}, {id: 'r2'}]}); + var rel2 = iD.osmRelation({id: 'r2', members: [{id: 'r1'}]}); + var base = iD.coreGraph([node, rel1, rel2]); + var head = base.replace(node.move([1, 2])); + var diff = iD.coreDifference(base, head); expect(diff.complete()).to.be.ok; }); diff --git a/test/spec/geo/vector.js b/test/spec/geo/vector.js index 20764e80a..fcb634b8d 100644 --- a/test/spec/geo/vector.js +++ b/test/spec/geo/vector.js @@ -76,6 +76,17 @@ describe('iD.geo - vector', function() { }); }); + describe('geoVecNormalize', function() { + it('gets unit vectors', function() { + expect(iD.geoVecNormalize([0, 0])).to.eql([0, 0]); + expect(iD.geoVecNormalize([1, 0])).to.eql([1, 0]); + expect(iD.geoVecNormalize([5, 0])).to.eql([1, 0]); + expect(iD.geoVecNormalize([-5, 0])).to.eql([-1, 0]); + expect(iD.geoVecNormalize([1, 1])[0]).to.be.closeTo(Math.sqrt(2)/2, 1e-6); + expect(iD.geoVecNormalize([1, 1])[1]).to.be.closeTo(Math.sqrt(2)/2, 1e-6); + }); + }); + describe('geoVecAngle', function() { it('returns angle between a and b', function() { expect(iD.geoVecAngle([0, 0], [1, 0])).to.be.closeTo(0, 1e-6); @@ -98,6 +109,24 @@ describe('iD.geo - vector', function() { }); }); + describe('geoVecNormalizedDot', function() { + it('normalized dot product of right angle is zero', function() { + var a = [2, 0]; + var b = [0, 2]; + expect(iD.geoVecNormalizedDot(a, b)).to.eql(0); + }); + it('normalized dot product of same vector multiplies unit vectors', function() { + var a = [2, 0]; + var b = [2, 0]; + expect(iD.geoVecNormalizedDot(a, b)).to.eql(1); + }); + it('normalized dot product of 45 degrees', function() { + var a = [0, 2]; + var b = [2, 2]; + expect(iD.geoVecNormalizedDot(a, b)).to.be.closeTo(Math.sqrt(2)/2, 1e-6); + }); + }); + describe('geoVecCross', function() { it('2D cross product of right hand turn is positive', function() { var a = [2, 0]; @@ -116,4 +145,49 @@ describe('iD.geo - vector', function() { }); }); + + describe('geoVecProject', function() { + it('returns null for a degenerate path (no nodes)', function() { + expect(iD.geoVecProject([0, 1], [])).to.be.null; + }); + + it('returns null for a degenerate path (single node)', function() { + expect(iD.geoVecProject([0, 1], [0, 0])).to.be.null; + }); + + it('calculates the orthogonal projection of a point onto a path', function() { + // c + // | + // a --*--- b + // + // * = [2, 0] + var a = [0, 0]; + var b = [5, 0]; + var c = [2, 1]; + var choice = iD.geoVecProject(c, [a, b]); + expect(choice.index).to.eql(1); + expect(choice.distance).to.eql(1); + expect(choice.target).to.eql([2, 0]); + }); + + it('returns the starting vertex when the orthogonal projection is < 0', function() { + var a = [0, 0]; + var b = [5, 0]; + var c = [-3, 4]; + var choice = iD.geoVecProject(c, [a, b]); + expect(choice.index).to.eql(1); + expect(choice.distance).to.eql(5); + expect(choice.target).to.eql([0, 0]); + }); + + it('returns the ending vertex when the orthogonal projection is > 1', function() { + var a = [0, 0]; + var b = [5, 0]; + var c = [8, 4]; + var choice = iD.geoVecProject(c, [a, b]); + expect(choice.index).to.eql(1); + expect(choice.distance).to.eql(5); + expect(choice.target).to.eql([5, 0]); + }); + }); });