From b79b6ca97a8fe80004dfb4015df651967818818d Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 7 Dec 2017 16:44:18 -0500 Subject: [PATCH 01/70] Support rendering `direction` on vertices (stop sign, traffic_signals, etc) (closes #3815) --- css/20_map.css | 4 +- modules/svg/defs.js | 26 ++++++++++-- modules/svg/vertices.js | 87 +++++++++++++++++++++++++++++++---------- 3 files changed, 91 insertions(+), 26 deletions(-) diff --git a/css/20_map.css b/css/20_map.css index 4a66b00df..1554ce011 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -180,11 +180,11 @@ text { fill: #002F35; } -path.oneway { +.directiongroup path.directional, +.onewaygroup path.oneway { stroke-width: 6px; } - text.arealabel-halo, text.linelabel-halo, text.pointlabel-halo, diff --git a/modules/svg/defs.js b/modules/svg/defs.js index 93d9663a9..25d2c8db1 100644 --- a/modules/svg/defs.js +++ b/modules/svg/defs.js @@ -26,12 +26,12 @@ export function svgDefs(context) { return function drawDefs(selection) { var defs = selection.append('defs'); - // marker + // oneway marker defs.append('marker') .attr('id', 'oneway-marker') - .attr('viewBox', '0 0 10 10') - .attr('refY', 2.5) + .attr('viewBox', '0 0 10 5') .attr('refX', 5) + .attr('refY', 2.5) .attr('markerWidth', 2) .attr('markerHeight', 2) .attr('markerUnits', 'strokeWidth') @@ -39,11 +39,29 @@ export function svgDefs(context) { .append('path') .attr('class', 'oneway') - .attr('d', 'M 5 3 L 0 3 L 0 2 L 5 2 L 5 0 L 10 2.5 L 5 5 z') + .attr('d', 'M 5,3 L 0,3 L 0,2 L 5,2 L 5,0 L 10,2.5 L 5,5 z') .attr('stroke', 'none') .attr('fill', '#000') .attr('opacity', '0.75'); + defs.append('marker') + .attr('id', 'directional-marker') + .attr('viewBox', '0 0 15 5') + .attr('refX', 5.5) + .attr('refY', 2.5) + .attr('markerWidth', 7) + .attr('markerHeight', 7) + .attr('markerUnits', 'strokeWidth') + .attr('orient', 'auto') + + .append('path') + .attr('class', 'directional') + .attr('d', 'M 10,2.5 L 9,0 L 14,2.5 L 9,5 z') + .attr('stroke', '#fff') + .attr('fill', '#333') + .attr('stroke-width', '0.5px') + .attr('stroke-opacity', '0.75'); + // patterns var patterns = defs.selectAll('pattern') .data([ diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index 053fc5302..01544165c 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -1,6 +1,7 @@ import _values from 'lodash-es/values'; import { dataFeatureIcons } from '../../data'; +import { geoAngle } from '../geo'; import { osmEntity } from '../osm'; import { svgPointTransform } from './index'; @@ -56,27 +57,64 @@ export function svgVertices(projection, context) { function draw(selection, vertices, klass, graph, zoom, siblings) { + siblings = siblings || {}; + var icons = {}; + var directions = {}; + var z = (zoom < 17 ? 0 : zoom < 18 ? 1 : 2); - function icon(entity) { + + function getIcon(entity) { if (entity.id in icons) return icons[entity.id]; + icons[entity.id] = entity.hasInterestingTags() && context.presets().match(entity, graph).icon; return icons[entity.id]; } + function getDirections(entity) { + if (entity.id in directions) return directions[entity.id]; + + var dir = (entity.tags['traffic_signals:direction'] || entity.tags.direction || '').toLowerCase(); + var stop = (entity.tags.stop || '').toLowerCase(); + var goBackward = (dir === 'backward' || dir === 'both' || dir === 'all' || stop === 'all'); + var goForward = (dir === 'forward' || dir === 'both' || dir === 'all' || stop === 'all'); + if (!goForward && !goBackward) return; + + var nodeIds = {}; + graph.parentWays(entity).forEach(function (parent) { + var nodes = parent.nodes; + for (var i = 0; i < nodes.length; i++) { + if (nodes[i] === entity.id) { // match current entity + if (goBackward && i > 0) { + nodeIds[nodes[i - 1]] = true; + } + if (goForward && i < nodes.length - 1) { + nodeIds[nodes[i + 1]] = true; + } + } + } + }); + + var dirAngles = Object.keys(nodeIds).map(function (nodeId) { + return geoAngle(entity, graph.entity(nodeId), projection) * (180 / Math.PI); + }); + directions[entity.id] = dirAngles; + return directions[entity.id]; + } + function setClass(klass) { return function(entity) { this.setAttribute('class', 'node vertex ' + klass + ' ' + entity.id); }; } - function setAttributes(selection) { + function updateAttributes(selection) { ['shadow','stroke','fill'].forEach(function(klass) { var rads = radiuses[klass]; selection.selectAll('.' + klass) .each(function(entity) { - var i = z && icon(entity), + var i = z && getIcon(entity), c = i ? 0.5 : 0, r = rads[i ? 3 : z]; @@ -97,21 +135,13 @@ export function svgVertices(projection, context) { }); selection.selectAll('use') - .each(function() { - if (z) { - this.removeAttribute('visibility'); - } else { - this.setAttribute('visibility', 'hidden'); - } - }); + .attr('visibility', (z === 0 ? 'hidden' : null)); + + selection.selectAll('.directiongroup') + .attr('visibility', (zoom < 18 ? 'hidden' : null)); } - siblings = siblings || {}; - - var icons = {}, - z = (zoom < 17 ? 0 : zoom < 18 ? 1 : 2); - var groups = selection .data(vertices, osmEntity.key); @@ -122,18 +152,34 @@ export function svgVertices(projection, context) { .append('g') .attr('class', function(d) { return 'node vertex ' + klass + ' ' + d.id; }); - enter.append('circle') + // Directional vertices get arrows + var directionsEnter = enter.filter(function(d) { return getDirections(d); }) + .append('g') + .each(setClass('directiongroup')); + + directionsEnter.selectAll('.directional') + .data(function(d) { return getDirections(d); }) + .enter() + .append('path') + .attr('class', 'directional') + .attr('transform', function(d) { return 'rotate(' + d + ')'; }) + .attr('d', 'M0,0H0') + .attr('marker-start', 'url(#directional-marker)'); + + enter + .append('circle') .each(setClass('shadow')); - enter.append('circle') + enter + .append('circle') .each(setClass('stroke')); // Vertices with icons get a `use`. - enter.filter(function(d) { return icon(d); }) + enter.filter(function(d) { return getIcon(d); }) .append('use') .attr('transform', 'translate(-5, -6)') .attr('xlink:href', function(d) { - var picon = icon(d), + var picon = getIcon(d), isMaki = dataFeatureIcons.indexOf(picon) !== -1; return '#' + picon + (isMaki ? '-11' : ''); }) @@ -146,13 +192,14 @@ export function svgVertices(projection, context) { .append('circle') .each(setClass('fill')); + // Update groups .merge(enter) .attr('transform', svgPointTransform(projection)) .classed('sibling', function(entity) { return entity.id in siblings; }) .classed('shared', function(entity) { return graph.isShared(entity); }) .classed('endpoint', function(entity) { return entity.isEndpoint(graph); }) - .call(setAttributes); + .call(updateAttributes); } From 6aba27c84a6b64c37e7fa1b9f3e864d4c49be39c Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 8 Dec 2017 14:29:56 -0500 Subject: [PATCH 02/70] Display directional vertex with a viewfield, not an arrow --- css/20_map.css | 4 +-- modules/svg/defs.js | 68 ++++++++++++++++++----------------------- modules/svg/vertices.js | 12 ++++---- 3 files changed, 37 insertions(+), 47 deletions(-) diff --git a/css/20_map.css b/css/20_map.css index 1554ce011..6d3a39037 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -180,8 +180,8 @@ text { fill: #002F35; } -.directiongroup path.directional, -.onewaygroup path.oneway { +.onewaygroup path.oneway, +.viewfieldgroup path.viewfield { stroke-width: 6px; } diff --git a/modules/svg/defs.js b/modules/svg/defs.js index 25d2c8db1..6f4353769 100644 --- a/modules/svg/defs.js +++ b/modules/svg/defs.js @@ -26,8 +26,9 @@ export function svgDefs(context) { return function drawDefs(selection) { var defs = selection.append('defs'); - // oneway marker - defs.append('marker') + // markers + defs + .append('marker') .attr('id', 'oneway-marker') .attr('viewBox', '0 0 10 5') .attr('refX', 5) @@ -36,7 +37,6 @@ export function svgDefs(context) { .attr('markerHeight', 2) .attr('markerUnits', 'strokeWidth') .attr('orient', 'auto') - .append('path') .attr('class', 'oneway') .attr('d', 'M 5,3 L 0,3 L 0,2 L 5,2 L 5,0 L 10,2.5 L 5,5 z') @@ -44,21 +44,22 @@ export function svgDefs(context) { .attr('fill', '#000') .attr('opacity', '0.75'); - defs.append('marker') - .attr('id', 'directional-marker') - .attr('viewBox', '0 0 15 5') - .attr('refX', 5.5) - .attr('refY', 2.5) - .attr('markerWidth', 7) - .attr('markerHeight', 7) + defs + .append('marker') + .attr('id', 'viewfield-marker') + .attr('viewBox', '0 0 16 16') + .attr('refX', 8) + .attr('refY', 16) + .attr('markerWidth', 4) + .attr('markerHeight', 4) .attr('markerUnits', 'strokeWidth') .attr('orient', 'auto') - .append('path') - .attr('class', 'directional') - .attr('d', 'M 10,2.5 L 9,0 L 14,2.5 L 9,5 z') - .attr('stroke', '#fff') + .attr('class', 'viewfield') + .attr('d', 'M 6,14 C 8,13.4 8,13.4 10,14 L 16,3 C 12,0 4,0 0,3 z') .attr('fill', '#333') + .attr('fill-opacity', '0.75') + .attr('stroke', '#fff') .attr('stroke-width', '0.5px') .attr('stroke-opacity', '0.75'); @@ -77,23 +78,21 @@ export function svgDefs(context) { ]) .enter() .append('pattern') - .attr('id', function (d) { - return 'pattern-' + d[0]; - }) + .attr('id', function (d) { return 'pattern-' + d[0]; }) .attr('width', 32) .attr('height', 32) .attr('patternUnits', 'userSpaceOnUse'); - patterns.append('rect') + patterns + .append('rect') .attr('x', 0) .attr('y', 0) .attr('width', 32) .attr('height', 32) - .attr('class', function (d) { - return 'pattern-color-' + d[0]; - }); + .attr('class', function (d) { return 'pattern-color-' + d[0]; }); - patterns.append('image') + patterns + .append('image') .attr('x', 0) .attr('y', 0) .attr('width', 32) @@ -103,29 +102,20 @@ export function svgDefs(context) { }); // clip paths - defs.selectAll() + defs.selectAll('clipPath') .data([12, 18, 20, 32, 45]) .enter() .append('clipPath') - .attr('id', function (d) { - return 'clip-square-' + d; - }) + .attr('id', function (d) { return 'clip-square-' + d; }) .append('rect') .attr('x', 0) .attr('y', 0) - .attr('width', function (d) { - return d; - }) - .attr('height', function (d) { - return d; - }); + .attr('width', function (d) { return d; }) + .attr('height', function (d) { return d; }); - defs.call(SVGSpriteDefinition( - 'iD-sprite', - context.imagePath('iD-sprite.svg'))); - - defs.call(SVGSpriteDefinition( - 'maki-sprite', - context.imagePath('maki-sprite.svg'))); + // symbol spritesheets + defs + .call(SVGSpriteDefinition('iD-sprite', context.imagePath('iD-sprite.svg'))) + .call(SVGSpriteDefinition('maki-sprite', context.imagePath('maki-sprite.svg'))); }; } diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index 01544165c..fa449fdc2 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -152,19 +152,19 @@ export function svgVertices(projection, context) { .append('g') .attr('class', function(d) { return 'node vertex ' + klass + ' ' + d.id; }); - // Directional vertices get arrows + // Directional vertices get viewfields var directionsEnter = enter.filter(function(d) { return getDirections(d); }) .append('g') - .each(setClass('directiongroup')); + .each(setClass('viewfieldgroup')); - directionsEnter.selectAll('.directional') + directionsEnter.selectAll('.viewfield') .data(function(d) { return getDirections(d); }) .enter() .append('path') - .attr('class', 'directional') - .attr('transform', function(d) { return 'rotate(' + d + ')'; }) + .attr('class', 'viewfield') + .attr('transform', function(d) { return 'rotate(' + (d + 90) + ')'; }) // +90 because marker is oriented along Y not X .attr('d', 'M0,0H0') - .attr('marker-start', 'url(#directional-marker)'); + .attr('marker-start', 'url(#viewfield-marker)'); enter .append('circle') From 515094cb56f500cfaefa82bd68ccf90f3f906ed3 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 8 Dec 2017 14:36:45 -0500 Subject: [PATCH 03/70] Only add viewfields for an all-way stop on a highway intersection. Sometimes people tag all-way stop signs at the junction node, othertimes people tag all-way stop signs at the stop sign location. What we're doing here is: - if `stop=all` tagged at the junction, show viewfield in all directions - if `stop=all` tagged at the sign location, show viewfield according to `direction=` tag --- modules/svg/vertices.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index fa449fdc2..dc8eb4dc1 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -76,7 +76,7 @@ export function svgVertices(projection, context) { if (entity.id in directions) return directions[entity.id]; var dir = (entity.tags['traffic_signals:direction'] || entity.tags.direction || '').toLowerCase(); - var stop = (entity.tags.stop || '').toLowerCase(); + var stop = ((entity.isHighwayIntersection(graph) && entity.tags.stop) || '').toLowerCase(); var goBackward = (dir === 'backward' || dir === 'both' || dir === 'all' || stop === 'all'); var goForward = (dir === 'forward' || dir === 'both' || dir === 'all' || stop === 'all'); if (!goForward && !goBackward) return; From 9c649b74cd3289c7b6c0d31e856cae1ac4e9c4b7 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 8 Dec 2017 14:54:26 -0500 Subject: [PATCH 04/70] After switching from arrows to viewfields, forward/backward is different --- modules/svg/vertices.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index dc8eb4dc1..c0704b9bc 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -86,11 +86,11 @@ export function svgVertices(projection, context) { var nodes = parent.nodes; for (var i = 0; i < nodes.length; i++) { if (nodes[i] === entity.id) { // match current entity - if (goBackward && i > 0) { - nodeIds[nodes[i - 1]] = true; + if (goForward && i > 0) { + nodeIds[nodes[i - 1]] = true; // viewfield point back to prev node } - if (goForward && i < nodes.length - 1) { - nodeIds[nodes[i + 1]] = true; + if (goBackward && i < nodes.length - 1) { + nodeIds[nodes[i + 1]] = true; // viewfield point ahead to next node } } } From 42043b2ce1dfbc5d169503fab9775ef3700774d3 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 8 Dec 2017 16:23:35 -0500 Subject: [PATCH 05/70] Support more tags, cardinal and numeric directions --- modules/svg/vertices.js | 60 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index c0704b9bc..8a57b19ed 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -72,13 +72,60 @@ export function svgVertices(projection, context) { return icons[entity.id]; } + function getDirections(entity) { if (entity.id in directions) return directions[entity.id]; - var dir = (entity.tags['traffic_signals:direction'] || entity.tags.direction || '').toLowerCase(); - var stop = ((entity.isHighwayIntersection(graph) && entity.tags.stop) || '').toLowerCase(); - var goBackward = (dir === 'backward' || dir === 'both' || dir === 'all' || stop === 'all'); - var goForward = (dir === 'forward' || dir === 'both' || dir === 'all' || stop === 'all'); + var dir = ''; + + if (entity.isHighwayIntersection(graph) && (entity.tags.stop || '').toLowerCase() === 'all') { + // all-way stop tag on a highway intersection + dir = 'all'; + } else { + // direction tag + dir = ( + entity.tags['railway:signal:direction'] || + entity.tags['traffic_signals:direction'] || + entity.tags.direction || + '' + ).toLowerCase(); + } + + // swap cardinal for numeric directions + var cardinal = { + north: 0, n: 0, + northnortheast: 22, nne: 22, + northeast: 45, ne: 45, + eastnortheast: 67, ene: 67, + east: 90, e: 90, + eastsoutheast: 112, ese: 112, + southeast: 135, se: 135, + southsoutheast: 157, sse: 157, + south: 180, s: 180, + southsouthwest: 202, ssw: 202, + southwest: 225, sw: 225, + westsouthwest: 247, wsw: 247, + west: 270, w: 270, + westnorthwest: 292, wnw: 292, + northwest: 315, nw: 315, + northnorthwest: 337, nnw: 337 + }; + if (cardinal[dir] !== undefined) { + dir = cardinal[dir]; + } + + // if direction tag is numeric, return early + if (dir !== '' && !isNaN(+dir)) { + directions[entity.id] = [(+dir) - 90]; // -90 because marker is oriented along Y not X + return directions[entity.id]; + } + + // determine which direction(s) this feature points + var goBackward = + (entity.tags['traffic_sign:backward'] || dir === 'backward' || dir === 'both' || dir === 'all'); + var goForward = + (entity.tags['traffic_sign:forward'] || dir === 'forward' || dir === 'both' || dir === 'all'); + if (!goForward && !goBackward) return; var nodeIds = {}; @@ -87,10 +134,10 @@ export function svgVertices(projection, context) { for (var i = 0; i < nodes.length; i++) { if (nodes[i] === entity.id) { // match current entity if (goForward && i > 0) { - nodeIds[nodes[i - 1]] = true; // viewfield point back to prev node + nodeIds[nodes[i - 1]] = true; // viewfield points back to prev node } if (goBackward && i < nodes.length - 1) { - nodeIds[nodes[i + 1]] = true; // viewfield point ahead to next node + nodeIds[nodes[i + 1]] = true; // viewfield points ahead to next node } } } @@ -99,6 +146,7 @@ export function svgVertices(projection, context) { var dirAngles = Object.keys(nodeIds).map(function (nodeId) { return geoAngle(entity, graph.entity(nodeId), projection) * (180 / Math.PI); }); + directions[entity.id] = dirAngles; return directions[entity.id]; } From 34c98b94e67b9ef8f1def8fabfccc1c50ddd4064 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sat, 9 Dec 2017 12:53:08 -0500 Subject: [PATCH 06/70] Move directions code from vertices to node.js --- modules/osm/node.js | 76 +++++++++++++++++++++++++++++++++- modules/svg/vertices.js | 91 +++++------------------------------------ 2 files changed, 86 insertions(+), 81 deletions(-) diff --git a/modules/osm/node.js b/modules/osm/node.js index 59d299dbb..38b882c41 100644 --- a/modules/osm/node.js +++ b/modules/osm/node.js @@ -3,7 +3,7 @@ import _map from 'lodash-es/map'; import _some from 'lodash-es/some'; import { osmEntity } from './entity'; -import { geoExtent } from '../geo'; +import { geoAngle, geoExtent } from '../geo'; export function osmNode() { @@ -49,6 +49,80 @@ _extend(osmNode.prototype, { }, + // Inspect tags and geometry to determine which direction(s) this node/vertex points + directions: function(resolver, projection) { + var val; + + if (this.isHighwayIntersection(resolver) && (this.tags.stop || '').toLowerCase() === 'all') { + // all-way stop tag on a highway intersection + val = 'all'; + } else { + // direction tag + val = ( + this.tags['railway:signal:direction'] || + this.tags['traffic_signals:direction'] || + this.tags.direction || + '' + ).toLowerCase(); + } + + // swap cardinal for numeric directions + var cardinal = { + north: 0, n: 0, + northnortheast: 22, nne: 22, + northeast: 45, ne: 45, + eastnortheast: 67, ene: 67, + east: 90, e: 90, + eastsoutheast: 112, ese: 112, + southeast: 135, se: 135, + southsoutheast: 157, sse: 157, + south: 180, s: 180, + southsouthwest: 202, ssw: 202, + southwest: 225, sw: 225, + westsouthwest: 247, wsw: 247, + west: 270, w: 270, + westnorthwest: 292, wnw: 292, + northwest: 315, nw: 315, + northnorthwest: 337, nnw: 337 + }; + if (cardinal[val] !== undefined) { + val = cardinal[val]; + } + + // if direction is numeric, return early + if (val !== '' && !isNaN(+val)) { + return [(+val)]; + } + + var lookBackward = + (this.tags['traffic_sign:backward'] || val === 'backward' || val === 'both' || val === 'all'); + var lookForward = + (this.tags['traffic_sign:forward'] || val === 'forward' || val === 'both' || val === 'all'); + + if (!lookForward && !lookBackward) return null; + + var nodeIds = {}; + resolver.parentWays(this).forEach(function(parent) { + var nodes = parent.nodes; + for (var i = 0; i < nodes.length; i++) { + if (nodes[i] === this.id) { // match current entity + if (lookForward && i > 0) { + nodeIds[nodes[i - 1]] = true; // look back to prev node + } + if (lookBackward && i < nodes.length - 1) { + nodeIds[nodes[i + 1]] = true; // look ahead to next node + } + } + } + }, this); + + return Object.keys(nodeIds).map(function(nodeId) { + // +90 because geoAngle returns angle from X axis, not Y (north) + return (geoAngle(this, resolver.entity(nodeId), projection) * (180 / Math.PI)) + 90; + }, this); + }, + + isEndpoint: function(resolver) { return resolver.transient(this, 'isEndpoint', function() { var id = this.id; diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index 8a57b19ed..170c4ba2e 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -1,7 +1,6 @@ import _values from 'lodash-es/values'; import { dataFeatureIcons } from '../../data'; -import { geoAngle } from '../geo'; import { osmEntity } from '../osm'; import { svgPointTransform } from './index'; @@ -14,7 +13,7 @@ export function svgVertices(projection, context) { fill: [1, 1.5, 1.5, 1.5] }; - var hover; + var _hover; function siblingAndChildVertices(ids, graph, extent) { @@ -76,87 +75,19 @@ export function svgVertices(projection, context) { function getDirections(entity) { if (entity.id in directions) return directions[entity.id]; - var dir = ''; - - if (entity.isHighwayIntersection(graph) && (entity.tags.stop || '').toLowerCase() === 'all') { - // all-way stop tag on a highway intersection - dir = 'all'; - } else { - // direction tag - dir = ( - entity.tags['railway:signal:direction'] || - entity.tags['traffic_signals:direction'] || - entity.tags.direction || - '' - ).toLowerCase(); - } - - // swap cardinal for numeric directions - var cardinal = { - north: 0, n: 0, - northnortheast: 22, nne: 22, - northeast: 45, ne: 45, - eastnortheast: 67, ene: 67, - east: 90, e: 90, - eastsoutheast: 112, ese: 112, - southeast: 135, se: 135, - southsoutheast: 157, sse: 157, - south: 180, s: 180, - southsouthwest: 202, ssw: 202, - southwest: 225, sw: 225, - westsouthwest: 247, wsw: 247, - west: 270, w: 270, - westnorthwest: 292, wnw: 292, - northwest: 315, nw: 315, - northnorthwest: 337, nnw: 337 - }; - if (cardinal[dir] !== undefined) { - dir = cardinal[dir]; - } - - // if direction tag is numeric, return early - if (dir !== '' && !isNaN(+dir)) { - directions[entity.id] = [(+dir) - 90]; // -90 because marker is oriented along Y not X - return directions[entity.id]; - } - - // determine which direction(s) this feature points - var goBackward = - (entity.tags['traffic_sign:backward'] || dir === 'backward' || dir === 'both' || dir === 'all'); - var goForward = - (entity.tags['traffic_sign:forward'] || dir === 'forward' || dir === 'both' || dir === 'all'); - - if (!goForward && !goBackward) return; - - var nodeIds = {}; - graph.parentWays(entity).forEach(function (parent) { - var nodes = parent.nodes; - for (var i = 0; i < nodes.length; i++) { - if (nodes[i] === entity.id) { // match current entity - if (goForward && i > 0) { - nodeIds[nodes[i - 1]] = true; // viewfield points back to prev node - } - if (goBackward && i < nodes.length - 1) { - nodeIds[nodes[i + 1]] = true; // viewfield points ahead to next node - } - } - } - }); - - var dirAngles = Object.keys(nodeIds).map(function (nodeId) { - return geoAngle(entity, graph.entity(nodeId), projection) * (180 / Math.PI); - }); - - directions[entity.id] = dirAngles; - return directions[entity.id]; + var angles = entity.directions(graph, projection); + if (angles) directions[entity.id] = angles; + return angles; } + function setClass(klass) { return function(entity) { this.setAttribute('class', 'node vertex ' + klass + ' ' + entity.id); }; } + function updateAttributes(selection) { ['shadow','stroke','fill'].forEach(function(klass) { var rads = radiuses[klass]; @@ -185,7 +116,7 @@ export function svgVertices(projection, context) { selection.selectAll('use') .attr('visibility', (z === 0 ? 'hidden' : null)); - selection.selectAll('.directiongroup') + selection.selectAll('.viewfieldgroup') .attr('visibility', (zoom < 18 ? 'hidden' : null)); } @@ -210,7 +141,7 @@ export function svgVertices(projection, context) { .enter() .append('path') .attr('class', 'viewfield') - .attr('transform', function(d) { return 'rotate(' + (d + 90) + ')'; }) // +90 because marker is oriented along Y not X + .attr('transform', function(d) { return 'rotate(' + d + ')'; }) .attr('d', 'M0,0H0') .attr('marker-start', 'url(#viewfield-marker)'); @@ -286,7 +217,7 @@ export function svgVertices(projection, context) { function drawHover(selection, graph, extent, zoom) { - var hovered = hover ? siblingAndChildVertices([hover.id], graph, extent) : {}; + var hovered = _hover ? siblingAndChildVertices([_hover.id], graph, extent) : {}; var layer = selection.selectAll('.layer-hit'); layer.selectAll('g.vertex.vertex-hover') @@ -295,8 +226,8 @@ export function svgVertices(projection, context) { drawVertices.drawHover = function(selection, graph, target, extent, zoom) { - if (target === hover) return; - hover = target; + if (target === _hover) return; + _hover = target; drawHover(selection, graph, extent, zoom); }; From 2a8bf6c7bf9607eb9bd6d43ed9de8e2e1d008111 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sun, 10 Dec 2017 00:43:46 -0500 Subject: [PATCH 07/70] Don't draw viewfields on hover, rearrange code --- modules/svg/vertices.js | 50 ++++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index 170c4ba2e..0e58d8dcb 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -124,27 +124,15 @@ export function svgVertices(projection, context) { var groups = selection .data(vertices, osmEntity.key); + // exit groups.exit() .remove(); + // enter var enter = groups.enter() .append('g') .attr('class', function(d) { return 'node vertex ' + klass + ' ' + d.id; }); - // Directional vertices get viewfields - var directionsEnter = enter.filter(function(d) { return getDirections(d); }) - .append('g') - .each(setClass('viewfieldgroup')); - - directionsEnter.selectAll('.viewfield') - .data(function(d) { return getDirections(d); }) - .enter() - .append('path') - .attr('class', 'viewfield') - .attr('transform', function(d) { return 'rotate(' + d + ')'; }) - .attr('d', 'M0,0H0') - .attr('marker-start', 'url(#viewfield-marker)'); - enter .append('circle') .each(setClass('shadow')); @@ -171,14 +159,44 @@ export function svgVertices(projection, context) { .append('circle') .each(setClass('fill')); - // Update - groups + // update + groups = groups .merge(enter) .attr('transform', svgPointTransform(projection)) .classed('sibling', function(entity) { return entity.id in siblings; }) .classed('shared', function(entity) { return graph.isShared(entity); }) .classed('endpoint', function(entity) { return entity.isEndpoint(graph); }) .call(updateAttributes); + + + // Directional vertices get viewfields + var dgroups = groups.filter(function(d) { return getDirections(d); }) + .selectAll('.viewfieldgroup') + .data(function(d) { return klass === 'vertex-hover' ? [] : [d]; }, osmEntity.key); + + // exit + dgroups.exit() + .remove(); + + // enter + var dgroupsEnter = dgroups.enter() + .insert('g', '.shadow') + .each(setClass('viewfieldgroup')); + + dgroupsEnter + .selectAll('.viewfield') + .data(getDirections, function key(d) { return d; }) + .enter() + .append('path') + .attr('class', 'viewfield') + .attr('d', 'M0,0H0') + .attr('marker-start', 'url(#viewfield-marker)'); + + // update + dgroups + .merge(dgroupsEnter) + .selectAll('.viewfield') + .attr('transform', function(d) { return 'rotate(' + d + ')'; }); } From b42c096fe53adec9711320360c6219b4f6b65567 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sun, 10 Dec 2017 14:34:50 -0500 Subject: [PATCH 08/70] Make sure viewfields actually update on update selection --- modules/svg/vertices.js | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index 0e58d8dcb..80638b7bd 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -178,24 +178,26 @@ export function svgVertices(projection, context) { dgroups.exit() .remove(); - // enter - var dgroupsEnter = dgroups.enter() + // enter/update + dgroups = dgroups.enter() .insert('g', '.shadow') - .each(setClass('viewfieldgroup')); + .each(setClass('viewfieldgroup')) + .merge(dgroups); - dgroupsEnter - .selectAll('.viewfield') - .data(getDirections, function key(d) { return d; }) - .enter() + var viewfields = dgroups.selectAll('.viewfield') + .data(getDirections, function key(d) { return d; }); + + // exit + viewfields.exit() + .remove(); + + // enter/update + viewfields.enter() .append('path') .attr('class', 'viewfield') .attr('d', 'M0,0H0') - .attr('marker-start', 'url(#viewfield-marker)'); - - // update - dgroups - .merge(dgroupsEnter) - .selectAll('.viewfield') + .attr('marker-start', 'url(#viewfield-marker)') + .merge(viewfields) .attr('transform', function(d) { return 'rotate(' + d + ')'; }); } From 8e19474293108b77a1edddeb27ad15249b353deb Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 11 Dec 2017 10:48:04 -0500 Subject: [PATCH 09/70] Render directional points (e.g. benches, cameras, signs) as vertices --- modules/osm/node.js | 2 +- modules/svg/points.js | 14 +++++++++----- modules/svg/vertices.js | 5 +++-- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/modules/osm/node.js b/modules/osm/node.js index 38b882c41..cd3c8a79a 100644 --- a/modules/osm/node.js +++ b/modules/osm/node.js @@ -99,7 +99,7 @@ _extend(osmNode.prototype, { var lookForward = (this.tags['traffic_sign:forward'] || val === 'forward' || val === 'both' || val === 'all'); - if (!lookForward && !lookBackward) return null; + if (!lookForward && !lookBackward) return []; var nodeIds = {}; resolver.parentWays(this).forEach(function(parent) { diff --git a/modules/svg/points.js b/modules/svg/points.js index e48f31163..054a7359e 100644 --- a/modules/svg/points.js +++ b/modules/svg/points.js @@ -22,7 +22,7 @@ export function svgPoints(projection, context) { return function drawPoints(selection, graph, entities, filter) { var wireframe = context.surface().classed('fill-wireframe'), points = wireframe ? [] : _filter(entities, function(e) { - return e.geometry(graph) === 'point'; + return e.geometry(graph) === 'point' && !e.directions(graph, projection).length; }); points.sort(sortY); @@ -41,20 +41,24 @@ export function svgPoints(projection, context) { .attr('class', function(d) { return 'node point ' + d.id; }) .order(); - enter.append('path') + enter + .append('path') .call(markerPath, 'shadow'); - enter.append('ellipse') + enter + .append('ellipse') .attr('cx', 0.5) .attr('cy', 1) .attr('rx', 6.5) .attr('ry', 3) .attr('class', 'stroke'); - enter.append('path') + enter + .append('path') .call(markerPath, 'stroke'); - enter.append('use') + enter + .append('use') .attr('transform', 'translate(-5, -19)') .attr('class', 'icon') .attr('width', '11px') diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index 80638b7bd..e7fa7a776 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -72,11 +72,12 @@ export function svgVertices(projection, context) { } + // memoize directions results, return false for empty arrays (for use in filter) function getDirections(entity) { if (entity.id in directions) return directions[entity.id]; var angles = entity.directions(graph, projection); - if (angles) directions[entity.id] = angles; + directions[entity.id] = angles.length ? angles : false; return angles; } @@ -211,7 +212,7 @@ export function svgVertices(projection, context) { var entity = entities[i], geometry = entity.geometry(graph); - if (wireframe && geometry === 'point') { + if ((geometry === 'point') && (wireframe || entity.directions(graph, projection).length)) { vertices.push(entity); continue; } From ee3083b113ef8bfbbd8d3e669a4e54fb94e5d427 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 11 Dec 2017 11:46:11 -0500 Subject: [PATCH 10/70] Support rendering `camera:direction` --- modules/osm/node.js | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/osm/node.js b/modules/osm/node.js index cd3c8a79a..362b4448c 100644 --- a/modules/osm/node.js +++ b/modules/osm/node.js @@ -59,6 +59,7 @@ _extend(osmNode.prototype, { } else { // direction tag val = ( + this.tags['camera:direction'] || this.tags['railway:signal:direction'] || this.tags['traffic_signals:direction'] || this.tags.direction || From 6b9ccdb45acbc1d776c32c6b2c55b7abbfe7cf13 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 11 Dec 2017 14:33:22 -0500 Subject: [PATCH 11/70] Add tests for osmNode#direction --- test/spec/osm/node.js | 555 +++++++++++++++++++++++++++++++++++------- 1 file changed, 471 insertions(+), 84 deletions(-) diff --git a/test/spec/osm/node.js b/test/spec/osm/node.js index 53de624dc..dd005c889 100644 --- a/test/spec/osm/node.js +++ b/test/spec/osm/node.js @@ -1,66 +1,66 @@ describe('iD.osmNode', function () { it('returns a node', function () { - expect(iD.Node()).to.be.an.instanceOf(iD.Node); - expect(iD.Node().type).to.equal('node'); + expect(iD.osmNode()).to.be.an.instanceOf(iD.osmNode); + expect(iD.osmNode().type).to.equal('node'); }); it('defaults tags to an empty object', function () { - expect(iD.Node().tags).to.eql({}); + expect(iD.osmNode().tags).to.eql({}); }); it('sets tags as specified', function () { - expect(iD.Node({tags: {foo: 'bar'}}).tags).to.eql({foo: 'bar'}); + expect(iD.osmNode({tags: {foo: 'bar'}}).tags).to.eql({foo: 'bar'}); }); describe('#extent', function() { it('returns a point extent', function() { - expect(iD.Node({loc: [5, 10]}).extent().equals([[5, 10], [5, 10]])).to.be.ok; + expect(iD.osmNode({loc: [5, 10]}).extent().equals([[5, 10], [5, 10]])).to.be.ok; }); }); describe('#intersects', function () { it('returns true for a node within the given extent', function () { - expect(iD.Node({loc: [0, 0]}).intersects([[-5, -5], [5, 5]])).to.equal(true); + expect(iD.osmNode({loc: [0, 0]}).intersects([[-5, -5], [5, 5]])).to.equal(true); }); it('returns false for a node outside the given extend', function () { - expect(iD.Node({loc: [6, 6]}).intersects([[-5, -5], [5, 5]])).to.equal(false); + expect(iD.osmNode({loc: [6, 6]}).intersects([[-5, -5], [5, 5]])).to.equal(false); }); }); describe('#geometry', function () { it('returns \'vertex\' if the node is a member of any way', function () { - var node = iD.Node(), - way = iD.Way({nodes: [node.id]}), - graph = iD.Graph([node, way]); + var node = iD.osmNode(), + way = iD.osmWay({nodes: [node.id]}), + graph = iD.coreGraph([node, way]); expect(node.geometry(graph)).to.equal('vertex'); }); it('returns \'point\' if the node is not a member of any way', function () { - var node = iD.Node(), - graph = iD.Graph([node]); + var node = iD.osmNode(), + graph = iD.coreGraph([node]); expect(node.geometry(graph)).to.equal('point'); }); }); describe('#isEndpoint', function () { it('returns true for a node at an endpoint along a linear way', function () { - var a = iD.Node({id: 'a'}), - b = iD.Node({id: 'b'}), - c = iD.Node({id: 'c'}), - w = iD.Way({nodes: ['a', 'b', 'c']}), - graph = iD.Graph([a, b, c, w]); + var a = iD.osmNode({id: 'a'}), + b = iD.osmNode({id: 'b'}), + c = iD.osmNode({id: 'c'}), + w = iD.osmWay({nodes: ['a', 'b', 'c']}), + graph = iD.coreGraph([a, b, c, w]); expect(a.isEndpoint(graph)).to.equal(true, 'linear way, beginning node'); expect(b.isEndpoint(graph)).to.equal(false, 'linear way, middle node'); expect(c.isEndpoint(graph)).to.equal(true, 'linear way, ending node'); }); it('returns false for nodes along a circular way', function () { - var a = iD.Node({id: 'a'}), - b = iD.Node({id: 'b'}), - c = iD.Node({id: 'c'}), - w = iD.Way({nodes: ['a', 'b', 'c', 'a']}), - graph = iD.Graph([a, b, c, w]); + var a = iD.osmNode({id: 'a'}), + b = iD.osmNode({id: 'b'}), + c = iD.osmNode({id: 'c'}), + w = iD.osmWay({nodes: ['a', 'b', 'c', 'a']}), + graph = iD.coreGraph([a, b, c, w]); expect(a.isEndpoint(graph)).to.equal(false, 'circular way, connector node'); expect(b.isEndpoint(graph)).to.equal(false, 'circular way, middle node'); expect(c.isEndpoint(graph)).to.equal(false, 'circular way, ending node'); @@ -69,120 +69,507 @@ describe('iD.osmNode', function () { describe('#isConnected', function () { it('returns true for a node with multiple parent ways, at least one interesting', function () { - var node = iD.Node(), - w1 = iD.Way({nodes: [node.id]}), - w2 = iD.Way({nodes: [node.id], tags: { highway: 'residential' }}), - graph = iD.Graph([node, w1, w2]); + var node = iD.osmNode(), + w1 = iD.osmWay({nodes: [node.id]}), + w2 = iD.osmWay({nodes: [node.id], tags: { highway: 'residential' }}), + graph = iD.coreGraph([node, w1, w2]); expect(node.isConnected(graph)).to.equal(true); }); it('returns false for a node with only area parent ways', function () { - var node = iD.Node(), - w1 = iD.Way({nodes: [node.id], tags: { area: 'yes' }}), - w2 = iD.Way({nodes: [node.id], tags: { area: 'yes' }}), - graph = iD.Graph([node, w1, w2]); + var node = iD.osmNode(), + w1 = iD.osmWay({nodes: [node.id], tags: { area: 'yes' }}), + w2 = iD.osmWay({nodes: [node.id], tags: { area: 'yes' }}), + graph = iD.coreGraph([node, w1, w2]); expect(node.isConnected(graph)).to.equal(false); }); it('returns false for a node with only uninteresting parent ways', function () { - var node = iD.Node(), - w1 = iD.Way({nodes: [node.id]}), - w2 = iD.Way({nodes: [node.id]}), - graph = iD.Graph([node, w1, w2]); + var node = iD.osmNode(), + w1 = iD.osmWay({nodes: [node.id]}), + w2 = iD.osmWay({nodes: [node.id]}), + graph = iD.coreGraph([node, w1, w2]); expect(node.isConnected(graph)).to.equal(false); }); it('returns false for a standalone node on a single parent way', function () { - var node = iD.Node(), - way = iD.Way({nodes: [node.id]}), - graph = iD.Graph([node, way]); + var node = iD.osmNode(), + way = iD.osmWay({nodes: [node.id]}), + graph = iD.coreGraph([node, way]); expect(node.isConnected(graph)).to.equal(false); }); it('returns true for a self-intersecting node on a single parent way', function () { - var a = iD.Node({id: 'a'}), - b = iD.Node({id: 'b'}), - c = iD.Node({id: 'c'}), - w = iD.Way({nodes: ['a', 'b', 'c', 'b']}), - graph = iD.Graph([a, b, c, w]); + var a = iD.osmNode({id: 'a'}), + b = iD.osmNode({id: 'b'}), + c = iD.osmNode({id: 'c'}), + w = iD.osmWay({nodes: ['a', 'b', 'c', 'b']}), + graph = iD.coreGraph([a, b, c, w]); expect(b.isConnected(graph)).to.equal(true); }); it('returns false for the connecting node of a closed way', function () { - var a = iD.Node({id: 'a'}), - b = iD.Node({id: 'b'}), - c = iD.Node({id: 'c'}), - w = iD.Way({nodes: ['a', 'b', 'c', 'a']}), - graph = iD.Graph([a, b, c, w]); + var a = iD.osmNode({id: 'a'}), + b = iD.osmNode({id: 'b'}), + c = iD.osmNode({id: 'c'}), + w = iD.osmWay({nodes: ['a', 'b', 'c', 'a']}), + graph = iD.coreGraph([a, b, c, w]); expect(a.isConnected(graph)).to.equal(false); }); }); describe('#isIntersection', function () { it('returns true for a node shared by more than one highway', function () { - var node = iD.Node(), - w1 = iD.Way({nodes: [node.id], tags: {highway: 'residential'}}), - w2 = iD.Way({nodes: [node.id], tags: {highway: 'residential'}}), - graph = iD.Graph([node, w1, w2]); + var node = iD.osmNode(), + w1 = iD.osmWay({nodes: [node.id], tags: {highway: 'residential'}}), + w2 = iD.osmWay({nodes: [node.id], tags: {highway: 'residential'}}), + graph = iD.coreGraph([node, w1, w2]); expect(node.isIntersection(graph)).to.equal(true); }); it('returns true for a node shared by more than one waterway', function () { - var node = iD.Node(), - w1 = iD.Way({nodes: [node.id], tags: {waterway: 'river'}}), - w2 = iD.Way({nodes: [node.id], tags: {waterway: 'river'}}), - graph = iD.Graph([node, w1, w2]); + var node = iD.osmNode(), + w1 = iD.osmWay({nodes: [node.id], tags: {waterway: 'river'}}), + w2 = iD.osmWay({nodes: [node.id], tags: {waterway: 'river'}}), + graph = iD.coreGraph([node, w1, w2]); expect(node.isIntersection(graph)).to.equal(true); }); }); describe('#isHighwayIntersection', function () { it('returns true for a node shared by more than one highway', function () { - var node = iD.Node(), - w1 = iD.Way({nodes: [node.id], tags: {highway: 'residential'}}), - w2 = iD.Way({nodes: [node.id], tags: {highway: 'residential'}}), - graph = iD.Graph([node, w1, w2]); + var node = iD.osmNode(), + w1 = iD.osmWay({nodes: [node.id], tags: {highway: 'residential'}}), + w2 = iD.osmWay({nodes: [node.id], tags: {highway: 'residential'}}), + graph = iD.coreGraph([node, w1, w2]); expect(node.isHighwayIntersection(graph)).to.equal(true); }); it('returns false for a node shared by more than one waterway', function () { - var node = iD.Node(), - w1 = iD.Way({nodes: [node.id], tags: {waterway: 'river'}}), - w2 = iD.Way({nodes: [node.id], tags: {waterway: 'river'}}), - graph = iD.Graph([node, w1, w2]); + var node = iD.osmNode(), + w1 = iD.osmWay({nodes: [node.id], tags: {waterway: 'river'}}), + w2 = iD.osmWay({nodes: [node.id], tags: {waterway: 'river'}}), + graph = iD.coreGraph([node, w1, w2]); expect(node.isHighwayIntersection(graph)).to.equal(false); }); }); describe('#isDegenerate', function () { it('returns true if node has invalid loc', function () { - expect(iD.Node().isDegenerate()).to.be.equal(true, 'no loc'); - expect(iD.Node({loc: ''}).isDegenerate()).to.be.equal(true, 'empty string loc'); - expect(iD.Node({loc: []}).isDegenerate()).to.be.equal(true, 'empty array loc'); - expect(iD.Node({loc: [0]}).isDegenerate()).to.be.equal(true, '1-array loc'); - expect(iD.Node({loc: [0, 0, 0]}).isDegenerate()).to.be.equal(true, '3-array loc'); - expect(iD.Node({loc: [-181, 0]}).isDegenerate()).to.be.equal(true, '< min lon'); - expect(iD.Node({loc: [181, 0]}).isDegenerate()).to.be.equal(true, '> max lon'); - expect(iD.Node({loc: [0, -91]}).isDegenerate()).to.be.equal(true, '< min lat'); - expect(iD.Node({loc: [0, 91]}).isDegenerate()).to.be.equal(true, '> max lat'); - expect(iD.Node({loc: [Infinity, 0]}).isDegenerate()).to.be.equal(true, 'Infinity lon'); - expect(iD.Node({loc: [0, Infinity]}).isDegenerate()).to.be.equal(true, 'Infinity lat'); - expect(iD.Node({loc: [NaN, 0]}).isDegenerate()).to.be.equal(true, 'NaN lon'); - expect(iD.Node({loc: [0, NaN]}).isDegenerate()).to.be.equal(true, 'NaN lat'); + expect(iD.osmNode().isDegenerate()).to.be.equal(true, 'no loc'); + expect(iD.osmNode({loc: ''}).isDegenerate()).to.be.equal(true, 'empty string loc'); + expect(iD.osmNode({loc: []}).isDegenerate()).to.be.equal(true, 'empty array loc'); + expect(iD.osmNode({loc: [0]}).isDegenerate()).to.be.equal(true, '1-array loc'); + expect(iD.osmNode({loc: [0, 0, 0]}).isDegenerate()).to.be.equal(true, '3-array loc'); + expect(iD.osmNode({loc: [-181, 0]}).isDegenerate()).to.be.equal(true, '< min lon'); + expect(iD.osmNode({loc: [181, 0]}).isDegenerate()).to.be.equal(true, '> max lon'); + expect(iD.osmNode({loc: [0, -91]}).isDegenerate()).to.be.equal(true, '< min lat'); + expect(iD.osmNode({loc: [0, 91]}).isDegenerate()).to.be.equal(true, '> max lat'); + expect(iD.osmNode({loc: [Infinity, 0]}).isDegenerate()).to.be.equal(true, 'Infinity lon'); + expect(iD.osmNode({loc: [0, Infinity]}).isDegenerate()).to.be.equal(true, 'Infinity lat'); + expect(iD.osmNode({loc: [NaN, 0]}).isDegenerate()).to.be.equal(true, 'NaN lon'); + expect(iD.osmNode({loc: [0, NaN]}).isDegenerate()).to.be.equal(true, 'NaN lat'); }); it('returns false if node has valid loc', function () { - expect(iD.Node({loc: [0, 0]}).isDegenerate()).to.be.equal(false, '2-array loc'); - expect(iD.Node({loc: [-180, 0]}).isDegenerate()).to.be.equal(false, 'min lon'); - expect(iD.Node({loc: [180, 0]}).isDegenerate()).to.be.equal(false, 'max lon'); - expect(iD.Node({loc: [0, -90]}).isDegenerate()).to.be.equal(false, 'min lat'); - expect(iD.Node({loc: [0, 90]}).isDegenerate()).to.be.equal(false, 'max lat'); + expect(iD.osmNode({loc: [0, 0]}).isDegenerate()).to.be.equal(false, '2-array loc'); + expect(iD.osmNode({loc: [-180, 0]}).isDegenerate()).to.be.equal(false, 'min lon'); + expect(iD.osmNode({loc: [180, 0]}).isDegenerate()).to.be.equal(false, 'max lon'); + expect(iD.osmNode({loc: [0, -90]}).isDegenerate()).to.be.equal(false, 'min lat'); + expect(iD.osmNode({loc: [0, 90]}).isDegenerate()).to.be.equal(false, 'max lat'); }); }); + describe('#directions', function () { + var projection = function (_) { return _; }; + it('returns empty array if no direction tag', function () { + var node1 = iD.osmNode({ loc: [0, 0], tags: {}}); + var graph = iD.coreGraph([node1]); + expect(node1.directions(graph, projection)).to.eql([], 'no direction tag'); + }); + + it('returns empty array if nonsense direction tag', function () { + var node1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'blah' }}); + var node2 = iD.osmNode({ loc: [0, 0], tags: { direction: '' }}); + var node3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'NaN' }}); + var node4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'eastwest' }}); + var graph = iD.coreGraph([node1, node2, node3, node4]); + + expect(node1.directions(graph, projection)).to.eql([], 'nonsense direction tag'); + expect(node2.directions(graph, projection)).to.eql([], 'empty string direction tag'); + expect(node3.directions(graph, projection)).to.eql([], 'NaN direction tag'); + expect(node4.directions(graph, projection)).to.eql([], 'eastwest direction tag'); + }); + + it('supports numeric direction tag', function () { + var node1 = iD.osmNode({ loc: [0, 0], tags: { direction: '0' }}); + var node2 = iD.osmNode({ loc: [0, 0], tags: { direction: '45' }}); + var node3 = iD.osmNode({ loc: [0, 0], tags: { direction: '-45' }}); + var node4 = iD.osmNode({ loc: [0, 0], tags: { direction: '360' }}); + var node5 = iD.osmNode({ loc: [0, 0], tags: { direction: '1000' }}); + var graph = iD.coreGraph([node1, node2, node3, node4, node5]); + + expect(node1.directions(graph, projection)).to.eql([0], 'numeric 0'); + expect(node2.directions(graph, projection)).to.eql([45], 'numeric 45'); + expect(node3.directions(graph, projection)).to.eql([-45], 'numeric -45'); + expect(node4.directions(graph, projection)).to.eql([360], 'numeric 360'); + expect(node5.directions(graph, projection)).to.eql([1000], 'numeric 1000'); + }); + + it('supports cardinal direction tags (test abbreviated and mixed case)', function () { + var nodeN1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'n' }}); + var nodeN2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'N' }}); + var nodeN3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'north' }}); + var nodeN4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'NOrth' }}); + + var nodeNNE1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'nne' }}); + var nodeNNE2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'NnE' }}); + var nodeNNE3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'northnortheast' }}); + var nodeNNE4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'NOrthnorTHEast' }}); + + var nodeNE1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'ne' }}); + var nodeNE2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'nE' }}); + var nodeNE3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'northeast' }}); + var nodeNE4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'norTHEast' }}); + + var nodeENE1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'ene' }}); + var nodeENE2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'EnE' }}); + var nodeENE3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'eastnortheast' }}); + var nodeENE4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'EAstnorTHEast' }}); + + var nodeE1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'e' }}); + var nodeE2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'E' }}); + var nodeE3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'east' }}); + var nodeE4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'EAst' }}); + + var nodeESE1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'ese' }}); + var nodeESE2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'EsE' }}); + var nodeESE3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'eastsoutheast' }}); + var nodeESE4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'EAstsouTHEast' }}); + + var nodeSE1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'se' }}); + var nodeSE2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'sE' }}); + var nodeSE3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'southeast' }}); + var nodeSE4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'souTHEast' }}); + + var nodeSSE1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'sse' }}); + var nodeSSE2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'SsE' }}); + var nodeSSE3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'southsoutheast' }}); + var nodeSSE4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'SOuthsouTHEast' }}); + + var nodeS1 = iD.osmNode({ loc: [0, 0], tags: { direction: 's' }}); + var nodeS2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'S' }}); + var nodeS3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'south' }}); + var nodeS4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'SOuth' }}); + + var nodeSSW1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'ssw' }}); + var nodeSSW2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'SsW' }}); + var nodeSSW3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'southsouthwest' }}); + var nodeSSW4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'SOuthsouTHWest' }}); + + var nodeSW1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'sw' }}); + var nodeSW2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'sW' }}); + var nodeSW3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'southwest' }}); + var nodeSW4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'souTHWest' }}); + + var nodeWSW1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'wsw' }}); + var nodeWSW2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'WsW' }}); + var nodeWSW3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'westsouthwest' }}); + var nodeWSW4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'WEstsouTHWest' }}); + + var nodeW1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'w' }}); + var nodeW2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'W' }}); + var nodeW3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'west' }}); + var nodeW4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'WEst' }}); + + var nodeWNW1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'wnw' }}); + var nodeWNW2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'WnW' }}); + var nodeWNW3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'westnorthwest' }}); + var nodeWNW4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'WEstnorTHWest' }}); + + var nodeNW1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'nw' }}); + var nodeNW2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'nW' }}); + var nodeNW3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'northwest' }}); + var nodeNW4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'norTHWest' }}); + + var nodeNNW1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'nnw' }}); + var nodeNNW2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'NnW' }}); + var nodeNNW3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'northnorthwest' }}); + var nodeNNW4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'NOrthnorTHWest' }}); + + var graph = iD.coreGraph([ + nodeN1, nodeN2, nodeN3, nodeN4, + nodeNNE1, nodeNNE2, nodeNNE3, nodeNNE4, + nodeNE1, nodeNE2, nodeNE3, nodeNE4, + nodeENE1, nodeENE2, nodeENE3, nodeENE4, + nodeE1, nodeE2, nodeE3, nodeE4, + nodeESE1, nodeESE2, nodeESE3, nodeESE4, + nodeSE1, nodeSE2, nodeSE3, nodeSE4, + nodeSSE1, nodeSSE2, nodeSSE3, nodeSSE4, + nodeS1, nodeS2, nodeS3, nodeS4, + nodeSSW1, nodeSSW2, nodeSSW3, nodeSSW4, + nodeSW1, nodeSW2, nodeSW3, nodeSW4, + nodeWSW1, nodeWSW2, nodeWSW3, nodeWSW4, + nodeW1, nodeW2, nodeW3, nodeW4, + nodeWNW1, nodeWNW2, nodeWNW3, nodeWNW4, + nodeNW1, nodeNW2, nodeNW3, nodeNW4, + nodeNNW1, nodeNNW2, nodeNNW3, nodeNNW4 + ]); + + expect(nodeN1.directions(graph, projection)).to.eql([0], 'cardinal n'); + expect(nodeN2.directions(graph, projection)).to.eql([0], 'cardinal N'); + expect(nodeN3.directions(graph, projection)).to.eql([0], 'cardinal north'); + expect(nodeN4.directions(graph, projection)).to.eql([0], 'cardinal NOrth'); + + expect(nodeNNE1.directions(graph, projection)).to.eql([22], 'cardinal nne'); + expect(nodeNNE2.directions(graph, projection)).to.eql([22], 'cardinal NnE'); + expect(nodeNNE3.directions(graph, projection)).to.eql([22], 'cardinal northnortheast'); + expect(nodeNNE4.directions(graph, projection)).to.eql([22], 'cardinal NOrthnorTHEast'); + + expect(nodeNE1.directions(graph, projection)).to.eql([45], 'cardinal ne'); + expect(nodeNE2.directions(graph, projection)).to.eql([45], 'cardinal nE'); + expect(nodeNE3.directions(graph, projection)).to.eql([45], 'cardinal northeast'); + expect(nodeNE4.directions(graph, projection)).to.eql([45], 'cardinal norTHEast'); + + expect(nodeENE1.directions(graph, projection)).to.eql([67], 'cardinal ene'); + expect(nodeENE2.directions(graph, projection)).to.eql([67], 'cardinal EnE'); + expect(nodeENE3.directions(graph, projection)).to.eql([67], 'cardinal eastnortheast'); + expect(nodeENE4.directions(graph, projection)).to.eql([67], 'cardinal EAstnorTHEast'); + + expect(nodeE1.directions(graph, projection)).to.eql([90], 'cardinal e'); + expect(nodeE2.directions(graph, projection)).to.eql([90], 'cardinal E'); + expect(nodeE3.directions(graph, projection)).to.eql([90], 'cardinal east'); + expect(nodeE4.directions(graph, projection)).to.eql([90], 'cardinal EAst'); + + expect(nodeESE1.directions(graph, projection)).to.eql([112], 'cardinal ese'); + expect(nodeESE2.directions(graph, projection)).to.eql([112], 'cardinal EsE'); + expect(nodeESE3.directions(graph, projection)).to.eql([112], 'cardinal eastsoutheast'); + expect(nodeESE4.directions(graph, projection)).to.eql([112], 'cardinal EAstsouTHEast'); + + expect(nodeSE1.directions(graph, projection)).to.eql([135], 'cardinal se'); + expect(nodeSE2.directions(graph, projection)).to.eql([135], 'cardinal sE'); + expect(nodeSE3.directions(graph, projection)).to.eql([135], 'cardinal southeast'); + expect(nodeSE4.directions(graph, projection)).to.eql([135], 'cardinal souTHEast'); + + expect(nodeSSE1.directions(graph, projection)).to.eql([157], 'cardinal sse'); + expect(nodeSSE2.directions(graph, projection)).to.eql([157], 'cardinal SsE'); + expect(nodeSSE3.directions(graph, projection)).to.eql([157], 'cardinal southsoutheast'); + expect(nodeSSE4.directions(graph, projection)).to.eql([157], 'cardinal SouthsouTHEast'); + + expect(nodeS1.directions(graph, projection)).to.eql([180], 'cardinal s'); + expect(nodeS2.directions(graph, projection)).to.eql([180], 'cardinal S'); + expect(nodeS3.directions(graph, projection)).to.eql([180], 'cardinal south'); + expect(nodeS4.directions(graph, projection)).to.eql([180], 'cardinal SOuth'); + + expect(nodeSSW1.directions(graph, projection)).to.eql([202], 'cardinal ssw'); + expect(nodeSSW2.directions(graph, projection)).to.eql([202], 'cardinal SsW'); + expect(nodeSSW3.directions(graph, projection)).to.eql([202], 'cardinal southsouthwest'); + expect(nodeSSW4.directions(graph, projection)).to.eql([202], 'cardinal SouthsouTHWest'); + + expect(nodeSW1.directions(graph, projection)).to.eql([225], 'cardinal sw'); + expect(nodeSW2.directions(graph, projection)).to.eql([225], 'cardinal sW'); + expect(nodeSW3.directions(graph, projection)).to.eql([225], 'cardinal southwest'); + expect(nodeSW4.directions(graph, projection)).to.eql([225], 'cardinal souTHWest'); + + expect(nodeWSW1.directions(graph, projection)).to.eql([247], 'cardinal wsw'); + expect(nodeWSW2.directions(graph, projection)).to.eql([247], 'cardinal WsW'); + expect(nodeWSW3.directions(graph, projection)).to.eql([247], 'cardinal westsouthwest'); + expect(nodeWSW4.directions(graph, projection)).to.eql([247], 'cardinal WEstsouTHWest'); + + expect(nodeW1.directions(graph, projection)).to.eql([270], 'cardinal w'); + expect(nodeW2.directions(graph, projection)).to.eql([270], 'cardinal W'); + expect(nodeW3.directions(graph, projection)).to.eql([270], 'cardinal west'); + expect(nodeW4.directions(graph, projection)).to.eql([270], 'cardinal WEst'); + + expect(nodeWNW1.directions(graph, projection)).to.eql([292], 'cardinal wnw'); + expect(nodeWNW2.directions(graph, projection)).to.eql([292], 'cardinal WnW'); + expect(nodeWNW3.directions(graph, projection)).to.eql([292], 'cardinal westnorthwest'); + expect(nodeWNW4.directions(graph, projection)).to.eql([292], 'cardinal WEstnorTHWest'); + + expect(nodeNW1.directions(graph, projection)).to.eql([315], 'cardinal nw'); + expect(nodeNW2.directions(graph, projection)).to.eql([315], 'cardinal nW'); + expect(nodeNW3.directions(graph, projection)).to.eql([315], 'cardinal northwest'); + expect(nodeNW4.directions(graph, projection)).to.eql([315], 'cardinal norTHWest'); + + expect(nodeNNW1.directions(graph, projection)).to.eql([337], 'cardinal nnw'); + expect(nodeNNW2.directions(graph, projection)).to.eql([337], 'cardinal NnW'); + expect(nodeNNW3.directions(graph, projection)).to.eql([337], 'cardinal northnorthwest'); + expect(nodeNNW4.directions(graph, projection)).to.eql([337], 'cardinal NOrthnorTHWest'); + }); + + it('supports direction=forward', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'direction': 'forward' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.eql([270]); + }); + + it('supports direction=backward', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'direction': 'backward' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.eql([90]); + }); + + it('supports direction=both', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'direction': 'both' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.have.members([90, 270]); + }); + + it('supports direction=all', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'direction': 'all' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.have.members([90, 270]); + }); + + it('supports traffic_signals:direction=forward', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'traffic_signals:direction': 'forward' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.eql([270]); + }); + + it('supports traffic_signals:direction=backward', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'traffic_signals:direction': 'backward' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.eql([90]); + }); + + it('supports traffic_signals:direction=both', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'traffic_signals:direction': 'both' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.have.members([90, 270]); + }); + + it('supports traffic_signals:direction=all', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'traffic_signals:direction': 'all' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.have.members([90, 270]); + }); + + it('supports railway:signal:direction=forward', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'railway:signal:direction': 'forward' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.eql([270]); + }); + + it('supports railway:signal:direction=backward', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'railway:signal:direction': 'backward' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.eql([90]); + }); + + it('supports railway:signal:direction=both', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'railway:signal:direction': 'both' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.have.members([90, 270]); + }); + + it('supports railway:signal:direction=all', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'railway:signal:direction': 'all' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.have.members([90, 270]); + }); + + it('supports camera:direction=forward', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'camera:direction': 'forward' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.eql([270]); + }); + + it('supports camera:direction=backward', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'camera:direction': 'backward' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.eql([90]); + }); + + it('supports camera:direction=both', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'camera:direction': 'both' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.have.members([90, 270]); + }); + + it('supports camera:direction=all', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'camera:direction': 'all' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.have.members([90, 270]); + }); + + it('returns directions for an all-way stop at a highway interstction', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'highway': 'stop', 'stop': 'all' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var node4 = iD.osmNode({ id: 'n4', loc: [0, -1] }); + var node5 = iD.osmNode({ id: 'n5', loc: [0, 1] }); + var way1 = iD.osmWay({ id: 'w1', nodes: ['n1','n2','n3'], tags: { 'highway': 'residential' } }); + var way2 = iD.osmWay({ id: 'w2', nodes: ['n4','n2','n5'], tags: { 'highway': 'residential' } }); + var graph = iD.coreGraph([node1, node2, node3, node4, node5, way1, way2]); + expect(node2.directions(graph, projection)).to.have.members([0, 90, 180, 270]); + }); + + it('does not return directions for an all-way stop not at a highway interstction', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0], tags: { 'highway': 'stop', 'stop': 'all' } }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0] }); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0], tags: { 'highway': 'stop', 'stop': 'all' } }); + var node4 = iD.osmNode({ id: 'n4', loc: [0, -1], tags: { 'highway': 'stop', 'stop': 'all' } }); + var node5 = iD.osmNode({ id: 'n5', loc: [0, 1], tags: { 'highway': 'stop', 'stop': 'all' } }); + var way1 = iD.osmWay({ id: 'w1', nodes: ['n1','n2','n3'], tags: { 'highway': 'residential' } }); + var way2 = iD.osmWay({ id: 'w2', nodes: ['n4','n2','n5'], tags: { 'highway': 'residential' } }); + var graph = iD.coreGraph([node1, node2, node3, node4, node5, way1, way2]); + expect(node2.directions(graph, projection)).to.eql([]); + }); + + }); + describe('#asJXON', function () { it('converts a node to jxon', function() { - var node = iD.Node({id: 'n-1', loc: [-77, 38], tags: {amenity: 'cafe'}}); + var node = iD.osmNode({id: 'n-1', loc: [-77, 38], tags: {amenity: 'cafe'}}); expect(node.asJXON()).to.eql({node: { '@id': '-1', '@lon': -77, @@ -192,13 +579,13 @@ describe('iD.osmNode', function () { }); it('includes changeset if provided', function() { - expect(iD.Node({loc: [0, 0]}).asJXON('1234').node['@changeset']).to.equal('1234'); + expect(iD.osmNode({loc: [0, 0]}).asJXON('1234').node['@changeset']).to.equal('1234'); }); }); describe('#asGeoJSON', function () { it('converts to a GeoJSON Point geometry', function () { - var node = iD.Node({tags: {amenity: 'cafe'}, loc: [1, 2]}), + var node = iD.osmNode({tags: {amenity: 'cafe'}, loc: [1, 2]}), json = node.asGeoJSON(); expect(json.type).to.equal('Point'); From 2edbcc4b82bf95bdd124dcfb5deac5c9f7abeb5c Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 11 Dec 2017 15:17:17 -0500 Subject: [PATCH 12/70] Match any `*:direction` key, rather than hardcoding a list --- modules/actions/reverse.js | 3 ++- modules/osm/node.js | 24 +++++++++++++++--------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/modules/actions/reverse.js b/modules/actions/reverse.js index 051f3e64b..3f92fcd91 100644 --- a/modules/actions/reverse.js +++ b/modules/actions/reverse.js @@ -87,7 +87,8 @@ export function actionReverse(wayId, options) { // Update the direction based tags as appropriate then return an updated node return node.update({tags: _transform(node.tags, function(acc, tagValue, tagKey) { // See if this is a direction tag and reverse (or use existing value if not recognised) - if (tagKey.match(/direction$/) !== null) { + var re = /direction$/; + if (re.test(tagKey)) { acc[tagKey] = {forward: 'backward', backward: 'forward', left: 'right', right: 'left'}[tagValue] || tagValue; } else { // Use the reverseKey method to cater for situations such as traffic_sign:forward=stop diff --git a/modules/osm/node.js b/modules/osm/node.js index 362b4448c..5083caada 100644 --- a/modules/osm/node.js +++ b/modules/osm/node.js @@ -52,19 +52,25 @@ _extend(osmNode.prototype, { // Inspect tags and geometry to determine which direction(s) this node/vertex points directions: function(resolver, projection) { var val; + var i; + // which tag to use? if (this.isHighwayIntersection(resolver) && (this.tags.stop || '').toLowerCase() === 'all') { // all-way stop tag on a highway intersection val = 'all'; } else { - // direction tag - val = ( - this.tags['camera:direction'] || - this.tags['railway:signal:direction'] || - this.tags['traffic_signals:direction'] || - this.tags.direction || - '' - ).toLowerCase(); + // generic direction tag + val = (this.tags.direction || '').toLowerCase(); + + // better suffix-style direction tag + var re = /:direction$/i; + var keys = Object.keys(this.tags); + for (i = 0; i < keys.length; i++) { + if (re.test(keys[i])) { + val = this.tags[keys[i]].toLowerCase(); + break; + } + } } // swap cardinal for numeric directions @@ -105,7 +111,7 @@ _extend(osmNode.prototype, { var nodeIds = {}; resolver.parentWays(this).forEach(function(parent) { var nodes = parent.nodes; - for (var i = 0; i < nodes.length; i++) { + for (i = 0; i < nodes.length; i++) { if (nodes[i] === this.id) { // match current entity if (lookForward && i > 0) { nodeIds[nodes[i - 1]] = true; // look back to prev node From 4b5260a5abdafb9f2cd47897fa28a8b78a7146ff Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 11 Dec 2017 15:48:08 -0500 Subject: [PATCH 13/70] Add wireframe viewfield marker (styling marker fill in svg is not currently supported) --- modules/svg/defs.js | 18 ++++++++++++++++++ modules/svg/vertices.js | 3 ++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/modules/svg/defs.js b/modules/svg/defs.js index 6f4353769..65f37a7ba 100644 --- a/modules/svg/defs.js +++ b/modules/svg/defs.js @@ -63,6 +63,24 @@ export function svgDefs(context) { .attr('stroke-width', '0.5px') .attr('stroke-opacity', '0.75'); + defs + .append('marker') + .attr('id', 'viewfield-marker-wireframe') + .attr('viewBox', '0 0 16 16') + .attr('refX', 8) + .attr('refY', 16) + .attr('markerWidth', 4) + .attr('markerHeight', 4) + .attr('markerUnits', 'strokeWidth') + .attr('orient', 'auto') + .append('path') + .attr('class', 'viewfield') + .attr('d', 'M 6,14 C 8,13.4 8,13.4 10,14 L 16,3 C 12,0 4,0 0,3 z') + .attr('fill', 'none') + .attr('stroke', '#fff') + .attr('stroke-width', '0.5px') + .attr('stroke-opacity', '0.75'); + // patterns var patterns = defs.selectAll('pattern') .data([ diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index e7fa7a776..c0425154c 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -59,6 +59,7 @@ export function svgVertices(projection, context) { siblings = siblings || {}; var icons = {}; var directions = {}; + var wireframe = context.surface().classed('fill-wireframe'); var z = (zoom < 17 ? 0 : zoom < 18 ? 1 : 2); @@ -197,8 +198,8 @@ export function svgVertices(projection, context) { .append('path') .attr('class', 'viewfield') .attr('d', 'M0,0H0') - .attr('marker-start', 'url(#viewfield-marker)') .merge(viewfields) + .attr('marker-start', 'url(#viewfield-marker' + (wireframe ? '-wireframe' : '') + ')') .attr('transform', function(d) { return 'rotate(' + d + ')'; }); } From a5bbc21728270ce51e5fe386a6e0db46a8fcb423 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 11 Dec 2017 17:34:01 -0500 Subject: [PATCH 14/70] Remove unnecessary zoom parameter --- modules/renderer/map.js | 8 ++--- modules/svg/vertices.js | 56 +++++++++++++++++-------------- modules/ui/fields/restrictions.js | 2 +- test/spec/svg/vertices.js | 27 ++++++++------- 4 files changed, 50 insertions(+), 43 deletions(-) diff --git a/modules/renderer/map.js b/modules/renderer/map.js index e53297ff4..1538f5a22 100644 --- a/modules/renderer/map.js +++ b/modules/renderer/map.js @@ -183,7 +183,7 @@ export function rendererMap(context) { if (map.editable() && !transformed) { var hover = d3_event.target.__data__; surface.selectAll('.data-layer-osm') - .call(drawVertices.drawHover, context.graph(), hover, map.extent(), map.zoom()); + .call(drawVertices.drawHover, context.graph(), hover, map.extent()); dispatch.call('drawn', this, {full: false}); } }) @@ -191,7 +191,7 @@ export function rendererMap(context) { if (map.editable() && !transformed) { var hover = d3_event.relatedTarget && d3_event.relatedTarget.__data__; surface.selectAll('.data-layer-osm') - .call(drawVertices.drawHover, context.graph(), hover, map.extent(), map.zoom()); + .call(drawVertices.drawHover, context.graph(), hover, map.extent()); dispatch.call('drawn', this, {full: false}); } }); @@ -207,7 +207,7 @@ export function rendererMap(context) { all = context.features().filter(all, graph); surface.selectAll('.data-layer-osm') - .call(drawVertices, graph, all, filter, map.extent(), map.zoom()) + .call(drawVertices, graph, all, filter, map.extent()) .call(drawMidpoints, graph, all, filter, map.trimmedExtent()); dispatch.call('drawn', this, {full: false}); } @@ -297,7 +297,7 @@ export function rendererMap(context) { data = features.filter(data, graph); surface.selectAll('.data-layer-osm') - .call(drawVertices, graph, data, filter, map.extent(), map.zoom()) + .call(drawVertices, graph, data, filter, map.extent()) .call(drawLines, graph, data, filter) .call(drawAreas, graph, data, filter) .call(drawMidpoints, graph, data, filter, map.trimmedExtent()) diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index c0425154c..a26f9c08f 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -1,10 +1,16 @@ import _values from 'lodash-es/values'; +import { select as d3_select } from 'd3-selection'; + import { dataFeatureIcons } from '../../data'; import { osmEntity } from '../osm'; import { svgPointTransform } from './index'; +var TAU = 2 * Math.PI; +function ktoz(k) { return Math.log(k * TAU) / Math.LN2 - 8; } + + export function svgVertices(projection, context) { var radiuses = { // z16-, z17, z18+, tagged @@ -55,11 +61,12 @@ export function svgVertices(projection, context) { } - function draw(selection, vertices, klass, graph, zoom, siblings) { + function draw(selection, vertices, klass, graph, siblings) { siblings = siblings || {}; var icons = {}; var directions = {}; var wireframe = context.surface().classed('fill-wireframe'); + var zoom = ktoz(projection.scale()); var z = (zoom < 17 ? 0 : zoom < 18 ? 1 : 2); @@ -95,23 +102,20 @@ export function svgVertices(projection, context) { var rads = radiuses[klass]; selection.selectAll('.' + klass) .each(function(entity) { - var i = z && getIcon(entity), - c = i ? 0.5 : 0, - r = rads[i ? 3 : z]; + var i = z && getIcon(entity); + var c = i ? 0.5 : 0; + var r = rads[i ? 3 : z]; // slightly increase the size of unconnected endpoints #3775 if (entity.isEndpoint(graph) && !entity.isConnected(graph)) { r += 1.5; } - this.setAttribute('cx', c); - this.setAttribute('cy', -c); - this.setAttribute('r', r); - if (i && klass === 'fill') { - this.setAttribute('visibility', 'hidden'); - } else { - this.removeAttribute('visibility'); - } + d3_select(this) + .attr('cx', c) + .attr('cy', -c) + .attr('r', r) + .attr('visibility', ((i && klass === 'fill') ? 'hidden' : null)); }); }); @@ -148,8 +152,8 @@ export function svgVertices(projection, context) { .append('use') .attr('transform', 'translate(-5, -6)') .attr('xlink:href', function(d) { - var picon = getIcon(d), - isMaki = dataFeatureIcons.indexOf(picon) !== -1; + var picon = getIcon(d); + var isMaki = dataFeatureIcons.indexOf(picon) !== -1; return '#' + picon + (isMaki ? '-11' : ''); }) .attr('width', '11px') @@ -204,14 +208,14 @@ export function svgVertices(projection, context) { } - function drawVertices(selection, graph, entities, filter, extent, zoom) { - var siblings = siblingAndChildVertices(context.selectedIDs(), graph, extent), - wireframe = context.surface().classed('fill-wireframe'), - vertices = []; + function drawVertices(selection, graph, entities, filter, extent) { + var siblings = siblingAndChildVertices(context.selectedIDs(), graph, extent); + var wireframe = context.surface().classed('fill-wireframe'); + var vertices = []; for (var i = 0; i < entities.length; i++) { - var entity = entities[i], - geometry = entity.geometry(graph); + var entity = entities[i]; + var geometry = entity.geometry(graph); if ((geometry === 'point') && (wireframe || entity.directions(graph, projection).length)) { vertices.push(entity); @@ -232,25 +236,25 @@ export function svgVertices(projection, context) { var layer = selection.selectAll('.layer-hit'); layer.selectAll('g.vertex.vertex-persistent') .filter(filter) - .call(draw, vertices, 'vertex-persistent', graph, zoom, siblings); + .call(draw, vertices, 'vertex-persistent', graph, siblings); - drawHover(selection, graph, extent, zoom); + drawHover(selection, graph, extent); } - function drawHover(selection, graph, extent, zoom) { + function drawHover(selection, graph, extent) { var hovered = _hover ? siblingAndChildVertices([_hover.id], graph, extent) : {}; var layer = selection.selectAll('.layer-hit'); layer.selectAll('g.vertex.vertex-hover') - .call(draw, _values(hovered), 'vertex-hover', graph, zoom); + .call(draw, _values(hovered), 'vertex-hover', graph); } - drawVertices.drawHover = function(selection, graph, target, extent, zoom) { + drawVertices.drawHover = function(selection, graph, target, extent) { if (target === _hover) return; _hover = target; - drawHover(selection, graph, extent, zoom); + drawHover(selection, graph, extent); }; return drawVertices; diff --git a/modules/ui/fields/restrictions.js b/modules/ui/fields/restrictions.js index f70b47f48..579e17aa5 100644 --- a/modules/ui/fields/restrictions.js +++ b/modules/ui/fields/restrictions.js @@ -115,7 +115,7 @@ export function uiFieldRestrictions(field, context) { surface .call(utilSetDimensions, d) - .call(drawVertices, graph, [vertex], filter, extent, z) + .call(drawVertices, graph, [vertex], filter, extent) .call(drawLines, graph, intersection.ways, filter) .call(drawTurns, graph, intersection.turns(fromNodeID)); diff --git a/test/spec/svg/vertices.js b/test/spec/svg/vertices.js index f539db5fa..2c5104d22 100644 --- a/test/spec/svg/vertices.js +++ b/test/spec/svg/vertices.js @@ -1,9 +1,14 @@ describe('iD.svgVertices', function () { - var context, surface, - projection = d3.geoProjection(function(x, y) { return [x, -y]; }) - .translate([0, 0]) - .scale(180 / Math.PI) - .clipExtent([[0, 0], [Infinity, Infinity]]); + var TAU = 2 * Math.PI; + function ztok(z) { return 256 * Math.pow(2, z) / TAU; } + + var context; + var surface; + var projection = d3.geoProjection(function(x, y) { return [x, -y]; }) + .translate([0, 0]) + .scale(ztok(17)) + .clipExtent([[0, 0], [Infinity, Infinity]]); + beforeEach(function () { context = iD.Context(); @@ -15,14 +20,12 @@ describe('iD.svgVertices', function () { it('adds the .shared class to vertices that are members of two or more ways', function () { - var zoom = 17, - node = iD.Node({loc: [0, 0]}), - way1 = iD.Way({nodes: [node.id], tags: {highway: 'residential'}}), - way2 = iD.Way({nodes: [node.id], tags: {highway: 'residential'}}), - graph = iD.Graph([node, way1, way2]); - - surface.call(iD.svgVertices(projection, context), graph, [node], zoom); + var node = iD.Node({loc: [0, 0]}); + var way1 = iD.Way({nodes: [node.id], tags: {highway: 'residential'}}); + var way2 = iD.Way({nodes: [node.id], tags: {highway: 'residential'}}); + var graph = iD.Graph([node, way1, way2]); + surface.call(iD.svgVertices(projection, context), graph, [node]); expect(surface.select('.vertex').classed('shared')).to.be.true; }); }); From b394cb6dfa10edb27d7c2b75d9ddce985d7a8c2e Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 11 Dec 2017 17:46:15 -0500 Subject: [PATCH 15/70] Variable cleanup, elminiate lodash _filter --- modules/svg/points.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/modules/svg/points.js b/modules/svg/points.js index 054a7359e..24c894174 100644 --- a/modules/svg/points.js +++ b/modules/svg/points.js @@ -1,5 +1,3 @@ -import _filter from 'lodash-es/filter'; - import { dataFeatureIcons } from '../../data'; import { osmEntity } from '../osm'; import { svgPointTransform, svgTagClasses } from './index'; @@ -20,10 +18,10 @@ export function svgPoints(projection, context) { return function drawPoints(selection, graph, entities, filter) { - var wireframe = context.surface().classed('fill-wireframe'), - points = wireframe ? [] : _filter(entities, function(e) { - return e.geometry(graph) === 'point' && !e.directions(graph, projection).length; - }); + var wireframe = context.surface().classed('fill-wireframe'); + var points = wireframe ? [] : entities.filter(function(e) { + return e.geometry(graph) === 'point' && !e.directions(graph, projection).length; + }); points.sort(sortY); @@ -75,8 +73,8 @@ export function svgPoints(projection, context) { groups.select('.stroke'); groups.select('.icon') .attr('xlink:href', function(entity) { - var preset = context.presets().match(entity, graph), - picon = preset && preset.icon; + var preset = context.presets().match(entity, graph); + var picon = preset && preset.icon; if (!picon) return ''; From 899abc7ef524465bfd29553acc3cfc4b7a4ca94c Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 12 Dec 2017 17:39:36 -0500 Subject: [PATCH 16/70] WIP to render vertices while dragging (re: #3003 / #4602) For now, drawHover is commented out. Still not sure what I will do with it. This means that things flicker a bit when dragging, also connecting nodes (and closing lines) does not currently work. There was lot going on preventing the vertices from rendering while dragging. 1. `modeDragNode` needed a proper `selectedIDs()` function that works like other modes.. Many other places in iD (including the vertex renderer) call `context.selectedIDs()`.. This means that `modeDragNode` needs a new function `restoreSelectedIDs()` to do what `selectedIDs()` was previously doing (a place to store selectedIDs so that we can reselect those entities after the user is done dragging a node in select mode) 2. Just so many things in svg/vertices.js - siblingAndChildVertices was missing some things for points that we render as vertices (points in wireframe, points with directions) - the sibling vertices weren't being included in the `filter` function, so would disappear when doing differenced/extent redraws - probably some other things --- css/20_map.css | 6 +- css/70_fills.css | 4 +- modules/modes/drag_node.js | 88 ++++++++++--------- modules/modes/select.js | 2 +- modules/renderer/map.js | 49 +++++------ modules/svg/vertices.js | 172 ++++++++++++++++++++----------------- 6 files changed, 173 insertions(+), 148 deletions(-) diff --git a/css/20_map.css b/css/20_map.css index 6d3a39037..a29a1c85a 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -88,7 +88,7 @@ g.midpoint .shadow { fill-opacity: 0; } -g.vertex.vertex-hover { +/*g.vertex.vertex-hover { display: none; } @@ -109,7 +109,7 @@ g.vertex.vertex-hover { .mode-drag-node .hover-disabled g.vertex.vertex-hover { display: none; } - +*/ g.vertex.related:not(.selected) .shadow, g.vertex.hover:not(.selected) .shadow, g.midpoint.related:not(.selected) .shadow, @@ -126,7 +126,7 @@ g.vertex.selected .shadow { .mode-add-area g.midpoint, .mode-add-line g.midpoint, .mode-add-point g.midpoint { - display: none; + display: none; } /* lines */ diff --git a/css/70_fills.css b/css/70_fills.css index 9402eec5d..c0cb1b167 100644 --- a/css/70_fills.css +++ b/css/70_fills.css @@ -38,12 +38,12 @@ /* Modes */ -.mode-draw-line .vertex.active, +/*.mode-draw-line .vertex.active, .mode-draw-area .vertex.active, .mode-drag-node .vertex.active { display: none; } - +*/ .mode-draw-line .way.active, .mode-draw-area .way.active, .mode-drag-node .active { diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index baed024ae..50cd8231b 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -1,5 +1,3 @@ -import _map from 'lodash-es/map'; - import { event as d3_event, select as d3_select @@ -36,15 +34,16 @@ export function modeDragNode(context) { id: 'drag-node', button: 'browse' }; + var hover = behaviorHover(context).altDisables(true).on('hover', context.ui().sidebar.hover); + var edit = behaviorEdit(context); - var nudgeInterval, - activeIDs, - wasMidpoint, - isCancelled, - lastLoc, - selectedIDs = [], - hover = behaviorHover(context).altDisables(true).on('hover', context.ui().sidebar.hover), - edit = behaviorEdit(context); + var _nudgeInterval; + var _restoreSelectedIDs = []; + var _activeIDs = []; + var _wasMidpoint = false; + var _isCancelled = false; + var _dragEntity; + var _lastLoc; function vecSub(a, b) { @@ -52,9 +51,9 @@ export function modeDragNode(context) { } function edge(point, size) { - var pad = [80, 20, 50, 20], // top, right, bottom, left - x = 0, - y = 0; + var pad = [80, 20, 50, 20]; // top, right, bottom, left + var x = 0; + var y = 0; if (point[0] > size[0] - pad[1]) x = -10; @@ -74,8 +73,8 @@ export function modeDragNode(context) { function startNudge(entity, nudge) { - if (nudgeInterval) window.clearInterval(nudgeInterval); - nudgeInterval = window.setInterval(function() { + if (_nudgeInterval) window.clearInterval(_nudgeInterval); + _nudgeInterval = window.setInterval(function() { context.pan(nudge); doMove(entity, nudge); }, 50); @@ -83,9 +82,9 @@ export function modeDragNode(context) { function stopNudge() { - if (nudgeInterval) { - window.clearInterval(nudgeInterval); - nudgeInterval = null; + if (_nudgeInterval) { + window.clearInterval(_nudgeInterval); + _nudgeInterval = null; } } @@ -106,19 +105,19 @@ export function modeDragNode(context) { function start(entity) { - wasMidpoint = entity.type === 'midpoint'; + _wasMidpoint = entity.type === 'midpoint'; var hasHidden = context.features().hasHiddenConnections(entity, context.graph()); - isCancelled = d3_event.sourceEvent.shiftKey || hasHidden; + _isCancelled = d3_event.sourceEvent.shiftKey || hasHidden; - if (isCancelled) { + if (_isCancelled) { if (hasHidden) { uiFlash().text(t('modes.drag_node.connected_to_hidden'))(); } return behavior.cancel(); } - if (wasMidpoint) { + if (_wasMidpoint) { var midpoint = entity; entity = osmNode(); context.perform(actionAddMidpoint(midpoint, entity)); @@ -130,10 +129,13 @@ export function modeDragNode(context) { context.perform(actionNoop()); } + _dragEntity = entity; + // activeIDs generate no pointer events. This prevents the node or vertex // being dragged from trying to connect to itself or its parent element. - activeIDs = _map(context.graph().parentWays(entity), 'id'); - activeIDs.push(entity.id); + _activeIDs = context.graph().parentWays(entity) + .map(function(parent) { return parent.id; }); + _activeIDs.push(entity.id); setActiveElements(); context.enter(mode); @@ -153,12 +155,12 @@ export function modeDragNode(context) { function doMove(entity, nudge) { nudge = nudge || [0, 0]; - var currPoint = (d3_event && d3_event.point) || context.projection(lastLoc), - currMouse = vecSub(currPoint, nudge), - loc = context.projection.invert(currMouse), - d = datum(); + var currPoint = (d3_event && d3_event.point) || context.projection(_lastLoc); + var currMouse = vecSub(currPoint, nudge); + var loc = context.projection.invert(currMouse); + var d = datum(); - if (!nudgeInterval) { + if (!_nudgeInterval) { if (d.type === 'node' && d.id !== entity.id) { loc = d.loc; } else if (d.type === 'way' && !d3_select(d3_event.sourceEvent.target).classed('fill')) { @@ -171,14 +173,15 @@ export function modeDragNode(context) { moveAnnotation(entity) ); - lastLoc = loc; + _lastLoc = loc; } function move(entity) { - if (isCancelled) return; + if (_isCancelled) return; + d3_event.sourceEvent.stopPropagation(); - lastLoc = context.projection.invert(d3_event.point); + _lastLoc = context.projection.invert(d3_event.point); doMove(entity); var nudge = edge(d3_event.point, context.map().dimensions()); @@ -191,7 +194,7 @@ export function modeDragNode(context) { function end(entity) { - if (isCancelled) return; + if (_isCancelled) return; var d = datum(); @@ -208,7 +211,7 @@ export function modeDragNode(context) { connectAnnotation(d) ); - } else if (wasMidpoint) { + } else if (_wasMidpoint) { context.replace( actionNoop(), t('operations.add.annotation.vertex') @@ -221,7 +224,7 @@ export function modeDragNode(context) { ); } - var reselection = selectedIDs.filter(function(id) { + var reselection = _restoreSelectedIDs.filter(function(id) { return context.graph().hasEntity(id); }); @@ -240,7 +243,7 @@ export function modeDragNode(context) { function setActiveElements() { - context.surface().selectAll(utilEntitySelector(activeIDs)) + context.surface().selectAll(utilEntitySelector(_activeIDs)) .classed('active', true); } @@ -287,9 +290,16 @@ export function modeDragNode(context) { }; - mode.selectedIDs = function(_) { - if (!arguments.length) return selectedIDs; - selectedIDs = _; + mode.selectedIDs = function() { + if (!arguments.length) return _dragEntity ? [_dragEntity.id] : []; + // no assign + return mode; + }; + + + mode.restoreSelectedIDs = function(_) { + if (!arguments.length) return _restoreSelectedIDs; + _restoreSelectedIDs = _; return mode; }; diff --git a/modules/modes/select.js b/modules/modes/select.js index 281e77709..fa5a3ddd9 100644 --- a/modules/modes/select.js +++ b/modules/modes/select.js @@ -63,7 +63,7 @@ export function modeSelect(context, selectedIDs) { behaviorHover(context), behaviorSelect(context), behaviorLasso(context), - modeDragNode(context).selectedIDs(selectedIDs).behavior + modeDragNode(context).restoreSelectedIDs(selectedIDs).behavior ], inspector, editMenu, diff --git a/modules/renderer/map.js b/modules/renderer/map.js index 1538f5a22..0240844df 100644 --- a/modules/renderer/map.js +++ b/modules/renderer/map.js @@ -178,38 +178,38 @@ export function rendererMap(context) { }) .on('mousemove.map', function() { mousemove = d3_event; - }) - .on('mouseover.vertices', function() { - if (map.editable() && !transformed) { - var hover = d3_event.target.__data__; - surface.selectAll('.data-layer-osm') - .call(drawVertices.drawHover, context.graph(), hover, map.extent()); - dispatch.call('drawn', this, {full: false}); - } - }) - .on('mouseout.vertices', function() { - if (map.editable() && !transformed) { - var hover = d3_event.relatedTarget && d3_event.relatedTarget.__data__; - surface.selectAll('.data-layer-osm') - .call(drawVertices.drawHover, context.graph(), hover, map.extent()); - dispatch.call('drawn', this, {full: false}); - } }); + // .on('mouseover.vertices', function() { + // if (map.editable() && !transformed) { + // var hover = d3_event.target.__data__; + // surface.selectAll('.data-layer-osm') + // .call(drawVertices.drawHover, context.graph(), hover, map.extent()); + // dispatch.call('drawn', this, { full: false }); + // } + // }) + // .on('mouseout.vertices', function() { + // if (map.editable() && !transformed) { + // var hover = d3_event.relatedTarget && d3_event.relatedTarget.__data__; + // surface.selectAll('.data-layer-osm') + // .call(drawVertices.drawHover, context.graph(), hover, map.extent()); + // dispatch.call('drawn', this, { full: false }); + // } + // }); supersurface .call(context.background()); context.on('enter.map', function() { if (map.editable() && !transformed) { - var all = context.intersects(map.extent()), - filter = utilFunctor(true), - graph = context.graph(); + var all = context.intersects(map.extent()); + var filter = utilFunctor(true); + var graph = context.graph(); all = context.features().filter(all, graph); surface.selectAll('.data-layer-osm') .call(drawVertices, graph, all, filter, map.extent()) .call(drawMidpoints, graph, all, filter, map.trimmedExtent()); - dispatch.call('drawn', this, {full: false}); + dispatch.call('drawn', this, { full: false }); } }); @@ -265,10 +265,11 @@ export function rendererMap(context) { function drawVector(difference, extent) { - var graph = context.graph(), - features = context.features(), - all = context.intersects(map.extent()), - data, filter; + var graph = context.graph(); + var features = context.features(); + var all = context.intersects(map.extent()); + var data; + var filter; if (difference) { var complete = difference.complete(map.extent()); diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index a26f9c08f..ad0c41786 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -1,3 +1,4 @@ +import _clone from 'lodash-es/clone'; import _values from 'lodash-es/values'; import { select as d3_select } from 'd3-selection'; @@ -13,55 +14,16 @@ function ktoz(k) { return Math.log(k * TAU) / Math.LN2 - 8; } export function svgVertices(projection, context) { var radiuses = { - // z16-, z17, z18+, tagged - shadow: [6, 7.5, 7.5, 11.5], - stroke: [2.5, 3.5, 3.5, 7], - fill: [1, 1.5, 1.5, 1.5] + // z16-, z17, z18+, tagged + shadow: [6, 7.5, 7.5, 11.5], + stroke: [2.5, 3.5, 3.5, 7], + fill: [1, 1.5, 1.5, 1.5] }; var _hover; - function siblingAndChildVertices(ids, graph, extent) { - var vertices = {}; - - function addChildVertices(entity) { - if (!context.features().isHiddenFeature(entity, graph, entity.geometry(graph))) { - var i; - if (entity.type === 'way') { - for (i = 0; i < entity.nodes.length; i++) { - addChildVertices(graph.entity(entity.nodes[i])); - } - } else if (entity.type === 'relation') { - for (i = 0; i < entity.members.length; i++) { - var member = context.hasEntity(entity.members[i].id); - if (member) { - addChildVertices(member); - } - } - } else if (entity.intersects(extent, graph)) { - vertices[entity.id] = entity; - } - } - } - - ids.forEach(function(id) { - var entity = context.hasEntity(id); - if (entity && entity.type === 'node') { - vertices[entity.id] = entity; - context.graph().parentWays(entity).forEach(function(entity) { - addChildVertices(entity); - }); - } else if (entity) { - addChildVertices(entity); - } - }); - - return vertices; - } - - - function draw(selection, vertices, klass, graph, siblings) { + function draw(selection, vertices, klass, graph, siblings, filter) { siblings = siblings || {}; var icons = {}; var directions = {}; @@ -127,7 +89,8 @@ export function svgVertices(projection, context) { } - var groups = selection + var groups = selection.selectAll('.vertex.' + klass) + .filter(filter) .data(vertices, osmEntity.key); // exit @@ -178,7 +141,7 @@ export function svgVertices(projection, context) { // Directional vertices get viewfields var dgroups = groups.filter(function(d) { return getDirections(d); }) .selectAll('.viewfieldgroup') - .data(function(d) { return klass === 'vertex-hover' ? [] : [d]; }, osmEntity.key); + .data(function(d) { return /*klass === 'vertex-hover' ? [] : */[d]; }, osmEntity.key); // exit dgroups.exit() @@ -209,53 +172,104 @@ export function svgVertices(projection, context) { function drawVertices(selection, graph, entities, filter, extent) { - var siblings = siblingAndChildVertices(context.selectedIDs(), graph, extent); var wireframe = context.surface().classed('fill-wireframe'); - var vertices = []; + var siblings = {}; + getSiblingAndChildVertices(context.selectedIDs(), graph, extent); + + // always render selected and sibling vertices.. + var vertices = _clone(siblings); + var filterWithSiblings = function(d) { return d.id in siblings || filter(d); }; + + // also render important vertices from the `entities` list.. for (var i = 0; i < entities.length; i++) { var entity = entities[i]; var geometry = entity.geometry(graph); - if ((geometry === 'point') && (wireframe || entity.directions(graph, projection).length)) { - vertices.push(entity); - continue; - } + if ((geometry === 'point') && renderAsVertex(entity)) { + vertices[entity.id] = entity; - if (geometry !== 'vertex') - continue; - - if (entity.id in siblings || - entity.hasInterestingTags() || - entity.isEndpoint(graph) || - entity.isConnected(graph)) { - vertices.push(entity); + } else if ((geometry === 'vertex') && + (entity.hasInterestingTags() || entity.isEndpoint(graph) || entity.isConnected(graph)) ) { + vertices[entity.id] = entity; } } - var layer = selection.selectAll('.layer-hit'); - layer.selectAll('g.vertex.vertex-persistent') - .filter(filter) - .call(draw, vertices, 'vertex-persistent', graph, siblings); - drawHover(selection, graph, extent); + selection.selectAll('.layer-hit') + .call(draw, _values(vertices), 'vertex-persistent', graph, siblings, filterWithSiblings); + +// drawHover(selection, graph, extent, true); + + + function renderAsVertex(entity) { + var geometry = entity.geometry(graph); + return (geometry === 'vertex') || + (geometry === 'point' && (wireframe || entity.directions(graph, projection).length)); + } + + + function getSiblingAndChildVertices(ids, graph, extent) { + + function addChildVertices(entity) { + var geometry = entity.geometry(graph); + if (!context.features().isHiddenFeature(entity, graph, geometry)) { + var i; + if (entity.type === 'way') { + for (i = 0; i < entity.nodes.length; i++) { + var child = context.hasEntity(entity.nodes[i]); + if (child) { + addChildVertices(child); + } + } + } else if (entity.type === 'relation') { + for (i = 0; i < entity.members.length; i++) { + var member = context.hasEntity(entity.members[i].id); + if (member) { + addChildVertices(member); + } + } + } else if (renderAsVertex(entity) && entity.intersects(extent, graph)) { + siblings[entity.id] = entity; + } + } + } + + ids.forEach(function(id) { + var entity = context.hasEntity(id); + if (!entity) return; + + if (entity.type === 'node') { + if (renderAsVertex(entity)) { + siblings[entity.id] = entity; + graph.parentWays(entity).forEach(function(entity) { + addChildVertices(entity); + }); + } + } else { // way, relation + addChildVertices(entity); + } + }); + + } } - function drawHover(selection, graph, extent) { - var hovered = _hover ? siblingAndChildVertices([_hover.id], graph, extent) : {}; - var layer = selection.selectAll('.layer-hit'); - - layer.selectAll('g.vertex.vertex-hover') - .call(draw, _values(hovered), 'vertex-hover', graph); - } - - - drawVertices.drawHover = function(selection, graph, target, extent) { - if (target === _hover) return; - _hover = target; - drawHover(selection, graph, extent); - }; +// function drawHover(selection, graph, extent, follow) { +// var hovered = _hover ? siblingAndChildVertices([_hover.id], graph, extent) : {}; +// var wireframe = context.surface().classed('fill-wireframe'); +// var layer = selection.selectAll('.layer-hit'); +// +// layer.selectAll('g.vertex.vertex-hover') +// .call(draw, _values(hovered), 'vertex-hover', graph, {}, false); +// } +// +// +// drawVertices.drawHover = function(selection, graph, target, extent) { +// if (target === _hover) return; +// _hover = target; +// drawHover(selection, graph, extent); +// }; return drawVertices; } From 789f1e5f6f380cd8eb5879e5a8b44ddb05860911 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 13 Dec 2017 08:57:43 -0500 Subject: [PATCH 17/70] Make sure points/vertices render the right things at different zooms --- modules/svg/points.js | 17 ++++++++++++++--- modules/svg/vertices.js | 14 ++++++++------ 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/modules/svg/points.js b/modules/svg/points.js index 24c894174..909533f3a 100644 --- a/modules/svg/points.js +++ b/modules/svg/points.js @@ -3,6 +3,10 @@ import { osmEntity } from '../osm'; import { svgPointTransform, svgTagClasses } from './index'; +var TAU = 2 * Math.PI; +function ktoz(k) { return Math.log(k * TAU) / Math.LN2 - 8; } + + export function svgPoints(projection, context) { function markerPath(selection, klass) { @@ -19,9 +23,16 @@ export function svgPoints(projection, context) { return function drawPoints(selection, graph, entities, filter) { var wireframe = context.surface().classed('fill-wireframe'); - var points = wireframe ? [] : entities.filter(function(e) { - return e.geometry(graph) === 'point' && !e.directions(graph, projection).length; - }); + var zoom = ktoz(projection.scale()); + + // points with a direction will render as vertices at higher zooms + function renderAsPoint(entity) { + return entity.geometry(graph) === 'point' && + !(zoom >= 18 && entity.directions(graph, projection).length); + } + + // all points will render as vertices in wireframe mode too + var points = wireframe ? [] : entities.filter(renderAsPoint); points.sort(sortY); diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index ad0c41786..7f2d5a49c 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -83,9 +83,6 @@ export function svgVertices(projection, context) { selection.selectAll('use') .attr('visibility', (z === 0 ? 'hidden' : null)); - - selection.selectAll('.viewfieldgroup') - .attr('visibility', (zoom < 18 ? 'hidden' : null)); } @@ -141,7 +138,7 @@ export function svgVertices(projection, context) { // Directional vertices get viewfields var dgroups = groups.filter(function(d) { return getDirections(d); }) .selectAll('.viewfieldgroup') - .data(function(d) { return /*klass === 'vertex-hover' ? [] : */[d]; }, osmEntity.key); + .data(function data(d) { return zoom < 18 ? [] : [d]; }, osmEntity.key); // exit dgroups.exit() @@ -173,6 +170,7 @@ export function svgVertices(projection, context) { function drawVertices(selection, graph, entities, filter, extent) { var wireframe = context.surface().classed('fill-wireframe'); + var zoom = ktoz(projection.scale()); var siblings = {}; getSiblingAndChildVertices(context.selectedIDs(), graph, extent); @@ -202,10 +200,14 @@ export function svgVertices(projection, context) { // drawHover(selection, graph, extent, true); + // Points can also render as vertices: + // 1. in wireframe mode or + // 2. at higher zooms if they have a direction function renderAsVertex(entity) { var geometry = entity.geometry(graph); - return (geometry === 'vertex') || - (geometry === 'point' && (wireframe || entity.directions(graph, projection).length)); + return geometry === 'vertex' || (geometry === 'point' && ( + wireframe || (zoom > 18 && entity.directions(graph, projection).length) + )); } From 5d5546d54dd5f07d87b219f7592e66e78a6ccdeb Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 13 Dec 2017 09:57:14 -0500 Subject: [PATCH 18/70] Minor code formatting --- modules/svg/labels.js | 178 +++++++++++++++++++++--------------------- 1 file changed, 89 insertions(+), 89 deletions(-) diff --git a/modules/svg/labels.js b/modules/svg/labels.js index a42022721..4c4cb70ce 100644 --- a/modules/svg/labels.js +++ b/modules/svg/labels.js @@ -28,13 +28,13 @@ import { export function svgLabels(projection, context) { - var path = d3_geoPath(projection), - detected = utilDetect(), - baselineHack = (detected.ie || detected.browser.toLowerCase() === 'edge'), - rdrawn = rbush(), - rskipped = rbush(), - textWidthCache = {}, - entitybboxes = {}; + var path = d3_geoPath(projection); + var detected = utilDetect(); + var baselineHack = (detected.ie || detected.browser.toLowerCase() === 'edge'); + var _rdrawn = rbush(); + var _rskipped = rbush(); + var _textWidthCache = {}; + var _entitybboxes = {}; // Listed from highest to lowest priority var labelStack = [ @@ -87,8 +87,8 @@ export function svgLabels(projection, context) { function textWidth(text, size, elem) { - var c = textWidthCache[size]; - if (!c) c = textWidthCache[size] = {}; + var c = _textWidthCache[size]; + if (!c) c = _textWidthCache[size] = {}; if (c[text]) { return c[text]; @@ -207,12 +207,12 @@ export function svgLabels(projection, context) { icons .attr('transform', get(labels, 'transform')) .attr('xlink:href', function(d) { - var preset = context.presets().match(d, context.graph()), - picon = preset && preset.icon; + var preset = context.presets().match(d, context.graph()); + var picon = preset && preset.icon; - if (!picon) + if (!picon) { return ''; - else { + } else { var isMaki = dataFeatureIcons.indexOf(picon) !== -1; return '#' + picon + (isMaki ? '-15' : ''); } @@ -221,12 +221,11 @@ export function svgLabels(projection, context) { function drawCollisionBoxes(selection, rtree, which) { - var showDebug = context.getDebug('collision'), - classes = 'debug ' + which + ' ' + - (which === 'debug-skipped' ? 'orange' : 'yellow'); + var showDebug = context.getDebug('collision'); + var classes = 'debug ' + which + ' ' + (which === 'debug-skipped' ? 'orange' : 'yellow'); var debug = selection.selectAll('.layer-label-debug') - .data(showDebug ? [true] : []); + .data(showDebug ? [true] : []); debug.exit() .remove(); @@ -266,26 +265,27 @@ export function svgLabels(projection, context) { function drawLabels(selection, graph, entities, filter, dimensions, fullRedraw) { var lowZoom = context.surface().classed('low-zoom'); + var labelable = [] + var i, j, k, entity, geometry; - var labelable = [], i, j, k, entity, geometry; for (i = 0; i < labelStack.length; i++) { labelable.push([]); } if (fullRedraw) { - rdrawn.clear(); - rskipped.clear(); - entitybboxes = {}; + _rdrawn.clear(); + _rskipped.clear(); + _entitybboxes = {}; } else { for (i = 0; i < entities.length; i++) { entity = entities[i]; var toRemove = [] - .concat(entitybboxes[entity.id] || []) - .concat(entitybboxes[entity.id + 'I'] || []); + .concat(_entitybboxes[entity.id] || []) + .concat(_entitybboxes[entity.id + 'I'] || []); for (j = 0; j < toRemove.length; j++) { - rdrawn.remove(toRemove[j]); - rskipped.remove(toRemove[j]); + _rdrawn.remove(toRemove[j]); + _rskipped.remove(toRemove[j]); } } } @@ -296,17 +296,17 @@ export function svgLabels(projection, context) { geometry = entity.geometry(graph); if (geometry === 'vertex') { geometry = 'point'; } // treat vertex like point - var preset = geometry === 'area' && context.presets().match(entity, graph), - icon = preset && !blacklisted(preset) && preset.icon; + var preset = geometry === 'area' && context.presets().match(entity, graph); + var icon = preset && !blacklisted(preset) && preset.icon; if (!icon && !utilDisplayName(entity)) continue; for (k = 0; k < labelStack.length; k++) { - var matchGeom = labelStack[k][0], - matchKey = labelStack[k][1], - matchVal = labelStack[k][2], - hasVal = entity.tags[matchKey]; + var matchGeom = labelStack[k][0]; + var matchKey = labelStack[k][1]; + var matchVal = labelStack[k][2]; + var hasVal = entity.tags[matchKey]; if (geometry === matchGeom && hasVal && (matchVal === '*' || matchVal === hasVal)) { labelable[k].push(entity); @@ -330,14 +330,15 @@ export function svgLabels(projection, context) { // Try and find a valid label for labellable entities for (k = 0; k < labelable.length; k++) { var fontSize = labelStack[k][3]; + for (i = 0; i < labelable[k].length; i++) { entity = labelable[k][i]; geometry = entity.geometry(graph); - var getName = (geometry === 'line') ? utilDisplayNameForPath : utilDisplayName, - name = getName(entity), - width = name && textWidth(name, fontSize), - p = null; + var getName = (geometry === 'line') ? utilDisplayNameForPath : utilDisplayName; + var name = getName(entity); + var width = name && textWidth(name, fontSize); + var p = null; if (geometry === 'point') { p = getPointLabel(entity, width, fontSize, geometry); @@ -361,24 +362,24 @@ export function svgLabels(projection, context) { function getPointLabel(entity, width, height, geometry) { - var y = (geometry === 'point' ? -12 : 0), - pointOffsets = { - ltr: [15, y, 'start'], - rtl: [-15, y, 'end'] - }; + var y = (geometry === 'point' ? -12 : 0); + var pointOffsets = { + ltr: [15, y, 'start'], + rtl: [-15, y, 'end'] + }; - var coord = projection(entity.loc), - margin = 2, - offset = pointOffsets[textDirection], - p = { - height: height, - width: width, - x: coord[0] + offset[0], - y: coord[1] + offset[1], - textAnchor: offset[2] - }, - bbox; + var coord = projection(entity.loc); + var margin = 2; + var offset = pointOffsets[textDirection]; + var p = { + height: height, + width: width, + x: coord[0] + offset[0], + y: coord[1] + offset[1], + textAnchor: offset[2] + }; + var bbox; if (textDirection === 'rtl') { bbox = { minX: p.x - width - margin, @@ -402,9 +403,9 @@ export function svgLabels(projection, context) { function getLineLabel(entity, width, height) { - var viewport = geoExtent(context.projection.clipExtent()).polygon(), - nodes = _map(graph.childNodes(entity), 'loc').map(projection), - length = geoPathLength(nodes); + var viewport = geoExtent(context.projection.clipExtent()).polygon(); + var nodes = _map(graph.childNodes(entity), 'loc').map(projection); + var length = geoPathLength(nodes); if (length < width + 20) return; @@ -414,9 +415,9 @@ export function svgLabels(projection, context) { var margin = 3; for (var i = 0; i < lineOffsets.length; i++) { - var offset = lineOffsets[i], - middle = offset / 100 * length, - start = middle - width / 2; + var offset = lineOffsets[i]; + var middle = offset / 100 * length; + var start = middle - width / 2; if (start < 0 || start + width > length) continue; @@ -431,8 +432,8 @@ export function svgLabels(projection, context) { sub = sub.reverse(); } - var bboxes = [], - boxsize = (height + 2) / 2; + var bboxes = []; + var boxsize = (height + 2) / 2; for (var j = 0; j < sub.length - 1; j++) { var a = sub[j]; @@ -474,12 +475,12 @@ export function svgLabels(projection, context) { } function subpath(nodes, from, to) { - var sofar = 0, - start, end, i0, i1; + var sofar = 0; + var start, end, i0, i1; for (var i = 0; i < nodes.length - 1; i++) { - var a = nodes[i], - b = nodes[i + 1]; + var a = nodes[i]; + var b = nodes[i + 1]; var current = geoEuclideanDistance(a, b); var portion; if (!start && sofar + current >= from) { @@ -510,17 +511,17 @@ export function svgLabels(projection, context) { function getAreaLabel(entity, width, height) { - var centroid = path.centroid(entity.asGeoJSON(graph, true)), - extent = entity.extent(graph), - areaWidth = projection(extent[1])[0] - projection(extent[0])[0]; + var centroid = path.centroid(entity.asGeoJSON(graph, true)); + var extent = entity.extent(graph); + var areaWidth = projection(extent[1])[0] - projection(extent[0])[0]; if (isNaN(centroid[0]) || areaWidth < 20) return; - var preset = context.presets().match(entity, context.graph()), - picon = preset && preset.icon, - iconSize = 17, - margin = 2, - p = {}; + var preset = context.presets().match(entity, context.graph()); + var picon = preset && preset.icon; + var iconSize = 17; + var margin = 2; + var p = {}; if (picon) { // icon and label.. if (addIcon()) { @@ -576,11 +577,10 @@ export function svgLabels(projection, context) { function tryInsert(bboxes, id, saveSkipped) { - var skipped = false, - bbox; + var skipped = false; for (var i = 0; i < bboxes.length; i++) { - bbox = bboxes[i]; + var bbox = bboxes[i]; bbox.id = id; // Check that label is visible @@ -588,28 +588,28 @@ export function svgLabels(projection, context) { skipped = true; break; } - if (rdrawn.collides(bbox)) { + if (_rdrawn.collides(bbox)) { skipped = true; break; } } - entitybboxes[id] = bboxes; + _entitybboxes[id] = bboxes; if (skipped) { if (saveSkipped) { - rskipped.load(bboxes); + _rskipped.load(bboxes); } } else { - rdrawn.load(bboxes); + _rdrawn.load(bboxes); } return !skipped; } - var label = selection.selectAll('.layer-label'), - halo = selection.selectAll('.layer-halo'); + var label = selection.selectAll('.layer-label'); + var halo = selection.selectAll('.layer-halo'); // points drawPointLabels(label, labelled.point, filter, 'pointlabel', positions.point); @@ -627,8 +627,8 @@ export function svgLabels(projection, context) { drawAreaIcons(halo, labelled.area, filter, 'areaicon-halo', positions.area); // debug - drawCollisionBoxes(label, rskipped, 'debug-skipped'); - drawCollisionBoxes(label, rdrawn, 'debug-drawn'); + drawCollisionBoxes(label, _rskipped, 'debug-skipped'); + drawCollisionBoxes(label, _rdrawn, 'debug-drawn'); selection.call(filterLabels); } @@ -641,17 +641,17 @@ export function svgLabels(projection, context) { layers.selectAll('.proximate') .classed('proximate', false); - var mouse = context.mouse(), - graph = context.graph(), - selectedIDs = context.selectedIDs(), - ids = [], - pad, bbox; + var mouse = context.mouse(); + var graph = context.graph(); + var selectedIDs = context.selectedIDs(); + var ids = []; + var pad, bbox; // hide labels near the mouse if (mouse) { pad = 20; bbox = { minX: mouse[0] - pad, minY: mouse[1] - pad, maxX: mouse[0] + pad, maxY: mouse[1] + pad }; - ids.push.apply(ids, _map(rdrawn.search(bbox), 'id')); + ids.push.apply(ids, _map(_rdrawn.search(bbox), 'id')); } // hide labels along selected ways, or near selected vertices @@ -666,7 +666,7 @@ export function svgLabels(projection, context) { var point = context.projection(entity.loc); pad = 10; bbox = { minX: point[0] - pad, minY: point[1] - pad, maxX: point[0] + pad, maxY: point[1] + pad }; - ids.push.apply(ids, _map(rdrawn.search(bbox), 'id')); + ids.push.apply(ids, _map(_rdrawn.search(bbox), 'id')); } } From 450392d2e59e1ea3ff23a6cb8ef311d149fb0c8f Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 13 Dec 2017 10:23:25 -0500 Subject: [PATCH 19/70] Fix label placement for directional points rendered as vertices --- modules/svg/labels.js | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/modules/svg/labels.js b/modules/svg/labels.js index 4c4cb70ce..ccf2425fa 100644 --- a/modules/svg/labels.js +++ b/modules/svg/labels.js @@ -27,6 +27,10 @@ import { } from '../util'; +var TAU = 2 * Math.PI; +function ktoz(k) { return Math.log(k * TAU) / Math.LN2 - 8; } + + export function svgLabels(projection, context) { var path = d3_geoPath(projection); var detected = utilDetect(); @@ -264,8 +268,10 @@ export function svgLabels(projection, context) { function drawLabels(selection, graph, entities, filter, dimensions, fullRedraw) { - var lowZoom = context.surface().classed('low-zoom'); - var labelable = [] + var wireframe = context.surface().classed('fill-wireframe'); + var zoom = ktoz(projection.scale()); + + var labelable = []; var i, j, k, entity, geometry; for (i = 0; i < labelStack.length; i++) { @@ -340,13 +346,23 @@ export function svgLabels(projection, context) { var width = name && textWidth(name, fontSize); var p = null; - if (geometry === 'point') { - p = getPointLabel(entity, width, fontSize, geometry); - } else if (geometry === 'vertex' && !lowZoom) { - // don't label vertices at low zoom because they don't have icons - p = getPointLabel(entity, width, fontSize, geometry); + if (geometry === 'point' || geometry === 'vertex') { + if (wireframe) continue; // no point or vertex labels in wireframe + + var hasDirections = entity.directions(graph, projection).length; + var renderAs; + + if (geometry === 'point' && !(zoom >= 18 && hasDirections)) { + renderAs = 'point'; + } else { + if (zoom < 17) continue; // no vertex labels at low zooms (vertices have no icons) + renderAs = 'vertex'; + } + p = getPointLabel(entity, width, fontSize, renderAs); + } else if (geometry === 'line') { p = getLineLabel(entity, width, fontSize); + } else if (geometry === 'area') { p = getAreaLabel(entity, width, fontSize); } From 006ee691bfa04d9939105334580132e4a7226d93 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 13 Dec 2017 14:31:17 -0500 Subject: [PATCH 20/70] Avoid placing labels in interesting points/vertices (closes #4271) --- modules/svg/labels.js | 121 +++++++++++++++++++++++++++++------------- 1 file changed, 83 insertions(+), 38 deletions(-) diff --git a/modules/svg/labels.js b/modules/svg/labels.js index ccf2425fa..e6db96a05 100644 --- a/modules/svg/labels.js +++ b/modules/svg/labels.js @@ -272,6 +272,7 @@ export function svgLabels(projection, context) { var zoom = ktoz(projection.scale()); var labelable = []; + var renderNodeAs = {}; var i, j, k, entity, geometry; for (i = 0; i < labelStack.length; i++) { @@ -282,6 +283,7 @@ export function svgLabels(projection, context) { _rdrawn.clear(); _rskipped.clear(); _entitybboxes = {}; + } else { for (i = 0; i < entities.length; i++) { entity = entities[i]; @@ -296,12 +298,42 @@ export function svgLabels(projection, context) { } } - // Split entities into groups specified by labelStack + // Loop through all the entities to do some preprocessing for (i = 0; i < entities.length; i++) { entity = entities[i]; geometry = entity.geometry(graph); - if (geometry === 'vertex') { geometry = 'point'; } // treat vertex like point + // Insert collision boxes around interesting points/vertices + if (geometry === 'point' || (geometry === 'vertex' && entity.hasInterestingTags())) { + var hasDirections = entity.directions(graph, projection).length; + var markerPadding; + + if (!wireframe && geometry === 'point' && !(zoom >= 18 && hasDirections)) { + renderNodeAs[entity.id] = 'point'; + markerPadding = 20; // extra y for marker height + } else { + renderNodeAs[entity.id] = 'vertex'; + markerPadding = 0; + } + + var coord = projection(entity.loc); + var nodePadding = 10; + var bbox = { + minX: coord[0] - nodePadding, + minY: coord[1] - nodePadding - markerPadding, + maxX: coord[0] + nodePadding, + maxY: coord[1] + nodePadding + }; + + doInsert(bbox, entity.id + 'P'); + } + + // From here on, treat vertices like points + if (geometry === 'vertex') { + geometry = 'point'; + } + + // Determine which entities are label-able var preset = geometry === 'area' && context.presets().match(entity, graph); var icon = preset && !blacklisted(preset) && preset.icon; @@ -347,17 +379,12 @@ export function svgLabels(projection, context) { var p = null; if (geometry === 'point' || geometry === 'vertex') { - if (wireframe) continue; // no point or vertex labels in wireframe + // no point or vertex labels in wireframe mode + // no vertex labels at low zooms (vertices have no icons) + if (wireframe) continue; + var renderAs = renderNodeAs[entity.id]; + if (renderAs === 'vertex' && zoom < 17) continue; - var hasDirections = entity.directions(graph, projection).length; - var renderAs; - - if (geometry === 'point' && !(zoom >= 18 && hasDirections)) { - renderAs = 'point'; - } else { - if (zoom < 17) continue; // no vertex labels at low zooms (vertices have no icons) - renderAs = 'vertex'; - } p = getPointLabel(entity, width, fontSize, renderAs); } else if (geometry === 'line') { @@ -385,7 +412,7 @@ export function svgLabels(projection, context) { }; var coord = projection(entity.loc); - var margin = 2; + var textPadding = 2; var offset = pointOffsets[textDirection]; var p = { height: height, @@ -395,20 +422,21 @@ export function svgLabels(projection, context) { textAnchor: offset[2] }; + // insert a collision box for the text label.. var bbox; if (textDirection === 'rtl') { bbox = { - minX: p.x - width - margin, - minY: p.y - (height / 2) - margin, - maxX: p.x + margin, - maxY: p.y + (height / 2) + margin + minX: p.x - width - textPadding, + minY: p.y - (height / 2) - textPadding, + maxX: p.x + textPadding, + maxY: p.y + (height / 2) + textPadding }; } else { bbox = { - minX: p.x - margin, - minY: p.y - (height / 2) - margin, - maxX: p.x + width + margin, - maxY: p.y + (height / 2) + margin + minX: p.x - textPadding, + minY: p.y - (height / 2) - textPadding, + maxX: p.x + width + textPadding, + maxY: p.y + (height / 2) + textPadding }; } @@ -428,7 +456,7 @@ export function svgLabels(projection, context) { // % along the line to attempt to place the label var lineOffsets = [50, 45, 55, 40, 60, 35, 65, 30, 70, 25, 75, 20, 80, 15, 95, 10, 90, 5, 95]; - var margin = 3; + var padding = 3; for (var i = 0; i < lineOffsets.length; i++) { var offset = lineOffsets[i]; @@ -454,14 +482,16 @@ export function svgLabels(projection, context) { for (var j = 0; j < sub.length - 1; j++) { var a = sub[j]; var b = sub[j + 1]; + + // split up the text into small collision boxes var num = Math.max(1, Math.floor(geoEuclideanDistance(a, b) / boxsize / 2)); for (var box = 0; box < num; box++) { var p = geoInterp(a, b, box / num); - var x0 = p[0] - boxsize - margin; - var y0 = p[1] - boxsize - margin; - var x1 = p[0] + boxsize + margin; - var y1 = p[1] + boxsize + margin; + var x0 = p[0] - boxsize - padding; + var y0 = p[1] - boxsize - padding; + var x1 = p[0] + boxsize + padding; + var y1 = p[1] + boxsize + padding; bboxes.push({ minX: Math.min(x0, x1), @@ -536,12 +566,12 @@ export function svgLabels(projection, context) { var preset = context.presets().match(entity, context.graph()); var picon = preset && preset.icon; var iconSize = 17; - var margin = 2; + var padding = 2; var p = {}; if (picon) { // icon and label.. if (addIcon()) { - addLabel(iconSize + margin); + addLabel(iconSize + padding); return p; } } else { // label only.. @@ -573,10 +603,10 @@ export function svgLabels(projection, context) { var labelX = centroid[0]; var labelY = centroid[1] + yOffset; var bbox = { - minX: labelX - (width / 2) - margin, - minY: labelY - (height / 2) - margin, - maxX: labelX + (width / 2) + margin, - maxY: labelY + (height / 2) + margin + minX: labelX - (width / 2) - padding, + minY: labelY - (height / 2) - padding, + maxX: labelX + (width / 2) + padding, + maxY: labelY + (height / 2) + padding }; if (tryInsert([bbox], entity.id, true)) { @@ -592,6 +622,20 @@ export function svgLabels(projection, context) { } + // force insert a singular bounding box + // singular box only, no array, id better be unique + function doInsert(bbox, id) { + bbox.id = id; + + var oldbox = _entitybboxes[id]; + if (oldbox) { + _rdrawn.remove(oldbox); + } + _entitybboxes[id] = bbox; + _rdrawn.insert(bbox); + } + + function tryInsert(bboxes, id, saveSkipped) { var skipped = false; @@ -676,14 +720,15 @@ export function svgLabels(projection, context) { if (!entity) continue; var geometry = entity.geometry(graph); - if (geometry === 'line') { + if (geometry === 'line' || geometry === 'point' || geometry === 'vertex') { ids.push(selectedIDs[i]); - } else if (geometry === 'vertex') { - var point = context.projection(entity.loc); - pad = 10; - bbox = { minX: point[0] - pad, minY: point[1] - pad, maxX: point[0] + pad, maxY: point[1] + pad }; - ids.push.apply(ids, _map(_rdrawn.search(bbox), 'id')); } + // } else if (geometry === 'vertex') { + // var point = context.projection(entity.loc); + // pad = 20; + // bbox = { minX: point[0] - pad, minY: point[1] - pad, maxX: point[0] + pad, maxY: point[1] + pad }; + // ids.push.apply(ids, _map(_rdrawn.search(bbox), 'id')); + // } } layers.selectAll(utilEntitySelector(ids)) From d9e3367836bd7c9158b06ab139c827c21e6d707d Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 13 Dec 2017 18:05:06 -0500 Subject: [PATCH 21/70] More improvements to label vertex avoidance (re: #4271, #3636) - better classification of "interesting" vertices (include tagged, selected, or child of selected) - now we can draw labels on selected lines again (revert #3636) because the labels will avoid the vertices - if debugging is on, draw a collision box for the mouse --- css/20_map.css | 2 +- modules/svg/labels.js | 64 +++++++++++++++++++++++++++++++++---------- 2 files changed, 50 insertions(+), 16 deletions(-) diff --git a/css/20_map.css b/css/20_map.css index a29a1c85a..371b51d5d 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -217,7 +217,7 @@ text.pointlabel { stroke-miterlimit: 1; } -text.proximate { +text.nolabel { opacity: 0; } diff --git a/modules/svg/labels.js b/modules/svg/labels.js index e6db96a05..b36895e1d 100644 --- a/modules/svg/labels.js +++ b/modules/svg/labels.js @@ -304,7 +304,7 @@ export function svgLabels(projection, context) { geometry = entity.geometry(graph); // Insert collision boxes around interesting points/vertices - if (geometry === 'point' || (geometry === 'vertex' && entity.hasInterestingTags())) { + if (geometry === 'point' || (geometry === 'vertex' && isInterestingVertex(entity))) { var hasDirections = entity.directions(graph, projection).length; var markerPadding; @@ -404,6 +404,19 @@ export function svgLabels(projection, context) { } + function isInterestingVertex(entity) { + var selectedIDs = context.selectedIDs(); + + return entity.hasInterestingTags() || + entity.isEndpoint(graph) || + entity.isConnected(graph) || + selectedIDs.indexOf(entity.id) !== -1 || + _some(graph.parentWays(entity), function(parent) { + return selectedIDs.indexOf(parent.id) !== -1; + }); + } + + function getPointLabel(entity, width, height, geometry) { var y = (geometry === 'point' ? -12 : 0); var pointOffsets = { @@ -698,8 +711,8 @@ export function svgLabels(projection, context) { var layers = selection .selectAll('.layer-label, .layer-halo'); - layers.selectAll('.proximate') - .classed('proximate', false); + layers.selectAll('.nolabel') + .classed('nolabel', false); var mouse = context.mouse(); var graph = context.graph(); @@ -714,25 +727,46 @@ export function svgLabels(projection, context) { ids.push.apply(ids, _map(_rdrawn.search(bbox), 'id')); } - // hide labels along selected ways, or near selected vertices + // hide labels on selected nodes (they look weird when dragging / haloed) for (var i = 0; i < selectedIDs.length; i++) { var entity = graph.hasEntity(selectedIDs[i]); - if (!entity) continue; - var geometry = entity.geometry(graph); - - if (geometry === 'line' || geometry === 'point' || geometry === 'vertex') { + if (entity && entity.type === 'node') { ids.push(selectedIDs[i]); } - // } else if (geometry === 'vertex') { - // var point = context.projection(entity.loc); - // pad = 20; - // bbox = { minX: point[0] - pad, minY: point[1] - pad, maxX: point[0] + pad, maxY: point[1] + pad }; - // ids.push.apply(ids, _map(_rdrawn.search(bbox), 'id')); - // } } layers.selectAll(utilEntitySelector(ids)) - .classed('proximate', true); + .classed('nolabel', true); + + + // draw the mouse bbox if debugging is on.. + if (context.getDebug('collision')) { + var gj = bbox ? [{ + type: 'Polygon', + coordinates: [[ + [bbox.minX, bbox.minY], + [bbox.maxX, bbox.minY], + [bbox.maxX, bbox.maxY], + [bbox.minX, bbox.maxY], + [bbox.minX, bbox.minY] + ]] + }] : []; + + var debug = selection.selectAll('.layer-label-debug'); + var debugMouse = debug.selectAll('.debug-mouse') + .data(gj); + + // exit + debugMouse.exit() + .remove(); + + // enter/update + debugMouse.enter() + .append('path') + .attr('class', 'debug debug-mouse yellow') + .merge(debugMouse) + .attr('d', d3_geoPath()); + } } From 24baa5390ec49960a579e8e2d719be66b5158d0e Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 13 Dec 2017 18:31:37 -0500 Subject: [PATCH 22/70] Adjust some variable names to better match what they do - `nodes` are for osm nodes - `points` are the projected locations of those node `loc` in screen space --- modules/svg/labels.js | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/modules/svg/labels.js b/modules/svg/labels.js index b36895e1d..7d16c1256 100644 --- a/modules/svg/labels.js +++ b/modules/svg/labels.js @@ -461,8 +461,8 @@ export function svgLabels(projection, context) { function getLineLabel(entity, width, height) { var viewport = geoExtent(context.projection.clipExtent()).polygon(); - var nodes = _map(graph.childNodes(entity), 'loc').map(projection); - var length = geoPathLength(nodes); + var points = _map(graph.childNodes(entity), 'loc').map(projection); + var length = geoPathLength(points); if (length < width + 20) return; @@ -479,7 +479,7 @@ export function svgLabels(projection, context) { if (start < 0 || start + width > length) continue; // generate subpath and ignore paths that are invalid or don't cross viewport. - var sub = subpath(nodes, start, start + width); + var sub = subpath(points, start, start + width); if (!sub || !geoPolygonIntersectsPolygon(viewport, sub, true)) { continue; } @@ -515,7 +515,7 @@ export function svgLabels(projection, context) { } } - if (tryInsert(bboxes, entity.id, false)) { + if (tryInsert(bboxes, entity.id, false)) { // accept this one return { 'font-size': height + 2, lineString: lineString(sub), @@ -529,17 +529,17 @@ export function svgLabels(projection, context) { return !(p[0][0] < p[p.length - 1][0] && angle < Math.PI/2 && angle > -Math.PI/2); } - function lineString(nodes) { - return 'M' + nodes.join('L'); + function lineString(points) { + return 'M' + points.join('L'); } - function subpath(nodes, from, to) { + function subpath(points, from, to) { var sofar = 0; var start, end, i0, i1; - for (var i = 0; i < nodes.length - 1; i++) { - var a = nodes[i]; - var b = nodes[i + 1]; + for (var i = 0; i < points.length - 1; i++) { + var a = points[i]; + var b = points[i + 1]; var current = geoEuclideanDistance(a, b); var portion; if (!start && sofar + current >= from) { @@ -561,10 +561,10 @@ export function svgLabels(projection, context) { sofar += current; } - var ret = nodes.slice(i0, i1); - ret.unshift(start); - ret.push(end); - return ret; + var result = points.slice(i0, i1); + result.unshift(start); + result.push(end); + return result; } } From bfaf17538ea8dbec65ae529ac7f0fee6c9a46939 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 14 Dec 2017 12:32:28 -0500 Subject: [PATCH 23/70] Move text to single group with subgroups for halo,label,debug --- css/20_map.css | 2 +- modules/svg/labels.js | 129 ++++++++++++++++++++++-------------------- modules/svg/osm.js | 2 +- test/spec/svg/osm.js | 5 +- 4 files changed, 72 insertions(+), 66 deletions(-) diff --git a/css/20_map.css b/css/20_map.css index 371b51d5d..845389778 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -210,7 +210,7 @@ text.pointlabel { dominant-baseline: auto; } -.layer-halo text { +.layer-labels-halo text { opacity: 0.7; stroke: #fff; stroke-width: 5px; diff --git a/modules/svg/labels.js b/modules/svg/labels.js index 7d16c1256..432959dca 100644 --- a/modules/svg/labels.js +++ b/modules/svg/labels.js @@ -117,9 +117,11 @@ export function svgLabels(projection, context) { .filter(filter) .data(entities, osmEntity.key); + // exit paths.exit() .remove(); + // enter/update paths.enter() .append('path') .style('stroke-width', get(labels, 'font-size')) @@ -135,9 +137,11 @@ export function svgLabels(projection, context) { .filter(filter) .data(entities, osmEntity.key); + // exit texts.exit() .remove(); + // enter texts.enter() .append('text') .attr('class', function(d, i) { return classes + ' ' + labels[i].classes + ' ' + d.id; }) @@ -145,9 +149,8 @@ export function svgLabels(projection, context) { .append('textPath') .attr('class', 'textpath'); - texts = selection.selectAll('text.' + classes); - - texts.selectAll('.textpath') + // update + selection.selectAll('text.' + classes).selectAll('.textpath') .filter(filter) .data(entities, osmEntity.key) .attr('startOffset', '50%') @@ -161,17 +164,17 @@ export function svgLabels(projection, context) { .filter(filter) .data(entities, osmEntity.key); + // exit texts.exit() .remove(); - texts = texts.enter() + // enter/update + texts.enter() .append('text') .attr('class', function(d, i) { return classes + ' ' + labels[i].classes + ' ' + d.id; }) - .merge(texts); - - texts + .merge(texts) .attr('x', get(labels, 'x')) .attr('y', get(labels, 'y')) .style('text-anchor', get(labels, 'textAnchor')) @@ -198,17 +201,17 @@ export function svgLabels(projection, context) { .filter(filter) .data(entities, osmEntity.key); + // exit icons.exit() .remove(); - icons = icons.enter() + // enter/update + icons.enter() .append('use') .attr('class', 'icon ' + classes) .attr('width', '17px') .attr('height', '17px') - .merge(icons); - - icons + .merge(icons) .attr('transform', get(labels, 'transform')) .attr('xlink:href', function(d) { var preset = context.presets().match(d, context.graph()); @@ -225,22 +228,11 @@ export function svgLabels(projection, context) { function drawCollisionBoxes(selection, rtree, which) { - var showDebug = context.getDebug('collision'); var classes = 'debug ' + which + ' ' + (which === 'debug-skipped' ? 'orange' : 'yellow'); - var debug = selection.selectAll('.layer-label-debug') - .data(showDebug ? [true] : []); - - debug.exit() - .remove(); - - debug = debug.enter() - .append('g') - .attr('class', 'layer-label-debug') - .merge(debug); - - if (showDebug) { - var gj = rtree.all().map(function(d) { + var gj = []; + if (context.getDebug('collision')) { + gj = rtree.all().map(function(d) { return { type: 'Polygon', coordinates: [[ [d.minX, d.minY], [d.maxX, d.minY], @@ -249,21 +241,21 @@ export function svgLabels(projection, context) { [d.minX, d.minY] ]]}; }); - - var debugboxes = debug.selectAll('.' + which) - .data(gj); - - debugboxes.exit() - .remove(); - - debugboxes = debugboxes.enter() - .append('path') - .attr('class', classes) - .merge(debugboxes); - - debugboxes - .attr('d', d3_geoPath()); } + + var boxes = selection.selectAll('.' + which) + .data(gj); + + // exit + boxes.exit() + .remove(); + + // enter/update + boxes.enter() + .append('path') + .attr('class', classes) + .merge(boxes) + .attr('d', d3_geoPath()); } @@ -466,6 +458,8 @@ export function svgLabels(projection, context) { if (length < width + 20) return; + // todo: properly clip points to viewport + // % along the line to attempt to place the label var lineOffsets = [50, 45, 55, 40, 60, 35, 65, 30, 70, 25, 75, 20, 80, 15, 95, 10, 90, 5, 95]; @@ -681,8 +675,20 @@ export function svgLabels(projection, context) { } - var label = selection.selectAll('.layer-label'); - var halo = selection.selectAll('.layer-halo'); + var layer = selection.selectAll('.layer-labels'); + + var groups = layer.selectAll('.layer-labels-group') + .data(['halo','label','debug']); + + groups = groups.enter() + .append('g') + .attr('class', function(d) { return 'layer-labels-group layer-labels-' + d; }) + .merge(groups); + + var halo = layer.selectAll('.layer-labels-halo'); + var label = layer.selectAll('.layer-labels-label'); + var debug = layer.selectAll('.layer-labels-debug'); + // points drawPointLabels(label, labelled.point, filter, 'pointlabel', positions.point); @@ -700,16 +706,16 @@ export function svgLabels(projection, context) { drawAreaIcons(halo, labelled.area, filter, 'areaicon-halo', positions.area); // debug - drawCollisionBoxes(label, _rskipped, 'debug-skipped'); - drawCollisionBoxes(label, _rdrawn, 'debug-drawn'); + drawCollisionBoxes(debug, _rskipped, 'debug-skipped'); + drawCollisionBoxes(debug, _rdrawn, 'debug-drawn'); - selection.call(filterLabels); + layer.call(filterLabels); } function filterLabels(selection) { var layers = selection - .selectAll('.layer-label, .layer-halo'); + .selectAll('.layer-labels-label, .layer-labels-halo'); layers.selectAll('.nolabel') .classed('nolabel', false); @@ -740,8 +746,10 @@ export function svgLabels(projection, context) { // draw the mouse bbox if debugging is on.. + var debug = selection.selectAll('.layer-labels-debug'); + var gj = []; if (context.getDebug('collision')) { - var gj = bbox ? [{ + gj = bbox ? [{ type: 'Polygon', coordinates: [[ [bbox.minX, bbox.minY], @@ -751,22 +759,21 @@ export function svgLabels(projection, context) { [bbox.minX, bbox.minY] ]] }] : []; - - var debug = selection.selectAll('.layer-label-debug'); - var debugMouse = debug.selectAll('.debug-mouse') - .data(gj); - - // exit - debugMouse.exit() - .remove(); - - // enter/update - debugMouse.enter() - .append('path') - .attr('class', 'debug debug-mouse yellow') - .merge(debugMouse) - .attr('d', d3_geoPath()); } + + var box = debug.selectAll('.debug-mouse') + .data(gj); + + // exit + box.exit() + .remove(); + + // enter/update + box.enter() + .append('path') + .attr('class', 'debug debug-mouse yellow') + .merge(box) + .attr('d', d3_geoPath()); } diff --git a/modules/svg/osm.js b/modules/svg/osm.js index 2a3b6ff72..d133bfbd0 100644 --- a/modules/svg/osm.js +++ b/modules/svg/osm.js @@ -4,7 +4,7 @@ export function svgOsm(projection, context, dispatch) { function drawOsm(selection) { selection.selectAll('.layer-osm') - .data(['areas', 'lines', 'hit', 'halo', 'label']) + .data(['areas', 'lines', 'hit', 'labels']) .enter() .append('g') .attr('class', function(d) { return 'layer-osm layer-' + d; }); diff --git a/test/spec/svg/osm.js b/test/spec/svg/osm.js index e532d4580..fe51c3741 100644 --- a/test/spec/svg/osm.js +++ b/test/spec/svg/osm.js @@ -8,12 +8,11 @@ describe('iD.svgOsm', function () { it('creates default osm layers', function () { container.call(iD.svgOsm()); var nodes = container.selectAll('.layer-osm').nodes(); - expect(nodes.length).to.eql(5); + expect(nodes.length).to.eql(4); expect(d3.select(nodes[0]).classed('layer-areas')).to.be.true; expect(d3.select(nodes[1]).classed('layer-lines')).to.be.true; expect(d3.select(nodes[2]).classed('layer-hit')).to.be.true; - expect(d3.select(nodes[3]).classed('layer-halo')).to.be.true; - expect(d3.select(nodes[4]).classed('layer-label')).to.be.true; + expect(d3.select(nodes[3]).classed('layer-labels')).to.be.true; }); }); From b9e48d1682e854daf825c3bdcdca173f49cc2716 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 14 Dec 2017 17:38:43 -0500 Subject: [PATCH 24/70] WIP: Move layer-hit to layer-points with explict sublayers, update tests This is more work to further isolate the layers that entities draw to. It makes it easier to debug what is going on, and can eventually lead to deferred drawing, if each draw function is in its own place and not dependant on anything else. I've started to replace the vertex-hover with an explicit layer for touch targets. Also had to change a lot of the svg tests, which are really brittle. Things would happen like - the surface would be created, it would kick of a deferred redraw, which would notice that the zoom was 0 and call editOff, which would remove the osm layers that were just created and that the tests were trying to draw to. These tests need proper zoom and projection otherwise nothing works. --- css/20_map.css | 15 ++- css/70_fills.css | 6 +- modules/renderer/map.js | 32 +++---- modules/svg/labels.js | 10 -- modules/svg/midpoints.js | 2 +- modules/svg/osm.js | 14 ++- modules/svg/points.js | 3 +- modules/svg/turns.js | 3 +- modules/svg/vertices.js | 175 +++++++++++++++++++---------------- test/spec/svg/areas.js | 142 ++++++++++++++-------------- test/spec/svg/layers.js | 15 +-- test/spec/svg/lines.js | 90 +++++++++--------- test/spec/svg/midpoints.js | 116 +++++++++++------------ test/spec/svg/osm.js | 32 +++++-- test/spec/svg/points.js | 21 +++-- test/spec/svg/tag_classes.js | 56 +++++------ test/spec/svg/vertices.js | 12 +-- 17 files changed, 405 insertions(+), 339 deletions(-) diff --git a/css/20_map.css b/css/20_map.css index 845389778..34061453f 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -45,6 +45,7 @@ g.point.selected .shadow { stroke-opacity: 0.7; } +g.vertex.active, g.vertex.active *, g.point.active, g.point.active * { pointer-events: none; } @@ -88,6 +89,12 @@ g.midpoint .shadow { fill-opacity: 0; } +/*debug*/ +.vertex.target { + fill: #f00; + fill-opacity: 0.5; +} + /*g.vertex.vertex-hover { display: none; } @@ -99,6 +106,7 @@ g.midpoint .shadow { .mode-add-point g.vertex.vertex-hover, .mode-drag-node g.vertex.vertex-hover { display: block; + color: #f00; } .mode-draw-area .hover-disabled g.vertex.vertex-hover, @@ -110,8 +118,9 @@ g.midpoint .shadow { display: none; } */ + g.vertex.related:not(.selected) .shadow, -g.vertex.hover:not(.selected) .shadow, +/*g.vertex.hover:not(.selected) .shadow,*/ g.midpoint.related:not(.selected) .shadow, g.midpoint.hover:not(.selected) .shadow { fill-opacity: 0.5; @@ -262,11 +271,11 @@ g.turn circle { } path.gpx { - stroke: #FF26D4; + stroke: #ff26d4; stroke-width: 2; fill: none; } text.gpx { - fill: #FF26D4; + fill: #ff26d4; } diff --git a/css/70_fills.css b/css/70_fills.css index c0cb1b167..ffb437d68 100644 --- a/css/70_fills.css +++ b/css/70_fills.css @@ -44,12 +44,12 @@ display: none; } */ -.mode-draw-line .way.active, -.mode-draw-area .way.active, +/*.mode-draw-line .active, +.mode-draw-area .active, .mode-drag-node .active { pointer-events: none; } - +*/ /* Ensure drawing doesn't interact with area fills. */ .mode-add-point path.area.fill, .mode-draw-line path.area.fill, diff --git a/modules/renderer/map.js b/modules/renderer/map.js index 0240844df..f9de7d460 100644 --- a/modules/renderer/map.js +++ b/modules/renderer/map.js @@ -178,23 +178,23 @@ export function rendererMap(context) { }) .on('mousemove.map', function() { mousemove = d3_event; + }) + .on('mouseover.vertices', function() { + if (map.editable() && !transformed) { + var hover = d3_event.target.__data__; + surface.selectAll('.data-layer-osm') + .call(drawVertices.drawHover, context.graph(), hover, map.extent()); + dispatch.call('drawn', this, { full: false }); + } + }) + .on('mouseout.vertices', function() { + if (map.editable() && !transformed) { + var hover = d3_event.relatedTarget && d3_event.relatedTarget.__data__; + surface.selectAll('.data-layer-osm') + .call(drawVertices.drawHover, context.graph(), hover, map.extent()); + dispatch.call('drawn', this, { full: false }); + } }); - // .on('mouseover.vertices', function() { - // if (map.editable() && !transformed) { - // var hover = d3_event.target.__data__; - // surface.selectAll('.data-layer-osm') - // .call(drawVertices.drawHover, context.graph(), hover, map.extent()); - // dispatch.call('drawn', this, { full: false }); - // } - // }) - // .on('mouseout.vertices', function() { - // if (map.editable() && !transformed) { - // var hover = d3_event.relatedTarget && d3_event.relatedTarget.__data__; - // surface.selectAll('.data-layer-osm') - // .call(drawVertices.drawHover, context.graph(), hover, map.extent()); - // dispatch.call('drawn', this, { full: false }); - // } - // }); supersurface .call(context.background()); diff --git a/modules/svg/labels.js b/modules/svg/labels.js index 432959dca..2e65d593a 100644 --- a/modules/svg/labels.js +++ b/modules/svg/labels.js @@ -676,20 +676,10 @@ export function svgLabels(projection, context) { var layer = selection.selectAll('.layer-labels'); - - var groups = layer.selectAll('.layer-labels-group') - .data(['halo','label','debug']); - - groups = groups.enter() - .append('g') - .attr('class', function(d) { return 'layer-labels-group layer-labels-' + d; }) - .merge(groups); - var halo = layer.selectAll('.layer-labels-halo'); var label = layer.selectAll('.layer-labels-label'); var debug = layer.selectAll('.layer-labels-debug'); - // points drawPointLabels(label, labelled.point, filter, 'pointlabel', positions.point); drawPointLabels(halo, labelled.point, filter, 'pointlabel-halo', positions.point); diff --git a/modules/svg/midpoints.js b/modules/svg/midpoints.js index 101625fee..27e548ca4 100644 --- a/modules/svg/midpoints.js +++ b/modules/svg/midpoints.js @@ -16,7 +16,7 @@ import { export function svgMidpoints(projection, context) { return function drawMidpoints(selection, graph, entities, filter, extent) { - var layer = selection.selectAll('.layer-hit'); + var layer = selection.selectAll('.layer-points .layer-points-midpoints'); var mode = context.mode(); if (mode && mode.id !== 'select') { diff --git a/modules/svg/osm.js b/modules/svg/osm.js index d133bfbd0..8a7defe94 100644 --- a/modules/svg/osm.js +++ b/modules/svg/osm.js @@ -4,10 +4,22 @@ export function svgOsm(projection, context, dispatch) { function drawOsm(selection) { selection.selectAll('.layer-osm') - .data(['areas', 'lines', 'hit', 'labels']) + .data(['areas', 'lines', 'points', 'labels']) .enter() .append('g') .attr('class', function(d) { return 'layer-osm layer-' + d; }); + + selection.selectAll('.layer-points').selectAll('.layer-points-group') + .data(['points', 'midpoints', 'vertices', 'turns', 'targets']) + .enter() + .append('g') + .attr('class', function(d) { return 'layer-points-group layer-points-' + d; }); + + selection.selectAll('.layer-labels').selectAll('.layer-labels-group') + .data(['halo', 'label', 'debug']) + .enter() + .append('g') + .attr('class', function(d) { return 'layer-labels-group layer-labels-' + d; }); } diff --git a/modules/svg/points.js b/modules/svg/points.js index 909533f3a..ab8f2361e 100644 --- a/modules/svg/points.js +++ b/modules/svg/points.js @@ -36,7 +36,8 @@ export function svgPoints(projection, context) { points.sort(sortY); - var layer = selection.selectAll('.layer-hit'); + + var layer = selection.selectAll('.layer-points .layer-points-points'); var groups = layer.selectAll('g.point') .filter(filter) diff --git a/modules/svg/turns.js b/modules/svg/turns.js index 537ccf5ff..cd16bd0dd 100644 --- a/modules/svg/turns.js +++ b/modules/svg/turns.js @@ -18,7 +18,8 @@ export function svgTurns(projection) { (!turn.indirect_restriction && /^only_/.test(restriction) ? 'only' : 'no') + u; } - var groups = selection.selectAll('.layer-hit').selectAll('g.turn') + var layer = selection.selectAll('.layer-points .layer-points-turns'); + var groups = layer.selectAll('g.turn') .data(turns, key); groups.exit() diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index 7f2d5a49c..707ed2414 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -54,7 +54,8 @@ export function svgVertices(projection, context) { function setClass(klass) { return function(entity) { - this.setAttribute('class', 'node vertex ' + klass + ' ' + entity.id); + d3_select(this) + .attr('class', 'node vertex ' + klass + ' ' + entity.id); }; } @@ -171,9 +172,7 @@ export function svgVertices(projection, context) { function drawVertices(selection, graph, entities, filter, extent) { var wireframe = context.surface().classed('fill-wireframe'); var zoom = ktoz(projection.scale()); - - var siblings = {}; - getSiblingAndChildVertices(context.selectedIDs(), graph, extent); + var siblings = getSiblingAndChildVertices(context.selectedIDs(), graph, extent, wireframe, zoom); // always render selected and sibling vertices.. var vertices = _clone(siblings); @@ -184,7 +183,7 @@ export function svgVertices(projection, context) { var entity = entities[i]; var geometry = entity.geometry(graph); - if ((geometry === 'point') && renderAsVertex(entity)) { + if ((geometry === 'point') && renderAsVertex(entity, graph, wireframe, zoom)) { vertices[entity.id] = entity; } else if ((geometry === 'vertex') && @@ -193,85 +192,105 @@ export function svgVertices(projection, context) { } } - - selection.selectAll('.layer-hit') + selection.selectAll('.layer-points .layer-points-vertices') .call(draw, _values(vertices), 'vertex-persistent', graph, siblings, filterWithSiblings); -// drawHover(selection, graph, extent, true); + drawTargets(selection, graph, _values(vertices), filter, extent); - - // Points can also render as vertices: - // 1. in wireframe mode or - // 2. at higher zooms if they have a direction - function renderAsVertex(entity) { - var geometry = entity.geometry(graph); - return geometry === 'vertex' || (geometry === 'point' && ( - wireframe || (zoom > 18 && entity.directions(graph, projection).length) - )); - } - - - function getSiblingAndChildVertices(ids, graph, extent) { - - function addChildVertices(entity) { - var geometry = entity.geometry(graph); - if (!context.features().isHiddenFeature(entity, graph, geometry)) { - var i; - if (entity.type === 'way') { - for (i = 0; i < entity.nodes.length; i++) { - var child = context.hasEntity(entity.nodes[i]); - if (child) { - addChildVertices(child); - } - } - } else if (entity.type === 'relation') { - for (i = 0; i < entity.members.length; i++) { - var member = context.hasEntity(entity.members[i].id); - if (member) { - addChildVertices(member); - } - } - } else if (renderAsVertex(entity) && entity.intersects(extent, graph)) { - siblings[entity.id] = entity; - } - } - } - - ids.forEach(function(id) { - var entity = context.hasEntity(id); - if (!entity) return; - - if (entity.type === 'node') { - if (renderAsVertex(entity)) { - siblings[entity.id] = entity; - graph.parentWays(entity).forEach(function(entity) { - addChildVertices(entity); - }); - } - } else { // way, relation - addChildVertices(entity); - } - }); - - } } -// function drawHover(selection, graph, extent, follow) { -// var hovered = _hover ? siblingAndChildVertices([_hover.id], graph, extent) : {}; -// var wireframe = context.surface().classed('fill-wireframe'); -// var layer = selection.selectAll('.layer-hit'); -// -// layer.selectAll('g.vertex.vertex-hover') -// .call(draw, _values(hovered), 'vertex-hover', graph, {}, false); -// } -// -// -// drawVertices.drawHover = function(selection, graph, target, extent) { -// if (target === _hover) return; -// _hover = target; -// drawHover(selection, graph, extent); -// }; + function drawTargets(selection, graph, entities, filter, extent) { +// todo coming soon +return; + var layer = selection.selectAll('.layer-points .layer-points-targets'); + + var targets = layer.selectAll('g.vertex.target') + .data(entities, osmEntity.key); + + // exit + targets.exit() + .remove(); + + // enter/update + targets.enter() + .append('circle') + .attr('r', radiuses.shadow[3]) // just use the biggest one for now + .attr('class', function(d) { return 'node vertex target ' + d.id; }) + .merge(targets) + .attr('transform', svgPointTransform(projection)); + } + + + // Points can also render as vertices: + // 1. in wireframe mode or + // 2. at higher zooms if they have a direction + function renderAsVertex(entity, graph, wireframe, zoom) { + var geometry = entity.geometry(graph); + return geometry === 'vertex' || (geometry === 'point' && ( + wireframe || (zoom > 18 && entity.directions(graph, projection).length) + )); + } + + + function getSiblingAndChildVertices(ids, graph, extent, wireframe, zoom) { + var results = {}; + + function addChildVertices(entity) { + var geometry = entity.geometry(graph); + if (!context.features().isHiddenFeature(entity, graph, geometry)) { + var i; + if (entity.type === 'way') { + for (i = 0; i < entity.nodes.length; i++) { + var child = context.hasEntity(entity.nodes[i]); + if (child) { + addChildVertices(child); + } + } + } else if (entity.type === 'relation') { + for (i = 0; i < entity.members.length; i++) { + var member = context.hasEntity(entity.members[i].id); + if (member) { + addChildVertices(member); + } + } + } else if (renderAsVertex(entity, graph, wireframe, zoom) && entity.intersects(extent, graph)) { + results[entity.id] = entity; + } + } + } + + ids.forEach(function(id) { + var entity = context.hasEntity(id); + if (!entity) return; + + if (entity.type === 'node') { + if (renderAsVertex(entity, graph, wireframe, zoom)) { + results[entity.id] = entity; + graph.parentWays(entity).forEach(function(entity) { + addChildVertices(entity); + }); + } + } else { // way, relation + addChildVertices(entity); + } + }); + + return results; + } + + + drawVertices.drawHover = function(selection, graph, target, extent) { + if (target === _hover) return; + _hover = target; + + var wireframe = context.surface().classed('fill-wireframe'); + var zoom = ktoz(projection.scale()); + var hovered = _hover ? getSiblingAndChildVertices([_hover.id], graph, extent, wireframe, zoom) : {}; + var filter = function() { return true; }; + + drawTargets(selection, graph, _values(hovered), filter, extent); + }; return drawVertices; } diff --git a/test/spec/svg/areas.js b/test/spec/svg/areas.js index 7397682bd..140576398 100644 --- a/test/spec/svg/areas.js +++ b/test/spec/svg/areas.js @@ -1,17 +1,21 @@ describe('iD.svgAreas', function () { - var context, surface, - projection = d3.geoProjection(function(x, y) { return [x, -y]; }) - .translate([0, 0]) - .scale(180 / Math.PI) - .clipExtent([[0, 0], [Infinity, Infinity]]), - all = function() { return true; }, - none = function() { return false; }; + var TAU = 2 * Math.PI; + function ztok(z) { return 256 * Math.pow(2, z) / TAU; } + + var context, surface; + var all = function() { return true; }; + var none = function() { return false; }; + var projection = d3.geoProjection(function(x, y) { return [x, -y]; }) + .translate([0, 0]) + .scale(ztok(17)) + .clipExtent([[0, 0], [Infinity, Infinity]]); + beforeEach(function () { - context = iD.Context(); + context = iD.coreContext(); d3.select(document.createElement('div')) .attr('id', 'map') - .call(context.map()); + .call(context.map().centerZoom([0, 0], 17)); surface = context.surface(); iD.setAreaKeys({ @@ -22,13 +26,13 @@ describe('iD.svgAreas', function () { }); it('adds way and area classes', function () { - var graph = iD.Graph([ - iD.Node({id: 'a', loc: [0, 0]}), - iD.Node({id: 'b', loc: [1, 0]}), - iD.Node({id: 'c', loc: [1, 1]}), - iD.Node({id: 'd', loc: [0, 1]}), - iD.Way({id: 'w', tags: {building: 'yes'}, nodes: ['a', 'b', 'c', 'a']}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [1, 0]}), + iD.osmNode({id: 'c', loc: [1, 1]}), + iD.osmNode({id: 'd', loc: [0, 1]}), + iD.osmWay({id: 'w', tags: {building: 'yes'}, nodes: ['a', 'b', 'c', 'a']}) + ]); surface.call(iD.svgAreas(projection, context), graph, [graph.entity('w')], none); @@ -37,13 +41,13 @@ describe('iD.svgAreas', function () { }); it('adds tag classes', function () { - var graph = iD.Graph([ - iD.Node({id: 'a', loc: [0, 0]}), - iD.Node({id: 'b', loc: [1, 0]}), - iD.Node({id: 'c', loc: [1, 1]}), - iD.Node({id: 'd', loc: [0, 1]}), - iD.Way({id: 'w', tags: {building: 'yes'}, nodes: ['a', 'b', 'c', 'a']}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [1, 0]}), + iD.osmNode({id: 'c', loc: [1, 1]}), + iD.osmNode({id: 'd', loc: [0, 1]}), + iD.osmWay({id: 'w', tags: {building: 'yes'}, nodes: ['a', 'b', 'c', 'a']}) + ]); surface.call(iD.svgAreas(projection, context), graph, [graph.entity('w')], none); @@ -52,14 +56,14 @@ describe('iD.svgAreas', function () { }); it('handles deletion of a way and a member vertex (#1903)', function () { - var graph = iD.Graph([ - iD.Node({id: 'a', loc: [0, 0]}), - iD.Node({id: 'b', loc: [1, 0]}), - iD.Node({id: 'c', loc: [1, 1]}), - iD.Node({id: 'd', loc: [1, 1]}), - iD.Way({id: 'w', tags: {area: 'yes'}, nodes: ['a', 'b', 'c', 'a']}), - iD.Way({id: 'x', tags: {area: 'yes'}, nodes: ['a', 'b', 'd', 'a']}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [1, 0]}), + iD.osmNode({id: 'c', loc: [1, 1]}), + iD.osmNode({id: 'd', loc: [1, 1]}), + iD.osmWay({id: 'w', tags: {area: 'yes'}, nodes: ['a', 'b', 'c', 'a']}), + iD.osmWay({id: 'x', tags: {area: 'yes'}, nodes: ['a', 'b', 'd', 'a']}) + ]); surface.call(iD.svgAreas(projection, context), graph, [graph.entity('x')], all); graph = graph.remove(graph.entity('x')).remove(graph.entity('d')); @@ -69,18 +73,18 @@ describe('iD.svgAreas', function () { }); describe('z-indexing', function() { - var graph = iD.Graph([ - iD.Node({id: 'a', loc: [-0.0002, 0.0001]}), - iD.Node({id: 'b', loc: [ 0.0002, 0.0001]}), - iD.Node({id: 'c', loc: [ 0.0002, -0.0001]}), - iD.Node({id: 'd', loc: [-0.0002, -0.0001]}), - iD.Node({id: 'e', loc: [-0.0004, 0.0002]}), - iD.Node({id: 'f', loc: [ 0.0004, 0.0002]}), - iD.Node({id: 'g', loc: [ 0.0004, -0.0002]}), - iD.Node({id: 'h', loc: [-0.0004, -0.0002]}), - iD.Way({id: 's', tags: {building: 'yes'}, nodes: ['a', 'b', 'c', 'd', 'a']}), - iD.Way({id: 'l', tags: {landuse: 'park'}, nodes: ['e', 'f', 'g', 'h', 'e']}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [-0.0002, 0.0001]}), + iD.osmNode({id: 'b', loc: [ 0.0002, 0.0001]}), + iD.osmNode({id: 'c', loc: [ 0.0002, -0.0001]}), + iD.osmNode({id: 'd', loc: [-0.0002, -0.0001]}), + iD.osmNode({id: 'e', loc: [-0.0004, 0.0002]}), + iD.osmNode({id: 'f', loc: [ 0.0004, 0.0002]}), + iD.osmNode({id: 'g', loc: [ 0.0004, -0.0002]}), + iD.osmNode({id: 'h', loc: [-0.0004, -0.0002]}), + iD.osmWay({id: 's', tags: {building: 'yes'}, nodes: ['a', 'b', 'c', 'd', 'a']}), + iD.osmWay({id: 'l', tags: {landuse: 'park'}, nodes: ['e', 'f', 'g', 'h', 'e']}) + ]); it('stacks smaller areas above larger ones in a single render', function () { surface.call(iD.svgAreas(projection, context), graph, [graph.entity('s'), graph.entity('l')], none); @@ -114,13 +118,13 @@ describe('iD.svgAreas', function () { }); it('renders fills for multipolygon areas', function () { - var a = iD.Node({loc: [1, 1]}), - b = iD.Node({loc: [2, 2]}), - c = iD.Node({loc: [3, 3]}), - w = iD.Way({nodes: [a.id, b.id, c.id, a.id]}), - r = iD.Relation({tags: {type: 'multipolygon'}, members: [{id: w.id, type: 'way'}]}), - graph = iD.Graph([a, b, c, w, r]), - areas = [w, r]; + var a = iD.osmNode({loc: [1, 1]}); + var b = iD.osmNode({loc: [2, 2]}); + var c = iD.osmNode({loc: [3, 3]}); + var w = iD.osmWay({nodes: [a.id, b.id, c.id, a.id]}); + var r = iD.osmRelation({tags: {type: 'multipolygon'}, members: [{id: w.id, type: 'way'}]}); + var graph = iD.coreGraph([a, b, c, w, r]); + var areas = [w, r]; surface.call(iD.svgAreas(projection, context), graph, areas, none); @@ -128,13 +132,13 @@ describe('iD.svgAreas', function () { }); it('renders no strokes for multipolygon areas', function () { - var a = iD.Node({loc: [1, 1]}), - b = iD.Node({loc: [2, 2]}), - c = iD.Node({loc: [3, 3]}), - w = iD.Way({nodes: [a.id, b.id, c.id, a.id]}), - r = iD.Relation({tags: {type: 'multipolygon'}, members: [{id: w.id, type: 'way'}]}), - graph = iD.Graph([a, b, c, w, r]), - areas = [w, r]; + var a = iD.osmNode({loc: [1, 1]}); + var b = iD.osmNode({loc: [2, 2]}); + var c = iD.osmNode({loc: [3, 3]}); + var w = iD.osmWay({nodes: [a.id, b.id, c.id, a.id]}); + var r = iD.osmRelation({tags: {type: 'multipolygon'}, members: [{id: w.id, type: 'way'}]}); + var graph = iD.coreGraph([a, b, c, w, r]); + var areas = [w, r]; surface.call(iD.svgAreas(projection, context), graph, areas, none); @@ -142,12 +146,12 @@ describe('iD.svgAreas', function () { }); it('renders fill for a multipolygon with tags on the outer way', function() { - var a = iD.Node({loc: [1, 1]}), - b = iD.Node({loc: [2, 2]}), - c = iD.Node({loc: [3, 3]}), - w = iD.Way({tags: {natural: 'wood'}, nodes: [a.id, b.id, c.id, a.id]}), - r = iD.Relation({members: [{id: w.id, type: 'way'}], tags: {type: 'multipolygon'}}), - graph = iD.Graph([a, b, c, w, r]); + var a = iD.osmNode({loc: [1, 1]}); + var b = iD.osmNode({loc: [2, 2]}); + var c = iD.osmNode({loc: [3, 3]}); + var w = iD.osmWay({tags: {natural: 'wood'}, nodes: [a.id, b.id, c.id, a.id]}); + var r = iD.osmRelation({members: [{id: w.id, type: 'way'}], tags: {type: 'multipolygon'}}); + var graph = iD.coreGraph([a, b, c, w, r]); surface.call(iD.svgAreas(projection, context), graph, [w, r], none); @@ -157,12 +161,12 @@ describe('iD.svgAreas', function () { }); it('renders no strokes for a multipolygon with tags on the outer way', function() { - var a = iD.Node({loc: [1, 1]}), - b = iD.Node({loc: [2, 2]}), - c = iD.Node({loc: [3, 3]}), - w = iD.Way({tags: {natural: 'wood'}, nodes: [a.id, b.id, c.id, a.id]}), - r = iD.Relation({members: [{id: w.id, type: 'way'}], tags: {type: 'multipolygon'}}), - graph = iD.Graph([a, b, c, w, r]); + var a = iD.osmNode({loc: [1, 1]}); + var b = iD.osmNode({loc: [2, 2]}); + var c = iD.osmNode({loc: [3, 3]}); + var w = iD.osmWay({tags: {natural: 'wood'}, nodes: [a.id, b.id, c.id, a.id]}); + var r = iD.osmRelation({members: [{id: w.id, type: 'way'}], tags: {type: 'multipolygon'}}); + var graph = iD.coreGraph([a, b, c, w, r]); surface.call(iD.svgAreas(projection, context), graph, [w, r], none); diff --git a/test/spec/svg/layers.js b/test/spec/svg/layers.js index e3cb29a89..fa70b6565 100644 --- a/test/spec/svg/layers.js +++ b/test/spec/svg/layers.js @@ -1,12 +1,15 @@ describe('iD.svgLayers', function () { - var context, container, - projection = d3.geoProjection(function(x, y) { return [x, -y]; }) - .translate([0, 0]) - .scale(180 / Math.PI) - .clipExtent([[0, 0], [Infinity, Infinity]]); + var TAU = 2 * Math.PI; + function ztok(z) { return 256 * Math.pow(2, z) / TAU; } + + var context, container; + var projection = d3.geoProjection(function(x, y) { return [x, -y]; }) + .translate([0, 0]) + .scale(ztok(17)) + .clipExtent([[0, 0], [Infinity, Infinity]]); beforeEach(function () { - context = iD.Context(); + context = iD.coreContext(); container = d3.select(document.createElement('div')); }); diff --git a/test/spec/svg/lines.js b/test/spec/svg/lines.js index c1748eeff..2a414afcd 100644 --- a/test/spec/svg/lines.js +++ b/test/spec/svg/lines.js @@ -1,26 +1,30 @@ describe('iD.svgLines', function () { - var context, surface, - projection = d3.geoProjection(function(x, y) { return [x, -y]; }) - .translate([0, 0]) - .scale(180 / Math.PI) - .clipExtent([[0, 0], [Infinity, Infinity]]), - all = function() { return true; }, - none = function() { return false; }; + var TAU = 2 * Math.PI; + function ztok(z) { return 256 * Math.pow(2, z) / TAU; } + + var context, surface; + var all = function() { return true; }; + var none = function() { return false; }; + var projection = d3.geoProjection(function(x, y) { return [x, -y]; }) + .translate([0, 0]) + .scale(ztok(17)) + .clipExtent([[0, 0], [Infinity, Infinity]]); + beforeEach(function () { - context = iD.Context(); + context = iD.coreContext(); d3.select(document.createElement('div')) .attr('id', 'map') - .call(context.map()); + .call(context.map().centerZoom([0, 0], 17)); surface = context.surface(); }); it('adds way and line classes', function () { - var a = iD.Node({loc: [0, 0]}), - b = iD.Node({loc: [1, 1]}), - line = iD.Way({nodes: [a.id, b.id]}), - graph = iD.Graph([a, b, line]); + var a = iD.osmNode({loc: [0, 0]}); + var b = iD.osmNode({loc: [1, 1]}); + var line = iD.osmWay({nodes: [a.id, b.id]}); + var graph = iD.coreGraph([a, b, line]); surface.call(iD.svgLines(projection, context), graph, [line], all); @@ -29,10 +33,10 @@ describe('iD.svgLines', function () { }); it('adds tag classes', function () { - var a = iD.Node({loc: [0, 0]}), - b = iD.Node({loc: [1, 1]}), - line = iD.Way({nodes: [a.id, b.id], tags: {highway: 'residential'}}), - graph = iD.Graph([a, b, line]); + var a = iD.osmNode({loc: [0, 0]}); + var b = iD.osmNode({loc: [1, 1]}); + var line = iD.osmWay({nodes: [a.id, b.id], tags: {highway: 'residential'}}); + var graph = iD.coreGraph([a, b, line]); surface.call(iD.svgLines(projection, context), graph, [line], all); @@ -41,11 +45,11 @@ describe('iD.svgLines', function () { }); it('adds stroke classes for the tags of the parent relation of multipolygon members', function() { - var a = iD.Node({loc: [0, 0]}), - b = iD.Node({loc: [1, 1]}), - line = iD.Way({nodes: [a.id, b.id]}), - relation = iD.Relation({members: [{id: line.id}], tags: {type: 'multipolygon', natural: 'wood'}}), - graph = iD.Graph([a, b, line, relation]); + var a = iD.osmNode({loc: [0, 0]}); + var b = iD.osmNode({loc: [1, 1]}); + var line = iD.osmWay({nodes: [a.id, b.id]}); + var relation = iD.osmRelation({members: [{id: line.id}], tags: {type: 'multipolygon', natural: 'wood'}}); + var graph = iD.coreGraph([a, b, line, relation]); surface.call(iD.svgLines(projection, context), graph, [line], all); @@ -53,12 +57,12 @@ describe('iD.svgLines', function () { }); it('renders stroke for outer way of multipolygon with tags on the outer way', function() { - var a = iD.Node({loc: [1, 1]}), - b = iD.Node({loc: [2, 2]}), - c = iD.Node({loc: [3, 3]}), - w = iD.Way({id: 'w-1', tags: {natural: 'wood'}, nodes: [a.id, b.id, c.id, a.id]}), - r = iD.Relation({members: [{id: w.id}], tags: {type: 'multipolygon'}}), - graph = iD.Graph([a, b, c, w, r]); + var a = iD.osmNode({loc: [1, 1]}); + var b = iD.osmNode({loc: [2, 2]}); + var c = iD.osmNode({loc: [3, 3]}); + var w = iD.osmWay({id: 'w-1', tags: {natural: 'wood'}, nodes: [a.id, b.id, c.id, a.id]}); + var r = iD.osmRelation({members: [{id: w.id}], tags: {type: 'multipolygon'}}); + var graph = iD.coreGraph([a, b, c, w, r]); surface.call(iD.svgLines(projection, context), graph, [w], all); @@ -67,13 +71,13 @@ describe('iD.svgLines', function () { }); it('adds stroke classes for the tags of the outer way of multipolygon with tags on the outer way', function() { - var a = iD.Node({loc: [1, 1]}), - b = iD.Node({loc: [2, 2]}), - c = iD.Node({loc: [3, 3]}), - o = iD.Way({id: 'w-1', nodes: [a.id, b.id, c.id, a.id], tags: {natural: 'wood'}}), - i = iD.Way({id: 'w-2', nodes: [a.id, b.id, c.id, a.id]}), - r = iD.Relation({members: [{id: o.id, role: 'outer'}, {id: i.id, role: 'inner'}], tags: {type: 'multipolygon'}}), - graph = iD.Graph([a, b, c, o, i, r]); + var a = iD.osmNode({loc: [1, 1]}); + var b = iD.osmNode({loc: [2, 2]}); + var c = iD.osmNode({loc: [3, 3]}); + var o = iD.osmWay({id: 'w-1', nodes: [a.id, b.id, c.id, a.id], tags: {natural: 'wood'}}); + var i = iD.osmWay({id: 'w-2', nodes: [a.id, b.id, c.id, a.id]}); + var r = iD.osmRelation({members: [{id: o.id, role: 'outer'}, {id: i.id, role: 'inner'}], tags: {type: 'multipolygon'}}); + var graph = iD.coreGraph([a, b, c, o, i, r]); surface.call(iD.svgLines(projection, context), graph, [i, o], all); @@ -84,14 +88,14 @@ describe('iD.svgLines', function () { }); describe('z-indexing', function() { - var graph = iD.Graph([ - iD.Node({id: 'a', loc: [0, 0]}), - iD.Node({id: 'b', loc: [1, 1]}), - iD.Node({id: 'c', loc: [0, 0]}), - iD.Node({id: 'd', loc: [1, 1]}), - iD.Way({id: 'lo', tags: {highway: 'residential', tunnel: 'yes'}, nodes: ['a', 'b']}), - iD.Way({id: 'hi', tags: {highway: 'residential', bridge: 'yes'}, nodes: ['c', 'd']}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [1, 1]}), + iD.osmNode({id: 'c', loc: [0, 0]}), + iD.osmNode({id: 'd', loc: [1, 1]}), + iD.osmWay({id: 'lo', tags: {highway: 'residential', tunnel: 'yes'}, nodes: ['a', 'b']}), + iD.osmWay({id: 'hi', tags: {highway: 'residential', bridge: 'yes'}, nodes: ['c', 'd']}) + ]); it('stacks higher lines above lower ones in a single render', function () { surface.call(iD.svgLines(projection, context), graph, [graph.entity('lo'), graph.entity('hi')], none); diff --git a/test/spec/svg/midpoints.js b/test/spec/svg/midpoints.js index 92266c5c6..bb4442415 100644 --- a/test/spec/svg/midpoints.js +++ b/test/spec/svg/midpoints.js @@ -1,103 +1,103 @@ describe('iD.svgMidpoints', function () { - var context, surface, - selectedIDs = [], - projection = d3.geoProjection(function(x, y) { return [x, -y]; }) - .translate([0, 0]) - .scale(180 / Math.PI) - .clipExtent([[0, 0], [Infinity, Infinity]]), - filter = function() { return true; }; + var TAU = 2 * Math.PI; + function ztok(z) { return 256 * Math.pow(2, z) / TAU; } + + var context, surface; + var _selectedIDs = []; + var filter = function() { return true; }; + var projection = d3.geoProjection(function(x, y) { return [x, -y]; }) + .translate([0, 0]) + .scale(ztok(17)) + .clipExtent([[0, 0], [Infinity, Infinity]]); beforeEach(function () { - context = iD.Context(); - context.mode = function() { - return { - id: 'select', - selectedIDs: function() { return selectedIDs; } - }; - }; - d3.select(document.createElement('div')) + context = iD.coreContext(); + context.enter({ + id: 'select', + enter: function() { }, + exit: function() { }, + selectedIDs: function() { return _selectedIDs; } + }); + + var map = d3.select(document.createElement('div')) .attr('id', 'map') - .call(context.map()); + .call(context.map().centerZoom([0, 0], 17)); + surface = context.surface(); }); it('creates midpoint on segment completely within the extent', function () { - var a = iD.Node({loc: [0, 0]}), - b = iD.Node({loc: [50, 0]}), - line = iD.Way({nodes: [a.id, b.id]}), - graph = iD.Graph([a, b, line]), - extent = iD.geoExtent([0, 0], [100, 100]); + var a = iD.osmNode({loc: [0, 0]}); + var b = iD.osmNode({loc: [1, 0]}); + var line = iD.osmWay({nodes: [a.id, b.id]}); + var graph = iD.coreGraph([a, b, line]); + var extent = iD.geoExtent([0, 0], [1, 1]); - selectedIDs = [line.id]; - context.selectedIDs = function() { return selectedIDs; }; + _selectedIDs = [line.id]; context.entity = function(id) { return graph.entity(id); }; - context.hasEntity = context.entity; + context.hasEntity = function(id) { return graph.entities[id]; }; surface.call(iD.svgMidpoints(projection, context), graph, [line], filter, extent); - expect(surface.selectAll('.midpoint').datum().loc).to.eql([25, 0]); + expect(surface.selectAll('.midpoint').datum().loc).to.eql([0.5, 0]); }); it('doesn\'t create midpoint on segment with pixel length less than 40', function () { - var a = iD.Node({loc: [0, 0]}), - b = iD.Node({loc: [39, 0]}), - line = iD.Way({nodes: [a.id, b.id]}), - graph = iD.Graph([a, b, line]), - extent = iD.geoExtent([0, 0], [100, 100]); + var a = iD.osmNode({loc: [0, 0]}); + var b = iD.osmNode({loc: [0.0001, 0]}); + var line = iD.osmWay({nodes: [a.id, b.id]}); + var graph = iD.coreGraph([a, b, line]); + var extent = iD.geoExtent([0, 0], [1, 1]); - selectedIDs = [line.id]; - context.selectedIDs = function() { return selectedIDs; }; + _selectedIDs = [line.id]; context.entity = function(id) { return graph.entity(id); }; - context.hasEntity = context.entity; + context.hasEntity = function(id) { return graph.entities[id]; }; surface.call(iD.svgMidpoints(projection, context), graph, [line], filter, extent); expect(surface.selectAll('.midpoint').nodes()).to.have.length(0); }); it('doesn\'t create midpoint on segment completely outside of the extent', function () { - var a = iD.Node({loc: [-100, 0]}), - b = iD.Node({loc: [-50, 0]}), - line = iD.Way({nodes: [a.id, b.id]}), - graph = iD.Graph([a, b, line]), - extent = iD.geoExtent([0, 0], [100, 100]); + var a = iD.osmNode({loc: [-1, 0]}); + var b = iD.osmNode({loc: [-0.5, 0]}); + var line = iD.osmWay({nodes: [a.id, b.id]}); + var graph = iD.coreGraph([a, b, line]); + var extent = iD.geoExtent([0, 0], [1, 1]); - selectedIDs = [line.id]; - context.selectedIDs = function() { return selectedIDs; }; + _selectedIDs = [line.id]; context.entity = function(id) { return graph.entity(id); }; - context.hasEntity = context.entity; + context.hasEntity = function(id) { return graph.entities[id]; }; surface.call(iD.svgMidpoints(projection, context), graph, [line], filter, extent); expect(surface.selectAll('.midpoint').nodes()).to.have.length(0); }); it('creates midpoint on extent edge for segment partially outside of the extent', function () { - var a = iD.Node({loc: [50, 0]}), - b = iD.Node({loc: [500, 0]}), - line = iD.Way({nodes: [a.id, b.id]}), - graph = iD.Graph([a, b, line]), - extent = iD.geoExtent([0, 0], [100, 100]); + var a = iD.osmNode({loc: [0.5, 0]}); + var b = iD.osmNode({loc: [2, 0]}); + var line = iD.osmWay({nodes: [a.id, b.id]}); + var graph = iD.coreGraph([a, b, line]); + var extent = iD.geoExtent([0, 0], [1, 1]); - selectedIDs = [line.id]; - context.selectedIDs = function() { return selectedIDs; }; + _selectedIDs = [line.id]; context.entity = function(id) { return graph.entity(id); }; - context.hasEntity = context.entity; + context.hasEntity = function(id) { return graph.entities[id]; }; surface.call(iD.svgMidpoints(projection, context), graph, [line], filter, extent); - expect(surface.selectAll('.midpoint').datum().loc).to.eql([100, 0]); + expect(surface.selectAll('.midpoint').datum().loc).to.eql([1, 0]); }); it('doesn\'t create midpoint on extent edge for segment with pixel length less than 20', function () { - var a = iD.Node({loc: [81, 0]}), - b = iD.Node({loc: [500, 0]}), - line = iD.Way({nodes: [a.id, b.id]}), - graph = iD.Graph([a, b, line]), - extent = iD.geoExtent([0, 0], [100, 100]); + var a = iD.osmNode({loc: [0.9999, 0]}); + var b = iD.osmNode({loc: [2, 0]}); + var line = iD.osmWay({nodes: [a.id, b.id]}); + var graph = iD.coreGraph([a, b, line]); + var extent = iD.geoExtent([0, 0], [1, 1]); - selectedIDs = [line.id]; - context.selectedIDs = function() { return selectedIDs; }; + _selectedIDs = [line.id]; context.entity = function(id) { return graph.entity(id); }; - context.hasEntity = context.entity; + context.hasEntity = function(id) { return graph.entities[id]; }; surface.call(iD.svgMidpoints(projection, context), graph, [line], filter, extent); expect(surface.selectAll('.midpoint').nodes()).to.have.length(0); diff --git a/test/spec/svg/osm.js b/test/spec/svg/osm.js index fe51c3741..43358b11f 100644 --- a/test/spec/svg/osm.js +++ b/test/spec/svg/osm.js @@ -7,12 +7,32 @@ describe('iD.svgOsm', function () { it('creates default osm layers', function () { container.call(iD.svgOsm()); - var nodes = container.selectAll('.layer-osm').nodes(); - expect(nodes.length).to.eql(4); - expect(d3.select(nodes[0]).classed('layer-areas')).to.be.true; - expect(d3.select(nodes[1]).classed('layer-lines')).to.be.true; - expect(d3.select(nodes[2]).classed('layer-hit')).to.be.true; - expect(d3.select(nodes[3]).classed('layer-labels')).to.be.true; + var layers = container.selectAll('g.layer-osm').nodes(); + expect(layers.length).to.eql(4); + expect(d3.select(layers[0]).classed('layer-areas')).to.be.true; + expect(d3.select(layers[1]).classed('layer-lines')).to.be.true; + expect(d3.select(layers[2]).classed('layer-points')).to.be.true; + expect(d3.select(layers[3]).classed('layer-labels')).to.be.true; + }); + + it('creates default osm point layers', function () { + container.call(iD.svgOsm()); + var layers = container.selectAll('g.layer-points g.layer-points-group').nodes(); + expect(layers.length).to.eql(5); + expect(d3.select(layers[0]).classed('layer-points-points')).to.be.true; + expect(d3.select(layers[1]).classed('layer-points-midpoints')).to.be.true; + expect(d3.select(layers[2]).classed('layer-points-vertices')).to.be.true; + expect(d3.select(layers[3]).classed('layer-points-turns')).to.be.true; + expect(d3.select(layers[4]).classed('layer-points-targets')).to.be.true; + }); + + it('creates default osm label layers', function () { + container.call(iD.svgOsm()); + var layers = container.selectAll('g.layer-labels g.layer-labels-group').nodes(); + expect(layers.length).to.eql(3); + expect(d3.select(layers[0]).classed('layer-labels-halo')).to.be.true; + expect(d3.select(layers[1]).classed('layer-labels-label')).to.be.true; + expect(d3.select(layers[2]).classed('layer-labels-debug')).to.be.true; }); }); diff --git a/test/spec/svg/points.js b/test/spec/svg/points.js index dccca9d11..8faaa54c2 100644 --- a/test/spec/svg/points.js +++ b/test/spec/svg/points.js @@ -1,22 +1,25 @@ describe('iD.svgPoints', function () { - var context, surface, - projection = d3.geoProjection(function(x, y) { return [x, -y]; }) - .translate([0, 0]) - .scale(180 / Math.PI) - .clipExtent([[0, 0], [Infinity, Infinity]]); + var TAU = 2 * Math.PI; + function ztok(z) { return 256 * Math.pow(2, z) / TAU; } + + var context, surface; + var projection = d3.geoProjection(function(x, y) { return [x, -y]; }) + .translate([0, 0]) + .scale(ztok(17)) + .clipExtent([[0, 0], [Infinity, Infinity]]); beforeEach(function () { - context = iD.Context(); + context = iD.coreContext(); d3.select(document.createElement('div')) .attr('id', 'map') - .call(context.map()); + .call(context.map().centerZoom([0, 0], 17)); surface = context.surface(); }); it('adds tag classes', function () { - var point = iD.Node({tags: {amenity: 'cafe'}, loc: [0, 0]}), - graph = iD.Graph([point]); + var point = iD.osmNode({tags: {amenity: 'cafe'}, loc: [0, 0]}); + var graph = iD.coreGraph([point]); surface.call(iD.svgPoints(projection, context), graph, [point]); diff --git a/test/spec/svg/tag_classes.js b/test/spec/svg/tag_classes.js index dffb8e14d..d454300d7 100644 --- a/test/spec/svg/tag_classes.js +++ b/test/spec/svg/tag_classes.js @@ -7,156 +7,156 @@ describe('iD.svgTagClasses', function () { it('adds no classes to elements whose datum has no tags', function() { selection - .datum(iD.Entity()) + .datum(iD.osmEntity()) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal(null); }); it('adds classes for primary tag key and key-value', function() { selection - .datum(iD.Entity({tags: {highway: 'primary'}})) + .datum(iD.osmEntity({tags: {highway: 'primary'}})) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal('tag-highway tag-highway-primary'); }); it('adds only one primary tag', function() { selection - .datum(iD.Entity({tags: {highway: 'primary', railway: 'rail'}})) + .datum(iD.osmEntity({tags: {highway: 'primary', railway: 'rail'}})) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal('tag-highway tag-highway-primary'); }); it('orders primary tags', function() { selection - .datum(iD.Entity({tags: {railway: 'rail', highway: 'primary'}})) + .datum(iD.osmEntity({tags: {railway: 'rail', highway: 'primary'}})) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal('tag-highway tag-highway-primary'); }); it('adds status tag when status in primary value (`railway=abandoned`)', function() { selection - .datum(iD.Entity({tags: {railway: 'abandoned'}})) + .datum(iD.osmEntity({tags: {railway: 'abandoned'}})) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal('tag-railway tag-status tag-status-abandoned'); }); it('adds status tag when status in key and value matches "yes" (railway=rail + abandoned=yes)', function() { selection - .datum(iD.Entity({tags: {railway: 'rail', abandoned: 'yes'}})) + .datum(iD.osmEntity({tags: {railway: 'rail', abandoned: 'yes'}})) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal('tag-railway tag-railway-rail tag-status tag-status-abandoned'); }); it('adds status tag when status in key and value matches primary (railway=rail + abandoned=railway)', function() { selection - .datum(iD.Entity({tags: {railway: 'rail', abandoned: 'railway'}})) + .datum(iD.osmEntity({tags: {railway: 'rail', abandoned: 'railway'}})) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal('tag-railway tag-railway-rail tag-status tag-status-abandoned'); }); it('adds primary and status tag when status in key and no primary (abandoned=railway)', function() { selection - .datum(iD.Entity({tags: {abandoned: 'railway'}})) + .datum(iD.osmEntity({tags: {abandoned: 'railway'}})) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal('tag-railway tag-status tag-status-abandoned'); }); it('does not add status tag for different primary tag (highway=path + abandoned=railway)', function() { selection - .datum(iD.Entity({tags: {highway: 'path', abandoned: 'railway'}})) + .datum(iD.osmEntity({tags: {highway: 'path', abandoned: 'railway'}})) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal('tag-highway tag-highway-path'); }); it('adds secondary tags', function() { selection - .datum(iD.Entity({tags: {highway: 'primary', bridge: 'yes'}})) + .datum(iD.osmEntity({tags: {highway: 'primary', bridge: 'yes'}})) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal('tag-highway tag-highway-primary tag-bridge tag-bridge-yes'); }); it('adds no bridge=no tags', function() { selection - .datum(iD.Entity({tags: {bridge: 'no'}})) + .datum(iD.osmEntity({tags: {bridge: 'no'}})) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal(null); }); it('adds tag-unpaved for highway=track with no surface tagging', function() { selection - .datum(iD.Entity({tags: {highway: 'track'}})) + .datum(iD.osmEntity({tags: {highway: 'track'}})) .call(iD.svgTagClasses()); expect(selection.classed('tag-unpaved')).to.be.true; }); it('does not add tag-unpaved for highway=track with explicit paved surface tagging', function() { selection - .datum(iD.Entity({tags: {highway: 'track', surface: 'asphalt'}})) + .datum(iD.osmEntity({tags: {highway: 'track', surface: 'asphalt'}})) .call(iD.svgTagClasses()); expect(selection.classed('tag-unpaved')).to.be.false; selection - .datum(iD.Entity({tags: {highway: 'track', tracktype: 'grade1'}})) + .datum(iD.osmEntity({tags: {highway: 'track', tracktype: 'grade1'}})) .call(iD.svgTagClasses()); expect(selection.classed('tag-unpaved')).to.be.false; }); it('adds tag-unpaved for highway=track with explicit unpaved surface tagging', function() { selection - .datum(iD.Entity({tags: {highway: 'track', surface: 'dirt'}})) + .datum(iD.osmEntity({tags: {highway: 'track', surface: 'dirt'}})) .call(iD.svgTagClasses()); expect(selection.classed('tag-unpaved')).to.be.true; selection - .datum(iD.Entity({tags: {highway: 'track', tracktype: 'grade3'}})) + .datum(iD.osmEntity({tags: {highway: 'track', tracktype: 'grade3'}})) .call(iD.svgTagClasses()); expect(selection.classed('tag-unpaved')).to.be.true; }); it('does not add tag-unpaved for other highway types with no surface tagging', function() { selection - .datum(iD.Entity({tags: {highway: 'tertiary'}})) + .datum(iD.osmEntity({tags: {highway: 'tertiary'}})) .call(iD.svgTagClasses()); expect(selection.classed('tag-unpaved')).to.be.false; selection - .datum(iD.Entity({tags: {highway: 'foo'}})) + .datum(iD.osmEntity({tags: {highway: 'foo'}})) .call(iD.svgTagClasses()); expect(selection.classed('tag-unpaved')).to.be.false; }); it('does not add tag-unpaved for other highway types with explicit paved surface tagging', function() { selection - .datum(iD.Entity({tags: {highway: 'tertiary', surface: 'asphalt'}})) + .datum(iD.osmEntity({tags: {highway: 'tertiary', surface: 'asphalt'}})) .call(iD.svgTagClasses()); expect(selection.classed('tag-unpaved')).to.be.false; selection - .datum(iD.Entity({tags: {highway: 'foo', tracktype: 'grade1'}})) + .datum(iD.osmEntity({tags: {highway: 'foo', tracktype: 'grade1'}})) .call(iD.svgTagClasses()); expect(selection.classed('tag-unpaved')).to.be.false; }); it('adds tag-unpaved for other highway types with explicit unpaved surface tagging', function() { selection - .datum(iD.Entity({tags: {highway: 'tertiary', surface: 'dirt'}})) + .datum(iD.osmEntity({tags: {highway: 'tertiary', surface: 'dirt'}})) .call(iD.svgTagClasses()); expect(selection.classed('tag-unpaved')).to.be.true; selection - .datum(iD.Entity({tags: {highway: 'foo', tracktype: 'grade3'}})) + .datum(iD.osmEntity({tags: {highway: 'foo', tracktype: 'grade3'}})) .call(iD.svgTagClasses()); expect(selection.classed('tag-unpaved')).to.be.true; }); it('does not add tag-unpaved for non-highways', function() { selection - .datum(iD.Entity({tags: {railway: 'abandoned', surface: 'gravel'}})) + .datum(iD.osmEntity({tags: {railway: 'abandoned', surface: 'gravel'}})) .call(iD.svgTagClasses()); expect(selection.classed('tag-unpaved')).to.be.false; selection - .datum(iD.Entity({tags: {amenity: 'parking', surface: 'dirt'}})) + .datum(iD.osmEntity({tags: {amenity: 'parking', surface: 'dirt'}})) .call(iD.svgTagClasses()); expect(selection.classed('tag-unpaved')).to.be.false; }); @@ -164,7 +164,7 @@ describe('iD.svgTagClasses', function () { it('adds tags based on the result of the `tags` accessor', function() { var primary = function () { return { highway: 'primary'}; }; selection - .datum(iD.Entity()) + .datum(iD.osmEntity()) .call(iD.svgTagClasses().tags(primary)); expect(selection.attr('class')).to.equal('tag-highway tag-highway-primary'); }); @@ -172,7 +172,7 @@ describe('iD.svgTagClasses', function () { it('removes classes for tags that are no longer present', function() { selection .attr('class', 'tag-highway tag-highway-primary') - .datum(iD.Entity()) + .datum(iD.osmEntity()) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal(''); }); @@ -180,7 +180,7 @@ describe('iD.svgTagClasses', function () { it('preserves existing non-"tag-"-prefixed classes', function() { selection .attr('class', 'selected') - .datum(iD.Entity()) + .datum(iD.osmEntity()) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal('selected'); }); @@ -188,7 +188,7 @@ describe('iD.svgTagClasses', function () { it('works on SVG elements', function() { selection = d3.select(document.createElementNS('http://www.w3.org/2000/svg', 'g')); selection - .datum(iD.Entity()) + .datum(iD.osmEntity()) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal(null); }); diff --git a/test/spec/svg/vertices.js b/test/spec/svg/vertices.js index 2c5104d22..6ab4d4e3b 100644 --- a/test/spec/svg/vertices.js +++ b/test/spec/svg/vertices.js @@ -11,19 +11,19 @@ describe('iD.svgVertices', function () { beforeEach(function () { - context = iD.Context(); + context = iD.coreContext(); d3.select(document.createElement('div')) .attr('id', 'map') - .call(context.map()); + .call(context.map().centerZoom([0, 0], 17)); surface = context.surface(); }); it('adds the .shared class to vertices that are members of two or more ways', function () { - var node = iD.Node({loc: [0, 0]}); - var way1 = iD.Way({nodes: [node.id], tags: {highway: 'residential'}}); - var way2 = iD.Way({nodes: [node.id], tags: {highway: 'residential'}}); - var graph = iD.Graph([node, way1, way2]); + var node = iD.osmNode({loc: [0, 0]}); + var way1 = iD.osmWay({nodes: [node.id], tags: {highway: 'residential'}}); + var way2 = iD.osmWay({nodes: [node.id], tags: {highway: 'residential'}}); + var graph = iD.coreGraph([node, way1, way2]); surface.call(iD.svgVertices(projection, context), graph, [node]); expect(surface.select('.vertex').classed('shared')).to.be.true; From ba5b3eee9c21c1908c84771a50a8cdd6e360218d Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 15 Dec 2017 00:26:37 -0500 Subject: [PATCH 25/70] More work on vertex drawing, add debug for touch targets --- css/20_map.css | 21 ++++---- modules/behavior/hover.js | 61 +++++++++++---------- modules/core/context.js | 11 ++-- modules/svg/debug.js | 35 ++++++------ modules/svg/vertices.js | 109 ++++++++++++++++++++++---------------- 5 files changed, 131 insertions(+), 106 deletions(-) diff --git a/css/20_map.css b/css/20_map.css index 34061453f..4ab4dae1c 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -11,9 +11,10 @@ use { pointer-events: none; } #turn-no-shape2, #turn-no-u-shape2 { fill: #E06D5F; } /* FF turn-no, turn-no-u */ #turn-yes-shape2, #turn-yes-u-shape2 { fill: #8CD05F; } /* FF turn-yes, turn-yes-u */ -g.point .shadow, -g.vertex .shadow, -g.midpoint .shadow { +.layer-points-group * { + pointer-events: none; +} +.layer-points-group.layer-points-targets * { pointer-events: all; } @@ -45,11 +46,11 @@ g.point.selected .shadow { stroke-opacity: 0.7; } -g.vertex.active, g.vertex.active *, +/*g.vertex.active, g.vertex.active *, g.point.active, g.point.active * { pointer-events: none; } - +*/ g.point ellipse.stroke { display: none; } @@ -89,10 +90,10 @@ g.midpoint .shadow { fill-opacity: 0; } -/*debug*/ -.vertex.target { - fill: #f00; - fill-opacity: 0.5; +.target { + color: rgba(0,0,0,0); + fill-opacity: 0.8; + fill: currentColor; } /*g.vertex.vertex-hover { @@ -120,7 +121,7 @@ g.midpoint .shadow { */ g.vertex.related:not(.selected) .shadow, -/*g.vertex.hover:not(.selected) .shadow,*/ +g.vertex.hover:not(.selected) .shadow, g.midpoint.related:not(.selected) .shadow, g.midpoint.hover:not(.selected) .shadow { fill-opacity: 0.5; diff --git a/modules/behavior/hover.js b/modules/behavior/hover.js index f7ea0b4eb..b9c4bfa5c 100644 --- a/modules/behavior/hover.js +++ b/modules/behavior/hover.js @@ -20,16 +20,16 @@ import { utilRebind } from '../util/rebind'; have the .hover class. */ export function behaviorHover(context) { - var dispatch = d3_dispatch('hover'), - _selection = d3_select(null), - newId = null, - buttonDown, - altDisables, - target; + var dispatch = d3_dispatch('hover'); + var _selection = d3_select(null); + var _newId = null; + var _buttonDown; + var _altDisables; + var _target; function keydown() { - if (altDisables && d3_event.keyCode === d3_keybinding.modifierCodes.alt) { + if (_altDisables && d3_event.keyCode === d3_keybinding.modifierCodes.alt) { _selection.selectAll('.hover') .classed('hover-suppressed', true) .classed('hover', false); @@ -43,7 +43,7 @@ export function behaviorHover(context) { function keyup() { - if (altDisables && d3_event.keyCode === d3_keybinding.modifierCodes.alt) { + if (_altDisables && d3_event.keyCode === d3_keybinding.modifierCodes.alt) { _selection.selectAll('.hover-suppressed') .classed('hover-suppressed', false) .classed('hover', true); @@ -51,14 +51,14 @@ export function behaviorHover(context) { _selection .classed('hover-disabled', false); - dispatch.call('hover', this, target ? target.id : null); + dispatch.call('hover', this, _target ? _target.id : null); } } var hover = function(selection) { _selection = selection; - newId = null; + _newId = null; _selection .on('mouseover.hover', mouseover) @@ -71,65 +71,65 @@ export function behaviorHover(context) { function mouseover() { - if (buttonDown) return; - var target = d3_event.target; - enter(target ? target.__data__ : null); + if (_buttonDown) return; + var _target = d3_event.target; + enter(_target ? _target.__data__ : null); } function mouseout() { - if (buttonDown) return; - var target = d3_event.relatedTarget; - enter(target ? target.__data__ : null); + if (_buttonDown) return; + var _target = d3_event.relatedTarget; + enter(_target ? _target.__data__ : null); } function mousedown() { - buttonDown = true; + _buttonDown = true; d3_select(window) .on('mouseup.hover', mouseup, true); } function mouseup() { - buttonDown = false; + _buttonDown = false; d3_select(window) .on('mouseup.hover', null, true); } function enter(d) { - if (d === target) return; - target = d; + if (d === _target) return; + _target = d; _selection.selectAll('.hover') .classed('hover', false); _selection.selectAll('.hover-suppressed') .classed('hover-suppressed', false); - if (target instanceof osmEntity && target.id !== newId) { + if (_target instanceof osmEntity && _target.id !== _newId) { // If drawing a way, don't hover on a node that was just placed. #3974 var mode = context.mode() && context.mode().id; - if ((mode === 'draw-line' || mode === 'draw-area') && !newId && target.type === 'node') { - newId = target.id; + if ((mode === 'draw-line' || mode === 'draw-area') && !_newId && _target.type === 'node') { + _newId = _target.id; return; } - var selector = '.' + target.id; + var selector = '.' + _target.id; - if (target.type === 'relation') { - target.members.forEach(function(member) { + if (_target.type === 'relation') { + _target.members.forEach(function(member) { selector += ', .' + member.id; }); } - var suppressed = altDisables && d3_event && d3_event.altKey; + var suppressed = _altDisables && d3_event && d3_event.altKey; _selection.selectAll(selector) .classed(suppressed ? 'hover-suppressed' : 'hover', true); - dispatch.call('hover', this, !suppressed && target.id); + dispatch.call('hover', this, !suppressed && _target.id); } else { dispatch.call('hover', this, null); @@ -147,7 +147,6 @@ export function behaviorHover(context) { selection .classed('hover-disabled', false); - selection .on('mouseover.hover', null) .on('mouseout.hover', null) @@ -160,8 +159,8 @@ export function behaviorHover(context) { hover.altDisables = function(_) { - if (!arguments.length) return altDisables; - altDisables = _; + if (!arguments.length) return _altDisables; + _altDisables = _; return hover; }; diff --git a/modules/core/context.js b/modules/core/context.js index ce0cf5787..95146e248 100644 --- a/modules/core/context.js +++ b/modules/core/context.js @@ -310,11 +310,12 @@ export function coreContext() { /* Debug */ var debugFlags = { - tile: false, - collision: false, - imagery: false, - imperial: false, - driveLeft: false + tile: false, // tile boundaries + collision: false, // label collision bounding boxes + imagery: false, // imagery bounding polygons + imperial: false, // imperial (not metric) bounding polygons + driveLeft: false, // driveLeft bounding polygons + target: false // touch targets }; context.debugFlags = function() { return debugFlags; diff --git a/modules/svg/debug.js b/modules/svg/debug.js index 9e1c82cb0..8ab5617d4 100644 --- a/modules/svg/debug.js +++ b/modules/svg/debug.js @@ -21,12 +21,13 @@ export function svgDebug(projection, context) { } function drawDebug(selection) { - var showsTile = context.getDebug('tile'), - showsCollision = context.getDebug('collision'), - showsImagery = context.getDebug('imagery'), - showsImperial = context.getDebug('imperial'), - showsDriveLeft = context.getDebug('driveLeft'), - path = d3_geoPath(projection); + var showsTile = context.getDebug('tile'); + var showsCollision = context.getDebug('collision'); + var showsImagery = context.getDebug('imagery'); + var showsImperial = context.getDebug('imperial'); + var showsDriveLeft = context.getDebug('driveLeft'); + var showsTouchTargets = context.getDebug('target'); + var path = d3_geoPath(projection); var debugData = []; @@ -45,6 +46,9 @@ export function svgDebug(projection, context) { if (showsDriveLeft) { debugData.push({ class: 'green', label: 'driveLeft' }); } + if (showsTouchTargets) { + debugData.push({ class: 'pink', label: 'touchTargets' }); + } var legend = d3_select('#content') @@ -84,14 +88,14 @@ export function svgDebug(projection, context) { .merge(layer); - var extent = context.map().extent(), - dataImagery = data.imagery || [], - availableImagery = showsImagery && multipolygons(dataImagery.filter(function(source) { - if (!source.polygon) return false; - return source.polygon.some(function(polygon) { - return geoPolygonIntersectsPolygon(polygon, extent, true); - }); - })); + var extent = context.map().extent(); + var dataImagery = data.imagery || []; + var availableImagery = showsImagery && multipolygons(dataImagery.filter(function(source) { + if (!source.polygon) return false; + return source.polygon.some(function(polygon) { + return geoPolygonIntersectsPolygon(polygon, extent, true); + }); + })); var imagery = layer.selectAll('path.debug-imagery') .data(showsImagery ? availableImagery : []); @@ -142,7 +146,8 @@ export function svgDebug(projection, context) { context.getDebug('collision') || context.getDebug('imagery') || context.getDebug('imperial') || - context.getDebug('driveLeft'); + context.getDebug('driveLeft') || + context.getDebug('target'); } else { return this; } diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index 707ed2414..85ab774ca 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -1,3 +1,4 @@ +import _assign from 'lodash-es/assign'; import _clone from 'lodash-es/clone'; import _values from 'lodash-es/values'; @@ -20,10 +21,11 @@ export function svgVertices(projection, context) { fill: [1, 1.5, 1.5, 1.5] }; - var _hover; + var _currHover; + var _currHoverSiblings = {}; - function draw(selection, vertices, klass, graph, siblings, filter) { + function draw(selection, graph, vertices, klass, siblings, filter) { siblings = siblings || {}; var icons = {}; var directions = {}; @@ -169,43 +171,9 @@ export function svgVertices(projection, context) { } - function drawVertices(selection, graph, entities, filter, extent) { - var wireframe = context.surface().classed('fill-wireframe'); - var zoom = ktoz(projection.scale()); - var siblings = getSiblingAndChildVertices(context.selectedIDs(), graph, extent, wireframe, zoom); - - // always render selected and sibling vertices.. - var vertices = _clone(siblings); - var filterWithSiblings = function(d) { return d.id in siblings || filter(d); }; - - // also render important vertices from the `entities` list.. - for (var i = 0; i < entities.length; i++) { - var entity = entities[i]; - var geometry = entity.geometry(graph); - - if ((geometry === 'point') && renderAsVertex(entity, graph, wireframe, zoom)) { - vertices[entity.id] = entity; - - } else if ((geometry === 'vertex') && - (entity.hasInterestingTags() || entity.isEndpoint(graph) || entity.isConnected(graph)) ) { - vertices[entity.id] = entity; - } - } - - selection.selectAll('.layer-points .layer-points-vertices') - .call(draw, _values(vertices), 'vertex-persistent', graph, siblings, filterWithSiblings); - - drawTargets(selection, graph, _values(vertices), filter, extent); - - } - - - function drawTargets(selection, graph, entities, filter, extent) { -// todo coming soon -return; - var layer = selection.selectAll('.layer-points .layer-points-targets'); - - var targets = layer.selectAll('g.vertex.target') + function drawTargets(selection, graph, entities, filter) { + var debugClass = 'pink'; + var targets = selection.selectAll('.target') .data(entities, osmEntity.key); // exit @@ -218,7 +186,8 @@ return; .attr('r', radiuses.shadow[3]) // just use the biggest one for now .attr('class', function(d) { return 'node vertex target ' + d.id; }) .merge(targets) - .attr('transform', svgPointTransform(projection)); + .attr('transform', svgPointTransform(projection)) + .classed(debugClass, context.getDebug('target')); } @@ -280,16 +249,66 @@ return; } + function drawVertices(selection, graph, entities, filter, extent) { + var wireframe = context.surface().classed('fill-wireframe'); + var zoom = ktoz(projection.scale()); + + var selected = getSiblingAndChildVertices(context.selectedIDs(), graph, extent, wireframe, zoom); + + // interesting vertices from the `entities` list.. + var interesting = {}; + for (var i = 0; i < entities.length; i++) { + var entity = entities[i]; + var geometry = entity.geometry(graph); + + if ((geometry === 'point') && renderAsVertex(entity, graph, wireframe, zoom)) { + interesting[entity.id] = entity; + + } else if ((geometry === 'vertex') && + (entity.hasInterestingTags() || entity.isEndpoint(graph) || entity.isConnected(graph)) ) { + interesting[entity.id] = entity; + } + } + + // 3 sets of vertices to consider + // - selected + siblings + // - hovered + siblings + // - interesting entities passed in + var all = _assign(selected, interesting, _currHoverSiblings); + + var filterWithSiblings = function(d) { + return d.id in selected || d.id in _currHoverSiblings || filter(d); + }; + selection.selectAll('.layer-points .layer-points-vertices') + .call(draw, graph, _values(all), 'vertex-persistent', {}, filterWithSiblings); + + + // draw touch targets for the hovered items only + var filterWithHover = function(d) { + return d.id in _currHoverSiblings || filter(d); + }; + selection.selectAll('.layer-points .layer-points-targets') + .call(drawTargets, graph, _values(_currHoverSiblings), filterWithHover); + } + + drawVertices.drawHover = function(selection, graph, target, extent) { - if (target === _hover) return; - _hover = target; + if (target === _currHover) return; var wireframe = context.surface().classed('fill-wireframe'); var zoom = ktoz(projection.scale()); - var hovered = _hover ? getSiblingAndChildVertices([_hover.id], graph, extent, wireframe, zoom) : {}; - var filter = function() { return true; }; + var prevHoverSiblings = _currHoverSiblings || {}; + var filter = function(d) { return d.id in prevHoverSiblings; }; - drawTargets(selection, graph, _values(hovered), filter, extent); + _currHover = target; + + if (_currHover) { + _currHoverSiblings = getSiblingAndChildVertices([_currHover.id], graph, extent, wireframe, zoom); + } else { + _currHoverSiblings = {}; + } + + drawVertices(selection, graph, _values(prevHoverSiblings), filter, extent); }; return drawVertices; From 89d8d37576b813b31e94eb44ab470b23522fcca2 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 15 Dec 2017 17:28:20 -0500 Subject: [PATCH 26/70] Drawing all the correct vertices now where I want them, simplify classes Some highlights - `getSiblingAndChildVertices` are expensive, so they're saved and called less frequently - draw touch targets for all the visible vertices - remove redundant css classes and `setClass` function --- css/20_map.css | 25 +------ modules/modes/drag_node.js | 2 +- modules/renderer/map.js | 8 +- modules/svg/vertices.js | 149 +++++++++++++++++++++++-------------- test/spec/svg/midpoints.js | 2 +- 5 files changed, 100 insertions(+), 86 deletions(-) diff --git a/css/20_map.css b/css/20_map.css index 4ab4dae1c..cbff2a7f5 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -14,6 +14,7 @@ use { pointer-events: none; } .layer-points-group * { pointer-events: none; } +.layer-points-group.layer-points-midpoints *, .layer-points-group.layer-points-targets * { pointer-events: all; } @@ -96,30 +97,6 @@ g.midpoint .shadow { fill: currentColor; } -/*g.vertex.vertex-hover { - display: none; -} - -.mode-draw-area g.vertex.vertex-hover, -.mode-draw-line g.vertex.vertex-hover, -.mode-add-area g.vertex.vertex-hover, -.mode-add-line g.vertex.vertex-hover, -.mode-add-point g.vertex.vertex-hover, -.mode-drag-node g.vertex.vertex-hover { - display: block; - color: #f00; -} - -.mode-draw-area .hover-disabled g.vertex.vertex-hover, -.mode-draw-line .hover-disabled g.vertex.vertex-hover, -.mode-add-area .hover-disabled g.vertex.vertex-hover, -.mode-add-line .hover-disabled g.vertex.vertex-hover, -.mode-add-point .hover-disabled g.vertex.vertex-hover, -.mode-drag-node .hover-disabled g.vertex.vertex-hover { - display: none; -} -*/ - g.vertex.related:not(.selected) .shadow, g.vertex.hover:not(.selected) .shadow, g.midpoint.related:not(.selected) .shadow, diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index 50cd8231b..bd5bb5a23 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -249,7 +249,7 @@ export function modeDragNode(context) { var behavior = behaviorDrag() - .selector('g.node, g.point, g.midpoint') + .selector('g.node, g.midpoint') .surface(d3_select('#map').node()) .origin(origin) .on('start', start) diff --git a/modules/renderer/map.js b/modules/renderer/map.js index f9de7d460..39996d04f 100644 --- a/modules/renderer/map.js +++ b/modules/renderer/map.js @@ -207,7 +207,7 @@ export function rendererMap(context) { all = context.features().filter(all, graph); surface.selectAll('.data-layer-osm') - .call(drawVertices, graph, all, filter, map.extent()) + .call(drawVertices.drawSelected, graph, all, map.extent()) .call(drawMidpoints, graph, all, filter, map.trimmedExtent()); dispatch.call('drawn', this, { full: false }); } @@ -268,6 +268,7 @@ export function rendererMap(context) { var graph = context.graph(); var features = context.features(); var all = context.intersects(map.extent()); + var fullRedraw = false; var data; var filter; @@ -291,6 +292,7 @@ export function rendererMap(context) { } else { data = all; + fullRedraw = true; filter = utilFunctor(true); } } @@ -298,11 +300,11 @@ export function rendererMap(context) { data = features.filter(data, graph); surface.selectAll('.data-layer-osm') - .call(drawVertices, graph, data, filter, map.extent()) + .call(drawVertices, graph, data, filter, map.extent(), fullRedraw) .call(drawLines, graph, data, filter) .call(drawAreas, graph, data, filter) .call(drawMidpoints, graph, data, filter, map.trimmedExtent()) - .call(drawLabels, graph, data, filter, dimensions, !difference && !extent) + .call(drawLabels, graph, data, filter, dimensions, fullRedraw) .call(drawPoints, graph, data, filter); dispatch.call('drawn', this, {full: true}); diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index 85ab774ca..e09eb40ea 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -1,5 +1,4 @@ import _assign from 'lodash-es/assign'; -import _clone from 'lodash-es/clone'; import _values from 'lodash-es/values'; import { select as d3_select } from 'd3-selection'; @@ -21,12 +20,21 @@ export function svgVertices(projection, context) { fill: [1, 1.5, 1.5, 1.5] }; - var _currHover; - var _currHoverSiblings = {}; + var _currHoverTarget; + var _currPersistent = {}; + var _currHover = {}; + var _prevHover = {}; + var _currSelected = {}; + var _prevSelected = {}; - function draw(selection, graph, vertices, klass, siblings, filter) { - siblings = siblings || {}; + function sortY(a, b) { + return b.loc[1] - a.loc[1]; + } + + + function draw(selection, graph, vertices, sets, filter) { + sets = sets || { selected: {}, important: {}, hovered: {} }; var icons = {}; var directions = {}; var wireframe = context.surface().classed('fill-wireframe'); @@ -54,16 +62,8 @@ export function svgVertices(projection, context) { } - function setClass(klass) { - return function(entity) { - d3_select(this) - .attr('class', 'node vertex ' + klass + ' ' + entity.id); - }; - } - - function updateAttributes(selection) { - ['shadow','stroke','fill'].forEach(function(klass) { + ['shadow', 'stroke', 'fill'].forEach(function(klass) { var rads = radiuses[klass]; selection.selectAll('.' + klass) .each(function(entity) { @@ -88,8 +88,9 @@ export function svgVertices(projection, context) { .attr('visibility', (z === 0 ? 'hidden' : null)); } + vertices.sort(sortY); - var groups = selection.selectAll('.vertex.' + klass) + var groups = selection.selectAll('g.vertex') .filter(filter) .data(vertices, osmEntity.key); @@ -100,41 +101,42 @@ export function svgVertices(projection, context) { // enter var enter = groups.enter() .append('g') - .attr('class', function(d) { return 'node vertex ' + klass + ' ' + d.id; }); + .attr('class', function(d) { return 'node vertex ' + d.id; }) + .order(); enter .append('circle') - .each(setClass('shadow')); + .attr('class', 'shadow'); enter .append('circle') - .each(setClass('stroke')); + .attr('class', 'stroke'); // Vertices with icons get a `use`. enter.filter(function(d) { return getIcon(d); }) .append('use') + .attr('class', 'icon') + .attr('width', '11px') + .attr('height', '11px') .attr('transform', 'translate(-5, -6)') .attr('xlink:href', function(d) { var picon = getIcon(d); var isMaki = dataFeatureIcons.indexOf(picon) !== -1; return '#' + picon + (isMaki ? '-11' : ''); - }) - .attr('width', '11px') - .attr('height', '11px') - .each(setClass('icon')); + }); // Vertices with tags get a fill. enter.filter(function(d) { return d.hasInterestingTags(); }) .append('circle') - .each(setClass('fill')); + .attr('class', 'fill'); // update groups = groups .merge(enter) .attr('transform', svgPointTransform(projection)) - .classed('sibling', function(entity) { return entity.id in siblings; }) - .classed('shared', function(entity) { return graph.isShared(entity); }) - .classed('endpoint', function(entity) { return entity.isEndpoint(graph); }) + .classed('sibling', function(d) { return d.id in sets.selected; }) + .classed('shared', function(d) { return graph.isShared(d); }) + .classed('endpoint', function(d) { return d.isEndpoint(graph); }) .call(updateAttributes); @@ -150,7 +152,7 @@ export function svgVertices(projection, context) { // enter/update dgroups = dgroups.enter() .insert('g', '.shadow') - .each(setClass('viewfieldgroup')) + .attr('class', 'viewfieldgroup') .merge(dgroups); var viewfields = dgroups.selectAll('.viewfield') @@ -174,6 +176,7 @@ export function svgVertices(projection, context) { function drawTargets(selection, graph, entities, filter) { var debugClass = 'pink'; var targets = selection.selectAll('.target') + .filter(filter) .data(entities, osmEntity.key); // exit @@ -202,7 +205,7 @@ export function svgVertices(projection, context) { } - function getSiblingAndChildVertices(ids, graph, extent, wireframe, zoom) { + function getSiblingAndChildVertices(ids, graph, wireframe, zoom) { var results = {}; function addChildVertices(entity) { @@ -223,7 +226,7 @@ export function svgVertices(projection, context) { addChildVertices(member); } } - } else if (renderAsVertex(entity, graph, wireframe, zoom) && entity.intersects(extent, graph)) { + } else if (renderAsVertex(entity, graph, wireframe, zoom)) { results[entity.id] = entity; } } @@ -249,66 +252,98 @@ export function svgVertices(projection, context) { } - function drawVertices(selection, graph, entities, filter, extent) { + function drawVertices(selection, graph, entities, filter, extent, fullRedraw) { var wireframe = context.surface().classed('fill-wireframe'); var zoom = ktoz(projection.scale()); + var mode = context.mode(); + var isDrawing = mode && /^(add|draw|drag)/.test(mode.id); - var selected = getSiblingAndChildVertices(context.selectedIDs(), graph, extent, wireframe, zoom); + // Collect important vertices from the `entities` list.. + // (during a paritial redraw, it will not contain everything) + if (fullRedraw) { + _currPersistent = {}; + } - // interesting vertices from the `entities` list.. - var interesting = {}; for (var i = 0; i < entities.length; i++) { var entity = entities[i]; var geometry = entity.geometry(graph); if ((geometry === 'point') && renderAsVertex(entity, graph, wireframe, zoom)) { - interesting[entity.id] = entity; + _currPersistent[entity.id] = entity; } else if ((geometry === 'vertex') && (entity.hasInterestingTags() || entity.isEndpoint(graph) || entity.isConnected(graph)) ) { - interesting[entity.id] = entity; + _currPersistent[entity.id] = entity; + + } else if (!fullRedraw) { + delete _currPersistent[entity.id]; } } - // 3 sets of vertices to consider - // - selected + siblings - // - hovered + siblings - // - interesting entities passed in - var all = _assign(selected, interesting, _currHoverSiblings); + // 3 sets of vertices to consider: + var sets = { + persistent: _currPersistent, // persistent = important vertices (render always) + selected: _currSelected, // selected + siblings of selected (render always) + hovered: _currHover // hovered + siblings of hovered (render only in draw modes) + }; - var filterWithSiblings = function(d) { - return d.id in selected || d.id in _currHoverSiblings || filter(d); + var all = _assign({}, _currPersistent, _currSelected, (isDrawing ? _currHover : {})); + + // Draw the vertices.. + // The filter function controls the scope of what objects d3 will touch (exit/enter/update) + // It's important to adjust the filter function to expand the scope beyond whatever entities were passed in. + var filterRendered = function(d) { + return d.id in _currPersistent || d.id in _currSelected || d.id in _currHover || filter(d); }; selection.selectAll('.layer-points .layer-points-vertices') - .call(draw, graph, _values(all), 'vertex-persistent', {}, filterWithSiblings); + .call(draw, graph, visible(all), sets, filterRendered); - - // draw touch targets for the hovered items only - var filterWithHover = function(d) { - return d.id in _currHoverSiblings || filter(d); - }; + // Draw touch targets.. selection.selectAll('.layer-points .layer-points-targets') - .call(drawTargets, graph, _values(_currHoverSiblings), filterWithHover); + .call(drawTargets, graph, visible(all), filterRendered); + + + function visible(which) { + return _values(which).filter(function (entity) { + return entity.intersects(extent, graph); + }); + } } + // partial redraw - only update the selected items.. + drawVertices.drawSelected = function(selection, graph, target, extent) { + var wireframe = context.surface().classed('fill-wireframe'); + var zoom = ktoz(projection.scale()); + + _prevSelected = _currSelected || {}; + _currSelected = getSiblingAndChildVertices(context.selectedIDs(), graph, wireframe, zoom); + + // note that drawVertices will add `_currSelected` automatically if needed.. + var filter = function(d) { return d.id in _prevSelected; }; + drawVertices(selection, graph, _values(_prevSelected), filter, extent, false); + }; + + + // partial redraw - only update the hovered items.. drawVertices.drawHover = function(selection, graph, target, extent) { - if (target === _currHover) return; + if (target === _currHoverTarget) return; // continue only if something changed var wireframe = context.surface().classed('fill-wireframe'); var zoom = ktoz(projection.scale()); - var prevHoverSiblings = _currHoverSiblings || {}; - var filter = function(d) { return d.id in prevHoverSiblings; }; - _currHover = target; + _prevHover = _currHover || {}; + _currHoverTarget = target; - if (_currHover) { - _currHoverSiblings = getSiblingAndChildVertices([_currHover.id], graph, extent, wireframe, zoom); + if (_currHoverTarget) { + _currHover = getSiblingAndChildVertices([_currHoverTarget.id], graph, wireframe, zoom); } else { - _currHoverSiblings = {}; + _currHover = {}; } - drawVertices(selection, graph, _values(prevHoverSiblings), filter, extent); + // note that drawVertices will add `_currHover` automatically if needed.. + var filter = function(d) { return d.id in _prevHover; }; + drawVertices(selection, graph, _values(_prevHover), filter, extent, false); }; return drawVertices; diff --git a/test/spec/svg/midpoints.js b/test/spec/svg/midpoints.js index bb4442415..230ad8446 100644 --- a/test/spec/svg/midpoints.js +++ b/test/spec/svg/midpoints.js @@ -20,7 +20,7 @@ describe('iD.svgMidpoints', function () { selectedIDs: function() { return _selectedIDs; } }); - var map = d3.select(document.createElement('div')) + d3.select(document.createElement('div')) .attr('id', 'map') .call(context.map().centerZoom([0, 0], 17)); From f0e2d3fbfe8a5a73843287324244c7d762650ae3 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 15 Dec 2017 17:50:00 -0500 Subject: [PATCH 27/70] Slightly adjust numbers for iconified vertices for better centering (maybe not changed after we switched to maki2 which has 11px icons) --- modules/svg/vertices.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index e09eb40ea..fa6634304 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -15,8 +15,8 @@ function ktoz(k) { return Math.log(k * TAU) / Math.LN2 - 8; } export function svgVertices(projection, context) { var radiuses = { // z16-, z17, z18+, tagged - shadow: [6, 7.5, 7.5, 11.5], - stroke: [2.5, 3.5, 3.5, 7], + shadow: [6, 7.5, 7.5, 12], + stroke: [2.5, 3.5, 3.5, 8], fill: [1, 1.5, 1.5, 1.5] }; @@ -68,7 +68,6 @@ export function svgVertices(projection, context) { selection.selectAll('.' + klass) .each(function(entity) { var i = z && getIcon(entity); - var c = i ? 0.5 : 0; var r = rads[i ? 3 : z]; // slightly increase the size of unconnected endpoints #3775 @@ -77,8 +76,6 @@ export function svgVertices(projection, context) { } d3_select(this) - .attr('cx', c) - .attr('cy', -c) .attr('r', r) .attr('visibility', ((i && klass === 'fill') ? 'hidden' : null)); }); @@ -118,7 +115,7 @@ export function svgVertices(projection, context) { .attr('class', 'icon') .attr('width', '11px') .attr('height', '11px') - .attr('transform', 'translate(-5, -6)') + .attr('transform', 'translate(-5.5, -5.5)') .attr('xlink:href', function(d) { var picon = getIcon(d); var isMaki = dataFeatureIcons.indexOf(picon) !== -1; From 6644c9db6b4129e0647df8ea33ad50b56533f49d Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 15 Dec 2017 18:17:12 -0500 Subject: [PATCH 28/70] Fix context.js and vertices.js tests --- test/spec/core/context.js | 3 ++- test/spec/svg/vertices.js | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/test/spec/core/context.js b/test/spec/core/context.js index 04f665400..df3fbfdbf 100644 --- a/test/spec/core/context.js +++ b/test/spec/core/context.js @@ -59,7 +59,8 @@ describe('iD.Context', function() { collision: false, imagery: false, imperial: false, - driveLeft: false + driveLeft: false, + target: false }; expect(context.debugFlags()).to.eql(flags); diff --git a/test/spec/svg/vertices.js b/test/spec/svg/vertices.js index 6ab4d4e3b..3f7789643 100644 --- a/test/spec/svg/vertices.js +++ b/test/spec/svg/vertices.js @@ -24,8 +24,10 @@ describe('iD.svgVertices', function () { var way1 = iD.osmWay({nodes: [node.id], tags: {highway: 'residential'}}); var way2 = iD.osmWay({nodes: [node.id], tags: {highway: 'residential'}}); var graph = iD.coreGraph([node, way1, way2]); + var filter = function() { return true; }; + var extent = iD.geoExtent([0, 0], [1, 1]); - surface.call(iD.svgVertices(projection, context), graph, [node]); + surface.call(iD.svgVertices(projection, context), graph, [node], filter, extent); expect(surface.select('.vertex').classed('shared')).to.be.true; }); }); From ba7437b4cc62f9a1854863aade47ef947273c3b5 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 15 Dec 2017 21:21:48 -0500 Subject: [PATCH 29/70] Fix order of vertex in _assign, always get latest entity (selected or hovered entities are old, if we're moving vertices around) --- modules/modes/drag_node.js | 2 +- modules/svg/vertices.js | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index bd5bb5a23..748bc97b9 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -249,7 +249,7 @@ export function modeDragNode(context) { var behavior = behaviorDrag() - .selector('g.node, g.midpoint') + .selector('.vertex.target, g.point, g.midpoint') .surface(d3_select('#map').node()) .origin(origin) .on('start', start) diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index fa6634304..f494e5328 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -284,7 +284,7 @@ export function svgVertices(projection, context) { hovered: _currHover // hovered + siblings of hovered (render only in draw modes) }; - var all = _assign({}, _currPersistent, _currSelected, (isDrawing ? _currHover : {})); + var all = _assign({}, (isDrawing ? _currHover : {}), _currSelected, _currPersistent); // Draw the vertices.. // The filter function controls the scope of what objects d3 will touch (exit/enter/update) @@ -293,17 +293,17 @@ export function svgVertices(projection, context) { return d.id in _currPersistent || d.id in _currSelected || d.id in _currHover || filter(d); }; selection.selectAll('.layer-points .layer-points-vertices') - .call(draw, graph, visible(all), sets, filterRendered); + .call(draw, graph, currentVisible(all), sets, filterRendered); // Draw touch targets.. selection.selectAll('.layer-points .layer-points-targets') - .call(drawTargets, graph, visible(all), filterRendered); + .call(drawTargets, graph, currentVisible(all), filterRendered); - function visible(which) { - return _values(which).filter(function (entity) { - return entity.intersects(extent, graph); - }); + function currentVisible(which) { + return Object.keys(which) + .map(context.hasEntity) // the current version of this entity + .filter(function (entity) { return entity && entity.intersects(extent, graph); }); } } From 9d42d470caf61f599a1bdff372c7bd79b47163ef Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 15 Dec 2017 21:26:58 -0500 Subject: [PATCH 30/70] Touch targets can be keyed on entity.id instead of osmEntity.key to avoid excessive exit/enter flickering --- modules/svg/vertices.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index f494e5328..d80302a0b 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -174,7 +174,7 @@ export function svgVertices(projection, context) { var debugClass = 'pink'; var targets = selection.selectAll('.target') .filter(filter) - .data(entities, osmEntity.key); + .data(entities, function key(d) { return d.id; }); // exit targets.exit() From 5cb5456869a07c0de38af4e16be50f907c3210cb Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 15 Dec 2017 22:50:55 -0500 Subject: [PATCH 31/70] Implement touch targets for midpoints and points --- css/20_map.css | 22 +++--------- modules/modes/drag_node.js | 2 +- modules/svg/midpoints.js | 69 ++++++++++++++++++++++++++++---------- modules/svg/points.js | 36 ++++++++++++++++++-- modules/svg/vertices.js | 12 +++---- 5 files changed, 98 insertions(+), 43 deletions(-) diff --git a/css/20_map.css b/css/20_map.css index cbff2a7f5..505b93f9b 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -7,19 +7,19 @@ use { pointer-events: none; } /* the above fill: none rule affects paths in shadow dom only in Firefox */ .layer-osm use.icon path { fill: #333; } /* FF svg Maki icons */ .layer-osm .turn use path { fill: #000; } /* FF turn restriction icons */ -#turn-only-shape2, #turn-only-u-shape2 { fill: #7092FF; } /* FF turn-only, turn-only-u */ -#turn-no-shape2, #turn-no-u-shape2 { fill: #E06D5F; } /* FF turn-no, turn-no-u */ -#turn-yes-shape2, #turn-yes-u-shape2 { fill: #8CD05F; } /* FF turn-yes, turn-yes-u */ +#turn-only-shape2, #turn-only-u-shape2 { fill: #7092ff; } /* FF turn-only, turn-only-u */ +#turn-no-shape2, #turn-no-u-shape2 { fill: #e06d5f; } /* FF turn-no, turn-no-u */ +#turn-yes-shape2, #turn-yes-u-shape2 { fill: #8cd05f; } /* FF turn-yes, turn-yes-u */ .layer-points-group * { pointer-events: none; } -.layer-points-group.layer-points-midpoints *, .layer-points-group.layer-points-targets * { pointer-events: all; } -path.shadow { +.layer-areas path.shadow, +.layer-lines path.shadow { pointer-events: stroke; } @@ -47,11 +47,6 @@ g.point.selected .shadow { stroke-opacity: 0.7; } -/*g.vertex.active, g.vertex.active *, -g.point.active, g.point.active * { - pointer-events: none; -} -*/ g.point ellipse.stroke { display: none; } @@ -108,13 +103,6 @@ g.vertex.selected .shadow { fill-opacity: 0.7; } -.mode-draw-area g.midpoint, -.mode-draw-line g.midpoint, -.mode-add-area g.midpoint, -.mode-add-line g.midpoint, -.mode-add-point g.midpoint { - display: none; -} /* lines */ diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index 748bc97b9..81bc6c22a 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -249,7 +249,7 @@ export function modeDragNode(context) { var behavior = behaviorDrag() - .selector('.vertex.target, g.point, g.midpoint') + .selector('.layer-points-targets .target') .surface(d3_select('#map').node()) .origin(origin) .on('start', start) diff --git a/modules/svg/midpoints.js b/modules/svg/midpoints.js index 27e548ca4..d6aaa0ab9 100644 --- a/modules/svg/midpoints.js +++ b/modules/svg/midpoints.js @@ -15,17 +15,44 @@ import { export function svgMidpoints(projection, context) { - return function drawMidpoints(selection, graph, entities, filter, extent) { + + function drawTargets(selection, graph, entities, filter) { + var debugClass = 'pink'; + var targets = selection.selectAll('.midpoint.target') + .filter(filter) + .data(entities, function key(d) { return d.id; }); + + // exit + targets.exit() + .remove(); + + // enter/update + targets.enter() + .append('circle') + .attr('r', 12) + .attr('class', function(d) { return 'midpoint target ' + d.id; }) + .merge(targets) + .attr('transform', svgPointTransform(projection)) + .classed(debugClass, context.getDebug('target')); + } + + + function drawMidpoints(selection, graph, entities, filter, extent) { var layer = selection.selectAll('.layer-points .layer-points-midpoints'); var mode = context.mode(); if (mode && mode.id !== 'select') { - layer.selectAll('g.midpoint').remove(); + layer.selectAll('g.midpoint') + .remove(); + + selection.selectAll('.layer-points .layer-points-targets .midpoint.target') + .remove(); + return; } - var poly = extent.polygon(), - midpoints = {}; + var poly = extent.polygon(); + var midpoints = {}; for (var i = 0; i < entities.length; i++) { var entity = entities[i]; @@ -40,16 +67,16 @@ export function svgMidpoints(projection, context) { var nodes = graph.childNodes(entity); for (var j = 0; j < nodes.length - 1; j++) { - var a = nodes[j], - b = nodes[j + 1], - id = [a.id, b.id].sort().join('-'); + var a = nodes[j]; + var b = nodes[j + 1]; + var id = [a.id, b.id].sort().join('-'); if (midpoints[id]) { midpoints[id].parents.push(entity); } else { if (geoEuclideanDistance(projection(a.loc), projection(b.loc)) > 40) { - var point = geoInterp(a.loc, b.loc, 0.5), - loc = null; + var point = geoInterp(a.loc, b.loc, 0.5); + var loc = null; if (extent.intersects(point)) { loc = point; @@ -107,22 +134,24 @@ export function svgMidpoints(projection, context) { .insert('g', ':first-child') .attr('class', 'midpoint'); - enter.append('polygon') + enter + .append('polygon') .attr('points', '-6,8 10,0 -6,-8') .attr('class', 'shadow'); - enter.append('polygon') + enter + .append('polygon') .attr('points', '-3,4 5,0 -3,-4') .attr('class', 'fill'); groups = groups .merge(enter) .attr('transform', function(d) { - var translate = svgPointTransform(projection), - a = graph.entity(d.edge[0]), - b = graph.entity(d.edge[1]), - angleVal = Math.round(geoAngle(a, b, projection) * (180 / Math.PI)); - return translate(d) + ' rotate(' + angleVal + ')'; + var translate = svgPointTransform(projection); + var a = graph.entity(d.edge[0]); + var b = graph.entity(d.edge[1]); + var angle = geoAngle(a, b, projection) * (180 / Math.PI); + return translate(d) + ' rotate(' + angle + ')'; }) .call(svgTagClasses().tags( function(d) { return d.parents[0].tags; } @@ -132,5 +161,11 @@ export function svgMidpoints(projection, context) { groups.select('polygon.shadow'); groups.select('polygon.fill'); - }; + + // Draw touch targets.. + selection.selectAll('.layer-points .layer-points-targets') + .call(drawTargets, graph, _values(midpoints), midpointFilter); + } + + return drawMidpoints; } diff --git a/modules/svg/points.js b/modules/svg/points.js index ab8f2361e..8f38733e8 100644 --- a/modules/svg/points.js +++ b/modules/svg/points.js @@ -21,7 +21,31 @@ export function svgPoints(projection, context) { } - return function drawPoints(selection, graph, entities, filter) { + function drawTargets(selection, graph, entities, filter) { + var debugClass = 'pink'; + var targets = selection.selectAll('.point.target') + .filter(filter) + .data(entities, function key(d) { return d.id; }); + + // exit + targets.exit() + .remove(); + + // enter/update + targets.enter() + .append('rect') + .attr('x', -15) + .attr('y', -30) + .attr('width', 30) + .attr('height', 36) + .attr('class', function(d) { return 'node point target ' + d.id; }) + .merge(targets) + .attr('transform', svgPointTransform(projection)) + .classed(debugClass, context.getDebug('target')); + } + + + function drawPoints(selection, graph, entities, filter) { var wireframe = context.surface().classed('fill-wireframe'); var zoom = ktoz(projection.scale()); @@ -95,5 +119,13 @@ export function svgPoints(projection, context) { return '#' + picon + (isMaki ? '-11' : ''); } }); - }; + + + // touch targets + selection.selectAll('.layer-points .layer-points-targets') + .call(drawTargets, graph, points, filter); + } + + + return drawPoints; } diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index d80302a0b..e2231b8c4 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -14,7 +14,7 @@ function ktoz(k) { return Math.log(k * TAU) / Math.LN2 - 8; } export function svgVertices(projection, context) { var radiuses = { - // z16-, z17, z18+, tagged + // z16-, z17, z18+, w/icon shadow: [6, 7.5, 7.5, 12], stroke: [2.5, 3.5, 3.5, 8], fill: [1, 1.5, 1.5, 1.5] @@ -172,7 +172,7 @@ export function svgVertices(projection, context) { function drawTargets(selection, graph, entities, filter) { var debugClass = 'pink'; - var targets = selection.selectAll('.target') + var targets = selection.selectAll('.vertex.target') .filter(filter) .data(entities, function key(d) { return d.id; }); @@ -211,14 +211,14 @@ export function svgVertices(projection, context) { var i; if (entity.type === 'way') { for (i = 0; i < entity.nodes.length; i++) { - var child = context.hasEntity(entity.nodes[i]); + var child = graph.hasEntity(entity.nodes[i]); if (child) { addChildVertices(child); } } } else if (entity.type === 'relation') { for (i = 0; i < entity.members.length; i++) { - var member = context.hasEntity(entity.members[i].id); + var member = graph.hasEntity(entity.members[i].id); if (member) { addChildVertices(member); } @@ -230,7 +230,7 @@ export function svgVertices(projection, context) { } ids.forEach(function(id) { - var entity = context.hasEntity(id); + var entity = graph.hasEntity(id); if (!entity) return; if (entity.type === 'node') { @@ -302,7 +302,7 @@ export function svgVertices(projection, context) { function currentVisible(which) { return Object.keys(which) - .map(context.hasEntity) // the current version of this entity + .map(graph.hasEntity, graph) // the current version of this entity .filter(function (entity) { return entity && entity.intersects(extent, graph); }); } } From 5a4faa84a1083a7d530c9f4d149bc6db56f72a61 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sat, 16 Dec 2017 01:33:12 -0500 Subject: [PATCH 32/70] Fixed some of the point/vertex/midpoint snapping issues Still working on snapping to lines/areas, and making sure drawing lines/areas will complete --- css/20_map.css | 7 +++ css/70_fills.css | 12 ----- modules/behavior/drag.js | 106 +++++++++++++++++++------------------ modules/behavior/draw.js | 88 +++++++++++++++--------------- modules/modes/drag_node.js | 7 ++- 5 files changed, 113 insertions(+), 107 deletions(-) diff --git a/css/20_map.css b/css/20_map.css index 505b93f9b..dc01e7a64 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -18,6 +18,13 @@ use { pointer-events: none; } pointer-events: all; } +/*.mode-draw-line .active, +.mode-draw-area .active, +*/ +.mode-drag-node .active { + pointer-events: none; +} + .layer-areas path.shadow, .layer-lines path.shadow { pointer-events: stroke; diff --git a/css/70_fills.css b/css/70_fills.css index ffb437d68..ec8f78d82 100644 --- a/css/70_fills.css +++ b/css/70_fills.css @@ -38,18 +38,6 @@ /* Modes */ -/*.mode-draw-line .vertex.active, -.mode-draw-area .vertex.active, -.mode-drag-node .vertex.active { - display: none; -} -*/ -/*.mode-draw-line .active, -.mode-draw-area .active, -.mode-drag-node .active { - pointer-events: none; -} -*/ /* Ensure drawing doesn't interact with area fills. */ .mode-add-point path.area.fill, .mode-draw-line path.area.fill, diff --git a/modules/behavior/drag.js b/modules/behavior/drag.js index ebd2ea706..52e9fb7ff 100644 --- a/modules/behavior/drag.js +++ b/modules/behavior/drag.js @@ -33,17 +33,19 @@ import { */ export function behaviorDrag() { - var event = d3_dispatch('start', 'move', 'end'), - origin = null, - selector = '', - filter = null, - event_, target, surface; + var dispatch = d3_dispatch('start', 'move', 'end'); + var _origin = null; + var _selector = ''; + var _filter = null; + var _event; + var _target; + var _surface; - var d3_event_userSelectProperty = utilPrefixCSSProperty('UserSelect'), - d3_event_userSelectSuppress = function() { - var selection = d3_selection(), - select = selection.style(d3_event_userSelectProperty); + var d3_event_userSelectProperty = utilPrefixCSSProperty('UserSelect'); + var d3_event_userSelectSuppress = function() { + var selection = d3_selection(); + var select = selection.style(d3_event_userSelectProperty); selection.style(d3_event_userSelectProperty, 'none'); return function() { selection.style(d3_event_userSelectProperty, select); @@ -60,29 +62,29 @@ export function behaviorDrag() { function eventOf(thiz, argumentz) { return function(e1) { e1.target = drag; - d3_customEvent(e1, event.apply, event, [e1.type, thiz, argumentz]); + d3_customEvent(e1, dispatch.apply, dispatch, [e1.type, thiz, argumentz]); }; } function dragstart() { - target = this; - event_ = eventOf(target, arguments); + _target = this; + _event = eventOf(_target, arguments); - var eventTarget = d3_event.target, - touchId = d3_event.touches ? d3_event.changedTouches[0].identifier : null, - offset, - origin_ = point(), - started = false, - selectEnable = d3_event_userSelectSuppress(touchId !== null ? 'drag-' + touchId : 'drag'); + var eventTarget = d3_event.target; + var touchId = d3_event.touches ? d3_event.changedTouches[0].identifier : null; + var offset; + var startOrigin = point(); + var started = false; + var selectEnable = d3_event_userSelectSuppress(touchId !== null ? 'drag-' + touchId : 'drag'); d3_select(window) .on(touchId !== null ? 'touchmove.drag-' + touchId : 'mousemove.drag', dragmove) .on(touchId !== null ? 'touchend.drag-' + touchId : 'mouseup.drag', dragend, true); - if (origin) { - offset = origin.apply(target, arguments); - offset = [offset[0] - origin_[0], offset[1] - origin_[1]]; + if (_origin) { + offset = _origin.apply(_target, arguments); + offset = [offset[0] - startOrigin[0], offset[1] - startOrigin[1]]; } else { offset = [0, 0]; } @@ -93,7 +95,7 @@ export function behaviorDrag() { function point() { - var p = surface || target.parentNode; + var p = _surface || _target.parentNode; return touchId !== null ? d3_touches(p).filter(function(p) { return p.identifier === touchId; })[0] : d3_mouse(p); @@ -101,22 +103,22 @@ export function behaviorDrag() { function dragmove() { - var p = point(), - dx = p[0] - origin_[0], - dy = p[1] - origin_[1]; + var p = point(); + var dx = p[0] - startOrigin[0]; + var dy = p[1] - startOrigin[1]; if (dx === 0 && dy === 0) return; if (!started) { started = true; - event_({ type: 'start' }); + _event({ type: 'start' }); } - origin_ = p; + startOrigin = p; d3_eventCancel(); - event_({ + _event({ type: 'move', point: [p[0] + offset[0], p[1] + offset[1]], delta: [dx, dy] @@ -126,7 +128,7 @@ export function behaviorDrag() { function dragend() { if (started) { - event_({ type: 'end' }); + _event({ type: 'end' }); d3_eventCancel(); if (d3_event.target === eventTarget) { @@ -152,16 +154,16 @@ export function behaviorDrag() { function drag(selection) { - var matchesSelector = utilPrefixDOMProperty('matchesSelector'), - delegate = dragstart; + var matchesSelector = utilPrefixDOMProperty('matchesSelector'); + var delegate = dragstart; - if (selector) { + if (_selector) { delegate = function() { - var root = this, - target = d3_event.target; + var root = this; + var target = d3_event.target; for (; target && target !== root; target = target.parentNode) { - if (target[matchesSelector](selector) && - (!filter || filter(target.__data__))) { + if (target[matchesSelector](_selector) && + (!_filter || _filter(target.__data__))) { return dragstart.call(target, target.__data__); } } @@ -169,35 +171,35 @@ export function behaviorDrag() { } selection - .on('mousedown.drag' + selector, delegate) - .on('touchstart.drag' + selector, delegate); + .on('mousedown.drag' + _selector, delegate) + .on('touchstart.drag' + _selector, delegate); } drag.off = function(selection) { selection - .on('mousedown.drag' + selector, null) - .on('touchstart.drag' + selector, null); + .on('mousedown.drag' + _selector, null) + .on('touchstart.drag' + _selector, null); }; drag.selector = function(_) { - if (!arguments.length) return selector; - selector = _; + if (!arguments.length) return _selector; + _selector = _; return drag; }; drag.filter = function(_) { - if (!arguments.length) return origin; - filter = _; + if (!arguments.length) return _filter; + _filter = _; return drag; }; drag.origin = function (_) { - if (!arguments.length) return origin; - origin = _; + if (!arguments.length) return _origin; + _origin = _; return drag; }; @@ -211,19 +213,19 @@ export function behaviorDrag() { drag.target = function() { - if (!arguments.length) return target; - target = arguments[0]; - event_ = eventOf(target, Array.prototype.slice.call(arguments, 1)); + if (!arguments.length) return _target; + _target = arguments[0]; + _event = eventOf(_target, Array.prototype.slice.call(arguments, 1)); return drag; }; drag.surface = function() { - if (!arguments.length) return surface; - surface = arguments[0]; + if (!arguments.length) return _surface; + _surface = arguments[0]; return drag; }; - return utilRebind(drag, event, 'on'); + return utilRebind(drag, dispatch, 'on'); } diff --git a/modules/behavior/draw.js b/modules/behavior/draw.js index 62c6031f6..f4629d7f5 100644 --- a/modules/behavior/draw.js +++ b/modules/behavior/draw.js @@ -20,31 +20,35 @@ import { import { utilRebind } from '../util/rebind'; -var usedTails = {}; -var disableSpace = false; -var lastSpace = null; +var _usedTails = {}; +var _disableSpace = false; +var _lastSpace = null; export function behaviorDraw(context) { - var dispatch = d3_dispatch('move', 'click', 'clickWay', - 'clickNode', 'undo', 'cancel', 'finish'), - keybinding = d3_keybinding('draw'), - hover = behaviorHover(context) - .altDisables(true) - .on('hover', context.ui().sidebar.hover), - tail = behaviorTail(), - edit = behaviorEdit(context), - closeTolerance = 4, - tolerance = 12, - mouseLeave = false, - lastMouse = null; + var dispatch = d3_dispatch( + 'move', 'click', 'clickWay', 'clickNode', 'undo', 'cancel', 'finish' + ); + + var keybinding = d3_keybinding('draw'); + + var hover = behaviorHover(context) + .altDisables(true) + .on('hover', context.ui().sidebar.hover); + var tail = behaviorTail(); + var edit = behaviorEdit(context); + + var closeTolerance = 4; + var tolerance = 12; + var _mouseLeave = false; + var _lastMouse = null; function datum() { if (d3_event.altKey) return {}; if (d3_event.type === 'keydown') { - return (lastMouse && lastMouse.target.__data__) || {}; + return (_lastMouse && _lastMouse.target.__data__) || {}; } else { return d3_event.target.__data__ || {}; } @@ -60,17 +64,17 @@ export function behaviorDraw(context) { })[0] : d3_mouse(p); } - var element = d3_select(this), - touchId = d3_event.touches ? d3_event.changedTouches[0].identifier : null, - t1 = +new Date(), - p1 = point(); + var element = d3_select(this); + var touchId = d3_event.touches ? d3_event.changedTouches[0].identifier : null; + var t1 = +new Date(); + var p1 = point(); element.on('mousemove.draw', null); d3_select(window).on('mouseup.draw', function() { - var t2 = +new Date(), - p2 = point(), - dist = geoEuclideanDistance(p1, p2); + var t2 = +new Date(); + var p2 = point(); + var dist = geoEuclideanDistance(p1, p2); element.on('mousemove.draw', mousemove); d3_select(window).on('mouseup.draw', null); @@ -95,33 +99,33 @@ export function behaviorDraw(context) { function mousemove() { - lastMouse = d3_event; + _lastMouse = d3_event; dispatch.call('move', this, datum()); } function mouseenter() { - mouseLeave = false; + _mouseLeave = false; } function mouseleave() { - mouseLeave = true; + _mouseLeave = true; } function click() { var d = datum(); if (d.type === 'way') { - var dims = context.map().dimensions(), - mouse = context.mouse(), - pad = 5, - trySnap = mouse[0] > pad && mouse[0] < dims[0] - pad && + var dims = context.map().dimensions(); + var mouse = context.mouse(); + var pad = 5; + var trySnap = mouse[0] > pad && mouse[0] < dims[0] - pad && mouse[1] > pad && mouse[1] < dims[1] - pad; if (trySnap) { - var choice = geoChooseEdge(context.childNodes(d), context.mouse(), context.projection), - edge = [d.nodes[choice.index - 1], d.nodes[choice.index]]; + var choice = geoChooseEdge(context.childNodes(d), context.mouse(), context.projection); + var edge = [d.nodes[choice.index - 1], d.nodes[choice.index]]; dispatch.call('clickWay', this, choice.loc, edge); } else { dispatch.call('click', this, context.map().mouseCoordinates()); @@ -141,23 +145,23 @@ export function behaviorDraw(context) { d3_event.stopPropagation(); var currSpace = context.mouse(); - if (disableSpace && lastSpace) { - var dist = geoEuclideanDistance(lastSpace, currSpace); + if (_disableSpace && _lastSpace) { + var dist = geoEuclideanDistance(_lastSpace, currSpace); if (dist > tolerance) { - disableSpace = false; + _disableSpace = false; } } - if (disableSpace || mouseLeave || !lastMouse) return; + if (_disableSpace || _mouseLeave || !_lastMouse) return; // user must move mouse or release space bar to allow another click - lastSpace = currSpace; - disableSpace = true; + _lastSpace = currSpace; + _disableSpace = true; d3_select(window).on('keyup.space-block', function() { d3_event.preventDefault(); d3_event.stopPropagation(); - disableSpace = false; + _disableSpace = false; d3_select(window).on('keyup.space-block', null); }); @@ -187,7 +191,7 @@ export function behaviorDraw(context) { context.install(hover); context.install(edit); - if (!context.inIntro() && !usedTails[tail.text()]) { + if (!context.inIntro() && !_usedTails[tail.text()]) { context.install(tail); } @@ -217,9 +221,9 @@ export function behaviorDraw(context) { context.uninstall(hover); context.uninstall(edit); - if (!context.inIntro() && !usedTails[tail.text()]) { + if (!context.inIntro() && !_usedTails[tail.text()]) { context.uninstall(tail); - usedTails[tail.text()] = true; + _usedTails[tail.text()] = true; } selection diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index 81bc6c22a..2a5824170 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -161,10 +161,15 @@ export function modeDragNode(context) { var d = datum(); if (!_nudgeInterval) { + // try to snap if (d.type === 'node' && d.id !== entity.id) { loc = d.loc; } else if (d.type === 'way' && !d3_select(d3_event.sourceEvent.target).classed('fill')) { - loc = geoChooseEdge(context.childNodes(d), context.mouse(), context.projection).loc; + var childNodes = context.childNodes(d); + var childIDs = childNodes.map(function(node) { return node.id; }); + if (childIDs.indexOf(entity.id) === -1) { + loc = geoChooseEdge(childNodes, context.mouse(), context.projection).loc; + } } } From 5e99f3cbacaadd3cf6566614a4389d8f9b6bd080 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sat, 16 Dec 2017 11:32:26 -0500 Subject: [PATCH 33/70] CSS pointer-events cleanup, also round linejoins --- css/20_map.css | 41 ++++++++++++++++++++--------------------- css/70_fills.css | 13 ------------- 2 files changed, 20 insertions(+), 34 deletions(-) diff --git a/css/20_map.css b/css/20_map.css index dc01e7a64..9f399ce4e 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -1,34 +1,36 @@ - -use { pointer-events: none; } - /* base styles */ .layer-osm path:not(.oneway) { fill: none; } /* IE needs :not(.oneway) */ /* the above fill: none rule affects paths in shadow dom only in Firefox */ .layer-osm use.icon path { fill: #333; } /* FF svg Maki icons */ .layer-osm .turn use path { fill: #000; } /* FF turn restriction icons */ -#turn-only-shape2, #turn-only-u-shape2 { fill: #7092ff; } /* FF turn-only, turn-only-u */ -#turn-no-shape2, #turn-no-u-shape2 { fill: #e06d5f; } /* FF turn-no, turn-no-u */ -#turn-yes-shape2, #turn-yes-u-shape2 { fill: #8cd05f; } /* FF turn-yes, turn-yes-u */ +#turn-only-shape2, #turn-only-u-shape2 { fill: #7092ff; } /* FF turn-only, turn-only-u */ +#turn-no-shape2, #turn-no-u-shape2 { fill: #e06d5f; } /* FF turn-no, turn-no-u */ +#turn-yes-shape2, #turn-yes-u-shape2 { fill: #8cd05f; } /* FF turn-yes, turn-yes-u */ -.layer-points-group * { - pointer-events: none; -} -.layer-points-group.layer-points-targets * { - pointer-events: all; -} -/*.mode-draw-line .active, -.mode-draw-area .active, -*/ -.mode-drag-node .active { +/* No interactivity except what we specifically allow */ +.layer-osm * { pointer-events: none; } +/* Line/Area shadows and point/vertex targets are interactive */ +/* They can be picked up, clicked, hovered, or things can connect to them */ .layer-areas path.shadow, .layer-lines path.shadow { pointer-events: stroke; } +.layer-points-targets * { + pointer-events: all; +} + +/* .active objects (currently being drawn or dragged) are not interactive */ +/* This is important to allow the events to drop through to whatever is */ +/* below them on the map, so you can still hover and connect to other things. */ +.layer-osm .active { + pointer-events: none !important; +} + /* points */ @@ -120,7 +122,7 @@ g.vertex.selected .shadow { path.line { stroke-linecap: round; - stroke-linejoin: bevel; + stroke-linejoin: round; } path.stroke { @@ -152,8 +154,7 @@ path.line.stroke { /* Labels / Markers */ text { - font-size:10px; - pointer-events: none; + font-size: 10px; color: #222; opacity: 1; } @@ -178,7 +179,6 @@ text.pointlabel { font-size: 12px; font-weight: bold; fill: #333; - pointer-events: none; -webkit-transition: opacity 100ms linear; transition: opacity 100ms linear; -moz-transition: opacity 100ms linear; @@ -229,7 +229,6 @@ g.turn circle { } .form-field-restrictions .vertex { - pointer-events: none; cursor: auto !important; } diff --git a/css/70_fills.css b/css/70_fills.css index ec8f78d82..34947b232 100644 --- a/css/70_fills.css +++ b/css/70_fills.css @@ -33,17 +33,4 @@ .fill-partial path.area.fill { fill-opacity: 0; stroke-width: 60px; - pointer-events: visibleStroke; -} - -/* Modes */ - -/* Ensure drawing doesn't interact with area fills. */ -.mode-add-point path.area.fill, -.mode-draw-line path.area.fill, -.mode-draw-area path.area.fill, -.mode-add-line path.area.fill, -.mode-add-area path.area.fill, -.mode-drag-node path.area.fill { - pointer-events: none; } From aa68b21d7a033c714577e2d1d343d68c9177dac1 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sun, 17 Dec 2017 22:53:58 -0500 Subject: [PATCH 34/70] Add touch targets for line/area --- css/20_map.css | 29 +++++++++++---------- css/70_fills.css | 1 + css/80_app.css | 1 + modules/behavior/draw.js | 3 +++ modules/modes/drag_node.js | 24 +++++++++++------ modules/svg/areas.js | 35 ++++++++++++++++++++++--- modules/svg/lines.js | 53 ++++++++++++++++++++++++++++---------- modules/svg/midpoints.js | 7 +++-- modules/svg/osm.js | 12 +++++++++ modules/svg/points.js | 15 +++++------ modules/svg/vertices.js | 7 +++-- 11 files changed, 132 insertions(+), 55 deletions(-) diff --git a/css/20_map.css b/css/20_map.css index 9f399ce4e..38106a223 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -14,17 +14,24 @@ pointer-events: none; } -/* Line/Area shadows and point/vertex targets are interactive */ +/* `.target` objects are interactive */ /* They can be picked up, clicked, hovered, or things can connect to them */ -.layer-areas path.shadow, -.layer-lines path.shadow { - pointer-events: stroke; -} -.layer-points-targets * { - pointer-events: all; +.node.target { + pointer-events: fill; + fill-opacity: 0.8; + fill: currentColor; + stroke: none; } -/* .active objects (currently being drawn or dragged) are not interactive */ +.way.target { + pointer-events: stroke; + fill: none; + stroke-width: 10; + stroke-opacity: 0.8; + stroke: currentColor; +} + +/* `.active` objects (currently being drawn or dragged) are not interactive */ /* This is important to allow the events to drop through to whatever is */ /* below them on the map, so you can still hover and connect to other things. */ .layer-osm .active { @@ -95,12 +102,6 @@ g.midpoint .shadow { fill-opacity: 0; } -.target { - color: rgba(0,0,0,0); - fill-opacity: 0.8; - fill: currentColor; -} - g.vertex.related:not(.selected) .shadow, g.vertex.hover:not(.selected) .shadow, g.midpoint.related:not(.selected) .shadow, diff --git a/css/70_fills.css b/css/70_fills.css index 34947b232..3587bbee8 100644 --- a/css/70_fills.css +++ b/css/70_fills.css @@ -33,4 +33,5 @@ .fill-partial path.area.fill { fill-opacity: 0; stroke-width: 60px; + pointer-events: visibleStroke; } diff --git a/css/80_app.css b/css/80_app.css index 83205fb0b..bc2f268c0 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -2864,6 +2864,7 @@ img.tile-removing { stroke-width: 1; } +.nocolor { color: rgba(0, 0, 0, 0); } .red { color: rgba(255, 0, 0, 0.75); } .green { color: rgba(0, 255, 0, 0.75); } .blue { color: rgba(0, 0, 255, 0.75); } diff --git a/modules/behavior/draw.js b/modules/behavior/draw.js index f4629d7f5..6a6ed2b2b 100644 --- a/modules/behavior/draw.js +++ b/modules/behavior/draw.js @@ -116,6 +116,9 @@ export function behaviorDraw(context) { function click() { var d = datum(); + + // Try to snap.. + // See also: `modes/drag_node.js doMove()` if (d.type === 'way') { var dims = context.map().dimensions(); var mouse = context.mouse(); diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index 2a5824170..90298a485 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -131,8 +131,8 @@ export function modeDragNode(context) { _dragEntity = entity; - // activeIDs generate no pointer events. This prevents the node or vertex - // being dragged from trying to connect to itself or its parent element. + // `.active` elements have `pointer-events: none`. + // This prevents the node or vertex being dragged from trying to connect to itself. _activeIDs = context.graph().parentWays(entity) .map(function(parent) { return parent.id; }); _activeIDs.push(entity.id); @@ -158,17 +158,25 @@ export function modeDragNode(context) { var currPoint = (d3_event && d3_event.point) || context.projection(_lastLoc); var currMouse = vecSub(currPoint, nudge); var loc = context.projection.invert(currMouse); - var d = datum(); if (!_nudgeInterval) { - // try to snap + // If we're not nudging at the edge of the viewport, try to snap.. + // See also `behavior/draw.js click()` + var d = datum(); + + // Snap to a node (not self) if (d.type === 'node' && d.id !== entity.id) { loc = d.loc; + + // Snap to a way (not an area fill) } else if (d.type === 'way' && !d3_select(d3_event.sourceEvent.target).classed('fill')) { - var childNodes = context.childNodes(d); - var childIDs = childNodes.map(function(node) { return node.id; }); - if (childIDs.indexOf(entity.id) === -1) { - loc = geoChooseEdge(childNodes, context.mouse(), context.projection).loc; + + // var childNodes = context.childNodes(d); + // var childIDs = childNodes.map(function(node) { return node.id; }); + var choice = geoChooseEdge(context.childNodes(d), context.mouse(), context.projection); + // (not along a segment adjacent to self) + if (entity.id !== d.nodes[choice.index - 1] && entity.id !== d.nodes[choice.index]) { + loc = choice.loc; } } } diff --git a/modules/svg/areas.js b/modules/svg/areas.js index 0f219738e..d540a08a0 100644 --- a/modules/svg/areas.js +++ b/modules/svg/areas.js @@ -41,7 +41,29 @@ export function svgAreas(projection, context) { } - return function drawAreas(selection, graph, entities, filter) { + function drawTargets(selection, graph, entities, filter) { + var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; + var getPath = svgPath(projection, graph); + + var targets = selection.selectAll('.area.target') + .filter(filter) + .data(entities, function key(d) { return d.id; }); + + // exit + targets.exit() + .remove(); + + // enter/update + targets.enter() + .append('path') + .merge(targets) + .attr('d', getPath) + .attr('class', function(d) { return 'way area target ' + fillClass + d.id; }); + } + + + + function drawAreas(selection, graph, entities, filter) { var path = svgPath(projection, graph, true), areas = {}, multipolygon; @@ -99,7 +121,7 @@ export function svgAreas(projection, context) { .attr('d', path); - var layer = selection.selectAll('.layer-areas'); + var layer = selection.selectAll('.layer-areas .layer-areas-areas'); var areagroup = layer .selectAll('g.areagroup') @@ -145,5 +167,12 @@ export function svgAreas(projection, context) { }) .call(svgTagClasses()) .attr('d', path); - }; + + + // touch targets + selection.selectAll('.layer-areas .layer-areas-targets') + .call(drawTargets, graph, data.stroke, filter); + } + + return drawAreas; } diff --git a/modules/svg/lines.js b/modules/svg/lines.js index 1646f083b..caf11c294 100644 --- a/modules/svg/lines.js +++ b/modules/svg/lines.js @@ -36,13 +36,33 @@ export function svgLines(projection, context) { }; + function drawTargets(selection, graph, entities, filter) { + var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; + var getPath = svgPath(projection, graph); + + var targets = selection.selectAll('.line.target') + .filter(filter) + .data(entities, function key(d) { return d.id; }); + + // exit + targets.exit() + .remove(); + + // enter/update + targets.enter() + .append('path') + .merge(targets) + .attr('d', getPath) + .attr('class', function(d) { return 'way line target ' + fillClass + d.id; }); + } + + function drawLines(selection, graph, entities, filter) { - function waystack(a, b) { - var selected = context.selectedIDs(), - scoreA = selected.indexOf(a.id) !== -1 ? 20 : 0, - scoreB = selected.indexOf(b.id) !== -1 ? 20 : 0; + var selected = context.selectedIDs(); + var scoreA = selected.indexOf(a.id) !== -1 ? 20 : 0; + var scoreB = selected.indexOf(b.id) !== -1 ? 20 : 0; if (a.tags.highway) { scoreA -= highway_stack[a.tags.highway]; } if (b.tags.highway) { scoreB -= highway_stack[b.tags.highway]; } @@ -91,15 +111,15 @@ export function svgLines(projection, context) { } - var getPath = svgPath(projection, graph), - ways = [], - pathdata = {}, - onewaydata = {}, - oldMultiPolygonOuters = {}; + var getPath = svgPath(projection, graph); + var ways = []; + var pathdata = {}; + var onewaydata = {}; + var oldMultiPolygonOuters = {}; for (var i = 0; i < entities.length; i++) { - var entity = entities[i], - outer = osmSimpleMultipolygonOuterMember(entity, graph); + var entity = entities[i]; + var outer = osmSimpleMultipolygonOuterMember(entity, graph); if (outer) { ways.push(entity.mergeTags(outer.tags)); oldMultiPolygonOuters[outer.id] = true; @@ -117,7 +137,7 @@ export function svgLines(projection, context) { }); - var layer = selection.selectAll('.layer-lines'); + var layer = selection.selectAll('.layer-lines .layer-lines-lines'); var layergroup = layer .selectAll('g.layergroup') @@ -164,8 +184,8 @@ export function svgLines(projection, context) { .selectAll('path') .filter(filter) .data( - function() { return onewaydata[this.parentNode.__data__] || []; }, - function(d) { return [d.id, d.index]; } + function data() { return onewaydata[this.parentNode.__data__] || []; }, + function key(d) { return [d.id, d.index]; } ); oneways.exit() @@ -181,6 +201,11 @@ export function svgLines(projection, context) { if (detected.ie) { oneways.each(function() { this.parentNode.insertBefore(this, this); }); } + + + // touch targets + selection.selectAll('.layer-lines .layer-lines-targets') + .call(drawTargets, graph, ways, filter); } diff --git a/modules/svg/midpoints.js b/modules/svg/midpoints.js index d6aaa0ab9..60ff03de8 100644 --- a/modules/svg/midpoints.js +++ b/modules/svg/midpoints.js @@ -17,7 +17,7 @@ export function svgMidpoints(projection, context) { function drawTargets(selection, graph, entities, filter) { - var debugClass = 'pink'; + var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; var targets = selection.selectAll('.midpoint.target') .filter(filter) .data(entities, function key(d) { return d.id; }); @@ -30,10 +30,9 @@ export function svgMidpoints(projection, context) { targets.enter() .append('circle') .attr('r', 12) - .attr('class', function(d) { return 'midpoint target ' + d.id; }) .merge(targets) - .attr('transform', svgPointTransform(projection)) - .classed(debugClass, context.getDebug('target')); + .attr('class', function(d) { return 'node midpoint target ' + fillClass + d.id; }) + .attr('transform', svgPointTransform(projection)); } diff --git a/modules/svg/osm.js b/modules/svg/osm.js index 8a7defe94..db9a26a12 100644 --- a/modules/svg/osm.js +++ b/modules/svg/osm.js @@ -9,6 +9,18 @@ export function svgOsm(projection, context, dispatch) { .append('g') .attr('class', function(d) { return 'layer-osm layer-' + d; }); + selection.selectAll('.layer-areas').selectAll('.layer-areas-group') + .data(['areas', 'targets']) + .enter() + .append('g') + .attr('class', function(d) { return 'layer-areas-group layer-areas-' + d; }); + + selection.selectAll('.layer-lines').selectAll('.layer-lines-group') + .data(['lines', 'targets']) + .enter() + .append('g') + .attr('class', function(d) { return 'layer-lines-group layer-lines-' + d; }); + selection.selectAll('.layer-points').selectAll('.layer-points-group') .data(['points', 'midpoints', 'vertices', 'turns', 'targets']) .enter() diff --git a/modules/svg/points.js b/modules/svg/points.js index 8f38733e8..dfa32e575 100644 --- a/modules/svg/points.js +++ b/modules/svg/points.js @@ -22,7 +22,7 @@ export function svgPoints(projection, context) { function drawTargets(selection, graph, entities, filter) { - var debugClass = 'pink'; + var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; var targets = selection.selectAll('.point.target') .filter(filter) .data(entities, function key(d) { return d.id; }); @@ -34,14 +34,13 @@ export function svgPoints(projection, context) { // enter/update targets.enter() .append('rect') - .attr('x', -15) - .attr('y', -30) - .attr('width', 30) - .attr('height', 36) - .attr('class', function(d) { return 'node point target ' + d.id; }) + .attr('x', -10) + .attr('y', -26) + .attr('width', 20) + .attr('height', 30) .merge(targets) - .attr('transform', svgPointTransform(projection)) - .classed(debugClass, context.getDebug('target')); + .attr('class', function(d) { return 'node point target ' + fillClass + d.id; }) + .attr('transform', svgPointTransform(projection)); } diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index e2231b8c4..35a1ac208 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -171,7 +171,7 @@ export function svgVertices(projection, context) { function drawTargets(selection, graph, entities, filter) { - var debugClass = 'pink'; + var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; var targets = selection.selectAll('.vertex.target') .filter(filter) .data(entities, function key(d) { return d.id; }); @@ -184,10 +184,9 @@ export function svgVertices(projection, context) { targets.enter() .append('circle') .attr('r', radiuses.shadow[3]) // just use the biggest one for now - .attr('class', function(d) { return 'node vertex target ' + d.id; }) .merge(targets) - .attr('transform', svgPointTransform(projection)) - .classed(debugClass, context.getDebug('target')); + .attr('class', function(d) { return 'node vertex target ' + fillClass + d.id; }) + .attr('transform', svgPointTransform(projection)); } From 57ba6a98629e3e48b6cbda20e14f24844cfe2896 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 18 Dec 2017 00:11:08 -0500 Subject: [PATCH 35/70] Add selectedIDs() for modeMove and modeRotate This allows the label collision boxes for the vertices to update during these modes. Otherwise the labels.js `isInterestingVertex()` function doesn't consider them interesting enough to update. --- modules/modes/move.js | 7 +++++++ modules/modes/rotate.js | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/modules/modes/move.js b/modules/modes/move.js index a6571d332..b9ce5b7b3 100644 --- a/modules/modes/move.js +++ b/modules/modes/move.js @@ -192,5 +192,12 @@ export function modeMove(context, entityIDs, baseGraph) { }; + mode.selectedIDs = function() { + if (!arguments.length) return entityIDs; + // no assign + return mode; + }; + + return mode; } diff --git a/modules/modes/rotate.js b/modules/modes/rotate.js index 4efd701dd..6448d5333 100644 --- a/modules/modes/rotate.js +++ b/modules/modes/rotate.js @@ -155,5 +155,12 @@ export function modeRotate(context, entityIDs) { }; + mode.selectedIDs = function() { + if (!arguments.length) return entityIDs; + // no assign + return mode; + }; + + return mode; } From b5eaa76d1a66cb2787d86667742cb60df1553482 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 18 Dec 2017 00:13:16 -0500 Subject: [PATCH 36/70] Optimization: when moving stuff around, don't exit/enter nodes This avoids thrashing the DOM. The positions of the nodes will still get updated by the update selection. --- modules/svg/points.js | 11 ++++++++++- modules/svg/vertices.js | 14 +++++++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/modules/svg/points.js b/modules/svg/points.js index dfa32e575..124dba5bc 100644 --- a/modules/svg/points.js +++ b/modules/svg/points.js @@ -21,6 +21,15 @@ export function svgPoints(projection, context) { } + // Avoid exit/enter if we're just moving stuff around. + // The node will get a new version but we only need to run the update selection. + function fastEntityKey(d) { + var mode = context.mode(); + var isMoving = mode && /^(add|draw|drag|move|rotate)/.test(mode.id); + return isMoving ? d.id : osmEntity.key(d); + } + + function drawTargets(selection, graph, entities, filter) { var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; var targets = selection.selectAll('.point.target') @@ -64,7 +73,7 @@ export function svgPoints(projection, context) { var groups = layer.selectAll('g.point') .filter(filter) - .data(points, osmEntity.key); + .data(points, fastEntityKey); groups.exit() .remove(); diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index 35a1ac208..06ab1e7b6 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -32,6 +32,14 @@ export function svgVertices(projection, context) { return b.loc[1] - a.loc[1]; } + // Avoid exit/enter if we're just moving stuff around. + // The node will get a new version but we only need to run the update selection. + function fastEntityKey(d) { + var mode = context.mode(); + var isMoving = mode && /^(add|draw|drag|move|rotate)/.test(mode.id); + return isMoving ? d.id : osmEntity.key(d); + } + function draw(selection, graph, vertices, sets, filter) { sets = sets || { selected: {}, important: {}, hovered: {} }; @@ -89,7 +97,7 @@ export function svgVertices(projection, context) { var groups = selection.selectAll('g.vertex') .filter(filter) - .data(vertices, osmEntity.key); + .data(vertices, fastEntityKey); // exit groups.exit() @@ -252,7 +260,7 @@ export function svgVertices(projection, context) { var wireframe = context.surface().classed('fill-wireframe'); var zoom = ktoz(projection.scale()); var mode = context.mode(); - var isDrawing = mode && /^(add|draw|drag)/.test(mode.id); + var isMoving = mode && /^(add|draw|drag|move|rotate)/.test(mode.id); // Collect important vertices from the `entities` list.. // (during a paritial redraw, it will not contain everything) @@ -283,7 +291,7 @@ export function svgVertices(projection, context) { hovered: _currHover // hovered + siblings of hovered (render only in draw modes) }; - var all = _assign({}, (isDrawing ? _currHover : {}), _currSelected, _currPersistent); + var all = _assign({}, (isMoving ? _currHover : {}), _currSelected, _currPersistent); // Draw the vertices.. // The filter function controls the scope of what objects d3 will touch (exit/enter/update) From bcd511573f096e3e3acea1fcf37f0fdf440e4c4e Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 18 Dec 2017 00:34:54 -0500 Subject: [PATCH 37/70] Fix extent in turn restrictions viewer, so vertices will render --- modules/ui/fields/restrictions.js | 47 ++++++++++++++++--------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/modules/ui/fields/restrictions.js b/modules/ui/fields/restrictions.js index 579e17aa5..a89c7a9b5 100644 --- a/modules/ui/fields/restrictions.js +++ b/modules/ui/fields/restrictions.js @@ -46,12 +46,12 @@ import { export function uiFieldRestrictions(field, context) { - var dispatch = d3_dispatch('change'), - breathe = behaviorBreathe(context), - hover = behaviorHover(context), - initialized = false, - vertexID, - fromNodeID; + var dispatch = d3_dispatch('change'); + var breathe = behaviorBreathe(context); + var hover = behaviorHover(context); + var initialized = false; + var vertexID; + var fromNodeID; function restrictions(selection) { @@ -73,16 +73,15 @@ export function uiFieldRestrictions(field, context) { .attr('class', 'restriction-help'); - var intersection = osmIntersection(context.graph(), vertexID), - graph = intersection.graph, - vertex = graph.entity(vertexID), - filter = utilFunctor(true), - extent = geoExtent(), - projection = geoRawMercator(); + var intersection = osmIntersection(context.graph(), vertexID); + var graph = intersection.graph; + var vertex = graph.entity(vertexID); + var filter = utilFunctor(true); + var projection = geoRawMercator(); - var d = utilGetDimensions(wrap.merge(enter)), - c = [d[0] / 2, d[1] / 2], - z = 24; + var d = utilGetDimensions(wrap.merge(enter)); + var c = [d[0] / 2, d[1] / 2]; + var z = 24; projection .scale(256 * Math.pow(2, z) / (2 * Math.PI)); @@ -93,10 +92,12 @@ export function uiFieldRestrictions(field, context) { .translate([c[0] - s[0], c[1] - s[1]]) .clipExtent([[0, 0], d]); - var drawLayers = svgLayers(projection, context).only('osm').dimensions(d), - drawVertices = svgVertices(projection, context), - drawLines = svgLines(projection, context), - drawTurns = svgTurns(projection, context); + var extent = geoExtent(projection.invert([0, d[1]]), projection.invert([d[0], 0])); + + var drawLayers = svgLayers(projection, context).only('osm').dimensions(d); + var drawVertices = svgVertices(projection, context); + var drawLines = svgLines(projection, context); + var drawTurns = svgTurns(projection, context); enter .call(drawLayers); @@ -115,7 +116,7 @@ export function uiFieldRestrictions(field, context) { surface .call(utilSetDimensions, d) - .call(drawVertices, graph, [vertex], filter, extent) + .call(drawVertices, graph, [vertex], filter, extent, true) .call(drawLines, graph, intersection.ways, filter) .call(drawTurns, graph, intersection.turns(fromNodeID)); @@ -174,9 +175,9 @@ export function uiFieldRestrictions(field, context) { function mouseover() { var datum = d3_event.target.__data__; if (datum instanceof osmTurn) { - var graph = context.graph(), - presets = context.presets(), - preset; + var graph = context.graph(); + var presets = context.presets(); + var preset; if (datum.restriction) { preset = presets.match(graph.entity(datum.restriction), graph); From 18c97d52c80fe9ebfe46b793556b94e3726b3016 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 18 Dec 2017 09:50:17 -0500 Subject: [PATCH 38/70] Extract viewport nudging code from several places to geoViewportEdge --- modules/behavior/draw.js | 36 +++++++-------- modules/geo/geo.js | 24 ++++++++++ modules/geo/index.js | 1 + modules/modes/drag_node.js | 25 +---------- modules/modes/move.js | 92 +++++++++++++++----------------------- modules/modes/rotate.js | 75 ++++++++++++++++--------------- test/spec/geo/geo.js | 32 +++++++++++++ 7 files changed, 150 insertions(+), 135 deletions(-) diff --git a/modules/behavior/draw.js b/modules/behavior/draw.js index 6a6ed2b2b..61ca00af5 100644 --- a/modules/behavior/draw.js +++ b/modules/behavior/draw.js @@ -14,7 +14,8 @@ import { behaviorTail } from './tail'; import { geoChooseEdge, - geoEuclideanDistance + geoEuclideanDistance, + geoViewportEdge } from '../geo'; import { utilRebind } from '../util/rebind'; @@ -115,31 +116,28 @@ export function behaviorDraw(context) { function click() { - var d = datum(); + var trySnap = geoViewportEdge(context.mouse, context.map().dimensions()) !== null; - // Try to snap.. - // See also: `modes/drag_node.js doMove()` - if (d.type === 'way') { - var dims = context.map().dimensions(); - var mouse = context.mouse(); - var pad = 5; - var trySnap = mouse[0] > pad && mouse[0] < dims[0] - pad && - mouse[1] > pad && mouse[1] < dims[1] - pad; + if (trySnap) { + // If we're not at the edge of the viewport, try to snap.. + // See also: `modes/drag_node.js doMove()` + var d = datum(); - if (trySnap) { + // Snap to a node + if (d.type === 'node') { + dispatch.call('clickNode', this, d); + return; + + // Snap to a way (not an area fill) + } else if (d.type === 'way' && !d3_select(d3_event.sourceEvent.target).classed('fill')) { var choice = geoChooseEdge(context.childNodes(d), context.mouse(), context.projection); var edge = [d.nodes[choice.index - 1], d.nodes[choice.index]]; dispatch.call('clickWay', this, choice.loc, edge); - } else { - dispatch.call('click', this, context.map().mouseCoordinates()); + return; } - - } else if (d.type === 'node') { - dispatch.call('clickNode', this, d); - - } else { - dispatch.call('click', this, context.map().mouseCoordinates()); } + + dispatch.call('click', this, context.map().mouseCoordinates()); } diff --git a/modules/geo/geo.js b/modules/geo/geo.js index 3d083d219..2f74eb40a 100644 --- a/modules/geo/geo.js +++ b/modules/geo/geo.js @@ -275,3 +275,27 @@ export function geoPathLength(path) { } return length; } + + +// If the given point is at the edge of the padded viewport, +// return a vector that will nudge the viewport in that direction +export function geoViewportEdge(point, dimensions) { + var pad = [80, 20, 50, 20]; // top, right, bottom, left + var x = 0; + var y = 0; + + if (point[0] > dimensions[0] - pad[1]) + x = -10; + if (point[0] < pad[3]) + x = 10; + if (point[1] > dimensions[1] - pad[2]) + y = -10; + if (point[1] < pad[0]) + y = 10; + + if (x || y) { + return [x, y]; + } else { + return null; + } +} diff --git a/modules/geo/index.js b/modules/geo/index.js index 5e37b16fe..8966c218b 100644 --- a/modules/geo/index.js +++ b/modules/geo/index.js @@ -21,3 +21,4 @@ export { geoPointInPolygon } from './geo.js'; export { geoPolygonContainsPolygon } from './geo.js'; export { geoPolygonIntersectsPolygon } from './geo.js'; export { geoSphericalDistance } from './geo.js'; +export { geoViewportEdge } from './geo.js'; diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index 90298a485..b52c2da97 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -23,7 +23,7 @@ import { modeSelect } from './index'; -import { geoChooseEdge } from '../geo'; +import { geoChooseEdge, geoViewportEdge } from '../geo'; import { osmNode } from '../osm'; import { utilEntitySelector } from '../util'; import { uiFlash } from '../ui'; @@ -50,27 +50,6 @@ export function modeDragNode(context) { return [a[0] - b[0], a[1] - b[1]]; } - function edge(point, size) { - var pad = [80, 20, 50, 20]; // top, right, bottom, left - var x = 0; - var y = 0; - - if (point[0] > size[0] - pad[1]) - x = -10; - if (point[0] < pad[3]) - x = 10; - if (point[1] > size[1] - pad[2]) - y = -10; - if (point[1] < pad[0]) - y = 10; - - if (x || y) { - return [x, y]; - } else { - return null; - } - } - function startNudge(entity, nudge) { if (_nudgeInterval) window.clearInterval(_nudgeInterval); @@ -197,7 +176,7 @@ export function modeDragNode(context) { _lastLoc = context.projection.invert(d3_event.point); doMove(entity); - var nudge = edge(d3_event.point, context.map().dimensions()); + var nudge = geoViewportEdge(d3_event.point, context.map().dimensions()); if (nudge) { startNudge(entity, nudge); } else { diff --git a/modules/modes/move.js b/modules/modes/move.js index b9ce5b7b3..e91439b1b 100644 --- a/modules/modes/move.js +++ b/modules/modes/move.js @@ -8,6 +8,7 @@ import { t } from '../util/locale'; import { actionMove } from '../actions'; import { behaviorEdit } from '../behavior'; +import { geoViewportEdge } from '../geo'; import { modeBrowse, @@ -30,23 +31,24 @@ export function modeMove(context, entityIDs, baseGraph) { button: 'browse' }; - var keybinding = d3_keybinding('move'), - behaviors = [ - behaviorEdit(context), - operationCircularize(entityIDs, context).behavior, - operationDelete(entityIDs, context).behavior, - operationOrthogonalize(entityIDs, context).behavior, - operationReflectLong(entityIDs, context).behavior, - operationReflectShort(entityIDs, context).behavior, - operationRotate(entityIDs, context).behavior - ], - annotation = entityIDs.length === 1 ? - t('operations.move.annotation.' + context.geometry(entityIDs[0])) : - t('operations.move.annotation.multiple'), - prevGraph, - cache, - origin, - nudgeInterval; + var keybinding = d3_keybinding('move'); + var behaviors = [ + behaviorEdit(context), + operationCircularize(entityIDs, context).behavior, + operationDelete(entityIDs, context).behavior, + operationOrthogonalize(entityIDs, context).behavior, + operationReflectLong(entityIDs, context).behavior, + operationReflectShort(entityIDs, context).behavior, + operationRotate(entityIDs, context).behavior + ]; + var annotation = entityIDs.length === 1 ? + t('operations.move.annotation.' + context.geometry(entityIDs[0])) : + t('operations.move.annotation.multiple'); + + var _prevGraph; + var _cache; + var _origin; + var _nudgeInterval; function vecSub(a, b) { @@ -54,52 +56,30 @@ export function modeMove(context, entityIDs, baseGraph) { } - function edge(point, size) { - var pad = [80, 20, 50, 20], // top, right, bottom, left - x = 0, - y = 0; - - if (point[0] > size[0] - pad[1]) - x = -10; - if (point[0] < pad[3]) - x = 10; - if (point[1] > size[1] - pad[2]) - y = -10; - if (point[1] < pad[0]) - y = 10; - - if (x || y) { - return [x, y]; - } else { - return null; - } - } - - function doMove(nudge) { nudge = nudge || [0, 0]; var fn; - if (prevGraph !== context.graph()) { - cache = {}; - origin = context.map().mouseCoordinates(); + if (_prevGraph !== context.graph()) { + _cache = {}; + _origin = context.map().mouseCoordinates(); fn = context.perform; } else { fn = context.overwrite; } - var currMouse = context.mouse(), - origMouse = context.projection(origin), - delta = vecSub(vecSub(currMouse, origMouse), nudge); + var currMouse = context.mouse(); + var origMouse = context.projection(_origin); + var delta = vecSub(vecSub(currMouse, origMouse), nudge); - fn(actionMove(entityIDs, delta, context.projection, cache), annotation); - prevGraph = context.graph(); + fn(actionMove(entityIDs, delta, context.projection, _cache), annotation); + _prevGraph = context.graph(); } function startNudge(nudge) { - if (nudgeInterval) window.clearInterval(nudgeInterval); - nudgeInterval = window.setInterval(function() { + if (_nudgeInterval) window.clearInterval(_nudgeInterval); + _nudgeInterval = window.setInterval(function() { context.pan(nudge); doMove(nudge); }, 50); @@ -107,16 +87,16 @@ export function modeMove(context, entityIDs, baseGraph) { function stopNudge() { - if (nudgeInterval) { - window.clearInterval(nudgeInterval); - nudgeInterval = null; + if (_nudgeInterval) { + window.clearInterval(_nudgeInterval); + _nudgeInterval = null; } } function move() { doMove(); - var nudge = edge(context.mouse(), context.map().dimensions()); + var nudge = geoViewportEdge(context.mouse(), context.map().dimensions()); if (nudge) { startNudge(nudge); } else { @@ -150,9 +130,9 @@ export function modeMove(context, entityIDs, baseGraph) { mode.enter = function() { - origin = context.map().mouseCoordinates(); - prevGraph = null; - cache = {}; + _origin = context.map().mouseCoordinates(); + _prevGraph = null; + _cache = {}; behaviors.forEach(function(behavior) { context.install(behavior); diff --git a/modules/modes/rotate.js b/modules/modes/rotate.js index 6448d5333..d889a174b 100644 --- a/modules/modes/rotate.js +++ b/modules/modes/rotate.js @@ -38,66 +38,67 @@ export function modeRotate(context, entityIDs) { button: 'browse' }; - var keybinding = d3_keybinding('rotate'), - behaviors = [ - behaviorEdit(context), - operationCircularize(entityIDs, context).behavior, - operationDelete(entityIDs, context).behavior, - operationMove(entityIDs, context).behavior, - operationOrthogonalize(entityIDs, context).behavior, - operationReflectLong(entityIDs, context).behavior, - operationReflectShort(entityIDs, context).behavior - ], - annotation = entityIDs.length === 1 ? - t('operations.rotate.annotation.' + context.geometry(entityIDs[0])) : - t('operations.rotate.annotation.multiple'), - prevGraph, - prevAngle, - prevTransform, - pivot; + var keybinding = d3_keybinding('rotate'); + var behaviors = [ + behaviorEdit(context), + operationCircularize(entityIDs, context).behavior, + operationDelete(entityIDs, context).behavior, + operationMove(entityIDs, context).behavior, + operationOrthogonalize(entityIDs, context).behavior, + operationReflectLong(entityIDs, context).behavior, + operationReflectShort(entityIDs, context).behavior + ]; + var annotation = entityIDs.length === 1 ? + t('operations.rotate.annotation.' + context.geometry(entityIDs[0])) : + t('operations.rotate.annotation.multiple'); + + var _prevGraph; + var _prevAngle; + var _prevTransform; + var _pivot; function doRotate() { var fn; - if (context.graph() !== prevGraph) { + if (context.graph() !== _prevGraph) { fn = context.perform; } else { fn = context.replace; } - // projection changed, recalculate pivot + // projection changed, recalculate _pivot var projection = context.projection; var currTransform = projection.transform(); - if (!prevTransform || - currTransform.k !== prevTransform.k || - currTransform.x !== prevTransform.x || - currTransform.y !== prevTransform.y) { + if (!_prevTransform || + currTransform.k !== _prevTransform.k || + currTransform.x !== _prevTransform.x || + currTransform.y !== _prevTransform.y) { - var nodes = utilGetAllNodes(entityIDs, context.graph()), - points = nodes.map(function(n) { return projection(n.loc); }); + var nodes = utilGetAllNodes(entityIDs, context.graph()); + var points = nodes.map(function(n) { return projection(n.loc); }); if (points.length === 1) { // degenerate case - pivot = points[0]; + _pivot = points[0]; } else if (points.length === 2) { - pivot = geoInterp(points[0], points[1], 0.5); + _pivot = geoInterp(points[0], points[1], 0.5); } else { - pivot = d3_polygonCentroid(d3_polygonHull(points)); + _pivot = d3_polygonCentroid(d3_polygonHull(points)); } - prevAngle = undefined; + _prevAngle = undefined; } - var currMouse = context.mouse(), - currAngle = Math.atan2(currMouse[1] - pivot[1], currMouse[0] - pivot[0]); + var currMouse = context.mouse(); + var currAngle = Math.atan2(currMouse[1] - _pivot[1], currMouse[0] - _pivot[0]); - if (typeof prevAngle === 'undefined') prevAngle = currAngle; - var delta = currAngle - prevAngle; + if (typeof _prevAngle === 'undefined') _prevAngle = currAngle; + var delta = currAngle - _prevAngle; - fn(actionRotate(entityIDs, pivot, delta, projection), annotation); + fn(actionRotate(entityIDs, _pivot, delta, projection), annotation); - prevTransform = currTransform; - prevAngle = currAngle; - prevGraph = context.graph(); + _prevTransform = currTransform; + _prevAngle = currAngle; + _prevGraph = context.graph(); } diff --git a/test/spec/geo/geo.js b/test/spec/geo/geo.js index 6b2e0cb85..b4ac874c6 100644 --- a/test/spec/geo/geo.js +++ b/test/spec/geo/geo.js @@ -411,4 +411,36 @@ describe('iD.geo', function() { expect(iD.geoPathLength(path)).to.eql(0); }); }); + + describe('geoViewportEdge', function() { + var dimensions = [1000, 1000]; + it('returns null if the point is not at the edge', function() { + expect(iD.geoViewportEdge([500, 500], dimensions)).to.be.null; + }); + it('nudges top edge', function() { + expect(iD.geoViewportEdge([500, 5], dimensions)).to.eql([0, 10]); + }); + it('nudges top-right corner', function() { + expect(iD.geoViewportEdge([995, 5], dimensions)).to.eql([-10, 10]); + }); + it('nudges right edge', function() { + expect(iD.geoViewportEdge([995, 500], dimensions)).to.eql([-10, 0]); + }); + it('nudges bottom-right corner', function() { + expect(iD.geoViewportEdge([995, 995], dimensions)).to.eql([-10, -10]); + }); + it('nudges bottom edge', function() { + expect(iD.geoViewportEdge([500, 995], dimensions)).to.eql([0, -10]); + }); + it('nudges bottom-left corner', function() { + expect(iD.geoViewportEdge([5, 995], dimensions)).to.eql([10, -10]); + }); + it('nudges left edge', function() { + expect(iD.geoViewportEdge([5, 500], dimensions)).to.eql([10, 0]); + }); + it('nudges top-left corner', function() { + expect(iD.geoViewportEdge([5, 5], dimensions)).to.eql([10, 10]); + }); + }); + }); From 2e2b037e36c51427ce0a329cd668c79b4ba114e3 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 18 Dec 2017 15:05:42 -0500 Subject: [PATCH 39/70] Move a bunch of commonly used vector and projection math functions into geo - geoVecAdd - geoVecSubtract - geoVecScale - geoZoomToScale - geoScaleToZoom --- modules/actions/move.js | 88 ++++----- modules/geo/geo.js | 180 +++++++++++-------- modules/geo/index.js | 8 +- modules/modes/drag_node.js | 14 +- modules/osm/way.js | 15 +- modules/renderer/tile_layer.js | 114 ++++++------ modules/services/osm.js | 28 +-- modules/svg/labels.js | 8 +- modules/svg/points.js | 7 +- modules/svg/vertices.js | 13 +- modules/ui/edit_menu.js | 4 +- modules/ui/fields/restrictions.js | 5 +- modules/ui/map_in_map.js | 119 ++++++------- modules/ui/radial_menu.js | 4 +- test/spec/geo/geo.js | 287 +++++++++++++++++------------- test/spec/svg/areas.js | 5 +- test/spec/svg/layers.js | 5 +- test/spec/svg/lines.js | 5 +- test/spec/svg/midpoints.js | 5 +- test/spec/svg/points.js | 5 +- test/spec/svg/vertices.js | 5 +- 21 files changed, 485 insertions(+), 439 deletions(-) diff --git a/modules/actions/move.js b/modules/actions/move.js index ddc1da8a8..baa75fb32 100644 --- a/modules/actions/move.js +++ b/modules/actions/move.js @@ -11,22 +11,21 @@ import _without from 'lodash-es/without'; import { osmNode } from '../osm'; import { - geoChooseEdge, geoAngle, + geoChooseEdge, geoInterp, geoPathIntersections, geoPathLength, - geoSphericalDistance + geoSphericalDistance, + geoVecAdd, + geoVecSubtract } from '../geo'; // https://github.com/openstreetmap/josm/blob/mirror/src/org/openstreetmap/josm/command/MoveCommand.java // https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/MoveNodeAction.as export function actionMove(moveIds, tryDelta, projection, cache) { - var delta = tryDelta; - - function vecAdd(a, b) { return [a[0] + b[0], a[1] + b[1]]; } - function vecSub(a, b) { return [a[0] - b[0], a[1] - b[1]]; } + var _delta = tryDelta; function setupCache(graph) { function canMove(nodeId) { @@ -118,11 +117,11 @@ export function actionMove(moveIds, tryDelta, projection, cache) { // Place a vertex where the moved vertex used to be, to preserve way shape.. - function replaceMovedVertex(nodeId, wayId, graph, delta) { - var way = graph.entity(wayId), - moved = graph.entity(nodeId), - movedIndex = way.nodes.indexOf(nodeId), - len, prevIndex, nextIndex; + function replaceMovedVertex(nodeId, wayId, graph, _delta) { + var way = graph.entity(wayId); + var moved = graph.entity(nodeId); + var movedIndex = way.nodes.indexOf(nodeId); + var len, prevIndex, nextIndex; if (way.isClosed()) { len = way.nodes.length - 1; @@ -134,14 +133,14 @@ export function actionMove(moveIds, tryDelta, projection, cache) { nextIndex = movedIndex + 1; } - var prev = graph.hasEntity(way.nodes[prevIndex]), - next = graph.hasEntity(way.nodes[nextIndex]); + var prev = graph.hasEntity(way.nodes[prevIndex]); + var next = graph.hasEntity(way.nodes[nextIndex]); // Don't add orig vertex at endpoint.. if (!prev || !next) return graph; - var key = wayId + '_' + nodeId, - orig = cache.replacedVertex[key]; + var key = wayId + '_' + nodeId; + var orig = cache.replacedVertex[key]; if (!orig) { orig = osmNode(); cache.replacedVertex[key] = orig; @@ -149,9 +148,9 @@ export function actionMove(moveIds, tryDelta, projection, cache) { } var start, end; - if (delta) { + if (_delta) { start = projection(cache.startLoc[nodeId]); - end = projection.invert(vecAdd(start, delta)); + end = projection.invert(geoVecAdd(start, _delta)); } else { end = cache.startLoc[nodeId]; } @@ -184,24 +183,24 @@ export function actionMove(moveIds, tryDelta, projection, cache) { // Reorder nodes around intersections that have moved.. function unZorroIntersection(intersection, graph) { - var vertex = graph.entity(intersection.nodeId), - way1 = graph.entity(intersection.movedId), - way2 = graph.entity(intersection.unmovedId), - isEP1 = intersection.movedIsEP, - isEP2 = intersection.unmovedIsEP; + var vertex = graph.entity(intersection.nodeId); + var way1 = graph.entity(intersection.movedId); + var way2 = graph.entity(intersection.unmovedId); + var isEP1 = intersection.movedIsEP; + var isEP2 = intersection.unmovedIsEP; // don't move the vertex if it is the endpoint of both ways. if (isEP1 && isEP2) return graph; - var nodes1 = _without(graph.childNodes(way1), vertex), - nodes2 = _without(graph.childNodes(way2), vertex); + var nodes1 = _without(graph.childNodes(way1), vertex); + var nodes2 = _without(graph.childNodes(way2), vertex); if (way1.isClosed() && way1.first() === vertex.id) nodes1.push(nodes1[0]); if (way2.isClosed() && way2.first() === vertex.id) nodes2.push(nodes2[0]); - var edge1 = !isEP1 && geoChooseEdge(nodes1, projection(vertex.loc), projection), - edge2 = !isEP2 && geoChooseEdge(nodes2, projection(vertex.loc), projection), - loc; + var edge1 = !isEP1 && geoChooseEdge(nodes1, projection(vertex.loc), projection); + var edge2 = !isEP2 && geoChooseEdge(nodes2, projection(vertex.loc), projection); + var loc; // snap vertex to nearest edge (or some point between them).. if (!isEP1 && !isEP2) { @@ -236,7 +235,7 @@ export function actionMove(moveIds, tryDelta, projection, cache) { function cleanupIntersections(graph) { _each(cache.intersection, function(obj) { - graph = replaceMovedVertex(obj.nodeId, obj.movedId, graph, delta); + graph = replaceMovedVertex(obj.nodeId, obj.movedId, graph, _delta); graph = replaceMovedVertex(obj.nodeId, obj.unmovedId, graph, null); graph = unZorroIntersection(obj, graph); }); @@ -245,7 +244,7 @@ export function actionMove(moveIds, tryDelta, projection, cache) { } - // check if moving way endpoint can cross an unmoved way, if so limit delta.. + // check if moving way endpoint can cross an unmoved way, if so limit _delta.. function limitDelta(graph) { _each(cache.intersection, function(obj) { // Don't limit movement if this is vertex joins 2 endpoints.. @@ -253,27 +252,28 @@ export function actionMove(moveIds, tryDelta, projection, cache) { // Don't limit movement if this vertex is not an endpoint anyway.. if (!obj.movedIsEP) return; - var node = graph.entity(obj.nodeId), - start = projection(node.loc), - end = vecAdd(start, delta), - movedNodes = graph.childNodes(graph.entity(obj.movedId)), - movedPath = _map(_map(movedNodes, 'loc'), - function(loc) { return vecAdd(projection(loc), delta); }), - unmovedNodes = graph.childNodes(graph.entity(obj.unmovedId)), - unmovedPath = _map(_map(unmovedNodes, 'loc'), projection), - hits = geoPathIntersections(movedPath, unmovedPath); + var node = graph.entity(obj.nodeId); + var start = projection(node.loc); + var end = geoVecAdd(start, _delta); + var movedNodes = graph.childNodes(graph.entity(obj.movedId)); + var movedPath = _map(_map(movedNodes, 'loc'), function(loc) { + return geoVecAdd(projection(loc), _delta); + }); + var unmovedNodes = graph.childNodes(graph.entity(obj.unmovedId)); + var unmovedPath = _map(_map(unmovedNodes, 'loc'), projection); + var hits = geoPathIntersections(movedPath, unmovedPath); for (var i = 0; i < hits.length; i++) { if (_isEqual(hits[i], end)) continue; var edge = geoChooseEdge(unmovedNodes, end, projection); - delta = vecSub(projection(edge.loc), start); + _delta = geoVecSubtract(projection(edge.loc), start); } }); } var action = function(graph) { - if (delta[0] === 0 && delta[1] === 0) return graph; + if (_delta[0] === 0 && _delta[1] === 0) return graph; setupCache(graph); @@ -282,9 +282,9 @@ export function actionMove(moveIds, tryDelta, projection, cache) { } _each(cache.nodes, function(id) { - var node = graph.entity(id), - start = projection(node.loc), - end = vecAdd(start, delta); + var node = graph.entity(id); + var start = projection(node.loc); + var end = geoVecAdd(start, _delta); graph = graph.replace(node.move(projection.invert(end))); }); @@ -297,7 +297,7 @@ export function actionMove(moveIds, tryDelta, projection, cache) { action.delta = function() { - return delta; + return _delta; }; diff --git a/modules/geo/geo.js b/modules/geo/geo.js index 2f74eb40a..e96bc81b9 100644 --- a/modules/geo/geo.js +++ b/modules/geo/geo.js @@ -2,94 +2,130 @@ import _every from 'lodash-es/every'; import _some from 'lodash-es/some'; -export function geoRoundCoords(c) { - return [Math.floor(c[0]), Math.floor(c[1])]; +// constants +var TAU = 2 * Math.PI; +var EQUATORIAL_RADIUS = 6356752.314245179; +var POLAR_RADIUS = 6378137.0; + + +// vector addition +export function geoVecAdd(a, b) { + return [ a[0] + b[0], a[1] + b[1] ]; } +// vector subtraction +export function geoVecSubtract(a, b) { + return [ a[0] - b[0], a[1] - b[1] ]; +} +// vector multiplication +export function geoVecScale(a, b) { + return [ a[0] * b, a[1] * b ]; +} + +// vector rounding (was: geoRoundCoordinates) +export function geoVecFloor(a) { + return [ Math.floor(a[0]), Math.floor(a[1]) ]; +} + +// linear interpolation export function geoInterp(p1, p2, t) { - return [p1[0] + (p2[0] - p1[0]) * t, - p1[1] + (p2[1] - p1[1]) * t]; + return [ + p1[0] + (p2[0] - p1[0]) * t, + p1[1] + (p2[1] - p1[1]) * t + ]; } -// 2D cross product of OA and OB vectors, i.e. z-component of their 3D cross product. +// dot product +export function geoDot(a, b, origin) { + origin = origin || [0, 0]; + return (a[0] - origin[0]) * (b[0] - origin[0]) + + (a[1] - origin[1]) * (b[1] - origin[1]); +} + + +// 2D cross product of OA and OB vectors, returns magnitude of Z vector // Returns a positive value, if OAB makes a counter-clockwise turn, // negative for clockwise turn, and zero if the points are collinear. -export function geoCross(o, a, b) { - return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]); +export function geoCross(a, b, origin) { + origin = origin || [0, 0]; + return (a[0] - origin[0]) * (b[1] - origin[1]) - + (a[1] - origin[1]) * (b[0] - origin[0]); } // http://jsperf.com/id-dist-optimization export function geoEuclideanDistance(a, b) { - var x = a[0] - b[0], y = a[1] - b[1]; + var x = a[0] - b[0]; + var y = a[1] - b[1]; return Math.sqrt((x * x) + (y * y)); } -// using WGS84 polar radius (6356752.314245179 m) -// const = 2 * PI * r / 360 export function geoLatToMeters(dLat) { - return dLat * 110946.257617; + return dLat * (TAU * POLAR_RADIUS / 360); } -// using WGS84 equatorial radius (6378137.0 m) -// const = 2 * PI * r / 360 export function geoLonToMeters(dLon, atLat) { return Math.abs(atLat) >= 90 ? 0 : - dLon * 111319.490793 * Math.abs(Math.cos(atLat * (Math.PI/180))); + dLon * (TAU * EQUATORIAL_RADIUS / 360) * Math.abs(Math.cos(atLat * (Math.PI / 180))); } -// using WGS84 polar radius (6356752.314245179 m) -// const = 2 * PI * r / 360 export function geoMetersToLat(m) { - return m / 110946.257617; + return m / (TAU * POLAR_RADIUS / 360); } -// using WGS84 equatorial radius (6378137.0 m) -// const = 2 * PI * r / 360 export function geoMetersToLon(m, atLat) { return Math.abs(atLat) >= 90 ? 0 : - m / 111319.490793 / Math.abs(Math.cos(atLat * (Math.PI/180))); + m / (TAU * EQUATORIAL_RADIUS / 360) / Math.abs(Math.cos(atLat * (Math.PI / 180))); } -export function geoOffsetToMeters(offset) { - var equatRadius = 6356752.314245179, - polarRadius = 6378137.0, - tileSize = 256; - +export function geoOffsetToMeters(offset, tileSize) { + tileSize = tileSize || 256; return [ - offset[0] * 2 * Math.PI * equatRadius / tileSize, - -offset[1] * 2 * Math.PI * polarRadius / tileSize + offset[0] * TAU * EQUATORIAL_RADIUS / tileSize, + -offset[1] * TAU * POLAR_RADIUS / tileSize ]; } -export function geoMetersToOffset(meters) { - var equatRadius = 6356752.314245179, - polarRadius = 6378137.0, - tileSize = 256; - +export function geoMetersToOffset(meters, tileSize) { + tileSize = tileSize || 256; return [ - meters[0] * tileSize / (2 * Math.PI * equatRadius), - -meters[1] * tileSize / (2 * Math.PI * polarRadius) + meters[0] * tileSize / (TAU * EQUATORIAL_RADIUS), + -meters[1] * tileSize / (TAU * POLAR_RADIUS) ]; } // Equirectangular approximation of spherical distances on Earth export function geoSphericalDistance(a, b) { - var x = geoLonToMeters(a[0] - b[0], (a[1] + b[1]) / 2), - y = geoLatToMeters(a[1] - b[1]); + var x = geoLonToMeters(a[0] - b[0], (a[1] + b[1]) / 2); + var y = geoLatToMeters(a[1] - b[1]); return Math.sqrt((x * x) + (y * y)); } +// zoom to scale +export function geoZoomToScale(z, tileSize) { + tileSize = tileSize || 256; + return tileSize * Math.pow(2, z) / TAU; +} + + +// scale to zoom +export function geoScaleToZoom(k, tileSize) { + tileSize = tileSize || 256; + var log2ts = Math.log(tileSize) * Math.LOG2E; + return Math.log(k * TAU) / Math.LN2 - log2ts; +} + + export function geoEdgeEqual(a, b) { return (a[0] === b[0] && a[1] === b[1]) || (a[0] === b[1] && a[1] === b[0]); @@ -122,23 +158,18 @@ export function geoRotate(points, angle, around) { // the closest vertex on that edge. Returns an object with the `index` of the // chosen edge, the chosen `loc` on that edge, and the `distance` to to it. export function geoChooseEdge(nodes, point, projection) { - var dist = geoEuclideanDistance, - points = nodes.map(function(n) { return projection(n.loc); }), - min = Infinity, - idx, loc; - - function dot(p, q) { - return p[0] * q[0] + p[1] * q[1]; - } + var dist = geoEuclideanDistance; + var points = nodes.map(function(n) { return projection(n.loc); }); + var min = Infinity; + var idx; + var loc; for (var i = 0; i < points.length - 1; i++) { - var o = points[i], - s = [points[i + 1][0] - o[0], - points[i + 1][1] - o[1]], - v = [point[0] - o[0], - point[1] - o[1]], - proj = dot(v, s) / dot(s, s), - p; + var o = points[i]; + var s = geoVecSubtract(points[i + 1], o); + var v = geoVecSubtract(point, o); + var proj = geoDot(v, s) / geoDot(s, s); + var p; if (proj < 0) { p = o; @@ -169,25 +200,18 @@ export function geoChooseEdge(nodes, point, projection) { // This uses the vector cross product approach described below: // http://stackoverflow.com/a/565282/786339 export function geoLineIntersection(a, b) { - function subtractPoints(point1, point2) { - return [point1[0] - point2[0], point1[1] - point2[1]]; - } - function crossProduct(point1, point2) { - return point1[0] * point2[1] - point1[1] * point2[0]; - } - - var p = [a[0][0], a[0][1]], - p2 = [a[1][0], a[1][1]], - q = [b[0][0], b[0][1]], - q2 = [b[1][0], b[1][1]], - r = subtractPoints(p2, p), - s = subtractPoints(q2, q), - uNumerator = crossProduct(subtractPoints(q, p), r), - denominator = crossProduct(r, s); + var p = [a[0][0], a[0][1]]; + var p2 = [a[1][0], a[1][1]]; + var q = [b[0][0], b[0][1]]; + var q2 = [b[1][0], b[1][1]]; + var r = geoVecSubtract(p2, p); + var s = geoVecSubtract(q2, q); + var uNumerator = geoCross(geoVecSubtract(q, p), r); + var denominator = geoCross(r, s); if (uNumerator && denominator) { - var u = uNumerator / denominator, - t = crossProduct(subtractPoints(q, p), s) / denominator; + var u = uNumerator / denominator; + var t = geoCross(geoVecSubtract(q, p), s) / denominator; if ((t >= 0) && (t <= 1) && (u >= 0) && (u <= 1)) { return geoInterp(p, p2, t); @@ -202,10 +226,12 @@ export function geoPathIntersections(path1, path2) { var intersections = []; for (var i = 0; i < path1.length - 1; i++) { for (var j = 0; j < path2.length - 1; j++) { - var a = [ path1[i], path1[i+1] ], - b = [ path2[j], path2[j+1] ], - hit = geoLineIntersection(a, b); - if (hit) intersections.push(hit); + var a = [ path1[i], path1[i+1] ]; + var b = [ path2[j], path2[j+1] ]; + var hit = geoLineIntersection(a, b); + if (hit) { + intersections.push(hit); + } } } return intersections; @@ -222,9 +248,9 @@ export function geoPathIntersections(path1, path2) { // http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html // export function geoPointInPolygon(point, polygon) { - var x = point[0], - y = point[1], - inside = false; + var x = point[0]; + var y = point[1]; + var inside = false; for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { var xi = polygon[i][0], yi = polygon[i][1]; @@ -250,8 +276,8 @@ export function geoPolygonIntersectsPolygon(outer, inner, checkSegments) { function testSegments(outer, inner) { for (var i = 0; i < outer.length - 1; i++) { for (var j = 0; j < inner.length - 1; j++) { - var a = [ outer[i], outer[i+1] ], - b = [ inner[j], inner[j+1] ]; + var a = [ outer[i], outer[i +1 ] ]; + var b = [ inner[j], inner[j + 1] ]; if (geoLineIntersection(a, b)) return true; } } diff --git a/modules/geo/index.js b/modules/geo/index.js index 8966c218b..2a1c8fa31 100644 --- a/modules/geo/index.js +++ b/modules/geo/index.js @@ -1,12 +1,12 @@ export { geoAngle } from './geo.js'; export { geoChooseEdge } from './geo.js'; export { geoCross } from './geo.js'; +export { geoDot } from './geo.js'; export { geoEdgeEqual } from './geo.js'; export { geoEuclideanDistance } from './geo.js'; export { geoExtent } from './extent.js'; export { geoInterp } from './geo.js'; export { geoRawMercator } from './raw_mercator.js'; -export { geoRoundCoords } from './geo.js'; export { geoRotate } from './geo.js'; export { geoLatToMeters } from './geo.js'; export { geoLineIntersection } from './geo.js'; @@ -20,5 +20,11 @@ export { geoPathLength } from './geo.js'; export { geoPointInPolygon } from './geo.js'; export { geoPolygonContainsPolygon } from './geo.js'; export { geoPolygonIntersectsPolygon } from './geo.js'; +export { geoScaleToZoom } from './geo.js'; export { geoSphericalDistance } from './geo.js'; +export { geoVecAdd } from './geo.js'; +export { geoVecFloor } from './geo.js'; +export { geoVecSubtract } from './geo.js'; +export { geoVecScale } from './geo.js'; +export { geoZoomToScale } from './geo.js'; export { geoViewportEdge } from './geo.js'; diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index b52c2da97..915706bbd 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -23,7 +23,12 @@ import { modeSelect } from './index'; -import { geoChooseEdge, geoViewportEdge } from '../geo'; +import { + geoChooseEdge, + geoVecSubtract, + geoViewportEdge +} from '../geo'; + import { osmNode } from '../osm'; import { utilEntitySelector } from '../util'; import { uiFlash } from '../ui'; @@ -46,11 +51,6 @@ export function modeDragNode(context) { var _lastLoc; - function vecSub(a, b) { - return [a[0] - b[0], a[1] - b[1]]; - } - - function startNudge(entity, nudge) { if (_nudgeInterval) window.clearInterval(_nudgeInterval); _nudgeInterval = window.setInterval(function() { @@ -135,7 +135,7 @@ export function modeDragNode(context) { nudge = nudge || [0, 0]; var currPoint = (d3_event && d3_event.point) || context.projection(_lastLoc); - var currMouse = vecSub(currPoint, nudge); + var currMouse = geoVecSubtract(currPoint, nudge); var loc = context.projection.invert(currMouse); if (!_nudgeInterval) { diff --git a/modules/osm/way.js b/modules/osm/way.js index 76918a52e..c93224146 100644 --- a/modules/osm/way.js +++ b/modules/osm/way.js @@ -133,15 +133,16 @@ _extend(osmWay.prototype, { isConvex: function(resolver) { if (!this.isClosed() || this.isDegenerate()) return null; - var nodes = _uniq(resolver.childNodes(this)), - coords = _map(nodes, 'loc'), - curr = 0, prev = 0; + var nodes = _uniq(resolver.childNodes(this)); + var coords = _map(nodes, 'loc'); + var curr = 0; + var prev = 0; for (var i = 0; i < coords.length; i++) { - var o = coords[(i+1) % coords.length], - a = coords[i], - b = coords[(i+2) % coords.length], - res = geoCross(o, a, b); + var o = coords[(i+1) % coords.length]; + var a = coords[i]; + var b = coords[(i+2) % coords.length]; + var res = geoCross(a, b, o); curr = (res > 0) ? 1 : (res < 0) ? -1 : 0; if (curr === 0) { diff --git a/modules/renderer/tile_layer.js b/modules/renderer/tile_layer.js index ceb846117..9d143f045 100644 --- a/modules/renderer/tile_layer.js +++ b/modules/renderer/tile_layer.js @@ -2,28 +2,29 @@ import { select as d3_select } from 'd3-selection'; import { t } from '../util/locale'; import { d3geoTile as d3_geoTile } from '../lib/d3.geo.tile'; -import { geoEuclideanDistance } from '../geo'; +import { geoEuclideanDistance, geoScaleToZoom } from '../geo'; import { utilPrefixCSSProperty } from '../util'; export function rendererTileLayer(context) { - var tileSize = 256, - geotile = d3_geoTile(), - projection, - cache = {}, - tileOrigin, - z, - transformProp = utilPrefixCSSProperty('Transform'), - source; + var tileSize = 256; + var transformProp = utilPrefixCSSProperty('Transform'); + var geotile = d3_geoTile(); + + var _projection; + var _cache = {}; + var _tileOrigin; + var _zoom; + var _source; // blacklist overlay tiles around Null Island.. function nearNullIsland(x, y, z) { if (z >= 7) { - var center = Math.pow(2, z - 1), - width = Math.pow(2, z - 6), - min = center - (width / 2), - max = center + (width / 2) - 1; + var center = Math.pow(2, z - 1); + var width = Math.pow(2, z - 6); + var min = center - (width / 2); + var max = center + (width / 2) - 1; return x >= min && x <= max && y >= min && y <= max; } return false; @@ -31,8 +32,8 @@ export function rendererTileLayer(context) { function tileSizeAtZoom(d, z) { - var epsilon = 0.002; - return ((tileSize * Math.pow(2, z - d[2])) / tileSize) + epsilon; + var EPSILON = 0.002; + return ((tileSize * Math.pow(2, z - d[2])) / tileSize) + EPSILON; } @@ -49,7 +50,7 @@ export function rendererTileLayer(context) { function lookUp(d) { for (var up = -1; up > -d[2]; up--) { var tile = atZoom(d, up); - if (cache[source.url(tile)] !== false) { + if (_cache[_source.url(tile)] !== false) { return tile; } } @@ -57,7 +58,8 @@ export function rendererTileLayer(context) { function uniqueBy(a, n) { - var o = [], seen = {}; + var o = []; + var seen = {}; for (var i = 0; i < a.length; i++) { if (seen[a[i][n]] === undefined) { o.push(a[i]); @@ -69,37 +71,37 @@ export function rendererTileLayer(context) { function addSource(d) { - d.push(source.url(d)); + d.push(_source.url(d)); return d; } // Update tiles based on current state of `projection`. function background(selection) { - z = Math.max(Math.log(projection.scale() * 2 * Math.PI) / Math.log(2) - 8, 0); + _zoom = geoScaleToZoom(_projection.scale(), tileSize); var pixelOffset; - if (source) { + if (_source) { pixelOffset = [ - source.offset()[0] * Math.pow(2, z), - source.offset()[1] * Math.pow(2, z) + _source.offset()[0] * Math.pow(2, _zoom), + _source.offset()[1] * Math.pow(2, _zoom) ]; } else { pixelOffset = [0, 0]; } var translate = [ - projection.translate()[0] + pixelOffset[0], - projection.translate()[1] + pixelOffset[1] + _projection.translate()[0] + pixelOffset[0], + _projection.translate()[1] + pixelOffset[1] ]; geotile - .scale(projection.scale() * 2 * Math.PI) + .scale(_projection.scale() * 2 * Math.PI) .translate(translate); - tileOrigin = [ - projection.scale() * Math.PI - translate[0], - projection.scale() * Math.PI - translate[1] + _tileOrigin = [ + _projection.scale() * Math.PI - translate[0], + _projection.scale() * Math.PI - translate[1] ]; render(selection); @@ -107,36 +109,36 @@ export function rendererTileLayer(context) { // Derive the tiles onscreen, remove those offscreen and position them. - // Important that this part not depend on `projection` because it's + // Important that this part not depend on `_projection` because it's // rentered when tiles load/error (see #644). function render(selection) { - if (!source) return; + if (!_source) return; var requests = []; - var showDebug = context.getDebug('tile') && !source.overlay; + var showDebug = context.getDebug('tile') && !_source.overlay; - if (source.validZoom(z)) { + if (_source.validZoom(_zoom)) { geotile().forEach(function(d) { addSource(d); if (d[3] === '') return; if (typeof d[3] !== 'string') return; // Workaround for #2295 requests.push(d); - if (cache[d[3]] === false && lookUp(d)) { + if (_cache[d[3]] === false && lookUp(d)) { requests.push(addSource(lookUp(d))); } }); requests = uniqueBy(requests, 3).filter(function(r) { - if (!!source.overlay && nearNullIsland(r[0], r[1], r[2])) { + if (!!_source.overlay && nearNullIsland(r[0], r[1], r[2])) { return false; } // don't re-request tiles which have failed in the past - return cache[r[3]] !== false; + return _cache[r[3]] !== false; }); } function load(d) { - cache[d[3]] = true; + _cache[d[3]] = true; d3_select(this) .on('error', null) .on('load', null) @@ -145,7 +147,7 @@ export function rendererTileLayer(context) { } function error(d) { - cache[d[3]] = false; + _cache[d[3]] = false; d3_select(this) .on('error', null) .on('load', null) @@ -154,19 +156,19 @@ export function rendererTileLayer(context) { } function imageTransform(d) { - var _ts = tileSize * Math.pow(2, z - d[2]); - var scale = tileSizeAtZoom(d, z); + var ts = tileSize * Math.pow(2, _zoom - d[2]); + var scale = tileSizeAtZoom(d, _zoom); return 'translate(' + - ((d[0] * _ts) - tileOrigin[0]) + 'px,' + - ((d[1] * _ts) - tileOrigin[1]) + 'px) ' + + ((d[0] * ts) - _tileOrigin[0]) + 'px,' + + ((d[1] * ts) - _tileOrigin[1]) + 'px) ' + 'scale(' + scale + ',' + scale + ')'; } function tileCenter(d) { - var _ts = tileSize * Math.pow(2, z - d[2]); + var ts = tileSize * Math.pow(2, _zoom - d[2]); return [ - ((d[0] * _ts) - tileOrigin[0] + (_ts / 2)), - ((d[1] * _ts) - tileOrigin[1] + (_ts / 2)) + ((d[0] * ts) - _tileOrigin[0] + (ts / 2)), + ((d[1] * ts) - _tileOrigin[1] + (ts / 2)) ]; } @@ -178,10 +180,10 @@ export function rendererTileLayer(context) { // Pick a representative tile near the center of the viewport // (This is useful for sampling the imagery vintage) - var dims = geotile.size(), - mapCenter = [dims[0] / 2, dims[1] / 2], - minDist = Math.max(dims[0], dims[1]), - nearCenter; + var dims = geotile.size(); + var mapCenter = [dims[0] / 2, dims[1] / 2]; + var minDist = Math.max(dims[0], dims[1]); + var nearCenter; requests.forEach(function(d) { var c = tileCenter(d); @@ -255,8 +257,8 @@ export function rendererTileLayer(context) { .selectAll('.tile-label-debug-vintage') .each(function(d) { var span = d3_select(this); - var center = context.projection.invert(tileCenter(d)); - source.getMetadata(center, d, function(err, result) { + var center = context._projection.invert(tileCenter(d)); + _source.getMetadata(center, d, function(err, result) { span.text((result && result.vintage && result.vintage.range) || t('info_panels.background.vintage') + ': ' + t('info_panels.background.unknown') ); @@ -268,8 +270,8 @@ export function rendererTileLayer(context) { background.projection = function(_) { - if (!arguments.length) return projection; - projection = _; + if (!arguments.length) return _projection; + _projection = _; return background; }; @@ -282,10 +284,10 @@ export function rendererTileLayer(context) { background.source = function(_) { - if (!arguments.length) return source; - source = _; - cache = {}; - geotile.scaleExtent(source.scaleExtent); + if (!arguments.length) return _source; + _source = _; + _cache = {}; + geotile.scaleExtent(_source.scaleExtent); return background; }; diff --git a/modules/services/osm.js b/modules/services/osm.js index cf33659a3..af96685a3 100644 --- a/modules/services/osm.js +++ b/modules/services/osm.js @@ -500,8 +500,8 @@ export default { } // update blacklists - var elements = xml.getElementsByTagName('blacklist'), - regexes = []; + var elements = xml.getElementsByTagName('blacklist'); + var regexes = []; for (var i = 0; i < elements.length; i++) { var regex = elements[i].getAttribute('regex'); // needs unencode? if (regex) { @@ -516,8 +516,8 @@ export default { if (rateLimitError) { callback(rateLimitError, 'rateLimited'); } else { - var apiStatus = xml.getElementsByTagName('status'), - val = apiStatus[0].getAttribute('api'); + var apiStatus = xml.getElementsByTagName('status'); + var val = apiStatus[0].getAttribute('api'); callback(undefined, val); } @@ -544,14 +544,14 @@ export default { loadTiles: function(projection, dimensions, callback) { if (off) return; - var that = this, - s = projection.scale() * 2 * Math.PI, - z = Math.max(Math.log(s) / Math.log(2) - 8, 0), - ts = 256 * Math.pow(2, z - tileZoom), - origin = [ - s / 2 - projection.translate()[0], - s / 2 - projection.translate()[1] - ]; + var that = this; + var s = projection.scale() * 2 * Math.PI; + var z = Math.max(Math.log(s) / Math.log(2) - 8, 0); + var ts = 256 * Math.pow(2, z - tileZoom); + var origin = [ + s / 2 - projection.translate()[0], + s / 2 - projection.translate()[1] + ]; var tiles = d3_geoTile() .scaleExtent([tileZoom, tileZoom]) @@ -559,8 +559,8 @@ export default { .size(dimensions) .translate(projection.translate())() .map(function(tile) { - var x = tile[0] * ts - origin[0], - y = tile[1] * ts - origin[1]; + var x = tile[0] * ts - origin[0]; + var y = tile[1] * ts - origin[1]; return { id: tile.toString(), diff --git a/modules/svg/labels.js b/modules/svg/labels.js index 2e65d593a..15c3d2b08 100644 --- a/modules/svg/labels.js +++ b/modules/svg/labels.js @@ -13,7 +13,8 @@ import { geoEuclideanDistance, geoInterp, geoPolygonIntersectsPolygon, - geoPathLength + geoPathLength, + geoScaleToZoom } from '../geo'; import { osmEntity } from '../osm'; @@ -27,9 +28,6 @@ import { } from '../util'; -var TAU = 2 * Math.PI; -function ktoz(k) { return Math.log(k * TAU) / Math.LN2 - 8; } - export function svgLabels(projection, context) { var path = d3_geoPath(projection); @@ -261,7 +259,7 @@ export function svgLabels(projection, context) { function drawLabels(selection, graph, entities, filter, dimensions, fullRedraw) { var wireframe = context.surface().classed('fill-wireframe'); - var zoom = ktoz(projection.scale()); + var zoom = geoScaleToZoom(projection.scale()); var labelable = []; var renderNodeAs = {}; diff --git a/modules/svg/points.js b/modules/svg/points.js index 124dba5bc..8c41f7a1a 100644 --- a/modules/svg/points.js +++ b/modules/svg/points.js @@ -1,12 +1,9 @@ import { dataFeatureIcons } from '../../data'; +import { geoScaleToZoom } from '../geo'; import { osmEntity } from '../osm'; import { svgPointTransform, svgTagClasses } from './index'; -var TAU = 2 * Math.PI; -function ktoz(k) { return Math.log(k * TAU) / Math.LN2 - 8; } - - export function svgPoints(projection, context) { function markerPath(selection, klass) { @@ -55,7 +52,7 @@ export function svgPoints(projection, context) { function drawPoints(selection, graph, entities, filter) { var wireframe = context.surface().classed('fill-wireframe'); - var zoom = ktoz(projection.scale()); + var zoom = geoScaleToZoom(projection.scale()); // points with a direction will render as vertices at higher zooms function renderAsPoint(entity) { diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index 06ab1e7b6..df681a2d1 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -4,14 +4,11 @@ import _values from 'lodash-es/values'; import { select as d3_select } from 'd3-selection'; import { dataFeatureIcons } from '../../data'; +import { geoScaleToZoom } from '../geo'; import { osmEntity } from '../osm'; import { svgPointTransform } from './index'; -var TAU = 2 * Math.PI; -function ktoz(k) { return Math.log(k * TAU) / Math.LN2 - 8; } - - export function svgVertices(projection, context) { var radiuses = { // z16-, z17, z18+, w/icon @@ -46,7 +43,7 @@ export function svgVertices(projection, context) { var icons = {}; var directions = {}; var wireframe = context.surface().classed('fill-wireframe'); - var zoom = ktoz(projection.scale()); + var zoom = geoScaleToZoom(projection.scale()); var z = (zoom < 17 ? 0 : zoom < 18 ? 1 : 2); @@ -258,7 +255,7 @@ export function svgVertices(projection, context) { function drawVertices(selection, graph, entities, filter, extent, fullRedraw) { var wireframe = context.surface().classed('fill-wireframe'); - var zoom = ktoz(projection.scale()); + var zoom = geoScaleToZoom(projection.scale()); var mode = context.mode(); var isMoving = mode && /^(add|draw|drag|move|rotate)/.test(mode.id); @@ -318,7 +315,7 @@ export function svgVertices(projection, context) { // partial redraw - only update the selected items.. drawVertices.drawSelected = function(selection, graph, target, extent) { var wireframe = context.surface().classed('fill-wireframe'); - var zoom = ktoz(projection.scale()); + var zoom = geoScaleToZoom(projection.scale()); _prevSelected = _currSelected || {}; _currSelected = getSiblingAndChildVertices(context.selectedIDs(), graph, wireframe, zoom); @@ -334,7 +331,7 @@ export function svgVertices(projection, context) { if (target === _currHoverTarget) return; // continue only if something changed var wireframe = context.surface().classed('fill-wireframe'); - var zoom = ktoz(projection.scale()); + var zoom = geoScaleToZoom(projection.scale()); _prevHover = _currHover || {}; _currHoverTarget = target; diff --git a/modules/ui/edit_menu.js b/modules/ui/edit_menu.js index b0856c56f..f29503bec 100644 --- a/modules/ui/edit_menu.js +++ b/modules/ui/edit_menu.js @@ -3,7 +3,7 @@ import { select as d3_select } from 'd3-selection'; -import { geoRoundCoords } from '../geo'; +import { geoVecFloor } from '../geo'; import { textDirection } from '../util/locale'; import { uiTooltipHtml } from './tooltipHtml'; @@ -81,7 +81,7 @@ export function uiEditMenu(context, operations) { .attr('class', function (d) { return 'edit-menu-item edit-menu-item-' + d.id; }) .classed('disabled', function (d) { return d.disabled(); }) .attr('transform', function (d, i) { - return 'translate(' + geoRoundCoords([ + return 'translate(' + geoVecFloor([ 0, m + i * buttonHeight ]).join(',') + ')'; diff --git a/modules/ui/fields/restrictions.js b/modules/ui/fields/restrictions.js index a89c7a9b5..f56bc868d 100644 --- a/modules/ui/fields/restrictions.js +++ b/modules/ui/fields/restrictions.js @@ -26,7 +26,8 @@ import { import { geoExtent, - geoRawMercator + geoRawMercator, + geoZoomToScale } from '../../geo'; import { @@ -84,7 +85,7 @@ export function uiFieldRestrictions(field, context) { var z = 24; projection - .scale(256 * Math.pow(2, z) / (2 * Math.PI)); + .scale(geoZoomToScale(z)); var s = projection(vertex.loc); diff --git a/modules/ui/map_in_map.js b/modules/ui/map_in_map.js index 4a6609c98..08ca8b44d 100644 --- a/modules/ui/map_in_map.js +++ b/modules/ui/map_in_map.js @@ -13,45 +13,44 @@ import { import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js'; import { t } from '../util/locale'; -import { svgDebug, svgGpx } from '../svg'; -import { geoRawMercator } from '../geo'; +import { + geoRawMercator, + geoScaleToZoom, + geoVecSubtract, + geoVecScale, + geoZoomToScale, +} from '../geo'; + import { rendererTileLayer } from '../renderer'; +import { svgDebug, svgGpx } from '../svg'; import { utilSetTransform } from '../util'; import { utilGetDimensions } from '../util/dimensions'; -var TAU = 2 * Math.PI; -function ztok(z) { return 256 * Math.pow(2, z) / TAU; } -function ktoz(k) { return Math.log(k * TAU) / Math.LN2 - 8; } -function vecSub(a, b) { return [ a[0] - b[0], a[1] - b[1] ]; } -function vecScale(a, b) { return [ a[0] * b, a[1] * b ]; } - - export function uiMapInMap(context) { - function map_in_map(selection) { - var backgroundLayer = rendererTileLayer(context), - overlayLayers = {}, - projection = geoRawMercator(), - gpxLayer = svgGpx(projection, context).showLabels(false), - debugLayer = svgDebug(projection, context), - zoom = d3_zoom() - .scaleExtent([ztok(0.5), ztok(24)]) - .on('start', zoomStarted) - .on('zoom', zoomed) - .on('end', zoomEnded), - isTransformed = false, - isHidden = true, - skipEvents = false, - gesture = null, - zDiff = 6, // by default, minimap renders at (main zoom - 6) - wrap = d3_select(null), - tiles = d3_select(null), - viewport = d3_select(null), - tStart, // transform at start of gesture - tCurr, // transform at most recent event - timeoutId; + var backgroundLayer = rendererTileLayer(context); + var overlayLayers = {}; + var projection = geoRawMercator(); + var gpxLayer = svgGpx(projection, context).showLabels(false); + var debugLayer = svgDebug(projection, context); + var zoom = d3_zoom() + .scaleExtent([geoZoomToScale(0.5), geoZoomToScale(24)]) + .on('start', zoomStarted) + .on('zoom', zoomed) + .on('end', zoomEnded); + var isTransformed = false; + var isHidden = true; + var skipEvents = false; + var gesture = null; + var zDiff = 6; // by default, minimap renders at (main zoom - 6) + var wrap = d3_select(null); + var tiles = d3_select(null); + var viewport = d3_select(null); + var tStart; // transform at start of gesture + var tCurr; // transform at most recent event + var timeoutId; function zoomStarted() { @@ -64,11 +63,11 @@ export function uiMapInMap(context) { function zoomed() { if (skipEvents) return; - var x = d3_event.transform.x, - y = d3_event.transform.y, - k = d3_event.transform.k, - isZooming = (k !== tStart.k), - isPanning = (x !== tStart.x || y !== tStart.y); + var x = d3_event.transform.x; + var y = d3_event.transform.y; + var k = d3_event.transform.k; + var isZooming = (k !== tStart.k); + var isPanning = (x !== tStart.x || y !== tStart.y); if (!isZooming && !isPanning) { return; // no change @@ -79,12 +78,12 @@ export function uiMapInMap(context) { gesture = isZooming ? 'zoom' : 'pan'; } - var tMini = projection.transform(), - tX, tY, scale; + var tMini = projection.transform(); + var tX, tY, scale; if (gesture === 'zoom') { - var dMini = utilGetDimensions(wrap), - cMini = vecScale(dMini, 0.5); + var dMini = utilGetDimensions(wrap); + var cMini = geoVecScale(dMini, 0.5); scale = k / tMini.k; tX = (cMini[0] / scale - cMini[0]) * scale; tY = (cMini[1] / scale - cMini[1]) * scale; @@ -100,8 +99,8 @@ export function uiMapInMap(context) { isTransformed = true; tCurr = d3_zoomIdentity.translate(x, y).scale(k); - var zMain = ktoz(context.projection.scale()), - zMini = ktoz(k); + var zMain = geoScaleToZoom(context.projection.scale()); + var zMini = geoScaleToZoom(k); zDiff = zMain - zMini; @@ -115,29 +114,29 @@ export function uiMapInMap(context) { updateProjection(); gesture = null; - var dMini = utilGetDimensions(wrap), - cMini = vecScale(dMini, 0.5); + var dMini = utilGetDimensions(wrap); + var cMini = geoVecScale(dMini, 0.5); context.map().center(projection.invert(cMini)); // recenter main map.. } function updateProjection() { - var loc = context.map().center(), - dMini = utilGetDimensions(wrap), - cMini = vecScale(dMini, 0.5), - tMain = context.projection.transform(), - zMain = ktoz(tMain.k), - zMini = Math.max(zMain - zDiff, 0.5), - kMini = ztok(zMini); + var loc = context.map().center(); + var dMini = utilGetDimensions(wrap); + var cMini = geoVecScale(dMini, 0.5); + var tMain = context.projection.transform(); + var zMain = geoScaleToZoom(tMain.k); + var zMini = Math.max(zMain - zDiff, 0.5); + var kMini = geoZoomToScale(zMini); projection .translate([tMain.x, tMain.y]) .scale(kMini); - var point = projection(loc), - mouse = (gesture === 'pan') ? vecSub([tCurr.x, tCurr.y], [tStart.x, tStart.y]) : [0, 0], - xMini = cMini[0] - point[0] + tMain.x + mouse[0], - yMini = cMini[1] - point[1] + tMain.y + mouse[1]; + var point = projection(loc); + var mouse = (gesture === 'pan') ? geoVecSubtract([tCurr.x, tCurr.y], [tStart.x, tStart.y]) : [0, 0]; + var xMini = cMini[0] - point[0] + tMain.x + mouse[0]; + var yMini = cMini[1] - point[1] + tMain.y + mouse[1]; projection .translate([xMini, yMini]) @@ -152,7 +151,7 @@ export function uiMapInMap(context) { } zoom - .scaleExtent([ztok(0.5), ztok(zMain - 3)]); + .scaleExtent([geoZoomToScale(0.5), geoZoomToScale(zMain - 3)]); skipEvents = true; wrap.call(zoom.transform, tCurr); @@ -166,8 +165,8 @@ export function uiMapInMap(context) { updateProjection(); - var dMini = utilGetDimensions(wrap), - zMini = ktoz(projection.scale()); + var dMini = utilGetDimensions(wrap); + var zMini = geoScaleToZoom(projection.scale()); // setup tile container tiles = wrap @@ -249,8 +248,8 @@ export function uiMapInMap(context) { // redraw viewport bounding box if (gesture !== 'pan') { - var getPath = d3_geoPath(projection), - bbox = { type: 'Polygon', coordinates: [context.map().extent().polygon()] }; + var getPath = d3_geoPath(projection); + var bbox = { type: 'Polygon', coordinates: [context.map().extent().polygon()] }; viewport = wrap.selectAll('.map-in-map-viewport') .data([0]); diff --git a/modules/ui/radial_menu.js b/modules/ui/radial_menu.js index 01b6d2458..3afd4f738 100644 --- a/modules/ui/radial_menu.js +++ b/modules/ui/radial_menu.js @@ -3,7 +3,7 @@ import { select as d3_select } from 'd3-selection'; -import { geoRoundCoords } from '../geo'; +import { geoVecFloor } from '../geo'; import { uiTooltipHtml } from './tooltipHtml'; @@ -58,7 +58,7 @@ export function uiRadialMenu(context, operations) { .attr('class', function(d) { return 'radial-menu-item radial-menu-item-' + d.id; }) .classed('disabled', function(d) { return d.disabled(); }) .attr('transform', function(d, i) { - return 'translate(' + geoRoundCoords([ + return 'translate(' + geoVecFloor([ r * Math.sin(a0 + i * a), r * Math.cos(a0 + i * a)]).join(',') + ')'; }); diff --git a/test/spec/geo/geo.js b/test/spec/geo/geo.js index b4ac874c6..989c55714 100644 --- a/test/spec/geo/geo.js +++ b/test/spec/geo/geo.js @@ -1,60 +1,96 @@ describe('iD.geo', function() { - describe('geoRoundCoords', function() { - it('rounds coordinates', function() { - expect(iD.geoRoundCoords([0.1, 1])).to.eql([0, 1]); - expect(iD.geoRoundCoords([0, 1])).to.eql([0, 1]); - expect(iD.geoRoundCoords([0, 1.1])).to.eql([0, 1]); + + describe('geoVecAdd', function() { + it('adds vectors', function() { + expect(iD.geoVecAdd([1, 2], [3, 4])).to.eql([4, 6]); + expect(iD.geoVecAdd([1, 2], [0, 0])).to.eql([1, 2]); + expect(iD.geoVecAdd([1, 2], [-3, -4])).to.eql([-2, -2]); + }); + }); + + describe('geoVecSubtract', function() { + it('subtracts vectors', function() { + expect(iD.geoVecSubtract([1, 2], [3, 4])).to.eql([-2, -2]); + expect(iD.geoVecSubtract([1, 2], [0, 0])).to.eql([1, 2]); + expect(iD.geoVecSubtract([1, 2], [-3, -4])).to.eql([4, 6]); + }); + }); + + describe('geoVecScale', function() { + it('multiplies vectors', function() { + expect(iD.geoVecScale([1, 2], 0)).to.eql([0, 0]); + expect(iD.geoVecScale([1, 2], 1)).to.eql([1, 2]); + expect(iD.geoVecScale([1, 2], 2)).to.eql([2, 4]); + expect(iD.geoVecScale([1, 2], 0.5)).to.eql([0.5, 1]); + }); + }); + + describe('geoVecFloor (was: geoRoundCoordinates)', function() { + it('rounds vectors', function() { + expect(iD.geoVecFloor([0.1, 1])).to.eql([0, 1]); + expect(iD.geoVecFloor([0, 1])).to.eql([0, 1]); + expect(iD.geoVecFloor([0, 1.1])).to.eql([0, 1]); }); }); describe('geoInterp', function() { it('interpolates halfway', function() { - var a = [0, 0], - b = [10, 10]; + var a = [0, 0]; + var b = [10, 10]; expect(iD.geoInterp(a, b, 0.5)).to.eql([5, 5]); }); it('interpolates to one side', function() { - var a = [0, 0], - b = [10, 10]; + var a = [0, 0]; + var b = [10, 10]; expect(iD.geoInterp(a, b, 0)).to.eql([0, 0]); }); }); + describe('geoDot', function() { + it('dot product of right angle is zero', function() { + var a = [1, 0]; + var b = [0, 1]; + expect(iD.geoDot(a, b)).to.eql(0); + }); + it('dot product of same vector multiplies', function() { + var a = [2, 0]; + var b = [2, 0]; + expect(iD.geoDot(a, b)).to.eql(4); + }); + }); + describe('geoCross', function() { - it('cross product of right hand turn is positive', function() { - var o = [0, 0], - a = [2, 0], - b = [0, 2]; - expect(iD.geoCross(o, a, b)).to.eql(4); + it('2D cross product of right hand turn is positive', function() { + var a = [2, 0]; + var b = [0, 2]; + expect(iD.geoCross(a, b)).to.eql(4); }); - it('cross product of left hand turn is negative', function() { - var o = [0, 0], - a = [2, 0], - b = [0, -2]; - expect(iD.geoCross(o, a, b)).to.eql(-4); + it('2D cross product of left hand turn is negative', function() { + var a = [2, 0]; + var b = [0, -2]; + expect(iD.geoCross(a, b)).to.eql(-4); }); - it('cross product of colinear points is zero', function() { - var o = [0, 0], - a = [-2, 0], - b = [2, 0]; - expect(iD.geoCross(o, a, b)).to.equal(0); + it('2D cross product of colinear points is zero', function() { + var a = [-2, 0]; + var b = [2, 0]; + expect(iD.geoCross(a, b)).to.equal(0); }); }); describe('geoEuclideanDistance', function() { it('distance between two same points is zero', function() { - var a = [0, 0], - b = [0, 0]; + var a = [0, 0]; + var b = [0, 0]; expect(iD.geoEuclideanDistance(a, b)).to.eql(0); }); it('a straight 10 unit line is 10', function() { - var a = [0, 0], - b = [10, 0]; + var a = [0, 0]; + var b = [10, 0]; expect(iD.geoEuclideanDistance(a, b)).to.eql(10); }); it('a pythagorean triangle is right', function() { - var a = [0, 0], - b = [4, 3]; + var a = [0, 0]; + var b = [4, 3]; expect(iD.geoEuclideanDistance(a, b)).to.eql(5); }); }); @@ -64,10 +100,10 @@ describe('iD.geo', function() { expect(iD.geoLatToMeters(0)).to.eql(0); }); it('1 degree latitude is approx 111 km', function() { - expect(iD.geoLatToMeters(1)).to.be.within(110E3, 112E3); + expect(iD.geoLatToMeters(1)).to.be.closeTo(111319, 10); }); it('-1 degree latitude is approx -111 km', function() { - expect(iD.geoLatToMeters(-1)).to.be.within(-112E3, -110E3); + expect(iD.geoLatToMeters(-1)).to.be.closeTo(-111319, 10); }); }); @@ -76,21 +112,21 @@ describe('iD.geo', function() { expect(iD.geoLonToMeters(0, 0)).to.eql(0); }); it('distance of 1 degree longitude varies with latitude', function() { - expect(iD.geoLonToMeters(1, 0)).to.be.within(110E3, 112E3); - expect(iD.geoLonToMeters(1, 15)).to.be.within(107E3, 108E3); - expect(iD.geoLonToMeters(1, 30)).to.be.within(96E3, 97E3); - expect(iD.geoLonToMeters(1, 45)).to.be.within(78E3, 79E3); - expect(iD.geoLonToMeters(1, 60)).to.be.within(55E3, 56E3); - expect(iD.geoLonToMeters(1, 75)).to.be.within(28E3, 29E3); + expect(iD.geoLonToMeters(1, 0)).to.be.closeTo(110946, 10); + expect(iD.geoLonToMeters(1, 15)).to.be.closeTo(107165, 10); + expect(iD.geoLonToMeters(1, 30)).to.be.closeTo(96082, 10); + expect(iD.geoLonToMeters(1, 45)).to.be.closeTo(78450, 10); + expect(iD.geoLonToMeters(1, 60)).to.be.closeTo(55473, 10); + expect(iD.geoLonToMeters(1, 75)).to.be.closeTo(28715, 10); expect(iD.geoLonToMeters(1, 90)).to.eql(0); }); it('distance of -1 degree longitude varies with latitude', function() { - expect(iD.geoLonToMeters(-1, 0)).to.be.within(-112E3, -110E3); - expect(iD.geoLonToMeters(-1, -15)).to.be.within(-108E3, -107E3); - expect(iD.geoLonToMeters(-1, -30)).to.be.within(-97E3, -96E3); - expect(iD.geoLonToMeters(-1, -45)).to.be.within(-79E3, -78E3); - expect(iD.geoLonToMeters(-1, -60)).to.be.within(-56E3, -55E3); - expect(iD.geoLonToMeters(-1, -75)).to.be.within(-29E3, -28E3); + expect(iD.geoLonToMeters(-1, -0)).to.be.closeTo(-110946, 10); + expect(iD.geoLonToMeters(-1, -15)).to.be.closeTo(-107165, 10); + expect(iD.geoLonToMeters(-1, -30)).to.be.closeTo(-96082, 10); + expect(iD.geoLonToMeters(-1, -45)).to.be.closeTo(-78450, 10); + expect(iD.geoLonToMeters(-1, -60)).to.be.closeTo(-55473, 10); + expect(iD.geoLonToMeters(-1, -75)).to.be.closeTo(-28715, 10); expect(iD.geoLonToMeters(-1, -90)).to.eql(0); }); }); @@ -100,10 +136,10 @@ describe('iD.geo', function() { expect(iD.geoMetersToLat(0)).to.eql(0); }); it('111 km is approx 1 degree latitude', function() { - expect(iD.geoMetersToLat(111E3)).to.be.within(0.995, 1.005); + expect(iD.geoMetersToLat(111319)).to.be.closeTo(1, 0.0001); }); it('-111 km is approx -1 degree latitude', function() { - expect(iD.geoMetersToLat(-111E3)).to.be.within(-1.005, -0.995); + expect(iD.geoMetersToLat(-111319)).to.be.closeTo(-1, 0.0001); }); }); @@ -112,22 +148,22 @@ describe('iD.geo', function() { expect(iD.geoMetersToLon(0, 0)).to.eql(0); }); it('distance of 1 degree longitude varies with latitude', function() { - expect(iD.geoMetersToLon(111320, 0)).to.be.within(0.995, 1.005); - expect(iD.geoMetersToLon(107551, 15)).to.be.within(0.995, 1.005); - expect(iD.geoMetersToLon(96486, 30)).to.be.within(0.995, 1.005); - expect(iD.geoMetersToLon(78847, 45)).to.be.within(0.995, 1.005); - expect(iD.geoMetersToLon(55800, 60)).to.be.within(0.995, 1.005); - expect(iD.geoMetersToLon(28902, 75)).to.be.within(0.995, 1.005); + expect(iD.geoMetersToLon(110946, 0)).to.be.closeTo(1, 1e-4); + expect(iD.geoMetersToLon(107165, 15)).to.be.closeTo(1, 1e-4); + expect(iD.geoMetersToLon(96082, 30)).to.be.closeTo(1, 1e-4); + expect(iD.geoMetersToLon(78450, 45)).to.be.closeTo(1, 1e-4); + expect(iD.geoMetersToLon(55473, 60)).to.be.closeTo(1, 1e-4); + expect(iD.geoMetersToLon(28715, 75)).to.be.closeTo(1, 1e-4); expect(iD.geoMetersToLon(1, 90)).to.eql(0); }); it('distance of -1 degree longitude varies with latitude', function() { - expect(iD.geoMetersToLon(-111320, 0)).to.be.within(-1.005, -0.995); - expect(iD.geoMetersToLon(-107551, 15)).to.be.within(-1.005, -0.995); - expect(iD.geoMetersToLon(-96486, 30)).to.be.within(-1.005, -0.995); - expect(iD.geoMetersToLon(-78847, 45)).to.be.within(-1.005, -0.995); - expect(iD.geoMetersToLon(-55800, 60)).to.be.within(-1.005, -0.995); - expect(iD.geoMetersToLon(-28902, 75)).to.be.within(-1.005, -0.995); - expect(iD.geoMetersToLon(-1, 90)).to.eql(0); + expect(iD.geoMetersToLon(-110946, -0)).to.be.closeTo(-1, 1e-4); + expect(iD.geoMetersToLon(-107165, -15)).to.be.closeTo(-1, 1e-4); + expect(iD.geoMetersToLon(-96082, -30)).to.be.closeTo(-1, 1e-4); + expect(iD.geoMetersToLon(-78450, -45)).to.be.closeTo(-1, 1e-4); + expect(iD.geoMetersToLon(-55473, -60)).to.be.closeTo(-1, 1e-4); + expect(iD.geoMetersToLon(-28715, -75)).to.be.closeTo(-1, 1e-4); + expect(iD.geoMetersToLon(-1, -90)).to.eql(0); }); }); @@ -159,43 +195,61 @@ describe('iD.geo', function() { describe('geoSphericalDistance', function() { it('distance between two same points is zero', function() { - var a = [0, 0], - b = [0, 0]; + var a = [0, 0]; + var b = [0, 0]; expect(iD.geoSphericalDistance(a, b)).to.eql(0); }); it('a straight 1 degree line at the equator is aproximately 111 km', function() { - var a = [0, 0], - b = [1, 0]; - expect(iD.geoSphericalDistance(a, b)).to.be.within(110E3, 112E3); + var a = [0, 0]; + var b = [1, 0]; + expect(iD.geoSphericalDistance(a, b)).to.be.closeTo(110946, 10); }); it('a pythagorean triangle is (nearly) right', function() { - var a = [0, 0], - b = [4, 3]; - expect(iD.geoSphericalDistance(a, b)).to.be.within(555E3, 556E3); + var a = [0, 0]; + var b = [4, 3]; + expect(iD.geoSphericalDistance(a, b)).to.be.closeTo(555282, 10); }); it('east-west distances at high latitude are shorter', function() { - var a = [0, 60], - b = [1, 60]; - expect(iD.geoSphericalDistance(a, b)).to.be.within(55E3, 56E3); + var a = [0, 60]; + var b = [1, 60]; + expect(iD.geoSphericalDistance(a, b)).to.be.closeTo(55473, 10); }); it('north-south distances at high latitude are not shorter', function() { - var a = [0, 60], - b = [0, 61]; - expect(iD.geoSphericalDistance(a, b)).to.be.within(110E3, 112E3); + var a = [0, 60]; + var b = [0, 61]; + expect(iD.geoSphericalDistance(a, b)).to.be.closeTo(111319, 10); + }); + }); + + describe('geoZoomToScale', function() { + it('converts from zoom to projection scale (tileSize = 256)', function() { + expect(iD.geoZoomToScale(17)).to.be.closeTo(5340353.715440872, 1e-6); + }); + it('converts from zoom to projection scale (tileSize = 512)', function() { + expect(iD.geoZoomToScale(17, 512)).to.be.closeTo(10680707.430881744, 1e-6); + }); + }); + + describe('geoScaleToZoom', function() { + it('converts from projection scale to zoom (tileSize = 256)', function() { + expect(iD.geoScaleToZoom(5340353.715440872)).to.be.closeTo(17, 1e-6); + }); + it('converts from projection scale to zoom (tileSize = 512)', function() { + expect(iD.geoScaleToZoom(10680707.430881744, 512)).to.be.closeTo(17, 1e-6); }); }); describe('geoEdgeEqual', function() { it('returns false for inequal edges', function() { - expect(iD.geoEdgeEqual(['a','b'], ['a','c'])).to.be.false; + expect(iD.geoEdgeEqual(['a', 'b'], ['a', 'c'])).to.be.false; }); it('returns true for equal edges along same direction', function() { - expect(iD.geoEdgeEqual(['a','b'], ['a','b'])).to.be.true; + expect(iD.geoEdgeEqual(['a', 'b'], ['a', 'b'])).to.be.true; }); it('returns true for equal edges along opposite direction', function() { - expect(iD.geoEdgeEqual(['a','b'], ['b','a'])).to.be.true; + expect(iD.geoEdgeEqual(['a', 'b'], ['b', 'a'])).to.be.true; }); }); @@ -211,10 +265,10 @@ describe('iD.geo', function() { describe('geoRotate', function() { it('rotates points around [0, 0]', function() { - var points = [[5, 0], [5, 1]], - angle = Math.PI, - around = [0, 0], - result = iD.geoRotate(points, angle, around); + var points = [[5, 0], [5, 1]]; + var angle = Math.PI; + var around = [0, 0]; + var result = iD.geoRotate(points, angle, around); expect(result[0][0]).to.be.closeTo(-5, 1e-6); expect(result[0][1]).to.be.closeTo(0, 1e-6); expect(result[1][0]).to.be.closeTo(-5, 1e-6); @@ -222,10 +276,10 @@ describe('iD.geo', function() { }); it('rotates points around [3, 0]', function() { - var points = [[5, 0], [5, 1]], - angle = Math.PI, - around = [3, 0], - result = iD.geoRotate(points, angle, around); + var points = [[5, 0], [5, 1]]; + var angle = Math.PI; + var around = [3, 0]; + var result = iD.geoRotate(points, angle, around); expect(result[0][0]).to.be.closeTo(1, 1e-6); expect(result[0][1]).to.be.closeTo(0, 1e-6); expect(result[1][0]).to.be.closeTo(1, 1e-6); @@ -246,7 +300,7 @@ describe('iD.geo', function() { }); it('returns undefined properties for a degenerate way (single node)', function() { - expect(iD.geoChooseEdge([iD.Node({loc: [0, 0]})], [0, 0], projection)).to.eql({ + expect(iD.geoChooseEdge([iD.osmNode({loc: [0, 0]})], [0, 0], projection)).to.eql({ index: undefined, distance: Infinity, loc: undefined @@ -259,14 +313,10 @@ describe('iD.geo', function() { // c // // * = [2, 0] - var a = [0, 0], - b = [5, 0], - c = [2, 1], - nodes = [ - iD.Node({loc: a}), - iD.Node({loc: b}) - ]; - + var a = [0, 0]; + var b = [5, 0]; + var c = [2, 1]; + var nodes = [ iD.osmNode({loc: a}), iD.osmNode({loc: b}) ]; var choice = iD.geoChooseEdge(nodes, c, projection); expect(choice.index).to.eql(1); expect(choice.distance).to.eql(1); @@ -274,14 +324,10 @@ describe('iD.geo', function() { }); it('returns the starting vertex when the orthogonal projection is < 0', function() { - var a = [0, 0], - b = [5, 0], - c = [-3, 4], - nodes = [ - iD.Node({loc: a}), - iD.Node({loc: b}) - ]; - + var a = [0, 0]; + var b = [5, 0]; + var c = [-3, 4]; + var nodes = [ iD.osmNode({loc: a}), iD.osmNode({loc: b}) ]; var choice = iD.geoChooseEdge(nodes, c, projection); expect(choice.index).to.eql(1); expect(choice.distance).to.eql(5); @@ -289,14 +335,10 @@ describe('iD.geo', function() { }); it('returns the ending vertex when the orthogonal projection is > 1', function() { - var a = [0, 0], - b = [5, 0], - c = [8, 4], - nodes = [ - iD.Node({loc: a}), - iD.Node({loc: b}) - ]; - + var a = [0, 0]; + var b = [5, 0]; + var c = [8, 4]; + var nodes = [ iD.osmNode({loc: a}), iD.osmNode({loc: b}) ]; var choice = iD.geoChooseEdge(nodes, c, projection); expect(choice.index).to.eql(1); expect(choice.distance).to.eql(5); @@ -306,28 +348,28 @@ describe('iD.geo', function() { describe('geoLineIntersection', function() { it('returns null if lines are colinear with overlap', function() { - var a = [[0, 0], [10, 0]], - b = [[-5, 0], [5, 0]]; + var a = [[0, 0], [10, 0]]; + var b = [[-5, 0], [5, 0]]; expect(iD.geoLineIntersection(a, b)).to.be.null; }); it('returns null if lines are colinear but disjoint', function() { - var a = [[5, 0], [10, 0]], - b = [[-10, 0], [-5, 0]]; + var a = [[5, 0], [10, 0]]; + var b = [[-10, 0], [-5, 0]]; expect(iD.geoLineIntersection(a, b)).to.be.null; }); it('returns null if lines are parallel', function() { - var a = [[0, 0], [10, 0]], - b = [[0, 5], [10, 5]]; + var a = [[0, 0], [10, 0]]; + var b = [[0, 5], [10, 5]]; expect(iD.geoLineIntersection(a, b)).to.be.null; }); it('returns the intersection point between 2 lines', function() { - var a = [[0, 0], [10, 0]], - b = [[5, 10], [5, -10]]; + var a = [[0, 0], [10, 0]]; + var b = [[5, 10], [5, -10]]; expect(iD.geoLineIntersection(a, b)).to.eql([5, 0]); }); it('returns null if lines are not parallel but not intersecting', function() { - var a = [[0, 0], [10, 0]], - b = [[-5, 10], [-5, -10]]; + var a = [[0, 0], [10, 0]]; + var b = [[-5, 10], [-5, -10]]; expect(iD.geoLineIntersection(a, b)).to.be.null; }); }); @@ -339,12 +381,7 @@ describe('iD.geo', function() { expect(iD.geoPointInPolygon(point, poly)).to.be.true; }); it('says a point outside of a polygon is outside', function() { - var poly = [ - [0, 0], - [0, 1], - [1, 1], - [1, 0], - [0, 0]]; + var poly = [[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]; var point = [0.5, 1.5]; expect(iD.geoPointInPolygon(point, poly)).to.be.false; }); diff --git a/test/spec/svg/areas.js b/test/spec/svg/areas.js index 140576398..1b9e59e08 100644 --- a/test/spec/svg/areas.js +++ b/test/spec/svg/areas.js @@ -1,13 +1,10 @@ describe('iD.svgAreas', function () { - var TAU = 2 * Math.PI; - function ztok(z) { return 256 * Math.pow(2, z) / TAU; } - var context, surface; var all = function() { return true; }; var none = function() { return false; }; var projection = d3.geoProjection(function(x, y) { return [x, -y]; }) .translate([0, 0]) - .scale(ztok(17)) + .scale(iD.geoZoomToScale(17)) .clipExtent([[0, 0], [Infinity, Infinity]]); diff --git a/test/spec/svg/layers.js b/test/spec/svg/layers.js index fa70b6565..75c957daa 100644 --- a/test/spec/svg/layers.js +++ b/test/spec/svg/layers.js @@ -1,11 +1,8 @@ describe('iD.svgLayers', function () { - var TAU = 2 * Math.PI; - function ztok(z) { return 256 * Math.pow(2, z) / TAU; } - var context, container; var projection = d3.geoProjection(function(x, y) { return [x, -y]; }) .translate([0, 0]) - .scale(ztok(17)) + .scale(iD.geoZoomToScale(17)) .clipExtent([[0, 0], [Infinity, Infinity]]); beforeEach(function () { diff --git a/test/spec/svg/lines.js b/test/spec/svg/lines.js index 2a414afcd..d10fe09a6 100644 --- a/test/spec/svg/lines.js +++ b/test/spec/svg/lines.js @@ -1,13 +1,10 @@ describe('iD.svgLines', function () { - var TAU = 2 * Math.PI; - function ztok(z) { return 256 * Math.pow(2, z) / TAU; } - var context, surface; var all = function() { return true; }; var none = function() { return false; }; var projection = d3.geoProjection(function(x, y) { return [x, -y]; }) .translate([0, 0]) - .scale(ztok(17)) + .scale(iD.geoZoomToScale(17)) .clipExtent([[0, 0], [Infinity, Infinity]]); diff --git a/test/spec/svg/midpoints.js b/test/spec/svg/midpoints.js index 230ad8446..c30530fae 100644 --- a/test/spec/svg/midpoints.js +++ b/test/spec/svg/midpoints.js @@ -1,13 +1,10 @@ describe('iD.svgMidpoints', function () { - var TAU = 2 * Math.PI; - function ztok(z) { return 256 * Math.pow(2, z) / TAU; } - var context, surface; var _selectedIDs = []; var filter = function() { return true; }; var projection = d3.geoProjection(function(x, y) { return [x, -y]; }) .translate([0, 0]) - .scale(ztok(17)) + .scale(iD.geoZoomToScale(17)) .clipExtent([[0, 0], [Infinity, Infinity]]); diff --git a/test/spec/svg/points.js b/test/spec/svg/points.js index 8faaa54c2..c1012a7cd 100644 --- a/test/spec/svg/points.js +++ b/test/spec/svg/points.js @@ -1,11 +1,8 @@ describe('iD.svgPoints', function () { - var TAU = 2 * Math.PI; - function ztok(z) { return 256 * Math.pow(2, z) / TAU; } - var context, surface; var projection = d3.geoProjection(function(x, y) { return [x, -y]; }) .translate([0, 0]) - .scale(ztok(17)) + .scale(iD.geoZoomToScale(17)) .clipExtent([[0, 0], [Infinity, Infinity]]); beforeEach(function () { diff --git a/test/spec/svg/vertices.js b/test/spec/svg/vertices.js index 3f7789643..1c29ca70d 100644 --- a/test/spec/svg/vertices.js +++ b/test/spec/svg/vertices.js @@ -1,12 +1,9 @@ describe('iD.svgVertices', function () { - var TAU = 2 * Math.PI; - function ztok(z) { return 256 * Math.pow(2, z) / TAU; } - var context; var surface; var projection = d3.geoProjection(function(x, y) { return [x, -y]; }) .translate([0, 0]) - .scale(ztok(17)) + .scale(iD.geoZoomToScale(17)) .clipExtent([[0, 0], [Infinity, Infinity]]); From 7155ef8bc6e46ef80cb9d191457877a543cd2842 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 18 Dec 2017 16:09:07 -0500 Subject: [PATCH 40/70] Fix double clicking on a way to create a vertex --- modules/modes/select.js | 12 ++++++------ modules/svg/vertices.js | 20 ++++++++++++++++---- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/modules/modes/select.js b/modules/modes/select.js index fa5a3ddd9..941c41751 100644 --- a/modules/modes/select.js +++ b/modules/modes/select.js @@ -245,13 +245,13 @@ export function modeSelect(context, selectedIDs) { function dblclick() { - var target = d3_select(d3_event.target), - datum = target.datum(); + var target = d3_select(d3_event.target); + var datum = target.datum(); - if (datum instanceof osmWay && !target.classed('fill')) { - var choice = geoChooseEdge(context.childNodes(datum), context.mouse(), context.projection), - prev = datum.nodes[choice.index - 1], - next = datum.nodes[choice.index]; + if (datum instanceof osmWay && target.classed('target')) { + var choice = geoChooseEdge(context.childNodes(datum), context.mouse(), context.projection); + var prev = datum.nodes[choice.index - 1]; + var next = datum.nodes[choice.index]; context.perform( actionAddMidpoint({loc: choice.loc, edge: [prev, next]}, osmNode()), diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index df681a2d1..a6bc3ca7d 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -268,15 +268,27 @@ export function svgVertices(projection, context) { for (var i = 0; i < entities.length; i++) { var entity = entities[i]; var geometry = entity.geometry(graph); + var keep = false; + // a point that looks like a vertex.. if ((geometry === 'point') && renderAsVertex(entity, graph, wireframe, zoom)) { _currPersistent[entity.id] = entity; + keep = true; - } else if ((geometry === 'vertex') && - (entity.hasInterestingTags() || entity.isEndpoint(graph) || entity.isConnected(graph)) ) { - _currPersistent[entity.id] = entity; + // a vertex of some importance.. + } else if (geometry === 'vertex') { + if (entity.hasInterestingTags() || entity.isEndpoint(graph) || entity.isConnected(graph)) { + _currPersistent[entity.id] = entity; + keep = true; + } + // partial redraw in select mode - probably because the user double clicked a way. + if (!fullRedraw && mode.id === 'select') { + _currSelected[entity.id] = entity; + } + } - } else if (!fullRedraw) { + // whatever this is, it's not a persistent vertex.. + if (!keep && !fullRedraw) { delete _currPersistent[entity.id]; } } From eafc2b43009db57ef75e2883501e2ca5706b23c2 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 18 Dec 2017 17:00:47 -0500 Subject: [PATCH 41/70] Adjust touch target radii --- modules/svg/midpoints.js | 4 ++-- modules/svg/vertices.js | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/modules/svg/midpoints.js b/modules/svg/midpoints.js index 60ff03de8..ed629c6c6 100644 --- a/modules/svg/midpoints.js +++ b/modules/svg/midpoints.js @@ -14,7 +14,7 @@ import { export function svgMidpoints(projection, context) { - + var targetRadius = 8; function drawTargets(selection, graph, entities, filter) { var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; @@ -29,7 +29,7 @@ export function svgMidpoints(projection, context) { // enter/update targets.enter() .append('circle') - .attr('r', 12) + .attr('r', targetRadius) .merge(targets) .attr('class', function(d) { return 'node midpoint target ' + fillClass + d.id; }) .attr('transform', svgPointTransform(projection)); diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index a6bc3ca7d..4b066cb59 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -23,6 +23,7 @@ export function svgVertices(projection, context) { var _prevHover = {}; var _currSelected = {}; var _prevSelected = {}; + var _radii = {}; function sortY(a, b) { @@ -40,6 +41,7 @@ export function svgVertices(projection, context) { function draw(selection, graph, vertices, sets, filter) { sets = sets || { selected: {}, important: {}, hovered: {} }; + var icons = {}; var directions = {}; var wireframe = context.surface().classed('fill-wireframe'); @@ -80,9 +82,13 @@ export function svgVertices(projection, context) { r += 1.5; } + if (klass === 'shadow') { // remember this value, so we don't need to + _radii[entity.id] = r; // recompute it when we draw the touch targets + } + d3_select(this) .attr('r', r) - .attr('visibility', ((i && klass === 'fill') ? 'hidden' : null)); + .attr('visibility', (i && klass === 'fill') ? 'hidden' : null); }); }); @@ -188,7 +194,7 @@ export function svgVertices(projection, context) { // enter/update targets.enter() .append('circle') - .attr('r', radiuses.shadow[3]) // just use the biggest one for now + .attr('r', function(d) { return _radii[d.id] || radiuses.shadow[3]; }) .merge(targets) .attr('class', function(d) { return 'node vertex target ' + fillClass + d.id; }) .attr('transform', svgPointTransform(projection)); @@ -259,12 +265,13 @@ export function svgVertices(projection, context) { var mode = context.mode(); var isMoving = mode && /^(add|draw|drag|move|rotate)/.test(mode.id); - // Collect important vertices from the `entities` list.. - // (during a paritial redraw, it will not contain everything) if (fullRedraw) { _currPersistent = {}; + _radii = {}; } + // Collect important vertices from the `entities` list.. + // (during a paritial redraw, it will not contain everything) for (var i = 0; i < entities.length; i++) { var entity = entities[i]; var geometry = entity.geometry(graph); @@ -282,7 +289,7 @@ export function svgVertices(projection, context) { keep = true; } // partial redraw in select mode - probably because the user double clicked a way. - if (!fullRedraw && mode.id === 'select') { + if (!fullRedraw && mode && mode.id === 'select') { _currSelected[entity.id] = entity; } } From 7a8f50c74e54e6749fc0f4c64e0644657044a0ef Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 18 Dec 2017 22:54:49 -0500 Subject: [PATCH 42/70] More fixes for drawing/snapping, don't draw touch targets for activeIDs --- css/20_map.css | 2 +- css/70_fills.css | 4 +++ modules/behavior/draw.js | 17 +++++++----- modules/behavior/draw_way.js | 51 ++++++++++++++++++------------------ modules/core/context.js | 7 +++++ modules/modes/drag_node.js | 17 +++++++----- modules/modes/draw_area.js | 8 ++++-- modules/modes/draw_line.js | 10 ++++--- modules/renderer/map.js | 6 ++++- modules/svg/areas.js | 5 +++- modules/svg/lines.js | 5 +++- modules/svg/points.js | 6 ++++- modules/svg/vertices.js | 6 ++++- 13 files changed, 95 insertions(+), 49 deletions(-) diff --git a/css/20_map.css b/css/20_map.css index 38106a223..87fea9309 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -26,7 +26,7 @@ .way.target { pointer-events: stroke; fill: none; - stroke-width: 10; + stroke-width: 12; stroke-opacity: 0.8; stroke: currentColor; } diff --git a/css/70_fills.css b/css/70_fills.css index 3587bbee8..1c536510c 100644 --- a/css/70_fills.css +++ b/css/70_fills.css @@ -33,5 +33,9 @@ .fill-partial path.area.fill { fill-opacity: 0; stroke-width: 60px; + pointer-events: none; +} +.mode-browse .fill-partial path.area.fill, +.mode-select .fill-partial path.area.fill { pointer-events: visibleStroke; } diff --git a/modules/behavior/draw.js b/modules/behavior/draw.js index 61ca00af5..4bce319dd 100644 --- a/modules/behavior/draw.js +++ b/modules/behavior/draw.js @@ -48,11 +48,17 @@ export function behaviorDraw(context) { function datum() { if (d3_event.altKey) return {}; + var element; if (d3_event.type === 'keydown') { - return (_lastMouse && _lastMouse.target.__data__) || {}; + element = _lastMouse && _lastMouse.target; } else { - return d3_event.target.__data__ || {}; + element = d3_event.target; } + + // When drawing, connect only to things classed as targets.. + // (this excludes area fills and active drawing elements) + var selection = d3_select(element); + return (selection.classed('target') && element.__data__) || {}; } @@ -116,8 +122,7 @@ export function behaviorDraw(context) { function click() { - var trySnap = geoViewportEdge(context.mouse, context.map().dimensions()) !== null; - + var trySnap = geoViewportEdge(context.mouse(), context.map().dimensions()) === null; if (trySnap) { // If we're not at the edge of the viewport, try to snap.. // See also: `modes/drag_node.js doMove()` @@ -128,8 +133,8 @@ export function behaviorDraw(context) { dispatch.call('clickNode', this, d); return; - // Snap to a way (not an area fill) - } else if (d.type === 'way' && !d3_select(d3_event.sourceEvent.target).classed('fill')) { + // Snap to a way + } else if (d.type === 'way') { var choice = geoChooseEdge(context.childNodes(d), context.mouse(), context.projection); var edge = [d.nodes[choice.index - 1], d.nodes[choice.index]]; dispatch.call('clickWay', this, choice.loc, edge); diff --git a/modules/behavior/draw_way.js b/modules/behavior/draw_way.js index 57ecfb193..045b8a855 100644 --- a/modules/behavior/draw_way.js +++ b/modules/behavior/draw_way.js @@ -13,7 +13,8 @@ import { behaviorDraw } from './draw'; import { geoChooseEdge, - geoEdgeEqual + geoEdgeEqual, + geoViewportEdge } from '../geo'; import { @@ -31,17 +32,18 @@ import { utilEntitySelector } from '../util'; export function behaviorDrawWay(context, wayId, index, mode, startGraph) { - var origWay = context.entity(wayId), - isArea = context.geometry(wayId) === 'area', - tempEdits = 0, - annotation = t((origWay.isDegenerate() ? - 'operations.start.annotation.' : - 'operations.continue.annotation.') + context.geometry(wayId)), - draw = behaviorDraw(context), - startIndex, - start, - end, - segment; + var origWay = context.entity(wayId); + var isArea = context.geometry(wayId) === 'area'; + var tempEdits = 0; + var annotation = t((origWay.isDegenerate() ? + 'operations.start.annotation.' : + 'operations.continue.annotation.') + context.geometry(wayId)); + var draw = behaviorDraw(context); + var _activeIDs = []; + var startIndex; + var start; + var end; + var segment; // initialize the temporary drawing entities @@ -49,7 +51,8 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { startIndex = typeof index === 'undefined' ? origWay.nodes.length - 1 : 0; start = osmNode({ id: 'nStart', loc: context.entity(origWay.nodes[startIndex]).loc }); end = osmNode({ id: 'nEnd', loc: context.map().mouseCoordinates() }); - segment = osmWay({ id: 'wTemp', + segment = osmWay({ + id: 'wTemp', nodes: typeof index === 'undefined' ? [start.id, end.id] : [end.id, start.id], tags: _clone(origWay.tags) }); @@ -70,20 +73,11 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { function move(datum) { var loc; - if (datum.type === 'node' && datum.id !== end.id) { loc = datum.loc; } else if (datum.type === 'way') { - var dims = context.map().dimensions(), - mouse = context.mouse(), - pad = 5, - trySnap = mouse[0] > pad && mouse[0] < dims[0] - pad && - mouse[1] > pad && mouse[1] < dims[1] - pad; - - if (trySnap) { - loc = geoChooseEdge(context.childNodes(datum), context.mouse(), context.projection).loc; - } + loc = geoChooseEdge(context.childNodes(datum), context.mouse(), context.projection).loc; } if (!loc) { @@ -110,8 +104,8 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { function setActiveElements() { - var active = isArea ? [wayId, end.id] : [segment.id, start.id, end.id]; - context.surface().selectAll(utilEntitySelector(active)) + _activeIDs = isArea ? [wayId, end.id] : [segment.id, start.id, end.id]; + context.surface().selectAll(utilEntitySelector(_activeIDs)) .classed('active', true); } @@ -326,6 +320,13 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { }; + drawWay.activeIDs = function() { + if (!arguments.length) return _activeIDs; + // no assign + return drawWay; + }; + + drawWay.tail = function(text) { draw.tail(text); return drawWay; diff --git a/modules/core/context.js b/modules/core/context.js index 95146e248..e7f0deacf 100644 --- a/modules/core/context.js +++ b/modules/core/context.js @@ -255,6 +255,13 @@ export function coreContext() { return []; } }; + context.activeIDs = function() { + if (mode && mode.activeIDs) { + return mode.activeIDs(); + } else { + return []; + } + }; /* Behaviors */ diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index 915706bbd..e39832b98 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -123,7 +123,7 @@ export function modeDragNode(context) { function datum() { var event = d3_event && d3_event.sourceEvent; - if (!event || event.altKey) { + if (!event || event.altKey || !d3_select(event.target).classed('target')) { return {}; } else { return event.target.__data__ || {}; @@ -147,11 +147,8 @@ export function modeDragNode(context) { if (d.type === 'node' && d.id !== entity.id) { loc = d.loc; - // Snap to a way (not an area fill) - } else if (d.type === 'way' && !d3_select(d3_event.sourceEvent.target).classed('fill')) { - - // var childNodes = context.childNodes(d); - // var childIDs = childNodes.map(function(node) { return node.id; }); + // Snap to a way + } else if (d.type === 'way') { var choice = geoChooseEdge(context.childNodes(d), context.mouse(), context.projection); // (not along a segment adjacent to self) if (entity.id !== d.nodes[choice.index - 1] && entity.id !== d.nodes[choice.index]) { @@ -274,6 +271,7 @@ export function modeDragNode(context) { context.map() .on('drawn.drag-node', null); + _activeIDs = []; context.surface() .selectAll('.active') .classed('active', false); @@ -289,6 +287,13 @@ export function modeDragNode(context) { }; + mode.activeIDs = function() { + if (!arguments.length) return _activeIDs; + // no assign + return mode; + }; + + mode.restoreSelectedIDs = function(_) { if (!arguments.length) return _restoreSelectedIDs; _restoreSelectedIDs = _; diff --git a/modules/modes/draw_area.js b/modules/modes/draw_area.js index e5478d339..23bd196b5 100644 --- a/modules/modes/draw_area.js +++ b/modules/modes/draw_area.js @@ -20,8 +20,8 @@ export function modeDrawArea(context, wayId, startGraph) { var addNode = behavior.addNode; behavior.addNode = function(node) { - var length = way.nodes.length, - penultimate = length > 2 ? way.nodes[length - 2] : null; + var length = way.nodes.length; + var penultimate = length > 2 ? way.nodes[length - 2] : null; if (node.id === way.first() || node.id === penultimate) { behavior.finish(); @@ -43,6 +43,10 @@ export function modeDrawArea(context, wayId, startGraph) { return [wayId]; }; + mode.activeIDs = function() { + return (behavior && behavior.activeIDs()) || []; + }; + return mode; } diff --git a/modules/modes/draw_line.js b/modules/modes/draw_line.js index 5198fd444..e6099b9bd 100644 --- a/modules/modes/draw_line.js +++ b/modules/modes/draw_line.js @@ -12,15 +12,14 @@ export function modeDrawLine(context, wayId, startGraph, affix) { mode.enter = function() { - var way = context.entity(wayId), - index = (affix === 'prefix') ? 0 : undefined, - headId = (affix === 'prefix') ? way.first() : way.last(); + var way = context.entity(wayId); + var index = (affix === 'prefix') ? 0 : undefined; + var headId = (affix === 'prefix') ? way.first() : way.last(); behavior = behaviorDrawWay(context, wayId, index, mode, startGraph) .tail(t('modes.draw_line.tail')); var addNode = behavior.addNode; - behavior.addNode = function(node) { if (node.id === headId) { behavior.finish(); @@ -42,6 +41,9 @@ export function modeDrawLine(context, wayId, startGraph, affix) { return [wayId]; }; + mode.activeIDs = function() { + return (behavior && behavior.activeIDs()) || []; + }; return mode; } diff --git a/modules/renderer/map.js b/modules/renderer/map.js index 39996d04f..5987d7f2b 100644 --- a/modules/renderer/map.js +++ b/modules/renderer/map.js @@ -199,8 +199,9 @@ export function rendererMap(context) { supersurface .call(context.background()); - context.on('enter.map', function() { + context.on('enter.map', function() { if (map.editable() && !transformed) { + // redraw immediately the objects that are affected by a chnage in selectedIDs. var all = context.intersects(map.extent()); var filter = utilFunctor(true); var graph = context.graph(); @@ -210,6 +211,9 @@ export function rendererMap(context) { .call(drawVertices.drawSelected, graph, all, map.extent()) .call(drawMidpoints, graph, all, filter, map.trimmedExtent()); dispatch.call('drawn', this, { full: false }); + + // redraw everything else later + scheduleRedraw(); } }); diff --git a/modules/svg/areas.js b/modules/svg/areas.js index d540a08a0..b017d531c 100644 --- a/modules/svg/areas.js +++ b/modules/svg/areas.js @@ -44,10 +44,13 @@ export function svgAreas(projection, context) { function drawTargets(selection, graph, entities, filter) { var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; var getPath = svgPath(projection, graph); + var passive = entities.filter(function(d) { + return context.activeIDs().indexOf(d.id) === -1; + }); var targets = selection.selectAll('.area.target') .filter(filter) - .data(entities, function key(d) { return d.id; }); + .data(passive, function key(d) { return d.id; }); // exit targets.exit() diff --git a/modules/svg/lines.js b/modules/svg/lines.js index caf11c294..d5b1010bb 100644 --- a/modules/svg/lines.js +++ b/modules/svg/lines.js @@ -39,10 +39,13 @@ export function svgLines(projection, context) { function drawTargets(selection, graph, entities, filter) { var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; var getPath = svgPath(projection, graph); + var passive = entities.filter(function(d) { + return context.activeIDs().indexOf(d.id) === -1; + }); var targets = selection.selectAll('.line.target') .filter(filter) - .data(entities, function key(d) { return d.id; }); + .data(passive, function key(d) { return d.id; }); // exit targets.exit() diff --git a/modules/svg/points.js b/modules/svg/points.js index 8c41f7a1a..40898e2aa 100644 --- a/modules/svg/points.js +++ b/modules/svg/points.js @@ -29,9 +29,13 @@ export function svgPoints(projection, context) { function drawTargets(selection, graph, entities, filter) { var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; + var passive = entities.filter(function(d) { + return context.activeIDs().indexOf(d.id) === -1; + }); + var targets = selection.selectAll('.point.target') .filter(filter) - .data(entities, function key(d) { return d.id; }); + .data(passive, function key(d) { return d.id; }); // exit targets.exit() diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index 4b066cb59..4ed0ea17b 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -183,9 +183,13 @@ export function svgVertices(projection, context) { function drawTargets(selection, graph, entities, filter) { var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; + var passive = entities.filter(function(d) { + return context.activeIDs().indexOf(d.id) === -1; + }); + var targets = selection.selectAll('.vertex.target') .filter(filter) - .data(entities, function key(d) { return d.id; }); + .data(passive, function key(d) { return d.id; }); // exit targets.exit() From 563c496a652018a3b487a3dd8908ac5e5bdd33a9 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 19 Dec 2017 09:08:59 -0500 Subject: [PATCH 43/70] Update selected vertices when drawing in select mode This is a better solution to catching and drawing new verteices that got added because a user double clicked on a line. --- modules/behavior/draw_way.js | 3 +-- modules/renderer/map.js | 10 +++++++++- modules/svg/vertices.js | 15 +++++---------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/modules/behavior/draw_way.js b/modules/behavior/draw_way.js index 045b8a855..6dd7c87b3 100644 --- a/modules/behavior/draw_way.js +++ b/modules/behavior/draw_way.js @@ -13,8 +13,7 @@ import { behaviorDraw } from './draw'; import { geoChooseEdge, - geoEdgeEqual, - geoViewportEdge + geoEdgeEqual } from '../geo'; import { diff --git a/modules/renderer/map.js b/modules/renderer/map.js index 5987d7f2b..b2718ec25 100644 --- a/modules/renderer/map.js +++ b/modules/renderer/map.js @@ -208,7 +208,7 @@ export function rendererMap(context) { all = context.features().filter(all, graph); surface.selectAll('.data-layer-osm') - .call(drawVertices.drawSelected, graph, all, map.extent()) + .call(drawVertices.drawSelected, graph, map.extent()) .call(drawMidpoints, graph, all, filter, map.trimmedExtent()); dispatch.call('drawn', this, { full: false }); @@ -269,6 +269,7 @@ export function rendererMap(context) { function drawVector(difference, extent) { + var mode = context.mode(); var graph = context.graph(); var features = context.features(); var all = context.intersects(map.extent()); @@ -303,6 +304,13 @@ export function rendererMap(context) { data = features.filter(data, graph); + if (mode && mode.id === 'select') { + // update selected vertices - the user might have just double-clicked a way, + // creating a new vertex, triggering a partial redraw without a mode change + surface.selectAll('.data-layer-osm') + .call(drawVertices.drawSelected, graph, map.extent()); + } + surface.selectAll('.data-layer-osm') .call(drawVertices, graph, data, filter, map.extent(), fullRedraw) .call(drawLines, graph, data, filter) diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index 4ed0ea17b..2c9bef290 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -287,15 +287,10 @@ export function svgVertices(projection, context) { keep = true; // a vertex of some importance.. - } else if (geometry === 'vertex') { - if (entity.hasInterestingTags() || entity.isEndpoint(graph) || entity.isConnected(graph)) { - _currPersistent[entity.id] = entity; - keep = true; - } - // partial redraw in select mode - probably because the user double clicked a way. - if (!fullRedraw && mode && mode.id === 'select') { - _currSelected[entity.id] = entity; - } + } else if (geometry === 'vertex' && + (entity.hasInterestingTags() || entity.isEndpoint(graph) || entity.isConnected(graph))) { + _currPersistent[entity.id] = entity; + keep = true; } // whatever this is, it's not a persistent vertex.. @@ -336,7 +331,7 @@ export function svgVertices(projection, context) { // partial redraw - only update the selected items.. - drawVertices.drawSelected = function(selection, graph, target, extent) { + drawVertices.drawSelected = function(selection, graph, extent) { var wireframe = context.surface().classed('fill-wireframe'); var zoom = geoScaleToZoom(projection.scale()); From 7994baae23823dce7db191d721a973bda20ee750 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 19 Dec 2017 11:23:35 -0500 Subject: [PATCH 44/70] WIP: trying singular activeID and smarter target drawing code The goal here is that the code that draws the targets should know better what parts of the lines/vertices are targetable, rather than just relying on CSS to ignore the pointer events on the whole line. e.g. when drawing a line, it's ok for it to loop back and connect to itself, just not on a segment or vertex adjacent to the active node. --- modules/behavior/draw_way.js | 19 ++++++++++++++----- modules/core/context.js | 15 +++++++++------ modules/modes/drag_node.js | 25 +++++++++++++++++-------- modules/modes/draw_area.js | 7 +++++-- modules/modes/draw_line.js | 7 +++++-- modules/svg/areas.js | 3 ++- modules/svg/lines.js | 3 ++- modules/svg/points.js | 3 ++- modules/svg/vertices.js | 11 +++++++---- 9 files changed, 63 insertions(+), 30 deletions(-) diff --git a/modules/behavior/draw_way.js b/modules/behavior/draw_way.js index 6dd7c87b3..163513b3c 100644 --- a/modules/behavior/draw_way.js +++ b/modules/behavior/draw_way.js @@ -38,7 +38,8 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { 'operations.start.annotation.' : 'operations.continue.annotation.') + context.geometry(wayId)); var draw = behaviorDraw(context); - var _activeIDs = []; + // var _activeIDs = []; + var _activeID; var startIndex; var start; var end; @@ -103,8 +104,11 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { function setActiveElements() { - _activeIDs = isArea ? [wayId, end.id] : [segment.id, start.id, end.id]; - context.surface().selectAll(utilEntitySelector(_activeIDs)) + // _activeIDs = isArea ? [wayId, end.id] : [segment.id, start.id, end.id]; + // context.surface().selectAll(utilEntitySelector(_activeIDs)) + // .classed('active', true); + _activeID = end.id; + context.surface().selectAll('.' + end.id) .classed('active', true); } @@ -319,8 +323,13 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { }; - drawWay.activeIDs = function() { - if (!arguments.length) return _activeIDs; + // drawWay.activeIDs = function() { + // if (!arguments.length) return _activeIDs; + // // no assign + // return drawWay; + // }; + drawWay.activeID = function() { + if (!arguments.length) return _activeID; // no assign return drawWay; }; diff --git a/modules/core/context.js b/modules/core/context.js index e7f0deacf..dcf0b6412 100644 --- a/modules/core/context.js +++ b/modules/core/context.js @@ -255,12 +255,15 @@ export function coreContext() { return []; } }; - context.activeIDs = function() { - if (mode && mode.activeIDs) { - return mode.activeIDs(); - } else { - return []; - } + // context.activeIDs = function() { + // if (mode && mode.activeIDs) { + // return mode.activeIDs(); + // } else { + // return []; + // } + // }; + context.activeID = function() { + return mode && mode.activeID && mode.activeID(); }; diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index e39832b98..956b78314 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -44,7 +44,8 @@ export function modeDragNode(context) { var _nudgeInterval; var _restoreSelectedIDs = []; - var _activeIDs = []; + // var _activeIDs = []; + var _activeID; var _wasMidpoint = false; var _isCancelled = false; var _dragEntity; @@ -112,9 +113,9 @@ export function modeDragNode(context) { // `.active` elements have `pointer-events: none`. // This prevents the node or vertex being dragged from trying to connect to itself. - _activeIDs = context.graph().parentWays(entity) - .map(function(parent) { return parent.id; }); - _activeIDs.push(entity.id); + // _activeIDs = context.graph().parentWays(entity).map(function(parent) { return parent.id; }); + // _activeIDs.push(entity.id); + _activeID = entity.id; setActiveElements(); context.enter(mode); @@ -232,7 +233,9 @@ export function modeDragNode(context) { function setActiveElements() { - context.surface().selectAll(utilEntitySelector(_activeIDs)) + // context.surface().selectAll(utilEntitySelector(_activeIDs)) + // .classed('active', true); + context.surface().selectAll('.' + _activeID) .classed('active', true); } @@ -271,7 +274,8 @@ export function modeDragNode(context) { context.map() .on('drawn.drag-node', null); - _activeIDs = []; + // _activeIDs = []; + _activeID = null; context.surface() .selectAll('.active') .classed('active', false); @@ -287,8 +291,13 @@ export function modeDragNode(context) { }; - mode.activeIDs = function() { - if (!arguments.length) return _activeIDs; + // mode.activeIDs = function() { + // if (!arguments.length) return _activeIDs; + // // no assign + // return mode; + // }; + mode.activeID = function() { + if (!arguments.length) return _activeID; // no assign return mode; }; diff --git a/modules/modes/draw_area.js b/modules/modes/draw_area.js index 23bd196b5..0dd5b1272 100644 --- a/modules/modes/draw_area.js +++ b/modules/modes/draw_area.js @@ -43,8 +43,11 @@ export function modeDrawArea(context, wayId, startGraph) { return [wayId]; }; - mode.activeIDs = function() { - return (behavior && behavior.activeIDs()) || []; + // mode.activeIDs = function() { + // return (behavior && behavior.activeIDs()) || []; + // }; + mode.activeID = function() { + return (behavior && behavior.activeID()) || []; }; diff --git a/modules/modes/draw_line.js b/modules/modes/draw_line.js index e6099b9bd..3e638cbd9 100644 --- a/modules/modes/draw_line.js +++ b/modules/modes/draw_line.js @@ -41,8 +41,11 @@ export function modeDrawLine(context, wayId, startGraph, affix) { return [wayId]; }; - mode.activeIDs = function() { - return (behavior && behavior.activeIDs()) || []; + // mode.activeIDs = function() { + // return (behavior && behavior.activeIDs()) || []; + // }; + mode.activeID = function() { + return (behavior && behavior.activeID()) || []; }; return mode; diff --git a/modules/svg/areas.js b/modules/svg/areas.js index b017d531c..c6ff49cbb 100644 --- a/modules/svg/areas.js +++ b/modules/svg/areas.js @@ -45,7 +45,8 @@ export function svgAreas(projection, context) { var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; var getPath = svgPath(projection, graph); var passive = entities.filter(function(d) { - return context.activeIDs().indexOf(d.id) === -1; + return true; + // return context.activeIDs().indexOf(d.id) === -1; }); var targets = selection.selectAll('.area.target') diff --git a/modules/svg/lines.js b/modules/svg/lines.js index d5b1010bb..4770bebd6 100644 --- a/modules/svg/lines.js +++ b/modules/svg/lines.js @@ -40,7 +40,8 @@ export function svgLines(projection, context) { var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; var getPath = svgPath(projection, graph); var passive = entities.filter(function(d) { - return context.activeIDs().indexOf(d.id) === -1; + return true; + // return context.activeIDs().indexOf(d.id) === -1; }); var targets = selection.selectAll('.line.target') diff --git a/modules/svg/points.js b/modules/svg/points.js index 40898e2aa..ef1d504d9 100644 --- a/modules/svg/points.js +++ b/modules/svg/points.js @@ -30,7 +30,8 @@ export function svgPoints(projection, context) { function drawTargets(selection, graph, entities, filter) { var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; var passive = entities.filter(function(d) { - return context.activeIDs().indexOf(d.id) === -1; + return d.id !== context.activeID(); + // return context.activeIDs().indexOf(d.id) === -1; }); var targets = selection.selectAll('.point.target') diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index 2c9bef290..e880363f7 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -183,13 +183,16 @@ export function svgVertices(projection, context) { function drawTargets(selection, graph, entities, filter) { var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; - var passive = entities.filter(function(d) { - return context.activeIDs().indexOf(d.id) === -1; - }); + // no targets for entities that are active, or adjacent to active. + function passive(d) { + return d.id !== context.activeID(); + } + + var data = entities.filter(passive); var targets = selection.selectAll('.vertex.target') .filter(filter) - .data(passive, function key(d) { return d.id; }); + .data(data, function key(d) { return d.id; }); // exit targets.exit() From f58349864c37ae9fcd3ddb12b59d3be9b1be2744 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 20 Dec 2017 13:52:16 -0500 Subject: [PATCH 45/70] Add support for nope targets, line sub-segment targeting --- css/20_map.css | 6 ++ modules/behavior/draw.js | 5 +- modules/behavior/hover.js | 16 ++-- modules/behavior/select.js | 22 ++--- modules/modes/drag_node.js | 3 +- modules/modes/select.js | 3 + modules/svg/lines.js | 163 +++++++++++++++++++++++++++++++++++-- modules/svg/path.js | 28 ++++--- modules/svg/vertices.js | 96 ++++++++++++++++++++-- 9 files changed, 293 insertions(+), 49 deletions(-) diff --git a/css/20_map.css b/css/20_map.css index 87fea9309..1004a3d1d 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -31,6 +31,12 @@ stroke: currentColor; } +/* `.target-nope` objects are explicitly forbidden to join to */ +.node.target.target-nope, +.way.target.target-nope { + cursor: not-allowed; +} + /* `.active` objects (currently being drawn or dragged) are not interactive */ /* This is important to allow the events to drop through to whatever is */ /* below them on the map, so you can still hover and connect to other things. */ diff --git a/modules/behavior/draw.js b/modules/behavior/draw.js index 4bce319dd..d2421ffc3 100644 --- a/modules/behavior/draw.js +++ b/modules/behavior/draw.js @@ -58,7 +58,10 @@ export function behaviorDraw(context) { // When drawing, connect only to things classed as targets.. // (this excludes area fills and active drawing elements) var selection = d3_select(element); - return (selection.classed('target') && element.__data__) || {}; + if (selection.classed('target')) return {}; + + var d = selection.datum(); + return (d && d.id && context.hasEntity(d.id)) || {}; } diff --git a/modules/behavior/hover.js b/modules/behavior/hover.js index b9c4bfa5c..1a496ee2d 100644 --- a/modules/behavior/hover.js +++ b/modules/behavior/hover.js @@ -6,7 +6,6 @@ import { } from 'd3-selection'; import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js'; -import { osmEntity } from '../osm/index'; import { utilRebind } from '../util/rebind'; @@ -107,19 +106,20 @@ export function behaviorHover(context) { _selection.selectAll('.hover-suppressed') .classed('hover-suppressed', false); - if (_target instanceof osmEntity && _target.id !== _newId) { + var entity = _target && _target.id && context.hasEntity(_target.id); + if (entity && entity.id !== _newId) { // If drawing a way, don't hover on a node that was just placed. #3974 var mode = context.mode() && context.mode().id; - if ((mode === 'draw-line' || mode === 'draw-area') && !_newId && _target.type === 'node') { - _newId = _target.id; + if ((mode === 'draw-line' || mode === 'draw-area') && !_newId && entity.type === 'node') { + _newId = entity.id; return; } - var selector = '.' + _target.id; + var selector = '.' + entity.id; - if (_target.type === 'relation') { - _target.members.forEach(function(member) { + if (entity.type === 'relation') { + entity.members.forEach(function(member) { selector += ', .' + member.id; }); } @@ -129,7 +129,7 @@ export function behaviorHover(context) { _selection.selectAll(selector) .classed(suppressed ? 'hover-suppressed' : 'hover', true); - dispatch.call('hover', this, !suppressed && _target.id); + dispatch.call('hover', this, !suppressed && entity.id); } else { dispatch.call('hover', this, null); diff --git a/modules/behavior/select.js b/modules/behavior/select.js index f3daf0bc2..a77044133 100644 --- a/modules/behavior/select.js +++ b/modules/behavior/select.js @@ -17,10 +17,10 @@ import { osmEntity } from '../osm'; export function behaviorSelect(context) { - var lastMouse = null, - suppressMenu = true, - tolerance = 4, - p1 = null; + var lastMouse = null; + var suppressMenu = true; + var tolerance = 4; + var p1 = null; function point() { @@ -102,19 +102,21 @@ export function behaviorSelect(context) { .on('mouseup.select', null, true); if (!p1) return; - var p2 = point(), - dist = geoEuclideanDistance(p1, p2); + var p2 = point(); + var dist = geoEuclideanDistance(p1, p2); p1 = null; if (dist > tolerance) { return; } - var isMultiselect = d3_event.shiftKey || d3_select('#surface .lasso').node(), - isShowAlways = +context.storage('edit-menu-show-always') === 1, - datum = d3_event.target.__data__ || (lastMouse && lastMouse.target.__data__), - mode = context.mode(); + var isMultiselect = d3_event.shiftKey || d3_select('#surface .lasso').node(); + var isShowAlways = +context.storage('edit-menu-show-always') === 1; + var datum = d3_event.target.__data__ || (lastMouse && lastMouse.target.__data__); + var mode = context.mode(); + var entity = datum && datum.id && context.hasEntity(datum.id); + if (entity) datum = entity; if (datum && datum.type === 'midpoint') { datum = datum.parents[0]; diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index 956b78314..2446ffbfa 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -127,7 +127,8 @@ export function modeDragNode(context) { if (!event || event.altKey || !d3_select(event.target).classed('target')) { return {}; } else { - return event.target.__data__ || {}; + var d = event.target.__data__; + return (d && d.id && context.hasEntity(d.id)) || {}; } } diff --git a/modules/modes/select.js b/modules/modes/select.js index 941c41751..1c87a96ed 100644 --- a/modules/modes/select.js +++ b/modules/modes/select.js @@ -246,7 +246,10 @@ export function modeSelect(context, selectedIDs) { function dblclick() { var target = d3_select(d3_event.target); + var datum = target.datum(); + var entity = datum && datum.id && context.hasEntity(datum.id); + if (entity) datum = entity; if (datum instanceof osmWay && target.classed('target')) { var choice = geoChooseEdge(context.childNodes(datum), context.mouse(), context.projection); diff --git a/modules/svg/lines.js b/modules/svg/lines.js index 4770bebd6..b9012f0d9 100644 --- a/modules/svg/lines.js +++ b/modules/svg/lines.js @@ -4,6 +4,7 @@ import _flatten from 'lodash-es/flatten'; import _forOwn from 'lodash-es/forOwn'; import _map from 'lodash-es/map'; +import { geoPath as d3_geoPath } from 'd3-geo'; import { range as d3_range } from 'd3-array'; import { @@ -37,16 +38,145 @@ export function svgLines(projection, context) { function drawTargets(selection, graph, entities, filter) { - var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; - var getPath = svgPath(projection, graph); - var passive = entities.filter(function(d) { - return true; - // return context.activeIDs().indexOf(d.id) === -1; + var targetClass = context.getDebug('target') ? 'pink ' : 'nocolor '; + var nopeClass = context.getDebug('target') ? 'red ' : 'nocolor '; + var getPath = svgPath(projection, graph).geojson; + // var getPath = d3_geoPath(projection); + + var activeID = context.activeID(); + + // Rather than drawing lines directly, we'll cut out pieces + // depending on which parts are active. + var data = { targets: [], nopes: [] }; + + // Touch targets control which other vertices we can drag a vertex onto. + // - the activeID - nope + // - next to the activeID - yes (vertices will be merged) + // - 2 away from the activeID - nope (would create a self intersecting segment) + // - all others on a closed way - nope (would create a self intersecting polygon) + // + // 0 = active vertex - no touch/connect + // 1 = passive vertex - yes touch/connect + // 2 = adjacent vertex - special rules + function passive(d) { + if (!activeID) return 1; + if (activeID === d.id) return 0; + + var parents = graph.parentWays(d); + var i, j; + + for (i = 0; i < parents.length; i++) { + var nodes = parents[i].nodes; + var isClosed = parents[i].isClosed(); + for (j = 0; j < nodes.length; j++) { // find this vertex, look nearby + if (nodes[j] === d.id) { + var ix1 = j - 2; + var ix2 = j - 1; + var ix3 = j + 1; + var ix4 = j + 2; + + if (isClosed) { // wraparound if needed + var max = nodes.length - 1; + if (ix1 < 0) ix1 = max + ix1; + if (ix2 < 0) ix2 = max + ix2; + if (ix3 > max) ix3 = ix3 - max; + if (ix4 > max) ix4 = ix4 - max; + } + + if (nodes[ix1] === activeID) return 0; // prevent self intersect + else if (nodes[ix2] === activeID) return 2; // adjacent - ok! + else if (nodes[ix3] === activeID) return 2; // adjacent - ok! + else if (nodes[ix4] === activeID) return 0; // prevent self intersect + else if (isClosed && nodes.indexOf(activeID) !== -1) return 0; // prevent self intersect + } + } + } + + return 1; + } + + entities.forEach(function(way) { + var coordGroups = { passive: [], active: [] }; + var segment = []; + var startType = null; // 0 = active, 1 = passive, 2 = adjacent + var currType = null; + var node; + + for (var i = 0; i < way.nodes.length; i++) { + + if (way.nodes[i] === activeID) { // vertex is the activeID + segment = []; // draw no segment here + startType = null; + continue; + } + + node = graph.entity(way.nodes[i]); + currType = passive(node); + + if (startType === null) { + startType = currType; + } + + if (currType !== startType) { // line changes here - try to save a segment + + if (segment.length > 0) { // finish previous segment + segment.push(node.loc); + + if (startType === 2 || currType === 2) { // one adjacent vertex + coordGroups.active.push(segment); + } else if (startType === 0 && currType === 0) { // both active vertices + coordGroups.active.push(segment); + } else { + coordGroups.passive.push(segment); + } + } + + segment = []; + startType = currType; + } + + segment.push(node.loc); + } + + // complete whatever segment we ended on + if (segment.length > 1) { + if (startType === 2 || currType === 2) { // one adjacent vertex + coordGroups.active.push(segment); + } else if (startType === 0 && currType === 0) { // both active vertices + coordGroups.active.push(segment); + } else { + coordGroups.passive.push(segment); + } + } + + if (coordGroups.passive.length) { + data.targets.push({ + 'type': 'Feature', + 'id': way.id, + 'geometry': { + 'type': 'MultiLineString', + 'coordinates': coordGroups.passive + } + }); + } + + if (coordGroups.active.length) { + data.nopes.push({ + 'type': 'Feature', + 'id': way.id + '-nope', // break the ids on purpose + 'geometry': { + 'type': 'MultiLineString', + 'coordinates': coordGroups.active + } + }); + } }); - var targets = selection.selectAll('.line.target') + + // Places to hover and connect + var targets = selection.selectAll('.line.target-allowed') .filter(filter) - .data(passive, function key(d) { return d.id; }); + .data(data.targets, function key(d) { return d.id; }); // exit targets.exit() @@ -57,7 +187,24 @@ export function svgLines(projection, context) { .append('path') .merge(targets) .attr('d', getPath) - .attr('class', function(d) { return 'way line target ' + fillClass + d.id; }); + .attr('class', function(d) { return 'way line target target-allowed ' + targetClass + d.id; }); + + + // NOPE + var nopes = selection.selectAll('.line.target-nope') + .data(data.nopes, function key(d) { return d.id; }); + + // exit + nopes.exit() + .remove(); + + // enter/update + nopes.enter() + .append('path') + .merge(nopes) + .attr('d', getPath) + .attr('class', function(d) { return 'way line target target-nope ' + nopeClass + d.id; }); + } diff --git a/modules/svg/path.js b/modules/svg/path.js index d2e522995..cf9f09b44 100644 --- a/modules/svg/path.js +++ b/modules/svg/path.js @@ -15,23 +15,27 @@ export function svgPath(projection, graph, isArea) { // When drawing areas, pad viewport by 65px in each direction to allow // for 60px area fill stroke (see ".fill-partial path.fill" css rule) - var cache = {}, - padding = isArea ? 65 : 5, - viewport = projection.clipExtent(), - paddedExtent = [ - [viewport[0][0] - padding, viewport[0][1] - padding], - [viewport[1][0] + padding, viewport[1][1] + padding] - ], - clip = d3_geoIdentity().clipExtent(paddedExtent).stream, - project = projection.stream, - path = d3_geoPath() - .projection({stream: function(output) { return project(clip(output)); }}); + var cache = {}; + var padding = isArea ? 65 : 5; + var viewport = projection.clipExtent(); + var paddedExtent = [ + [viewport[0][0] - padding, viewport[0][1] - padding], + [viewport[1][0] + padding, viewport[1][1] + padding] + ]; + var clip = d3_geoIdentity().clipExtent(paddedExtent).stream; + var project = projection.stream; + var path = d3_geoPath() + .projection({stream: function(output) { return project(clip(output)); }}); - return function(entity) { + var svgpath = function(entity) { if (entity.id in cache) { return cache[entity.id]; } else { return cache[entity.id] = path(entity.asGeoJSON(graph)); } }; + + svgpath.geojson = path; + + return svgpath; } diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index e880363f7..14b0ab369 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -182,17 +182,75 @@ export function svgVertices(projection, context) { function drawTargets(selection, graph, entities, filter) { - var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; + var targetClass = context.getDebug('target') ? 'pink ' : 'nocolor '; + var nopeClass = context.getDebug('target') ? 'red ' : 'nocolor '; + var activeID = context.activeID(); + var data = { targets: [], nopes: [] }; - // no targets for entities that are active, or adjacent to active. + // Touch targets control which other vertices we can drag a vertex onto. + // - the activeID - nope + // - next to the activeID - yes (vertices will be merged) + // - 2 away from the activeID - nope (would create a self intersecting segment) + // - all others on a closed way - nope (would create a self intersecting polygon) + // + // 0 = active vertex - no touch/connect + // 1 = passive vertex - yes touch/connect + // 2 = adjacent vertex - special rules function passive(d) { - return d.id !== context.activeID(); + if (!activeID) return 1; + if (activeID === d.id) return 0; + + var parents = graph.parentWays(d); + var i, j; + + for (i = 0; i < parents.length; i++) { + var nodes = parents[i].nodes; + var isClosed = parents[i].isClosed(); + for (j = 0; j < nodes.length; j++) { // find this vertex, look nearby + if (nodes[j] === d.id) { + var ix1 = j - 2; + var ix2 = j - 1; + var ix3 = j + 1; + var ix4 = j + 2; + + if (isClosed) { // wraparound if needed + var max = nodes.length - 1; + if (ix1 < 0) ix1 = max + ix1; + if (ix2 < 0) ix2 = max + ix2; + if (ix3 > max) ix3 = ix3 - max; + if (ix4 > max) ix4 = ix4 - max; + } + + if (nodes[ix1] === activeID) return 0; // prevent self intersect + else if (nodes[ix2] === activeID) return 2; // adjacent - ok! + else if (nodes[ix3] === activeID) return 2; // adjacent - ok! + else if (nodes[ix4] === activeID) return 0; // prevent self intersect + else if (isClosed && nodes.indexOf(activeID) !== -1) return 0; // prevent self intersect + } + } + } + + return 1; } - var data = entities.filter(passive); - var targets = selection.selectAll('.vertex.target') + + entities.forEach(function(node) { + if (activeID === node.id) return; // draw no vertex on the activeID + + var currType = passive(node); + if (currType !== 0) { + data.targets.push(node); // passive or adjacent - allow to connect + } else { + data.nopes.push({ + id: node.id + '-nope', // not a real osmNode, break the id on purpose + loc: node.loc + }); + } + }); + + var targets = selection.selectAll('.vertex.target-allowed') .filter(filter) - .data(data, function key(d) { return d.id; }); + .data(data.targets, function key(d) { return d.id; }); // exit targets.exit() @@ -201,9 +259,26 @@ export function svgVertices(projection, context) { // enter/update targets.enter() .append('circle') - .attr('r', function(d) { return _radii[d.id] || radiuses.shadow[3]; }) + .attr('r', function(d) { return (_radii[d.id] || radiuses.shadow[3]); }) .merge(targets) - .attr('class', function(d) { return 'node vertex target ' + fillClass + d.id; }) + .attr('class', function(d) { return 'node vertex target target-allowed ' + targetClass + d.id; }) + .attr('transform', svgPointTransform(projection)); + + + // NOPE + var nopes = selection.selectAll('.vertex.target-nope') + .data(data.nopes, function key(d) { return d.id; }); + + // exit + nopes.exit() + .remove(); + + // enter/update + nopes.enter() + .append('circle') + .attr('r', function(d) { return (_radii[d.id.replace('-nope','')] || radiuses.shadow[3]); }) + .merge(nopes) + .attr('class', function(d) { return 'node vertex target target-nope ' + nopeClass + d.id; }) .attr('transform', svgPointTransform(projection)); } @@ -321,8 +396,11 @@ export function svgVertices(projection, context) { .call(draw, graph, currentVisible(all), sets, filterRendered); // Draw touch targets.. + var filterTargets = function(d) { + return isMoving ? true : filterRendered(d); + }; selection.selectAll('.layer-points .layer-points-targets') - .call(drawTargets, graph, currentVisible(all), filterRendered); + .call(drawTargets, graph, currentVisible(all), filterTargets); function currentVisible(which) { From 6d7659b3bbffe506a54a668c20e5b099fe86adc9 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 21 Dec 2017 10:36:02 -0500 Subject: [PATCH 46/70] Refactor common helper code to svg/helpers.js, add area nopes --- modules/svg/areas.js | 43 ++++- modules/svg/debug.js | 12 +- modules/svg/helpers.js | 256 ++++++++++++++++++++++++++++ modules/svg/index.js | 10 +- modules/svg/lines.js | 135 +-------------- modules/svg/mapillary_images.js | 35 ++-- modules/svg/mapillary_signs.js | 8 +- modules/svg/one_way_segments.js | 67 -------- modules/svg/openstreetcam_images.js | 35 ++-- modules/svg/path.js | 41 ----- modules/svg/point_transform.js | 7 - modules/svg/relation_member_tags.js | 15 -- modules/svg/vertices.js | 59 +------ 13 files changed, 339 insertions(+), 384 deletions(-) create mode 100644 modules/svg/helpers.js delete mode 100644 modules/svg/one_way_segments.js delete mode 100644 modules/svg/path.js delete mode 100644 modules/svg/point_transform.js delete mode 100644 modules/svg/relation_member_tags.js diff --git a/modules/svg/areas.js b/modules/svg/areas.js index c6ff49cbb..3fb7c3723 100644 --- a/modules/svg/areas.js +++ b/modules/svg/areas.js @@ -4,7 +4,7 @@ import _values from 'lodash-es/values'; import { bisector as d3_bisector } from 'd3-array'; import { osmEntity, osmIsSimpleMultipolygonOuterMember } from '../osm'; -import { svgPath, svgTagClasses } from './index'; +import { svgPath, svgSegmentWay, svgTagClasses } from './index'; export function svgAreas(projection, context) { @@ -42,16 +42,25 @@ export function svgAreas(projection, context) { function drawTargets(selection, graph, entities, filter) { - var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; - var getPath = svgPath(projection, graph); - var passive = entities.filter(function(d) { - return true; - // return context.activeIDs().indexOf(d.id) === -1; + var targetClass = context.getDebug('target') ? 'pink ' : 'nocolor '; + var nopeClass = context.getDebug('target') ? 'red ' : 'nocolor '; + var getPath = svgPath(projection).geojson; + var activeID = context.activeID(); + + // The targets and nopes will be MultiLineString sub-segments of the ways + var data = { targets: [], nopes: [] }; + + entities.forEach(function(way) { + var features = svgSegmentWay(way, graph, activeID); + data.targets.push.apply(data.targets, features.passive); + data.nopes.push.apply(data.nopes, features.active); }); - var targets = selection.selectAll('.area.target') + + // Targets allow hover and vertex snapping + var targets = selection.selectAll('.area.target-allowed') .filter(filter) - .data(passive, function key(d) { return d.id; }); + .data(data.targets, function key(d) { return d.id; }); // exit targets.exit() @@ -62,7 +71,23 @@ export function svgAreas(projection, context) { .append('path') .merge(targets) .attr('d', getPath) - .attr('class', function(d) { return 'way area target ' + fillClass + d.id; }); + .attr('class', function(d) { return 'way area target target-allowed ' + targetClass + d.id; }); + + + // NOPE + var nopes = selection.selectAll('.area.target-nope') + .data(data.nopes, function key(d) { return d.id; }); + + // exit + nopes.exit() + .remove(); + + // enter/update + nopes.enter() + .append('path') + .merge(nopes) + .attr('d', getPath) + .attr('class', function(d) { return 'way area target target-nope ' + nopeClass + d.id; }); } diff --git a/modules/svg/debug.js b/modules/svg/debug.js index 8ab5617d4..71f007417 100644 --- a/modules/svg/debug.js +++ b/modules/svg/debug.js @@ -1,12 +1,8 @@ -import { geoPath as d3_geoPath } from 'd3-geo'; import { select as d3_select } from 'd3-selection'; import { geoPolygonIntersectsPolygon } from '../geo'; -import { - data, - dataImperial, - dataDriveLeft -} from '../../data'; +import { data, dataImperial, dataDriveLeft } from '../../data'; +import { svgPath } from './index'; export function svgDebug(projection, context) { @@ -27,8 +23,6 @@ export function svgDebug(projection, context) { var showsImperial = context.getDebug('imperial'); var showsDriveLeft = context.getDebug('driveLeft'); var showsTouchTargets = context.getDebug('target'); - var path = d3_geoPath(projection); - var debugData = []; if (showsTile) { @@ -134,7 +128,7 @@ export function svgDebug(projection, context) { // update layer.selectAll('path') - .attr('d', path); + .attr('d', svgPath(projection).geojson); } diff --git a/modules/svg/helpers.js b/modules/svg/helpers.js new file mode 100644 index 000000000..88f04f578 --- /dev/null +++ b/modules/svg/helpers.js @@ -0,0 +1,256 @@ +import _extend from 'lodash-es/extend'; + +import { + geoIdentity as d3_geoIdentity, + geoPath as d3_geoPath, + geoStream as d3_geoStream +} from 'd3-geo'; + +import { geoEuclideanDistance } from '../geo'; + + +// Touch targets control which other vertices we can drag a vertex onto. +// +// - the activeID - nope +// - 1 away (adjacent) to the activeID - yes (vertices will be merged) +// - 2 away from the activeID - nope (would create a self intersecting segment) +// - all others on a linear way - yes +// - all others on a closed way - nope (would create a self intersecting polygon) +// +// returns +// 0 = active vertex - no touch/connect +// 1 = passive vertex - yes touch/connect +// 2 = adjacent vertex - yes but pay attention segmenting a line here +// +export function svgPassiveVertex(node, graph, activeID) { + if (!activeID) return 1; + if (activeID === node.id) return 0; + + var parents = graph.parentWays(node); + + for (var i = 0; i < parents.length; i++) { + var nodes = parents[i].nodes; + var isClosed = parents[i].isClosed(); + for (var j = 0; j < nodes.length; j++) { // find this vertex, look nearby + if (nodes[j] === node.id) { + var ix1 = j - 2; + var ix2 = j - 1; + var ix3 = j + 1; + var ix4 = j + 2; + + if (isClosed) { // wraparound if needed + var max = nodes.length - 1; + if (ix1 < 0) ix1 = max + ix1; + if (ix2 < 0) ix2 = max + ix2; + if (ix3 > max) ix3 = ix3 - max; + if (ix4 > max) ix4 = ix4 - max; + } + + if (nodes[ix1] === activeID) return 0; // no - prevent self intersect + else if (nodes[ix2] === activeID) return 2; // ok - adjacent + else if (nodes[ix3] === activeID) return 2; // ok - adjacent + else if (nodes[ix4] === activeID) return 0; // no - prevent self intersect + else if (isClosed && nodes.indexOf(activeID) !== -1) return 0; // no - prevent self intersect + } + } + } + + return 1; // ok +} + + +export function svgOneWaySegments(projection, graph, dt) { + return function(entity) { + var i = 0; + var offset = dt; + var segments = []; + var clip = d3_geoIdentity().clipExtent(projection.clipExtent()).stream; + var coordinates = graph.childNodes(entity).map(function(n) { return n.loc; }); + var a, b; + + if (entity.tags.oneway === '-1') { + coordinates.reverse(); + } + + d3_geoStream({ + type: 'LineString', + coordinates: coordinates + }, projection.stream(clip({ + lineStart: function() {}, + lineEnd: function() { a = null; }, + point: function(x, y) { + b = [x, y]; + + if (a) { + var span = geoEuclideanDistance(a, b) - offset; + + if (span >= 0) { + var angle = Math.atan2(b[1] - a[1], b[0] - a[0]); + var dx = dt * Math.cos(angle); + var dy = dt * Math.sin(angle); + var p = [ + a[0] + offset * Math.cos(angle), + a[1] + offset * Math.sin(angle) + ]; + var segment = 'M' + a[0] + ',' + a[1] + 'L' + p[0] + ',' + p[1]; + + for (span -= dt; span >= 0; span -= dt) { + p[0] += dx; + p[1] += dy; + segment += 'L' + p[0] + ',' + p[1]; + } + + segment += 'L' + b[0] + ',' + b[1]; + segments.push({id: entity.id, index: i, d: segment}); + } + + offset = -span; + i++; + } + + a = b; + } + }))); + + return segments; + }; +} + + +export function svgPath(projection, graph, isArea) { + + // Explanation of magic numbers: + // "padding" here allows space for strokes to extend beyond the viewport, + // so that the stroke isn't drawn along the edge of the viewport when + // the shape is clipped. + // + // When drawing lines, pad viewport by 5px. + // When drawing areas, pad viewport by 65px in each direction to allow + // for 60px area fill stroke (see ".fill-partial path.fill" css rule) + + var cache = {}; + var padding = isArea ? 65 : 5; + var viewport = projection.clipExtent(); + var paddedExtent = [ + [viewport[0][0] - padding, viewport[0][1] - padding], + [viewport[1][0] + padding, viewport[1][1] + padding] + ]; + var clip = d3_geoIdentity().clipExtent(paddedExtent).stream; + var project = projection.stream; + var path = d3_geoPath() + .projection({stream: function(output) { return project(clip(output)); }}); + + var svgpath = function(entity) { + if (entity.id in cache) { + return cache[entity.id]; + } else { + return cache[entity.id] = path(entity.asGeoJSON(graph)); + } + }; + + svgpath.geojson = path; + + return svgpath; +} + + +export function svgPointTransform(projection) { + return function(entity) { + // http://jsperf.com/short-array-join + var pt = projection(entity.loc); + return 'translate(' + pt[0] + ',' + pt[1] + ')'; + }; +} + + +export function svgRelationMemberTags(graph) { + return function(entity) { + var tags = entity.tags; + graph.parentRelations(entity).forEach(function(relation) { + var type = relation.tags.type; + if (type === 'multipolygon' || type === 'boundary') { + tags = _extend({}, relation.tags, tags); + } + }); + return tags; + }; +} + + +export function svgSegmentWay(way, graph, activeID) { + var features = { passive: [], active: [] }; + var coordGroups = { passive: [], active: [] }; + var segment = []; + var startType = null; // 0 = active, 1 = passive, 2 = adjacent + var currType = null; + var node; + + for (var i = 0; i < way.nodes.length; i++) { + if (way.nodes[i] === activeID) { // vertex is the activeID + segment = []; // draw no segment here + startType = null; + continue; + } + + node = graph.entity(way.nodes[i]); + currType = svgPassiveVertex(node, graph, activeID); + + if (startType === null) { + startType = currType; + } + + if (currType !== startType) { // line changes here - try to save a segment + + if (segment.length > 0) { // finish previous segment + segment.push(node.loc); + if (startType === 2 || currType === 2) { // one adjacent vertex + coordGroups.active.push(segment); + } else if (startType === 0 && currType === 0) { // both active vertices + coordGroups.active.push(segment); + } else { + coordGroups.passive.push(segment); + } + } + + segment = []; + startType = currType; + } + + segment.push(node.loc); + } + + // complete whatever segment we ended on + if (segment.length > 1) { + if (startType === 2 || currType === 2) { // one adjacent vertex + coordGroups.active.push(segment); + } else if (startType === 0 && currType === 0) { // both active vertices + coordGroups.active.push(segment); + } else { + coordGroups.passive.push(segment); + } + } + + if (coordGroups.passive.length) { + features.passive.push({ + 'type': 'Feature', + 'id': way.id, + 'geometry': { + 'type': 'MultiLineString', + 'coordinates': coordGroups.passive + } + }); + } + + if (coordGroups.active.length) { + features.active.push({ + 'type': 'Feature', + 'id': way.id + '-nope', // break the ids on purpose + 'geometry': { + 'type': 'MultiLineString', + 'coordinates': coordGroups.active + } + }); + } + + return features; +} diff --git a/modules/svg/index.js b/modules/svg/index.js index 8f54f5a5e..68e2f2f28 100644 --- a/modules/svg/index.js +++ b/modules/svg/index.js @@ -9,13 +9,15 @@ export { svgLines } from './lines.js'; export { svgMapillaryImages } from './mapillary_images.js'; export { svgMapillarySigns } from './mapillary_signs.js'; export { svgMidpoints } from './midpoints.js'; -export { svgOneWaySegments } from './one_way_segments.js'; +export { svgOneWaySegments } from './helpers.js'; export { svgOpenstreetcamImages } from './openstreetcam_images.js'; export { svgOsm } from './osm.js'; -export { svgPath } from './path.js'; -export { svgPointTransform } from './point_transform.js'; +export { svgPassiveVertex } from './helpers.js'; +export { svgPath } from './helpers.js'; +export { svgPointTransform } from './helpers.js'; export { svgPoints } from './points.js'; -export { svgRelationMemberTags } from './relation_member_tags.js'; +export { svgRelationMemberTags } from './helpers.js'; +export { svgSegmentWay } from './helpers.js'; export { svgTagClasses } from './tag_classes.js'; export { svgTurns } from './turns.js'; export { svgVertices } from './vertices.js'; diff --git a/modules/svg/lines.js b/modules/svg/lines.js index b9012f0d9..fbecbed7b 100644 --- a/modules/svg/lines.js +++ b/modules/svg/lines.js @@ -4,13 +4,13 @@ import _flatten from 'lodash-es/flatten'; import _forOwn from 'lodash-es/forOwn'; import _map from 'lodash-es/map'; -import { geoPath as d3_geoPath } from 'd3-geo'; import { range as d3_range } from 'd3-array'; import { svgOneWaySegments, svgPath, svgRelationMemberTags, + svgSegmentWay, svgTagClasses } from './index'; @@ -40,140 +40,20 @@ export function svgLines(projection, context) { function drawTargets(selection, graph, entities, filter) { var targetClass = context.getDebug('target') ? 'pink ' : 'nocolor '; var nopeClass = context.getDebug('target') ? 'red ' : 'nocolor '; - var getPath = svgPath(projection, graph).geojson; - // var getPath = d3_geoPath(projection); - + var getPath = svgPath(projection).geojson; var activeID = context.activeID(); - // Rather than drawing lines directly, we'll cut out pieces - // depending on which parts are active. + // The targets and nopes will be MultiLineString sub-segments of the ways var data = { targets: [], nopes: [] }; - // Touch targets control which other vertices we can drag a vertex onto. - // - the activeID - nope - // - next to the activeID - yes (vertices will be merged) - // - 2 away from the activeID - nope (would create a self intersecting segment) - // - all others on a closed way - nope (would create a self intersecting polygon) - // - // 0 = active vertex - no touch/connect - // 1 = passive vertex - yes touch/connect - // 2 = adjacent vertex - special rules - function passive(d) { - if (!activeID) return 1; - if (activeID === d.id) return 0; - - var parents = graph.parentWays(d); - var i, j; - - for (i = 0; i < parents.length; i++) { - var nodes = parents[i].nodes; - var isClosed = parents[i].isClosed(); - for (j = 0; j < nodes.length; j++) { // find this vertex, look nearby - if (nodes[j] === d.id) { - var ix1 = j - 2; - var ix2 = j - 1; - var ix3 = j + 1; - var ix4 = j + 2; - - if (isClosed) { // wraparound if needed - var max = nodes.length - 1; - if (ix1 < 0) ix1 = max + ix1; - if (ix2 < 0) ix2 = max + ix2; - if (ix3 > max) ix3 = ix3 - max; - if (ix4 > max) ix4 = ix4 - max; - } - - if (nodes[ix1] === activeID) return 0; // prevent self intersect - else if (nodes[ix2] === activeID) return 2; // adjacent - ok! - else if (nodes[ix3] === activeID) return 2; // adjacent - ok! - else if (nodes[ix4] === activeID) return 0; // prevent self intersect - else if (isClosed && nodes.indexOf(activeID) !== -1) return 0; // prevent self intersect - } - } - } - - return 1; - } - entities.forEach(function(way) { - var coordGroups = { passive: [], active: [] }; - var segment = []; - var startType = null; // 0 = active, 1 = passive, 2 = adjacent - var currType = null; - var node; - - for (var i = 0; i < way.nodes.length; i++) { - - if (way.nodes[i] === activeID) { // vertex is the activeID - segment = []; // draw no segment here - startType = null; - continue; - } - - node = graph.entity(way.nodes[i]); - currType = passive(node); - - if (startType === null) { - startType = currType; - } - - if (currType !== startType) { // line changes here - try to save a segment - - if (segment.length > 0) { // finish previous segment - segment.push(node.loc); - - if (startType === 2 || currType === 2) { // one adjacent vertex - coordGroups.active.push(segment); - } else if (startType === 0 && currType === 0) { // both active vertices - coordGroups.active.push(segment); - } else { - coordGroups.passive.push(segment); - } - } - - segment = []; - startType = currType; - } - - segment.push(node.loc); - } - - // complete whatever segment we ended on - if (segment.length > 1) { - if (startType === 2 || currType === 2) { // one adjacent vertex - coordGroups.active.push(segment); - } else if (startType === 0 && currType === 0) { // both active vertices - coordGroups.active.push(segment); - } else { - coordGroups.passive.push(segment); - } - } - - if (coordGroups.passive.length) { - data.targets.push({ - 'type': 'Feature', - 'id': way.id, - 'geometry': { - 'type': 'MultiLineString', - 'coordinates': coordGroups.passive - } - }); - } - - if (coordGroups.active.length) { - data.nopes.push({ - 'type': 'Feature', - 'id': way.id + '-nope', // break the ids on purpose - 'geometry': { - 'type': 'MultiLineString', - 'coordinates': coordGroups.active - } - }); - } + var features = svgSegmentWay(way, graph, activeID); + data.targets.push.apply(data.targets, features.passive); + data.nopes.push.apply(data.nopes, features.active); }); - // Places to hover and connect + // Targets allow hover and vertex snapping var targets = selection.selectAll('.line.target-allowed') .filter(filter) .data(data.targets, function key(d) { return d.id; }); @@ -204,7 +84,6 @@ export function svgLines(projection, context) { .merge(nopes) .attr('d', getPath) .attr('class', function(d) { return 'way line target target-nope ' + nopeClass + d.id; }); - } diff --git a/modules/svg/mapillary_images.js b/modules/svg/mapillary_images.js index 1bae3436c..fb8a4b4b8 100644 --- a/modules/svg/mapillary_images.js +++ b/modules/svg/mapillary_images.js @@ -1,23 +1,16 @@ import _throttle from 'lodash-es/throttle'; - -import { - geoIdentity as d3_geoIdentity, - geoPath as d3_geoPath -} from 'd3-geo'; - import { select as d3_select } from 'd3-selection'; - -import { svgPointTransform } from './point_transform'; +import { svgPath, svgPointTransform } from './index'; import { services } from '../services'; export function svgMapillaryImages(projection, context, dispatch) { - var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000), - minZoom = 12, - minMarkerZoom = 16, - minViewfieldZoom = 18, - layer = d3_select(null), - _mapillary; + var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000); + var minZoom = 12; + var minMarkerZoom = 16; + var minViewfieldZoom = 18; + var layer = d3_select(null); + var _mapillary; function init() { @@ -128,25 +121,19 @@ export function svgMapillaryImages(projection, context, dispatch) { var sequences = (service ? service.sequences(projection) : []); var images = (service && showMarkers ? service.images(projection) : []); - var clip = d3_geoIdentity().clipExtent(projection.clipExtent()).stream; - var project = projection.stream; - var makePath = d3_geoPath().projection({ stream: function(output) { - return project(clip(output)); - }}); - var traces = layer.selectAll('.sequences').selectAll('.sequence') .data(sequences, function(d) { return d.properties.key; }); + // exit traces.exit() .remove(); + // enter/update traces = traces.enter() .append('path') .attr('class', 'sequence') - .merge(traces); - - traces - .attr('d', makePath); + .merge(traces) + .attr('d', svgPath(projection).geojson); var groups = layer.selectAll('.markers').selectAll('.viewfield-group') diff --git a/modules/svg/mapillary_signs.js b/modules/svg/mapillary_signs.js index a327c108d..4b6095cd2 100644 --- a/modules/svg/mapillary_signs.js +++ b/modules/svg/mapillary_signs.js @@ -5,10 +5,10 @@ import { services } from '../services'; export function svgMapillarySigns(projection, context, dispatch) { - var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000), - minZoom = 12, - layer = d3_select(null), - _mapillary; + var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000); + var minZoom = 12; + var layer = d3_select(null); + var _mapillary; function init() { diff --git a/modules/svg/one_way_segments.js b/modules/svg/one_way_segments.js deleted file mode 100644 index 66759d66d..000000000 --- a/modules/svg/one_way_segments.js +++ /dev/null @@ -1,67 +0,0 @@ -import { - geoIdentity as d3_geoIdentity, - geoStream as d3_geoStream -} from 'd3-geo'; - -import { geoEuclideanDistance } from '../geo'; - - -export function svgOneWaySegments(projection, graph, dt) { - return function(entity) { - var a, - b, - i = 0, - offset = dt, - segments = [], - clip = d3_geoIdentity().clipExtent(projection.clipExtent()).stream, - coordinates = graph.childNodes(entity).map(function(n) { - return n.loc; - }); - - if (entity.tags.oneway === '-1') coordinates.reverse(); - - d3_geoStream({ - type: 'LineString', - coordinates: coordinates - }, projection.stream(clip({ - lineStart: function() {}, - lineEnd: function() { - a = null; - }, - point: function(x, y) { - b = [x, y]; - - if (a) { - var span = geoEuclideanDistance(a, b) - offset; - - if (span >= 0) { - var angle = Math.atan2(b[1] - a[1], b[0] - a[0]), - dx = dt * Math.cos(angle), - dy = dt * Math.sin(angle), - p = [a[0] + offset * Math.cos(angle), - a[1] + offset * Math.sin(angle)]; - - var segment = 'M' + a[0] + ',' + a[1] + - 'L' + p[0] + ',' + p[1]; - - for (span -= dt; span >= 0; span -= dt) { - p[0] += dx; - p[1] += dy; - segment += 'L' + p[0] + ',' + p[1]; - } - - segment += 'L' + b[0] + ',' + b[1]; - segments.push({id: entity.id, index: i, d: segment}); - } - - offset = -span; - i++; - } - - a = b; - } - }))); - - return segments; - }; -} diff --git a/modules/svg/openstreetcam_images.js b/modules/svg/openstreetcam_images.js index 3184a7bc2..35fc76cc0 100644 --- a/modules/svg/openstreetcam_images.js +++ b/modules/svg/openstreetcam_images.js @@ -1,23 +1,16 @@ import _throttle from 'lodash-es/throttle'; - -import { - geoIdentity as d3_geoIdentity, - geoPath as d3_geoPath -} from 'd3-geo'; - import { select as d3_select } from 'd3-selection'; - -import { svgPointTransform } from './point_transform'; +import { svgPath, svgPointTransform } from './index'; import { services } from '../services'; export function svgOpenstreetcamImages(projection, context, dispatch) { - var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000), - minZoom = 12, - minMarkerZoom = 16, - minViewfieldZoom = 18, - layer = d3_select(null), - _openstreetcam; + var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000); + var minZoom = 12; + var minMarkerZoom = 16; + var minViewfieldZoom = 18; + var layer = d3_select(null); + var _openstreetcam; function init() { @@ -128,25 +121,19 @@ export function svgOpenstreetcamImages(projection, context, dispatch) { var sequences = (service ? service.sequences(projection) : []); var images = (service && showMarkers ? service.images(projection) : []); - var clip = d3_geoIdentity().clipExtent(projection.clipExtent()).stream; - var project = projection.stream; - var makePath = d3_geoPath().projection({ stream: function(output) { - return project(clip(output)); - }}); - var traces = layer.selectAll('.sequences').selectAll('.sequence') .data(sequences, function(d) { return d.properties.key; }); + // exit traces.exit() .remove(); + // enter/update traces = traces.enter() .append('path') .attr('class', 'sequence') - .merge(traces); - - traces - .attr('d', makePath); + .merge(traces) + .attr('d', svgPath(projection).geojson); var groups = layer.selectAll('.markers').selectAll('.viewfield-group') diff --git a/modules/svg/path.js b/modules/svg/path.js deleted file mode 100644 index cf9f09b44..000000000 --- a/modules/svg/path.js +++ /dev/null @@ -1,41 +0,0 @@ -import { - geoIdentity as d3_geoIdentity, - geoPath as d3_geoPath -} from 'd3-geo'; - - -export function svgPath(projection, graph, isArea) { - - // Explanation of magic numbers: - // "padding" here allows space for strokes to extend beyond the viewport, - // so that the stroke isn't drawn along the edge of the viewport when - // the shape is clipped. - // - // When drawing lines, pad viewport by 5px. - // When drawing areas, pad viewport by 65px in each direction to allow - // for 60px area fill stroke (see ".fill-partial path.fill" css rule) - - var cache = {}; - var padding = isArea ? 65 : 5; - var viewport = projection.clipExtent(); - var paddedExtent = [ - [viewport[0][0] - padding, viewport[0][1] - padding], - [viewport[1][0] + padding, viewport[1][1] + padding] - ]; - var clip = d3_geoIdentity().clipExtent(paddedExtent).stream; - var project = projection.stream; - var path = d3_geoPath() - .projection({stream: function(output) { return project(clip(output)); }}); - - var svgpath = function(entity) { - if (entity.id in cache) { - return cache[entity.id]; - } else { - return cache[entity.id] = path(entity.asGeoJSON(graph)); - } - }; - - svgpath.geojson = path; - - return svgpath; -} diff --git a/modules/svg/point_transform.js b/modules/svg/point_transform.js deleted file mode 100644 index cf80f845d..000000000 --- a/modules/svg/point_transform.js +++ /dev/null @@ -1,7 +0,0 @@ -export function svgPointTransform(projection) { - return function(entity) { - // http://jsperf.com/short-array-join - var pt = projection(entity.loc); - return 'translate(' + pt[0] + ',' + pt[1] + ')'; - }; -} diff --git a/modules/svg/relation_member_tags.js b/modules/svg/relation_member_tags.js deleted file mode 100644 index 824d56fe0..000000000 --- a/modules/svg/relation_member_tags.js +++ /dev/null @@ -1,15 +0,0 @@ -import _extend from 'lodash-es/extend'; - - -export function svgRelationMemberTags(graph) { - return function(entity) { - var tags = entity.tags; - graph.parentRelations(entity).forEach(function(relation) { - var type = relation.tags.type; - if (type === 'multipolygon' || type === 'boundary') { - tags = _extend({}, relation.tags, tags); - } - }); - return tags; - }; -} diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index 14b0ab369..77aad0a1d 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -6,7 +6,7 @@ import { select as d3_select } from 'd3-selection'; import { dataFeatureIcons } from '../../data'; import { geoScaleToZoom } from '../geo'; import { osmEntity } from '../osm'; -import { svgPointTransform } from './index'; +import { svgPassiveVertex, svgPointTransform } from './index'; export function svgVertices(projection, context) { @@ -187,67 +187,22 @@ export function svgVertices(projection, context) { var activeID = context.activeID(); var data = { targets: [], nopes: [] }; - // Touch targets control which other vertices we can drag a vertex onto. - // - the activeID - nope - // - next to the activeID - yes (vertices will be merged) - // - 2 away from the activeID - nope (would create a self intersecting segment) - // - all others on a closed way - nope (would create a self intersecting polygon) - // - // 0 = active vertex - no touch/connect - // 1 = passive vertex - yes touch/connect - // 2 = adjacent vertex - special rules - function passive(d) { - if (!activeID) return 1; - if (activeID === d.id) return 0; - - var parents = graph.parentWays(d); - var i, j; - - for (i = 0; i < parents.length; i++) { - var nodes = parents[i].nodes; - var isClosed = parents[i].isClosed(); - for (j = 0; j < nodes.length; j++) { // find this vertex, look nearby - if (nodes[j] === d.id) { - var ix1 = j - 2; - var ix2 = j - 1; - var ix3 = j + 1; - var ix4 = j + 2; - - if (isClosed) { // wraparound if needed - var max = nodes.length - 1; - if (ix1 < 0) ix1 = max + ix1; - if (ix2 < 0) ix2 = max + ix2; - if (ix3 > max) ix3 = ix3 - max; - if (ix4 > max) ix4 = ix4 - max; - } - - if (nodes[ix1] === activeID) return 0; // prevent self intersect - else if (nodes[ix2] === activeID) return 2; // adjacent - ok! - else if (nodes[ix3] === activeID) return 2; // adjacent - ok! - else if (nodes[ix4] === activeID) return 0; // prevent self intersect - else if (isClosed && nodes.indexOf(activeID) !== -1) return 0; // prevent self intersect - } - } - } - - return 1; - } - - entities.forEach(function(node) { - if (activeID === node.id) return; // draw no vertex on the activeID + if (activeID === node.id) return; // draw no target on the activeID - var currType = passive(node); + var currType = svgPassiveVertex(node, graph, activeID); if (currType !== 0) { - data.targets.push(node); // passive or adjacent - allow to connect + data.targets.push(node); // passive or adjacent - allow to connect } else { data.nopes.push({ - id: node.id + '-nope', // not a real osmNode, break the id on purpose + id: node.id + '-nope', // not a real osmNode, break the id on purpose loc: node.loc }); } }); + + // Targets allow hover and vertex snapping var targets = selection.selectAll('.vertex.target-allowed') .filter(filter) .data(data.targets, function key(d) { return d.id; }); From fcacdb440049a23bef2efb956d181e2370da146f Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 21 Dec 2017 11:18:06 -0500 Subject: [PATCH 47/70] Fix hover test (requires context.hasEntity stub) --- test/spec/behavior/hover.js | 105 +++++++++++++++++++----------------- 1 file changed, 57 insertions(+), 48 deletions(-) diff --git a/test/spec/behavior/hover.js b/test/spec/behavior/hover.js index 8ad0cf3d8..4dbc5ac9a 100644 --- a/test/spec/behavior/hover.js +++ b/test/spec/behavior/hover.js @@ -1,108 +1,117 @@ describe('iD.behaviorHover', function() { - var container, context; + var _container; + var _context; + var _graph; beforeEach(function() { - container = d3.select('body').append('div'); - context = { + _container = d3.select('body').append('div'); + _context = { hover: function() {}, - mode: function() { return { id: 'browse' }; } + mode: function() { return { id: 'browse' }; }, + hasEntity: function(d) { return _graph && _graph.hasEntity(d); } }; }); afterEach(function() { - container.remove(); + _container.remove(); + _graph = null; }); describe('#off', function () { it('removes the .hover class from all elements', function () { - container.append('span').attr('class', 'hover'); - container.call(iD.behaviorHover(context).off); - expect(container.select('span').classed('hover')).to.be.false; + _container.append('span').attr('class', 'hover'); + _container.call(iD.behaviorHover(_context).off); + expect(_container.select('span').classed('hover')).to.be.false; }); it('removes the .hover-disabled class from the surface element', function () { - container.attr('class', 'hover-disabled'); - container.call(iD.behaviorHover(context).off); - expect(container.classed('hover-disabled')).to.be.false; + _container.attr('class', 'hover-disabled'); + _container.call(iD.behaviorHover(_context).off); + expect(_container.classed('hover-disabled')).to.be.false; }); }); describe('mouseover', function () { it('adds the .hover class to all elements to which the same datum is bound', function () { - var a = iD.Node({id: 'a'}), - b = iD.Node({id: 'b'}); + var a = iD.osmNode({id: 'a'}); + var b = iD.osmNode({id: 'b'}); + _graph = iD.coreGraph([a, b]); - container.selectAll('span') + _container.selectAll('span') .data([a, b, a, b]) .enter().append('span').attr('class', function(d) { return d.id; }); - container.call(iD.behaviorHover(context)); - iD.utilTriggerEvent(container.selectAll('.a'), 'mouseover'); + _container.call(iD.behaviorHover(_context)); + iD.utilTriggerEvent(_container.selectAll('.a'), 'mouseover'); - expect(container.selectAll('.a.hover').nodes()).to.have.length(2); - expect(container.selectAll('.b.hover').nodes()).to.have.length(0); + expect(_container.selectAll('.a.hover').nodes()).to.have.length(2); + expect(_container.selectAll('.b.hover').nodes()).to.have.length(0); }); it('adds the .hover class to all members of a relation', function() { - container.selectAll('span') - .data([iD.Relation({id: 'a', members: [{id: 'b'}]}), iD.Node({id: 'b'})]) + var a = iD.osmRelation({id: 'a', members: [{id: 'b'}]}); + var b = iD.osmNode({id: 'b'}); + _graph = iD.coreGraph([a, b]); + + _container.selectAll('span') + .data([a, b]) .enter().append('span').attr('class', function(d) { return d.id; }); - container.call(iD.behaviorHover(context)); - iD.utilTriggerEvent(container.selectAll('.a'), 'mouseover'); + _container.call(iD.behaviorHover(_context)); + iD.utilTriggerEvent(_container.selectAll('.a'), 'mouseover'); - expect(container.selectAll('.a.hover').nodes()).to.have.length(1); - expect(container.selectAll('.b.hover').nodes()).to.have.length(1); + expect(_container.selectAll('.a.hover').nodes()).to.have.length(1); + expect(_container.selectAll('.b.hover').nodes()).to.have.length(1); }); }); describe('mouseout', function () { it('removes the .hover class from all elements', function () { - container.append('span').attr('class', 'hover'); + _container.append('span').attr('class', 'hover'); - container.call(iD.behaviorHover(context)); - iD.utilTriggerEvent(container.selectAll('.hover'), 'mouseout'); + _container.call(iD.behaviorHover(_context)); + iD.utilTriggerEvent(_container.selectAll('.hover'), 'mouseout'); - expect(container.selectAll('.hover').nodes()).to.have.length(0); + expect(_container.selectAll('.hover').nodes()).to.have.length(0); }); }); describe('alt keydown', function () { it('replaces the .hover class with .hover-suppressed', function () { - container.append('span').attr('class', 'hover'); - container.call(iD.behaviorHover(context).altDisables(true)); + _container.append('span').attr('class', 'hover'); + _container.call(iD.behaviorHover(_context).altDisables(true)); - happen.keydown(window, {keyCode: 18}); - expect(container.selectAll('.hover').nodes()).to.have.length(0); - expect(container.selectAll('.hover-suppressed').nodes()).to.have.length(1); - happen.keyup(window, {keyCode: 18}); + happen.keydown(window, { keyCode: 18 }); + expect(_container.selectAll('.hover').nodes()).to.have.length(0); + expect(_container.selectAll('.hover-suppressed').nodes()).to.have.length(1); + happen.keyup(window, { keyCode: 18 }); }); it('adds the .hover-disabled class to the surface', function () { - container.call(iD.behaviorHover(context).altDisables(true)); + _container.call(iD.behaviorHover(_context).altDisables(true)); - happen.keydown(window, {keyCode: 18}); - expect(container.classed('hover-disabled')).to.be.true; - happen.keyup(window, {keyCode: 18}); + happen.keydown(window, { keyCode: 18 }); + expect(_container.classed('hover-disabled')).to.be.true; + happen.keyup(window, { keyCode: 18 }); }); }); describe('alt keyup', function () { it('replaces the .hover-suppressed class with .hover', function () { - container.append('span').attr('class', 'hover-suppressed'); - container.call(iD.behaviorHover(context).altDisables(true)); + _container.append('span').attr('class', 'hover-suppressed'); + _container.call(iD.behaviorHover(_context).altDisables(true)); - happen.keydown(window, {keyCode: 18}); - happen.keyup(window, {keyCode: 18}); - expect(container.selectAll('.hover').nodes()).to.have.length(1); - expect(container.selectAll('.hover-suppressed').nodes()).to.have.length(0); + happen.keydown(window, { keyCode: 18 }); + happen.keyup(window, { keyCode: 18 }); + expect(_container.selectAll('.hover').nodes()).to.have.length(1); + expect(_container.selectAll('.hover-suppressed').nodes()).to.have.length(0); }); it('removes the .hover-disabled class from the surface', function () { - container.call(iD.behaviorHover(context).altDisables(true)); + _container.call(iD.behaviorHover(_context).altDisables(true)); - happen.keydown(window, {keyCode: 18}); - happen.keyup(window, {keyCode: 18}); - expect(container.classed('hover-disabled')).to.be.false; + happen.keydown(window, { keyCode: 18 }); + happen.keyup(window, { keyCode: 18 }); + expect(_container.classed('hover-disabled')).to.be.false; }); }); }); From d82d5dc3d04b5f407118444a22afc38437291add Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 21 Dec 2017 14:54:15 -0500 Subject: [PATCH 48/70] Add skipID to geoChooseEdge, to ignore dragging node --- modules/geo/geo.js | 15 +++++---- test/spec/geo/geo.js | 77 +++++++++++++++++++++++++++++++++++++------- 2 files changed, 74 insertions(+), 18 deletions(-) diff --git a/modules/geo/geo.js b/modules/geo/geo.js index e96bc81b9..318283931 100644 --- a/modules/geo/geo.js +++ b/modules/geo/geo.js @@ -157,14 +157,17 @@ export function geoRotate(points, angle, around) { // projection onto that edge, if such a projection exists, or the distance to // the closest vertex on that edge. Returns an object with the `index` of the // chosen edge, the chosen `loc` on that edge, and the `distance` to to it. -export function geoChooseEdge(nodes, point, projection) { +export function geoChooseEdge(nodes, point, projection, skipID) { var dist = geoEuclideanDistance; var points = nodes.map(function(n) { return projection(n.loc); }); + var ids = nodes.map(function(n) { return n.id; }); var min = Infinity; var idx; var loc; for (var i = 0; i < points.length - 1; i++) { + if (ids[i] === skipID || ids[i + 1] === skipID) continue; + var o = points[i]; var s = geoVecSubtract(points[i + 1], o); var v = geoVecSubtract(point, o); @@ -187,11 +190,11 @@ export function geoChooseEdge(nodes, point, projection) { } } - return { - index: idx, - distance: min, - loc: loc - }; + if (idx !== undefined) { + return { index: idx, distance: min, loc: loc }; + } else { + return null; + } } diff --git a/test/spec/geo/geo.js b/test/spec/geo/geo.js index 989c55714..c9762e051 100644 --- a/test/spec/geo/geo.js +++ b/test/spec/geo/geo.js @@ -291,20 +291,12 @@ describe('iD.geo', function() { var projection = function (l) { return l; }; projection.invert = projection; - it('returns undefined properties for a degenerate way (no nodes)', function() { - expect(iD.geoChooseEdge([], [0, 0], projection)).to.eql({ - index: undefined, - distance: Infinity, - loc: undefined - }); + it('returns null for a degenerate way (no nodes)', function() { + expect(iD.geoChooseEdge([], [0, 0], projection)).to.be.null; }); - it('returns undefined properties for a degenerate way (single node)', function() { - expect(iD.geoChooseEdge([iD.osmNode({loc: [0, 0]})], [0, 0], projection)).to.eql({ - index: undefined, - distance: Infinity, - loc: undefined - }); + it('returns null for a degenerate way (single node)', function() { + expect(iD.geoChooseEdge([iD.osmNode({loc: [0, 0]})], [0, 0], projection)).to.be.null; }); it('calculates the orthogonal projection of a point onto a segment', function() { @@ -344,6 +336,67 @@ describe('iD.geo', function() { expect(choice.distance).to.eql(5); expect(choice.loc).to.eql([5, 0]); }); + + it('skips the given nodeID at end of way', function() { + // + // a --*-- b + // e | + // | | + // d - c + // + // * = [2, 0] + var a = [0, 0]; + var b = [5, 0]; + var c = [5, 5]; + var d = [2, 5]; + var e = [2, 0.1]; // e.g. user is dragging e onto ab + var nodes = [ + iD.osmNode({id: 'a', loc: a}), + iD.osmNode({id: 'b', loc: b}), + iD.osmNode({id: 'c', loc: c}), + iD.osmNode({id: 'd', loc: d}), + iD.osmNode({id: 'e', loc: e}) + ]; + var choice = iD.geoChooseEdge(nodes, e, projection, 'e'); + expect(choice.index).to.eql(1); + expect(choice.distance).to.eql(0.1); + expect(choice.loc).to.eql([2, 0]); + }); + + it('skips the given nodeID in middle of way', function() { + // + // a --*-- b + // d | + // / \ | + // e c + // + // * = [2, 0] + var a = [0, 0]; + var b = [5, 0]; + var c = [5, 5]; + var d = [2, 0.1]; // e.g. user is dragging d onto ab + var e = [0, 5]; + var nodes = [ + iD.osmNode({id: 'a', loc: a}), + iD.osmNode({id: 'b', loc: b}), + iD.osmNode({id: 'c', loc: c}), + iD.osmNode({id: 'd', loc: d}), + iD.osmNode({id: 'e', loc: e}) + ]; + var choice = iD.geoChooseEdge(nodes, d, projection, 'd'); + expect(choice.index).to.eql(1); + expect(choice.distance).to.eql(0.1); + expect(choice.loc).to.eql([2, 0]); + }); + + it('returns null if all nodes are skipped', function() { + var nodes = [ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [5, 0]}), + ]; + var choice = iD.geoChooseEdge(nodes, [2, 2], projection, 'a'); + expect(choice).to.be.null; + }); }); describe('geoLineIntersection', function() { From 5d9b051f84c9c45f2a4235d063717e34965bd445 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 21 Dec 2017 20:31:20 -0500 Subject: [PATCH 49/70] Fix drag_node for touch targets and line snapping --- css/20_map.css | 2 + modules/behavior/draw.js | 42 +++++----- modules/behavior/draw_way.js | 152 +++++++++++++++++------------------ modules/core/context.js | 7 -- modules/modes/drag_node.js | 121 ++++++++++------------------ modules/modes/draw_area.js | 4 +- modules/modes/draw_line.js | 4 +- modules/svg/points.js | 1 - 8 files changed, 144 insertions(+), 189 deletions(-) diff --git a/css/20_map.css b/css/20_map.css index 1004a3d1d..6eec27261 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -29,6 +29,8 @@ stroke-width: 12; stroke-opacity: 0.8; stroke: currentColor; + stroke-linecap: round; + stroke-linejoin: round; } /* `.target-nope` objects are explicitly forbidden to join to */ diff --git a/modules/behavior/draw.js b/modules/behavior/draw.js index d2421ffc3..513c0b30c 100644 --- a/modules/behavior/draw.js +++ b/modules/behavior/draw.js @@ -33,8 +33,7 @@ export function behaviorDraw(context) { var keybinding = d3_keybinding('draw'); - var hover = behaviorHover(context) - .altDisables(true) + var hover = behaviorHover(context).altDisables(true) .on('hover', context.ui().sidebar.hover); var tail = behaviorTail(); var edit = behaviorEdit(context); @@ -58,10 +57,8 @@ export function behaviorDraw(context) { // When drawing, connect only to things classed as targets.. // (this excludes area fills and active drawing elements) var selection = d3_select(element); - if (selection.classed('target')) return {}; - - var d = selection.datum(); - return (d && d.id && context.hasEntity(d.id)) || {}; + if (!selection.classed('target')) return {}; + return selection.datum(); } @@ -124,28 +121,33 @@ export function behaviorDraw(context) { } + // related code + // - `mode/drag_node.js` `doMode()` + // - `behavior/draw.js` `click()` + // - `behavior/draw_way.js` `move()` function click() { + var d = datum(); + var target = d && d.id && context.hasEntity(d.id); + var trySnap = geoViewportEdge(context.mouse(), context.map().dimensions()) === null; if (trySnap) { - // If we're not at the edge of the viewport, try to snap.. - // See also: `modes/drag_node.js doMove()` - var d = datum(); - - // Snap to a node - if (d.type === 'node') { - dispatch.call('clickNode', this, d); + if (target && target.type === 'node') { // Snap to a node + dispatch.call('clickNode', this, target); return; - // Snap to a way - } else if (d.type === 'way') { - var choice = geoChooseEdge(context.childNodes(d), context.mouse(), context.projection); - var edge = [d.nodes[choice.index - 1], d.nodes[choice.index]]; - dispatch.call('clickWay', this, choice.loc, edge); - return; + } else if (target && target.type === 'way') { // Snap to a way + var choice = geoChooseEdge( + context.childNodes(target), context.mouse(), context.projection, context.activeID() + ); + if (choice) { + var edge = [target.nodes[choice.index - 1], target.nodes[choice.index]]; + dispatch.call('clickWay', this, choice.loc, edge); + return; + } } } - dispatch.call('click', this, context.map().mouseCoordinates()); + dispatch.call('click', this, context.map().mouseCoordinates(), d); } diff --git a/modules/behavior/draw_way.js b/modules/behavior/draw_way.js index 163513b3c..c9a34c850 100644 --- a/modules/behavior/draw_way.js +++ b/modules/behavior/draw_way.js @@ -10,37 +10,22 @@ import { } from '../actions'; import { behaviorDraw } from './draw'; - -import { - geoChooseEdge, - geoEdgeEqual -} from '../geo'; - -import { - modeBrowse, - modeSelect -} from '../modes'; - -import { - osmNode, - osmWay -} from '../osm'; - -import { utilEntitySelector } from '../util'; +import { geoChooseEdge, geoEdgeEqual } from '../geo'; +import { modeBrowse, modeSelect } from '../modes'; +import { osmNode, osmWay } from '../osm'; export function behaviorDrawWay(context, wayId, index, mode, startGraph) { - var origWay = context.entity(wayId); var isArea = context.geometry(wayId) === 'area'; - var tempEdits = 0; var annotation = t((origWay.isDegenerate() ? 'operations.start.annotation.' : - 'operations.continue.annotation.') + context.geometry(wayId)); - var draw = behaviorDraw(context); - // var _activeIDs = []; - var _activeID; - var startIndex; + 'operations.continue.annotation.') + context.geometry(wayId) + ); + var behavior = behaviorDraw(context); + var _tempEdits = 0; + var _startIndex; + var start; var end; var segment; @@ -48,9 +33,15 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // initialize the temporary drawing entities if (!isArea) { - startIndex = typeof index === 'undefined' ? origWay.nodes.length - 1 : 0; - start = osmNode({ id: 'nStart', loc: context.entity(origWay.nodes[startIndex]).loc }); - end = osmNode({ id: 'nEnd', loc: context.map().mouseCoordinates() }); + _startIndex = (typeof index === 'undefined' ? origWay.nodes.length - 1 : 0); + start = osmNode({ + id: 'nStart', + loc: context.entity(origWay.nodes[_startIndex]).loc + }); + end = osmNode({ + id: 'nEnd', + loc: context.map().mouseCoordinates() + }); segment = osmWay({ id: 'wTemp', nodes: typeof index === 'undefined' ? [start.id, end.id] : [end.id, start.id], @@ -63,21 +54,30 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // Push an annotated state for undo to return back to. // We must make sure to remove this edit later. context.perform(actionNoop(), annotation); - tempEdits++; + _tempEdits++; // Add the temporary drawing entities to the graph. // We must make sure to remove this edit later. context.perform(AddDrawEntities()); - tempEdits++; + _tempEdits++; + // related code + // - `mode/drag_node.js` `doMode()` + // - `behavior/draw.js` `click()` + // - `behavior/draw_way.js` `move()` function move(datum) { var loc; - if (datum.type === 'node' && datum.id !== end.id) { + var target = datum && datum.id && context.hasEntity(datum.id); + if (target && target.type === 'node') { // snap to node loc = datum.loc; - - } else if (datum.type === 'way') { - loc = geoChooseEdge(context.childNodes(datum), context.mouse(), context.projection).loc; + } else if (target && target.type === 'way') { // snap to way + var choice = geoChooseEdge( + context.childNodes(target), context.mouse(), context.projection, end.id + ); + if (choice) { + loc = choice.loc; + } } if (!loc) { @@ -93,7 +93,7 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // Undo popped the history back to the initial annotated no-op edit. // Remove initial no-op edit and whatever edit happened immediately before it. context.pop(2); - tempEdits = 0; + _tempEdits = 0; if (context.hasEntity(wayId)) { context.enter(mode); @@ -104,17 +104,14 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { function setActiveElements() { - // _activeIDs = isArea ? [wayId, end.id] : [segment.id, start.id, end.id]; - // context.surface().selectAll(utilEntitySelector(_activeIDs)) - // .classed('active', true); - _activeID = end.id; context.surface().selectAll('.' + end.id) .classed('active', true); } var drawWay = function(surface) { - draw.on('move', move) + behavior + .on('move', move) .on('click', drawWay.add) .on('clickWay', drawWay.addWay) .on('clickNode', drawWay.addNode) @@ -128,7 +125,7 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { setActiveElements(); - surface.call(draw); + surface.call(behavior); context.history() .on('undone.draw', undone); @@ -139,8 +136,8 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // Drawing was interrupted unexpectedly. // This can happen if the user changes modes, // clicks geolocate button, a hashchange event occurs, etc. - if (tempEdits) { - context.pop(tempEdits); + if (_tempEdits) { + context.pop(_tempEdits); while (context.graph() !== startGraph) { context.pop(); } @@ -149,7 +146,7 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { context.map() .on('drawn.draw', null); - surface.call(draw.off) + surface.call(behavior.off) .selectAll('.active') .classed('active', false); @@ -201,12 +198,18 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // Accept the current position of the temporary node and continue drawing. - drawWay.add = function(loc) { + drawWay.add = function(loc, datum) { +// shouldn't happen now? // prevent duplicate nodes - var last = context.hasEntity(origWay.nodes[origWay.nodes.length - (isArea ? 2 : 1)]); - if (last && last.loc[0] === loc[0] && last.loc[1] === loc[1]) return; + // var last = context.hasEntity(origWay.nodes[origWay.nodes.length - (isArea ? 2 : 1)]); + // if (last && last.loc[0] === loc[0] && last.loc[1] === loc[1]) return; - context.pop(tempEdits); + if (datum && datum.id && /-nope/.test(datum.id)) { // can't click here + return; + } + + context.pop(_tempEdits); + _tempEdits = 0; if (isArea) { context.perform( @@ -222,31 +225,30 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { ); } - tempEdits = 0; context.enter(mode); }; // Connect the way to an existing way. drawWay.addWay = function(loc, edge) { - if (isArea) { - context.pop(tempEdits); + context.pop(_tempEdits); + _tempEdits = 0; + if (isArea) { context.perform( AddDrawEntities(), actionAddMidpoint({ loc: loc, edge: edge}, end), annotation ); } else { - var previousEdge = startIndex ? - [origWay.nodes[startIndex], origWay.nodes[startIndex - 1]] : - [origWay.nodes[0], origWay.nodes[1]]; +// shouldn't happen now? + // var previousEdge = _startIndex ? + // [origWay.nodes[_startIndex], origWay.nodes[_startIndex - 1]] : + // [origWay.nodes[0], origWay.nodes[1]]; - // Avoid creating duplicate segments - if (geoEdgeEqual(edge, previousEdge)) - return; - - context.pop(tempEdits); + // // Avoid creating duplicate segments + // if (geoEdgeEqual(edge, previousEdge)) + // return; var newNode = osmNode({ loc: loc }); context.perform( @@ -256,7 +258,6 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { ); } - tempEdits = 0; context.enter(mode); }; @@ -264,23 +265,25 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // Connect the way to an existing node and continue drawing. drawWay.addNode = function(node) { // Avoid creating duplicate segments - if (origWay.areAdjacent(node.id, origWay.nodes[origWay.nodes.length - 1])) return; +// shouldn't happen now? + // if (origWay.areAdjacent(node.id, origWay.nodes[origWay.nodes.length - 1])) return; // Clicks should not occur on the drawing node, however a space keypress can // sometimes grab that node's datum (before it gets classed as `active`?) #4016 - if (node.id === end.id) { - drawWay.add(node.loc); - return; - } +// shouldn't happen now? + // if (node.id === end.id) { + // drawWay.add(node.loc); + // return; + // } - context.pop(tempEdits); + context.pop(_tempEdits); + _tempEdits = 0; context.perform( ReplaceDrawEntities(node), annotation ); - tempEdits = 0; context.enter(mode); }; @@ -289,8 +292,8 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // If the way has enough nodes to be valid, it's selected. // Otherwise, delete everything and return to browse mode. drawWay.finish = function() { - context.pop(tempEdits); - tempEdits = 0; + context.pop(_tempEdits); + _tempEdits = 0; var way = context.hasEntity(wayId); if (!way || way.isDegenerate()) { @@ -308,8 +311,8 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // Cancel the draw operation, delete everything, and return to browse mode. drawWay.cancel = function() { - context.pop(tempEdits); - tempEdits = 0; + context.pop(_tempEdits); + _tempEdits = 0; while (context.graph() !== startGraph) { context.pop(); @@ -323,20 +326,15 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { }; - // drawWay.activeIDs = function() { - // if (!arguments.length) return _activeIDs; - // // no assign - // return drawWay; - // }; drawWay.activeID = function() { - if (!arguments.length) return _activeID; + if (!arguments.length) return end.id; // no assign return drawWay; }; drawWay.tail = function(text) { - draw.tail(text); + behavior.tail(text); return drawWay; }; diff --git a/modules/core/context.js b/modules/core/context.js index dcf0b6412..1f606d5fe 100644 --- a/modules/core/context.js +++ b/modules/core/context.js @@ -255,13 +255,6 @@ export function coreContext() { return []; } }; - // context.activeIDs = function() { - // if (mode && mode.activeIDs) { - // return mode.activeIDs(); - // } else { - // return []; - // } - // }; context.activeID = function() { return mode && mode.activeID && mode.activeID(); }; diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index 2446ffbfa..4e905f98d 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -12,25 +12,10 @@ import { actionNoop } from '../actions'; -import { - behaviorEdit, - behaviorHover, - behaviorDrag -} from '../behavior'; - -import { - modeBrowse, - modeSelect -} from './index'; - -import { - geoChooseEdge, - geoVecSubtract, - geoViewportEdge -} from '../geo'; - +import { behaviorEdit, behaviorHover, behaviorDrag } from '../behavior'; +import { geoChooseEdge, geoVecSubtract, geoViewportEdge } from '../geo'; +import { modeBrowse, modeSelect } from './index'; import { osmNode } from '../osm'; -import { utilEntitySelector } from '../util'; import { uiFlash } from '../ui'; @@ -39,16 +24,15 @@ export function modeDragNode(context) { id: 'drag-node', button: 'browse' }; - var hover = behaviorHover(context).altDisables(true).on('hover', context.ui().sidebar.hover); + var hover = behaviorHover(context).altDisables(true) + .on('hover', context.ui().sidebar.hover); var edit = behaviorEdit(context); var _nudgeInterval; var _restoreSelectedIDs = []; - // var _activeIDs = []; - var _activeID; var _wasMidpoint = false; var _isCancelled = false; - var _dragEntity; + var _activeEntity; var _lastLoc; @@ -94,7 +78,7 @@ export function modeDragNode(context) { if (hasHidden) { uiFlash().text(t('modes.drag_node.connected_to_hidden'))(); } - return behavior.cancel(); + return drag.cancel(); } if (_wasMidpoint) { @@ -103,20 +87,15 @@ export function modeDragNode(context) { context.perform(actionAddMidpoint(midpoint, entity)); var vertex = context.surface().selectAll('.' + entity.id); - behavior.target(vertex.node(), entity); + drag.target(vertex.node(), entity); } else { context.perform(actionNoop()); } - _dragEntity = entity; - - // `.active` elements have `pointer-events: none`. - // This prevents the node or vertex being dragged from trying to connect to itself. - // _activeIDs = context.graph().parentWays(entity).map(function(parent) { return parent.id; }); - // _activeIDs.push(entity.id); - _activeID = entity.id; - setActiveElements(); + _activeEntity = entity; + context.surface().selectAll('.' + _activeEntity.id) + .classed('active', true); context.enter(mode); } @@ -127,8 +106,7 @@ export function modeDragNode(context) { if (!event || event.altKey || !d3_select(event.target).classed('target')) { return {}; } else { - var d = event.target.__data__; - return (d && d.id && context.hasEntity(d.id)) || {}; + return event.target.__data__ || {}; } } @@ -140,20 +118,21 @@ export function modeDragNode(context) { var currMouse = geoVecSubtract(currPoint, nudge); var loc = context.projection.invert(currMouse); - if (!_nudgeInterval) { - // If we're not nudging at the edge of the viewport, try to snap.. - // See also `behavior/draw.js click()` + if (!_nudgeInterval) { // If not nudging at the edge of the viewport, try to snap.. + // related code + // - `mode/drag_node.js` `doMode()` + // - `behavior/draw.js` `click()` + // - `behavior/draw_way.js` `move()` var d = datum(); + var target = d && d.id && context.hasEntity(d.id); - // Snap to a node (not self) - if (d.type === 'node' && d.id !== entity.id) { - loc = d.loc; - - // Snap to a way - } else if (d.type === 'way') { - var choice = geoChooseEdge(context.childNodes(d), context.mouse(), context.projection); - // (not along a segment adjacent to self) - if (entity.id !== d.nodes[choice.index - 1] && entity.id !== d.nodes[choice.index]) { + if (target && target.type === 'node') { + loc = target.loc; + } else if (target && target.type === 'way') { + var choice = geoChooseEdge( + context.childNodes(target), context.mouse(), context.projection, entity.id + ); + if (choice) { loc = choice.loc; } } @@ -188,18 +167,22 @@ export function modeDragNode(context) { if (_isCancelled) return; var d = datum(); + var target = d && d.id && context.hasEntity(d.id); - if (d.type === 'way') { - var choice = geoChooseEdge(context.childNodes(d), context.mouse(), context.projection); + if (target && target.type === 'way') { + var choice = geoChooseEdge(context.childNodes(target), context.mouse(), context.projection, entity.id); context.replace( - actionAddMidpoint({ loc: choice.loc, edge: [d.nodes[choice.index - 1], d.nodes[choice.index]] }, entity), - connectAnnotation(d) + actionAddMidpoint({ + loc: choice.loc, + edge: [target.nodes[choice.index - 1], target.nodes[choice.index]] + }, entity), + connectAnnotation(target) ); - } else if (d.type === 'node' && d.id !== entity.id) { + } else if (target && target.type === 'node') { context.replace( - actionConnect([d.id, entity.id]), - connectAnnotation(d) + actionConnect([target.id, entity.id]), + connectAnnotation(target) ); } else if (_wasMidpoint) { @@ -228,20 +211,12 @@ export function modeDragNode(context) { function cancel() { - behavior.cancel(); + drag.cancel(); context.enter(modeBrowse(context)); } - function setActiveElements() { - // context.surface().selectAll(utilEntitySelector(_activeIDs)) - // .classed('active', true); - context.surface().selectAll('.' + _activeID) - .classed('active', true); - } - - - var behavior = behaviorDrag() + var drag = behaviorDrag() .selector('.layer-points-targets .target') .surface(d3_select('#map').node()) .origin(origin) @@ -256,11 +231,6 @@ export function modeDragNode(context) { context.history() .on('undone.drag-node', cancel); - - context.map() - .on('drawn.drag-node', setActiveElements); - - setActiveElements(); }; @@ -275,8 +245,8 @@ export function modeDragNode(context) { context.map() .on('drawn.drag-node', null); - // _activeIDs = []; - _activeID = null; + _activeEntity = null; + context.surface() .selectAll('.active') .classed('active', false); @@ -286,19 +256,14 @@ export function modeDragNode(context) { mode.selectedIDs = function() { - if (!arguments.length) return _dragEntity ? [_dragEntity.id] : []; + if (!arguments.length) return _activeEntity ? [_activeEntity.id] : []; // no assign return mode; }; - // mode.activeIDs = function() { - // if (!arguments.length) return _activeIDs; - // // no assign - // return mode; - // }; mode.activeID = function() { - if (!arguments.length) return _activeID; + if (!arguments.length) return _activeEntity && _activeEntity.id; // no assign return mode; }; @@ -311,7 +276,7 @@ export function modeDragNode(context) { }; - mode.behavior = behavior; + mode.behavior = drag; return mode; diff --git a/modules/modes/draw_area.js b/modules/modes/draw_area.js index 0dd5b1272..0890e3592 100644 --- a/modules/modes/draw_area.js +++ b/modules/modes/draw_area.js @@ -43,9 +43,7 @@ export function modeDrawArea(context, wayId, startGraph) { return [wayId]; }; - // mode.activeIDs = function() { - // return (behavior && behavior.activeIDs()) || []; - // }; + mode.activeID = function() { return (behavior && behavior.activeID()) || []; }; diff --git a/modules/modes/draw_line.js b/modules/modes/draw_line.js index 3e638cbd9..ca567601e 100644 --- a/modules/modes/draw_line.js +++ b/modules/modes/draw_line.js @@ -41,9 +41,7 @@ export function modeDrawLine(context, wayId, startGraph, affix) { return [wayId]; }; - // mode.activeIDs = function() { - // return (behavior && behavior.activeIDs()) || []; - // }; + mode.activeID = function() { return (behavior && behavior.activeID()) || []; }; diff --git a/modules/svg/points.js b/modules/svg/points.js index ef1d504d9..0b3366ab3 100644 --- a/modules/svg/points.js +++ b/modules/svg/points.js @@ -31,7 +31,6 @@ export function svgPoints(projection, context) { var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; var passive = entities.filter(function(d) { return d.id !== context.activeID(); - // return context.activeIDs().indexOf(d.id) === -1; }); var targets = selection.selectAll('.point.target') From 2be62fffe5a105b55709e13969ed1f34b463ca1a Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 21 Dec 2017 23:48:52 -0500 Subject: [PATCH 50/70] All the complicated code in drawWay can be removed Now that we can target specific segments along a line, we don't need temporary drawing objects or extra code to check for self intersections --- modules/behavior/draw_way.js | 148 ++++++----------------------------- 1 file changed, 25 insertions(+), 123 deletions(-) diff --git a/modules/behavior/draw_way.js b/modules/behavior/draw_way.js index c9a34c850..8f138c45a 100644 --- a/modules/behavior/draw_way.js +++ b/modules/behavior/draw_way.js @@ -1,64 +1,36 @@ -import _clone from 'lodash-es/clone'; - import { t } from '../util/locale'; import { - actionAddEntity, actionAddMidpoint, actionMoveNode, actionNoop } from '../actions'; import { behaviorDraw } from './draw'; -import { geoChooseEdge, geoEdgeEqual } from '../geo'; +import { geoChooseEdge } from '../geo'; import { modeBrowse, modeSelect } from '../modes'; -import { osmNode, osmWay } from '../osm'; +import { osmNode } from '../osm'; export function behaviorDrawWay(context, wayId, index, mode, startGraph) { var origWay = context.entity(wayId); - var isArea = context.geometry(wayId) === 'area'; var annotation = t((origWay.isDegenerate() ? 'operations.start.annotation.' : 'operations.continue.annotation.') + context.geometry(wayId) ); var behavior = behaviorDraw(context); var _tempEdits = 0; - var _startIndex; - var start; - var end; - var segment; - - - // initialize the temporary drawing entities - if (!isArea) { - _startIndex = (typeof index === 'undefined' ? origWay.nodes.length - 1 : 0); - start = osmNode({ - id: 'nStart', - loc: context.entity(origWay.nodes[_startIndex]).loc - }); - end = osmNode({ - id: 'nEnd', - loc: context.map().mouseCoordinates() - }); - segment = osmWay({ - id: 'wTemp', - nodes: typeof index === 'undefined' ? [start.id, end.id] : [end.id, start.id], - tags: _clone(origWay.tags) - }); - } else { - end = osmNode({ loc: context.map().mouseCoordinates() }); - } + var end = osmNode({ loc: context.map().mouseCoordinates() }); // Push an annotated state for undo to return back to. // We must make sure to remove this edit later. context.perform(actionNoop(), annotation); _tempEdits++; - // Add the temporary drawing entities to the graph. + // Add the drawing node to the graph. // We must make sure to remove this edit later. - context.perform(AddDrawEntities()); + context.perform(_actionAddDrawNode()); _tempEdits++; @@ -155,75 +127,35 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { }; - function AddDrawEntities() { + function _actionAddDrawNode() { return function(graph) { - if (isArea) { - // For area drawing, there is no need for a temporary node. - // `end` gets inserted into the way as the penultimate node. - return graph - .replace(end) - .replace(origWay.addNode(end.id)); - } else { - // For line drawing, add a temporary start, end, and segment to the graph. - // This allows us to class the new segment as `active`, but still - // connect it back to parts of the way that have already been drawn. - return graph - .replace(start) - .replace(end) - .replace(segment); - } + return graph + .replace(end) + .replace(origWay.addNode(end.id)); }; } - function ReplaceDrawEntities(newNode) { + function _actionReplaceDrawNode(newNode) { return function(graph) { - if (isArea) { - // For area drawing, we didn't create a temporary node. - // `newNode` gets inserted into the _original_ way as the penultimate node. - return graph - .replace(origWay.addNode(newNode.id)) - .remove(end); - } else { - // For line drawing, add the `newNode` to the way at specified index, - // and remove the temporary start, end, and segment. - return graph - .replace(origWay.addNode(newNode.id, index)) - .remove(end) - .remove(segment) - .remove(start); - } + return graph + .replace(origWay.addNode(newNode.id)) + .remove(end); }; } - // Accept the current position of the temporary node and continue drawing. + // Accept the current position of the drawing node and continue drawing. drawWay.add = function(loc, datum) { -// shouldn't happen now? - // prevent duplicate nodes - // var last = context.hasEntity(origWay.nodes[origWay.nodes.length - (isArea ? 2 : 1)]); - // if (last && last.loc[0] === loc[0] && last.loc[1] === loc[1]) return; - - if (datum && datum.id && /-nope/.test(datum.id)) { // can't click here - return; - } + if (datum && datum.id && /-nope/.test(datum.id)) return; // can't click here context.pop(_tempEdits); _tempEdits = 0; - if (isArea) { - context.perform( - AddDrawEntities(), - annotation - ); - } else { - var newNode = osmNode({loc: loc}); - context.perform( - actionAddEntity(newNode), - ReplaceDrawEntities(newNode), - annotation - ); - } + context.perform( + _actionAddDrawNode(), + annotation + ); context.enter(mode); }; @@ -234,29 +166,11 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { context.pop(_tempEdits); _tempEdits = 0; - if (isArea) { - context.perform( - AddDrawEntities(), - actionAddMidpoint({ loc: loc, edge: edge}, end), - annotation - ); - } else { -// shouldn't happen now? - // var previousEdge = _startIndex ? - // [origWay.nodes[_startIndex], origWay.nodes[_startIndex - 1]] : - // [origWay.nodes[0], origWay.nodes[1]]; - - // // Avoid creating duplicate segments - // if (geoEdgeEqual(edge, previousEdge)) - // return; - - var newNode = osmNode({ loc: loc }); - context.perform( - actionAddMidpoint({ loc: loc, edge: edge}, newNode), - ReplaceDrawEntities(newNode), - annotation - ); - } + context.perform( + _actionAddDrawNode(), + actionAddMidpoint({ loc: loc, edge: edge }, end), + annotation + ); context.enter(mode); }; @@ -264,23 +178,11 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // Connect the way to an existing node and continue drawing. drawWay.addNode = function(node) { - // Avoid creating duplicate segments -// shouldn't happen now? - // if (origWay.areAdjacent(node.id, origWay.nodes[origWay.nodes.length - 1])) return; - - // Clicks should not occur on the drawing node, however a space keypress can - // sometimes grab that node's datum (before it gets classed as `active`?) #4016 -// shouldn't happen now? - // if (node.id === end.id) { - // drawWay.add(node.loc); - // return; - // } - context.pop(_tempEdits); _tempEdits = 0; context.perform( - ReplaceDrawEntities(node), + _actionReplaceDrawNode(node), annotation ); From 7851c49e80b59c9c8b8da0975d774488ee0566fc Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 22 Dec 2017 00:26:39 -0500 Subject: [PATCH 51/70] Fix nopefilters by testing the original id for the filter --- modules/svg/areas.js | 1 + modules/svg/helpers.js | 3 +++ modules/svg/lines.js | 12 +++++++++--- modules/svg/vertices.js | 7 +++---- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/modules/svg/areas.js b/modules/svg/areas.js index 3fb7c3723..90f35eb57 100644 --- a/modules/svg/areas.js +++ b/modules/svg/areas.js @@ -76,6 +76,7 @@ export function svgAreas(projection, context) { // NOPE var nopes = selection.selectAll('.area.target-nope') + .filter(function(d) { return filter({ id: d.properties.originalID }); }) .data(data.nopes, function key(d) { return d.id; }); // exit diff --git a/modules/svg/helpers.js b/modules/svg/helpers.js index 88f04f578..bbd22f11d 100644 --- a/modules/svg/helpers.js +++ b/modules/svg/helpers.js @@ -245,6 +245,9 @@ export function svgSegmentWay(way, graph, activeID) { features.active.push({ 'type': 'Feature', 'id': way.id + '-nope', // break the ids on purpose + 'properties': { + 'originalID': way.id + }, 'geometry': { 'type': 'MultiLineString', 'coordinates': coordGroups.active diff --git a/modules/svg/lines.js b/modules/svg/lines.js index fbecbed7b..67e433ab8 100644 --- a/modules/svg/lines.js +++ b/modules/svg/lines.js @@ -72,6 +72,7 @@ export function svgLines(projection, context) { // NOPE var nopes = selection.selectAll('.line.target-nope') + .filter(function(d) { return filter({ id: d.properties.originalID }); }) .data(data.nopes, function key(d) { return d.id; }); // exit @@ -101,6 +102,11 @@ export function svgLines(projection, context) { function drawLineGroup(selection, klass, isSelected) { + // Note: Don't add `.selected` class in draw modes + var mode = context.mode(); + var isDrawing = mode && /^draw/.test(mode.id); + var selectedClass = (!isDrawing && isSelected) ? 'selected ' : ''; + var lines = selection .selectAll('path') .filter(filter) @@ -109,13 +115,13 @@ export function svgLines(projection, context) { lines.exit() .remove(); - // Optimization: call simple TagClasses only on enter selection. This + // Optimization: Call expensive TagClasses only on enter selection. This // works because osmEntity.key is defined to include the entity v attribute. lines.enter() .append('path') .attr('class', function(d) { - return 'way line ' + klass + ' ' + d.id + (isSelected ? ' selected' : '') + - (oldMultiPolygonOuters[d.id] ? ' old-multipolygon' : ''); + var oldMPClass = oldMultiPolygonOuters[d.id] ? 'old-multipolygon ' : ''; + return 'way line ' + klass + ' ' + selectedClass + oldMPClass + d.id; }) .call(svgTagClasses()) .merge(lines) diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index 77aad0a1d..a0584b501 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -196,6 +196,7 @@ export function svgVertices(projection, context) { } else { data.nopes.push({ id: node.id + '-nope', // not a real osmNode, break the id on purpose + originalID: node.id, loc: node.loc }); } @@ -222,6 +223,7 @@ export function svgVertices(projection, context) { // NOPE var nopes = selection.selectAll('.vertex.target-nope') + .filter(function(d) { return filter({ id: d.originalID }); }) .data(data.nopes, function key(d) { return d.id; }); // exit @@ -351,11 +353,8 @@ export function svgVertices(projection, context) { .call(draw, graph, currentVisible(all), sets, filterRendered); // Draw touch targets.. - var filterTargets = function(d) { - return isMoving ? true : filterRendered(d); - }; selection.selectAll('.layer-points .layer-points-targets') - .call(drawTargets, graph, currentVisible(all), filterTargets); + .call(drawTargets, graph, currentVisible(all), filterRendered); function currentVisible(which) { From be00a526b6c8caf611e18b864dcee8cee8d6a88c Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 22 Dec 2017 11:42:21 -0500 Subject: [PATCH 52/70] Make sure all targets are redrawn during a mode change There was an issue where the lines did not redraw their targets right away when entering drag node, which could make it possible for a quick drag node to try to connect to its parent line. With the chooseEdge exclusion it would not connect to the parent nearby, but in another weird part of the line. --- modules/renderer/map.js | 30 ++++++++++++++++++++++++------ modules/svg/vertices.js | 8 ++++++-- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/modules/renderer/map.js b/modules/renderer/map.js index b2718ec25..a4ebac8a4 100644 --- a/modules/renderer/map.js +++ b/modules/renderer/map.js @@ -201,17 +201,35 @@ export function rendererMap(context) { context.on('enter.map', function() { if (map.editable() && !transformed) { - // redraw immediately the objects that are affected by a chnage in selectedIDs. - var all = context.intersects(map.extent()); - var filter = utilFunctor(true); - var graph = context.graph(); - all = context.features().filter(all, graph); + // redraw immediately any objects affected by a change in selectedIDs. + var graph = context.graph(); + var selectedAndParents = {}; + context.selectedIDs().forEach(function(id) { + var entity = graph.hasEntity(id); + if (entity) { + selectedAndParents[entity.id] = entity; + if (entity.type === 'node') { + graph.parentWays(entity).forEach(function(parent) { + selectedAndParents[parent.id] = parent; + }); + } + } + }); + var data = _values(selectedAndParents); + var filter = function(d) { return d.id in selectedAndParents; }; + + data = context.features().filter(data, graph); + surface.selectAll('.data-layer-osm') .call(drawVertices.drawSelected, graph, map.extent()) - .call(drawMidpoints, graph, all, filter, map.trimmedExtent()); + .call(drawLines, graph, data, filter) + .call(drawAreas, graph, data, filter) + .call(drawMidpoints, graph, data, filter, map.trimmedExtent()); + dispatch.call('drawn', this, { full: false }); + // redraw everything else later scheduleRedraw(); } diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index a0584b501..1a5ed0a88 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -345,7 +345,7 @@ export function svgVertices(projection, context) { // Draw the vertices.. // The filter function controls the scope of what objects d3 will touch (exit/enter/update) - // It's important to adjust the filter function to expand the scope beyond whatever entities were passed in. + // Adjust the filter function to expand the scope beyond whatever entities were passed in. var filterRendered = function(d) { return d.id in _currPersistent || d.id in _currSelected || d.id in _currHover || filter(d); }; @@ -353,8 +353,12 @@ export function svgVertices(projection, context) { .call(draw, graph, currentVisible(all), sets, filterRendered); // Draw touch targets.. + // When drawing, render all targets (not just those affected by a partial redraw) + var filterTouch = function(d) { + return isMoving ? true : filterRendered(d); + }; selection.selectAll('.layer-points .layer-points-targets') - .call(drawTargets, graph, currentVisible(all), filterRendered); + .call(drawTargets, graph, currentVisible(all), filterTouch); function currentVisible(which) { From fc680545ad976fc3c94326d5e1e9fce29bee9cc5 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 22 Dec 2017 13:58:27 -0500 Subject: [PATCH 53/70] Don't dispatch drag start and move together Drag start is responsible for switching into drag mode, classing stuff as `active` and kicking off a bunch of other things. If the drag move happens immediately after this, and includes the target from the initial active drag, it can cause weird snapping from the dragnode to its own parent way. (Happened if the user did a very fast drag from the node along the parent way just next to it) --- modules/behavior/drag.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/modules/behavior/drag.js b/modules/behavior/drag.js index 52e9fb7ff..b9ea9c0af 100644 --- a/modules/behavior/drag.js +++ b/modules/behavior/drag.js @@ -110,19 +110,19 @@ export function behaviorDrag() { if (dx === 0 && dy === 0) return; - if (!started) { - started = true; - _event({ type: 'start' }); - } - startOrigin = p; d3_eventCancel(); - _event({ - type: 'move', - point: [p[0] + offset[0], p[1] + offset[1]], - delta: [dx, dy] - }); + if (!started) { + started = true; + _event({ type: 'start' }); + } else { + _event({ + type: 'move', + point: [p[0] + offset[0], p[1] + offset[1]], + delta: [dx, dy] + }); + } } From 64a11f4cbfa30d291a1e7ced620880d84060e82b Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 22 Dec 2017 15:08:05 -0500 Subject: [PATCH 54/70] Restore ability to extend line at beginning or end --- modules/behavior/draw_way.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/behavior/draw_way.js b/modules/behavior/draw_way.js index 8f138c45a..68e56c182 100644 --- a/modules/behavior/draw_way.js +++ b/modules/behavior/draw_way.js @@ -131,7 +131,7 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { return function(graph) { return graph .replace(end) - .replace(origWay.addNode(end.id)); + .replace(origWay.addNode(end.id, index)); }; } @@ -139,7 +139,7 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { function _actionReplaceDrawNode(newNode) { return function(graph) { return graph - .replace(origWay.addNode(newNode.id)) + .replace(origWay.addNode(newNode.id, index)) .remove(end); }; } From 117ad7d6b65acf4f3211f26b62a1653c269a45fa Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 22 Dec 2017 16:57:27 -0500 Subject: [PATCH 55/70] Transitionable actionMoveNode --- modules/actions/move_node.js | 21 ++++++++++++---- test/spec/actions/move_node.js | 46 +++++++++++++++++++++++++++++++--- 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/modules/actions/move_node.js b/modules/actions/move_node.js index 6e2593b45..8de29a16a 100644 --- a/modules/actions/move_node.js +++ b/modules/actions/move_node.js @@ -1,7 +1,18 @@ -// https://github.com/openstreetmap/josm/blob/mirror/src/org/openstreetmap/josm/command/MoveCommand.java -// https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/MoveNodeAction.as -export function actionMoveNode(nodeId, loc) { - return function(graph) { - return graph.replace(graph.entity(nodeId).move(loc)); +import { geoInterp } from '../geo'; + +export function actionMoveNode(nodeID, toLoc) { + + var action = function(graph, t) { + if (t === null || !isFinite(t)) t = 1; + t = Math.min(Math.max(+t, 0), 1); + + var node = graph.entity(nodeID); + return graph.replace( + node.move(geoInterp(node.loc, toLoc, t)) + ); }; + + action.transitionable = true; + + return action; } diff --git a/test/spec/actions/move_node.js b/test/spec/actions/move_node.js index bc28f748d..771f66b4a 100644 --- a/test/spec/actions/move_node.js +++ b/test/spec/actions/move_node.js @@ -1,8 +1,46 @@ describe('iD.actionMoveNode', function () { it('changes a node\'s location', function () { - var node = iD.Node(), - loc = [2, 3], - graph = iD.actionMoveNode(node.id, loc)(iD.Graph([node])); - expect(graph.entity(node.id).loc).to.eql(loc); + var node = iD.osmNode({id: 'a', loc: [0, 0]}); + var toLoc = [2, 3]; + var graph = iD.coreGraph([node]); + + graph = iD.actionMoveNode('a', toLoc)(graph); + expect(graph.entity('a').loc).to.eql(toLoc); + }); + + describe('transitions', function () { + it('is transitionable', function() { + expect(iD.actionMoveNode().transitionable).to.be.true; + }); + + it('move node at t = 0', function() { + var node = iD.osmNode({id: 'a', loc: [0, 0]}); + var toLoc = [2, 3]; + var graph = iD.coreGraph([node]); + + graph = iD.actionMoveNode('a', toLoc)(graph, 0); + expect(graph.entity('a').loc[0]).to.be.closeTo(0, 1e-6); + expect(graph.entity('a').loc[1]).to.be.closeTo(0, 1e-6); + }); + + it('move node at t = 0.5', function() { + var node = iD.osmNode({id: 'a', loc: [0, 0]}); + var toLoc = [2, 3]; + var graph = iD.coreGraph([node]); + + graph = iD.actionMoveNode('a', toLoc)(graph, 0.5); + expect(graph.entity('a').loc[0]).to.be.closeTo(1, 1e-6); + expect(graph.entity('a').loc[1]).to.be.closeTo(1.5, 1e-6); + }); + + it('move node at t = 1', function() { + var node = iD.osmNode({id: 'a', loc: [0, 0]}); + var toLoc = [2, 3]; + var graph = iD.coreGraph([node]); + + graph = iD.actionMoveNode('a', toLoc)(graph, 1); + expect(graph.entity('a').loc[0]).to.be.closeTo(2, 1e-6); + expect(graph.entity('a').loc[1]).to.be.closeTo(3, 1e-6); + }); }); }); From d6e8ca2a1c87f2cb2eecf93a3604de7403bb066a Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 22 Dec 2017 21:06:44 -0500 Subject: [PATCH 56/70] Transitioned bouncebacks when user drags node onto a nope --- modules/modes/drag_node.js | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index 4e905f98d..9218527f1 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -33,6 +33,7 @@ export function modeDragNode(context) { var _wasMidpoint = false; var _isCancelled = false; var _activeEntity; + var _startLoc; var _lastLoc; @@ -85,6 +86,7 @@ export function modeDragNode(context) { var midpoint = entity; entity = osmNode(); context.perform(actionAddMidpoint(midpoint, entity)); + entity = context.entity(entity.id); // get post-action entity var vertex = context.surface().selectAll('.' + entity.id); drag.target(vertex.node(), entity); @@ -94,6 +96,8 @@ export function modeDragNode(context) { } _activeEntity = entity; + _startLoc = entity.loc; + context.surface().selectAll('.' + _activeEntity.id) .classed('active', true); @@ -167,9 +171,15 @@ export function modeDragNode(context) { if (_isCancelled) return; var d = datum(); - var target = d && d.id && context.hasEntity(d.id); + var nope = d && d.id && /-nope$/.test(d.id); // can't drag here + var target = d && d.id && context.hasEntity(d.id); // entity to snap to - if (target && target.type === 'way') { + if (nope) { // bounce back + context.perform( + _actionBounceBack(entity.id, _startLoc) + ); + + } else if (target && target.type === 'way') { var choice = geoChooseEdge(context.childNodes(target), context.mouse(), context.projection, entity.id); context.replace( actionAddMidpoint({ @@ -210,6 +220,19 @@ export function modeDragNode(context) { } + function _actionBounceBack(nodeID, toLoc) { + var moveNode = actionMoveNode(nodeID, toLoc); + var action = function(graph, t) { + // last time through, pop off the bounceback perform. + // it will then overwrite the initial perform with a moveNode that does nothing + if (t === 1) context.pop(); + return moveNode(graph, t); + }; + action.transitionable = true; + return action; + } + + function cancel() { drag.cancel(); context.enter(modeBrowse(context)); From 9fbb4d350f8b7d0ffc57dd6577245570dc82951c Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sat, 23 Dec 2017 22:33:35 -0500 Subject: [PATCH 57/70] Add geoVecEquals for strict comparisons --- modules/geo/geo.js | 5 +++++ modules/geo/index.js | 1 + test/spec/geo/geo.js | 8 ++++++++ 3 files changed, 14 insertions(+) diff --git a/modules/geo/geo.js b/modules/geo/geo.js index 318283931..7a4fe121a 100644 --- a/modules/geo/geo.js +++ b/modules/geo/geo.js @@ -8,6 +8,11 @@ var EQUATORIAL_RADIUS = 6356752.314245179; var POLAR_RADIUS = 6378137.0; +// vector addition +export function geoVecEquals(a, b) { + return (a[0] === b[0]) && (a[1] === b[1]); +} + // vector addition export function geoVecAdd(a, b) { return [ a[0] + b[0], a[1] + b[1] ]; diff --git a/modules/geo/index.js b/modules/geo/index.js index 2a1c8fa31..95bba82c5 100644 --- a/modules/geo/index.js +++ b/modules/geo/index.js @@ -23,6 +23,7 @@ export { geoPolygonIntersectsPolygon } from './geo.js'; export { geoScaleToZoom } from './geo.js'; export { geoSphericalDistance } from './geo.js'; export { geoVecAdd } from './geo.js'; +export { geoVecEquals } from './geo.js'; export { geoVecFloor } from './geo.js'; export { geoVecSubtract } from './geo.js'; export { geoVecScale } from './geo.js'; diff --git a/test/spec/geo/geo.js b/test/spec/geo/geo.js index c9762e051..585e7a2e7 100644 --- a/test/spec/geo/geo.js +++ b/test/spec/geo/geo.js @@ -1,5 +1,13 @@ describe('iD.geo', function() { + describe('geoVecEquals', function() { + it('tests vectors for equality', function() { + expect(iD.geoVecEquals([1, 2], [1, 2])).to.be.true; + expect(iD.geoVecEquals([1, 2], [1, 0])).to.be.false; + expect(iD.geoVecEquals([1, 2], [2, 1])).to.be.false; + }); + }); + describe('geoVecAdd', function() { it('adds vectors', function() { expect(iD.geoVecAdd([1, 2], [3, 4])).to.eql([4, 6]); From 009d7b0d65343195ccf2525d1361308fb9eaf1d0 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sun, 24 Dec 2017 09:21:32 -0500 Subject: [PATCH 58/70] Add layer blocker and polygon self-intersection geometry check --- css/20_map.css | 2 ++ modules/modes/drag_node.js | 74 ++++++++++++++++++++++++++++++++++++-- modules/svg/blocker.js | 26 ++++++++++++++ modules/svg/index.js | 1 + 4 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 modules/svg/blocker.js diff --git a/css/20_map.css b/css/20_map.css index 6eec27261..3b7225dd2 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -16,6 +16,7 @@ /* `.target` objects are interactive */ /* They can be picked up, clicked, hovered, or things can connect to them */ +.layer-blocker.target, .node.target { pointer-events: fill; fill-opacity: 0.8; @@ -34,6 +35,7 @@ } /* `.target-nope` objects are explicitly forbidden to join to */ +.layer-blocker.target.target-nope, .node.target.target-nope, .way.target.target-nope { cursor: not-allowed; diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index 9218527f1..e76180f47 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -12,10 +12,23 @@ import { actionNoop } from '../actions'; -import { behaviorEdit, behaviorHover, behaviorDrag } from '../behavior'; -import { geoChooseEdge, geoVecSubtract, geoViewportEdge } from '../geo'; +import { + behaviorEdit, + behaviorHover, + behaviorDrag +} from '../behavior'; + +import { + geoChooseEdge, + geoLineIntersection, + geoVecEquals, + geoVecSubtract, + geoViewportEdge +} from '../geo'; + import { modeBrowse, modeSelect } from './index'; import { osmNode } from '../osm'; +import { svgBlocker } from '../svg'; import { uiFlash } from '../ui'; @@ -27,6 +40,7 @@ export function modeDragNode(context) { var hover = behaviorHover(context).altDisables(true) .on('hover', context.ui().sidebar.hover); var edit = behaviorEdit(context); + var blocker = svgBlocker(context.projection, context); var _nudgeInterval; var _restoreSelectedIDs = []; @@ -147,10 +161,63 @@ export function modeDragNode(context) { moveAnnotation(entity) ); + + checkGeometry(entity); _lastLoc = loc; } + function checkGeometry(entity) { + var doBlock = false; + var graph = context.graph(); + var parents = graph.parentWays(entity); + + function checkSelfIntersections(way, activeID) { + // check active (dragged) segments against inactive segments + var actives = []; + var inactives = []; + var j, k; + for (j = 0; j < way.nodes.length - 1; j++) { + var n1 = graph.entity(way.nodes[j]); + var n2 = graph.entity(way.nodes[j+1]); + var segment = [n1.loc, n2.loc]; + if (n1.id === activeID || n2.id === activeID) { + actives.push(segment); + } else { + inactives.push(segment); + } + } + for (j = 0; j < actives.length; j++) { + for (k = 0; k < inactives.length; k++) { + var p = actives[j]; + var q = inactives[k]; + // skip if segments share an endpoint + if (geoVecEquals(p[1], q[0]) || geoVecEquals(p[0], q[1]) || + geoVecEquals(p[0], q[0]) || geoVecEquals(p[1], q[1]) ) { + continue; + } else if (geoLineIntersection(p, q)) { + return true; + } + } + } + return false; + } + + for (var i = 0; i < parents.length; i++) { + var parent = parents[i]; + if (parent.isClosed()) { // check for self intersections + if (checkSelfIntersections(parent, entity.id)) { + doBlock = true; + break; + } + } + } + + d3_select('.data-layer-osm') + .call(doBlock ? blocker : blocker.off); + } + + function move(entity) { if (_isCancelled) return; @@ -270,6 +337,9 @@ export function modeDragNode(context) { _activeEntity = null; + d3_select('.data-layer-osm') + .call(blocker.off); + context.surface() .selectAll('.active') .classed('active', false); diff --git a/modules/svg/blocker.js b/modules/svg/blocker.js new file mode 100644 index 000000000..93a3a5f1c --- /dev/null +++ b/modules/svg/blocker.js @@ -0,0 +1,26 @@ +export function svgBlocker(projection, context) { + + function blocker(selection) { + var dimensions = projection.clipExtent()[1]; + var fillClass = context.getDebug('target') ? 'red ' : 'nocolor '; + + var blocker = selection.selectAll('.layer-blocker') + .data([{id: 'target-nope'}]); + + blocker.enter() + .append('rect') + .attr('class', 'layer-blocker target target-nope ' + fillClass) + .attr('x', 0) + .attr('y', 0) + .merge(blocker) + .attr('width', dimensions[0]) + .attr('height', dimensions[1]); + } + + blocker.off = function(selection) { + selection.selectAll('.layer-blocker') + .remove(); + }; + + return blocker; +} diff --git a/modules/svg/index.js b/modules/svg/index.js index 68e2f2f28..798893dbb 100644 --- a/modules/svg/index.js +++ b/modules/svg/index.js @@ -1,4 +1,5 @@ export { svgAreas } from './areas.js'; +export { svgBlocker } from './blocker.js'; export { svgDebug } from './debug.js'; export { svgDefs } from './defs.js'; export { svgGpx } from './gpx.js'; From ee617779a4efa590a27807b155fcc4096ab92725 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 25 Dec 2017 23:11:00 -0500 Subject: [PATCH 59/70] Snap to nope targets too (snapping is useful feedback) Also remove "acting" cursor, which was overriding the no-action cursor in some situations. --- css/55_cursors.css | 10 ---------- modules/behavior/draw_way.js | 2 +- modules/modes/drag_node.js | 6 +++--- 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/css/55_cursors.css b/css/55_cursors.css index db5834e6d..9e72d7ddf 100644 --- a/css/55_cursors.css +++ b/css/55_cursors.css @@ -49,16 +49,6 @@ cursor: url(img/cursor-select-remove.png), pointer; /* FF */ } -#map .point:active, -#map .vertex:active, -#map .line:active, -#map .area:active, -#map .midpoint:active, -#map .mode-select .selected { - cursor: pointer; /* Opera */ - cursor: url(img/cursor-select-acting.png), pointer; /* FF */ -} - .mode-draw-line #map, .mode-draw-area #map, .mode-add-line #map, diff --git a/modules/behavior/draw_way.js b/modules/behavior/draw_way.js index 68e56c182..18e270c7e 100644 --- a/modules/behavior/draw_way.js +++ b/modules/behavior/draw_way.js @@ -41,7 +41,7 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { function move(datum) { var loc; var target = datum && datum.id && context.hasEntity(datum.id); - if (target && target.type === 'node') { // snap to node + if (datum.loc) { // snap to node/vertex - a real entity or a nope target with a `loc` loc = datum.loc; } else if (target && target.type === 'way') { // snap to way var choice = geoChooseEdge( diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index e76180f47..3759a1c3d 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -144,9 +144,9 @@ export function modeDragNode(context) { var d = datum(); var target = d && d.id && context.hasEntity(d.id); - if (target && target.type === 'node') { - loc = target.loc; - } else if (target && target.type === 'way') { + if (d.loc) { // snap to node/vertex - a real entity or a nope target with a `loc` + loc = d.loc; + } else if (target && target.type === 'way') { // snap to way var choice = geoChooseEdge( context.childNodes(target), context.mouse(), context.projection, entity.id ); From fa7a6ebb128801966cd4149991e702019ad34390 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 27 Dec 2017 22:51:36 -0500 Subject: [PATCH 60/70] Snap to nope line targets too, also remove svgBlocker, not needed (it's easier to just class the surface, and won't interfere with snapping) --- css/20_map.css | 3 +-- css/55_cursors.css | 4 +++ modules/modes/drag_node.js | 53 +++++++++++++++++++++----------------- modules/svg/blocker.js | 26 ------------------- modules/svg/helpers.js | 44 +++++++++++++++++++++---------- modules/svg/index.js | 1 - 6 files changed, 65 insertions(+), 66 deletions(-) delete mode 100644 modules/svg/blocker.js diff --git a/css/20_map.css b/css/20_map.css index 3b7225dd2..0c0905799 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -16,7 +16,6 @@ /* `.target` objects are interactive */ /* They can be picked up, clicked, hovered, or things can connect to them */ -.layer-blocker.target, .node.target { pointer-events: fill; fill-opacity: 0.8; @@ -35,12 +34,12 @@ } /* `.target-nope` objects are explicitly forbidden to join to */ -.layer-blocker.target.target-nope, .node.target.target-nope, .way.target.target-nope { cursor: not-allowed; } + /* `.active` objects (currently being drawn or dragged) are not interactive */ /* This is important to allow the events to drop through to whatever is */ /* below them on the map, so you can still hover and connect to other things. */ diff --git a/css/55_cursors.css b/css/55_cursors.css index 9e72d7ddf..0e4732b58 100644 --- a/css/55_cursors.css +++ b/css/55_cursors.css @@ -1,5 +1,9 @@ /* Cursors */ +.nope { + cursor: not-allowed !important; +} + .map-in-map, #map { cursor: auto; /* Opera */ diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index 3759a1c3d..d165949d5 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -28,7 +28,6 @@ import { import { modeBrowse, modeSelect } from './index'; import { osmNode } from '../osm'; -import { svgBlocker } from '../svg'; import { uiFlash } from '../ui'; @@ -40,7 +39,6 @@ export function modeDragNode(context) { var hover = behaviorHover(context).altDisables(true) .on('hover', context.ui().sidebar.hover); var edit = behaviorEdit(context); - var blocker = svgBlocker(context.projection, context); var _nudgeInterval; var _restoreSelectedIDs = []; @@ -135,6 +133,7 @@ export function modeDragNode(context) { var currPoint = (d3_event && d3_event.point) || context.projection(_lastLoc); var currMouse = geoVecSubtract(currPoint, nudge); var loc = context.projection.invert(currMouse); + var didSnap = false; if (!_nudgeInterval) { // If not nudging at the edge of the viewport, try to snap.. // related code @@ -142,16 +141,22 @@ export function modeDragNode(context) { // - `behavior/draw.js` `click()` // - `behavior/draw_way.js` `move()` var d = datum(); - var target = d && d.id && context.hasEntity(d.id); + var nodegroups = d && d.properties && d.properties.nodes; if (d.loc) { // snap to node/vertex - a real entity or a nope target with a `loc` loc = d.loc; - } else if (target && target.type === 'way') { // snap to way - var choice = geoChooseEdge( - context.childNodes(target), context.mouse(), context.projection, entity.id - ); - if (choice) { - loc = choice.loc; + didSnap = true; + + } else if (nodegroups) { // snap to way - a line touch target or nope target with nodes + var best = Infinity; + for (var i = 0; i < nodegroups.length; i++) { + var childNodes = nodegroups[i].map(function(id) { return context.entity(id); }); + var choice = geoChooseEdge(childNodes, context.mouse(), context.projection, entity.id); + if (choice && choice.distance < best) { + best = choice.distance; + loc = choice.loc; + didSnap = true; + } } } } @@ -162,17 +167,23 @@ export function modeDragNode(context) { ); - checkGeometry(entity); + // check if this movement causes the geometry to break + var doBlock = false; + if (!didSnap) { + doBlock = invalidGeometry(entity, context.graph()); + } + + context.surface() + .classed('nope', doBlock); + _lastLoc = loc; } - function checkGeometry(entity) { - var doBlock = false; - var graph = context.graph(); + function invalidGeometry(entity, graph) { var parents = graph.parentWays(entity); - function checkSelfIntersections(way, activeID) { + function hasSelfIntersections(way, activeID) { // check active (dragged) segments against inactive segments var actives = []; var inactives = []; @@ -206,15 +217,13 @@ export function modeDragNode(context) { for (var i = 0; i < parents.length; i++) { var parent = parents[i]; if (parent.isClosed()) { // check for self intersections - if (checkSelfIntersections(parent, entity.id)) { - doBlock = true; - break; + if (hasSelfIntersections(parent, entity.id)) { + return true; } } } - d3_select('.data-layer-osm') - .call(doBlock ? blocker : blocker.off); + return false; } @@ -238,7 +247,7 @@ export function modeDragNode(context) { if (_isCancelled) return; var d = datum(); - var nope = d && d.id && /-nope$/.test(d.id); // can't drag here + var nope = (d && d.id && /-nope$/.test(d.id)) || context.surface().classed('nope'); var target = d && d.id && context.hasEntity(d.id); // entity to snap to if (nope) { // bounce back @@ -337,10 +346,8 @@ export function modeDragNode(context) { _activeEntity = null; - d3_select('.data-layer-osm') - .call(blocker.off); - context.surface() + .classed('nope', false) .selectAll('.active') .classed('active', false); diff --git a/modules/svg/blocker.js b/modules/svg/blocker.js deleted file mode 100644 index 93a3a5f1c..000000000 --- a/modules/svg/blocker.js +++ /dev/null @@ -1,26 +0,0 @@ -export function svgBlocker(projection, context) { - - function blocker(selection) { - var dimensions = projection.clipExtent()[1]; - var fillClass = context.getDebug('target') ? 'red ' : 'nocolor '; - - var blocker = selection.selectAll('.layer-blocker') - .data([{id: 'target-nope'}]); - - blocker.enter() - .append('rect') - .attr('class', 'layer-blocker target target-nope ' + fillClass) - .attr('x', 0) - .attr('y', 0) - .merge(blocker) - .attr('width', dimensions[0]) - .attr('height', dimensions[1]); - } - - blocker.off = function(selection) { - selection.selectAll('.layer-blocker') - .remove(); - }; - - return blocker; -} diff --git a/modules/svg/helpers.js b/modules/svg/helpers.js index bbd22f11d..b4d00264e 100644 --- a/modules/svg/helpers.js +++ b/modules/svg/helpers.js @@ -180,14 +180,17 @@ export function svgRelationMemberTags(graph) { export function svgSegmentWay(way, graph, activeID) { var features = { passive: [], active: [] }; var coordGroups = { passive: [], active: [] }; - var segment = []; + var nodeGroups = { passive: [], active: [] }; + var coords = []; + var nodes = []; var startType = null; // 0 = active, 1 = passive, 2 = adjacent var currType = null; var node; for (var i = 0; i < way.nodes.length; i++) { if (way.nodes[i] === activeID) { // vertex is the activeID - segment = []; // draw no segment here + coords = []; // draw no segment here + nodes = []; startType = null; continue; } @@ -201,32 +204,41 @@ export function svgSegmentWay(way, graph, activeID) { if (currType !== startType) { // line changes here - try to save a segment - if (segment.length > 0) { // finish previous segment - segment.push(node.loc); + if (coords.length > 0) { // finish previous segment + coords.push(node.loc); + nodes.push(node.id); if (startType === 2 || currType === 2) { // one adjacent vertex - coordGroups.active.push(segment); + coordGroups.active.push(coords); + nodeGroups.active.push(nodes); } else if (startType === 0 && currType === 0) { // both active vertices - coordGroups.active.push(segment); + coordGroups.active.push(coords); + nodeGroups.active.push(nodes); } else { - coordGroups.passive.push(segment); + coordGroups.passive.push(coords); + nodeGroups.passive.push(nodes); } } - segment = []; + coords = []; + nodes = []; startType = currType; } - segment.push(node.loc); + coords.push(node.loc); + nodes.push(node.id); } // complete whatever segment we ended on - if (segment.length > 1) { + if (coords.length > 1) { if (startType === 2 || currType === 2) { // one adjacent vertex - coordGroups.active.push(segment); + coordGroups.active.push(coords); + nodeGroups.active.push(nodes); } else if (startType === 0 && currType === 0) { // both active vertices - coordGroups.active.push(segment); + coordGroups.active.push(coords); + nodeGroups.active.push(nodes); } else { - coordGroups.passive.push(segment); + coordGroups.passive.push(coords); + nodeGroups.passive.push(nodes); } } @@ -234,6 +246,9 @@ export function svgSegmentWay(way, graph, activeID) { features.passive.push({ 'type': 'Feature', 'id': way.id, + 'properties': { + 'nodes': nodeGroups.passive + }, 'geometry': { 'type': 'MultiLineString', 'coordinates': coordGroups.passive @@ -246,7 +261,8 @@ export function svgSegmentWay(way, graph, activeID) { 'type': 'Feature', 'id': way.id + '-nope', // break the ids on purpose 'properties': { - 'originalID': way.id + 'originalID': way.id, + 'nodes': nodeGroups.active }, 'geometry': { 'type': 'MultiLineString', diff --git a/modules/svg/index.js b/modules/svg/index.js index 798893dbb..68e2f2f28 100644 --- a/modules/svg/index.js +++ b/modules/svg/index.js @@ -1,5 +1,4 @@ export { svgAreas } from './areas.js'; -export { svgBlocker } from './blocker.js'; export { svgDebug } from './debug.js'; export { svgDefs } from './defs.js'; export { svgGpx } from './gpx.js'; From 96afbbd7852de0c584b744774ce7c4b539d47ccc Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 28 Dec 2017 01:08:11 -0500 Subject: [PATCH 61/70] Refactor vector math functions from geo.js to vector.js --- modules/actions/circularize.js | 20 +++--- modules/actions/move.js | 4 +- modules/actions/move_node.js | 4 +- modules/actions/orthogonalize.js | 11 ++- modules/actions/reflect.js | 10 +-- modules/actions/straighten.js | 8 +-- modules/behavior/draw.js | 6 +- modules/behavior/select.js | 4 +- modules/geo/geo.js | 89 +++++------------------- modules/geo/index.js | 19 ++--- modules/geo/vector.js | 62 +++++++++++++++++ modules/index.js | 4 ++ modules/modes/drag_node.js | 6 +- modules/modes/rotate.js | 4 +- modules/osm/way.js | 4 +- modules/renderer/tile_layer.js | 4 +- modules/svg/helpers.js | 4 +- modules/svg/labels.js | 12 ++-- modules/svg/midpoints.js | 14 ++-- test/index.html | 1 + test/spec/actions/circularize.js | 8 +-- test/spec/geo/geo.js | 103 --------------------------- test/spec/geo/vector.js | 115 +++++++++++++++++++++++++++++++ 23 files changed, 268 insertions(+), 248 deletions(-) create mode 100644 modules/geo/vector.js create mode 100644 test/spec/geo/vector.js diff --git a/modules/actions/circularize.js b/modules/actions/circularize.js index dc66825ac..908edb349 100644 --- a/modules/actions/circularize.js +++ b/modules/actions/circularize.js @@ -10,11 +10,7 @@ import { polygonCentroid as d3_polygonCentroid } from 'd3-polygon'; -import { - geoEuclideanDistance, - geoInterp -} from '../geo'; - +import { geoVecInterp, geoVecLength } from '../geo'; import { osmNode } from '../osm'; @@ -41,8 +37,8 @@ export function actionCircularize(wayId, projection, maxAngle) { keyNodes = nodes.filter(function(n) { return graph.parentWays(n).length !== 1; }), points = nodes.map(function(n) { return projection(n.loc); }), keyPoints = keyNodes.map(function(n) { return projection(n.loc); }), - centroid = (points.length === 2) ? geoInterp(points[0], points[1], 0.5) : d3_polygonCentroid(points), - radius = d3_median(points, function(p) { return geoEuclideanDistance(centroid, p); }), + centroid = (points.length === 2) ? geoVecInterp(points[0], points[1], 0.5) : d3_polygonCentroid(points), + radius = d3_median(points, function(p) { return geoVecLength(centroid, p); }), sign = d3_polygonArea(points) > 0 ? 1 : -1, ids; @@ -82,7 +78,7 @@ export function actionCircularize(wayId, projection, maxAngle) { } // position this key node - var distance = geoEuclideanDistance(centroid, keyPoints[i]); + var distance = geoVecLength(centroid, keyPoints[i]); if (distance === 0) { distance = 1e-4; } keyPoints[i] = [ centroid[0] + (keyPoints[i][0] - centroid[0]) / distance * radius, @@ -91,7 +87,7 @@ export function actionCircularize(wayId, projection, maxAngle) { loc = projection.invert(keyPoints[i]); node = keyNodes[i]; origNode = origNodes[node.id]; - node = node.move(geoInterp(origNode.loc, loc, t)); + node = node.move(geoVecInterp(origNode.loc, loc, t)); graph = graph.replace(node); // figure out the between delta angle we want to match to @@ -122,7 +118,7 @@ export function actionCircularize(wayId, projection, maxAngle) { origNode = origNodes[node.id]; nearNodes[node.id] = angle; - node = node.move(geoInterp(origNode.loc, loc, t)); + node = node.move(geoVecInterp(origNode.loc, loc, t)); graph = graph.replace(node); } @@ -145,7 +141,7 @@ export function actionCircularize(wayId, projection, maxAngle) { } } - node = osmNode({ loc: geoInterp(origNode.loc, loc, t) }); + node = osmNode({ loc: geoVecInterp(origNode.loc, loc, t) }); graph = graph.replace(node); nodes.splice(endNodeIndex + j, 0, node); @@ -220,7 +216,7 @@ export function actionCircularize(wayId, projection, maxAngle) { // move interior nodes to the surface of the convex hull.. for (var j = 1; j < indexRange; j++) { - var point = geoInterp(hull[i], hull[i+1], j / indexRange), + var point = geoVecInterp(hull[i], hull[i+1], j / indexRange), node = nodes[(j + startIndex) % nodes.length].move(projection.invert(point)); graph = graph.replace(node); } diff --git a/modules/actions/move.js b/modules/actions/move.js index baa75fb32..1f400085c 100644 --- a/modules/actions/move.js +++ b/modules/actions/move.js @@ -13,11 +13,11 @@ import { osmNode } from '../osm'; import { geoAngle, geoChooseEdge, - geoInterp, geoPathIntersections, geoPathLength, geoSphericalDistance, geoVecAdd, + geoVecInterp, geoVecSubtract } from '../geo'; @@ -206,7 +206,7 @@ export function actionMove(moveIds, tryDelta, projection, cache) { if (!isEP1 && !isEP2) { var epsilon = 1e-4, maxIter = 10; for (var i = 0; i < maxIter; i++) { - loc = geoInterp(edge1.loc, edge2.loc, 0.5); + loc = geoVecInterp(edge1.loc, edge2.loc, 0.5); edge1 = geoChooseEdge(nodes1, projection(loc), projection); edge2 = geoChooseEdge(nodes2, projection(loc), projection); if (Math.abs(edge1.distance - edge2.distance) < epsilon) break; diff --git a/modules/actions/move_node.js b/modules/actions/move_node.js index 8de29a16a..4288dcbc5 100644 --- a/modules/actions/move_node.js +++ b/modules/actions/move_node.js @@ -1,4 +1,4 @@ -import { geoInterp } from '../geo'; +import { geoVecInterp } from '../geo'; export function actionMoveNode(nodeID, toLoc) { @@ -8,7 +8,7 @@ export function actionMoveNode(nodeID, toLoc) { var node = graph.entity(nodeID); return graph.replace( - node.move(geoInterp(node.loc, toLoc, t)) + node.move(geoVecInterp(node.loc, toLoc, t)) ); }; diff --git a/modules/actions/orthogonalize.js b/modules/actions/orthogonalize.js index 33b4a4c61..b9d64b205 100644 --- a/modules/actions/orthogonalize.js +++ b/modules/actions/orthogonalize.js @@ -2,10 +2,7 @@ import _clone from 'lodash-es/clone'; import _uniq from 'lodash-es/uniq'; import { actionDeleteNode } from './delete_node'; -import { - geoEuclideanDistance, - geoInterp -} from '../geo'; +import { geoVecInterp, geoVecLength } from '../geo'; /* @@ -40,7 +37,7 @@ export function actionOrthogonalize(wayId, projection) { node = graph.entity(nodes[corner.i].id); loc = projection.invert(points[corner.i]); - graph = graph.replace(node.move(geoInterp(node.loc, loc, t))); + graph = graph.replace(node.move(geoVecInterp(node.loc, loc, t))); } else { var best, @@ -69,7 +66,7 @@ export function actionOrthogonalize(wayId, projection) { if (originalPoints[i][0] !== points[i][0] || originalPoints[i][1] !== points[i][1]) { loc = projection.invert(points[i]); node = graph.entity(nodes[i].id); - graph = graph.replace(node.move(geoInterp(node.loc, loc, t))); + graph = graph.replace(node.move(geoVecInterp(node.loc, loc, t))); } } @@ -100,7 +97,7 @@ export function actionOrthogonalize(wayId, projection) { q = subtractPoints(c, b), scale, dotp; - scale = 2 * Math.min(geoEuclideanDistance(p, [0, 0]), geoEuclideanDistance(q, [0, 0])); + scale = 2 * Math.min(geoVecLength(p, [0, 0]), geoVecLength(q, [0, 0])); p = normalizePoint(p, 1.0); q = normalizePoint(q, 1.0); diff --git a/modules/actions/reflect.js b/modules/actions/reflect.js index b27c9eadf..be435c5f9 100644 --- a/modules/actions/reflect.js +++ b/modules/actions/reflect.js @@ -4,10 +4,10 @@ import { } from 'd3-polygon'; import { - geoEuclideanDistance, geoExtent, - geoInterp, - geoRotate + geoRotate, + geoVecInterp, + geoVecLength } from '../geo'; import { utilGetAllNodes } from '../util'; @@ -69,7 +69,7 @@ export function actionReflect(reflectIds, projection) { q2 = [(ssr.poly[1][0] + ssr.poly[2][0]) / 2, (ssr.poly[1][1] + ssr.poly[2][1]) / 2 ], p, q; - var isLong = (geoEuclideanDistance(p1, q1) > geoEuclideanDistance(p2, q2)); + var isLong = (geoVecLength(p1, q1) > geoVecLength(p2, q2)); if ((useLongAxis && isLong) || (!useLongAxis && !isLong)) { p = p1; q = q1; @@ -92,7 +92,7 @@ export function actionReflect(reflectIds, projection) { b * (c[0] - p[0]) - a * (c[1] - p[1]) + p[1] ]; var loc2 = projection.invert(c2); - node = node.move(geoInterp(node.loc, loc2, t)); + node = node.move(geoVecInterp(node.loc, loc2, t)); graph = graph.replace(node); } diff --git a/modules/actions/straighten.js b/modules/actions/straighten.js index e16bdd613..e9456d220 100644 --- a/modules/actions/straighten.js +++ b/modules/actions/straighten.js @@ -1,8 +1,8 @@ import { actionDeleteNode } from './delete_node'; import { - geoEuclideanDistance, - geoInterp + geoVecInterp, + geoVecLength } from '../geo'; @@ -44,7 +44,7 @@ export function actionStraighten(wayId, projection) { ], loc2 = projection.invert(p); - graph = graph.replace(node.move(geoInterp(node.loc, loc2, t))); + graph = graph.replace(node.move(geoVecInterp(node.loc, loc2, t))); } else { // safe to delete @@ -69,7 +69,7 @@ export function actionStraighten(wayId, projection) { points = nodes.map(function(n) { return projection(n.loc); }), startPoint = points[0], endPoint = points[points.length-1], - threshold = 0.2 * geoEuclideanDistance(startPoint, endPoint), + threshold = 0.2 * geoVecLength(startPoint, endPoint), i; if (threshold === 0) { diff --git a/modules/behavior/draw.js b/modules/behavior/draw.js index 513c0b30c..77315e03b 100644 --- a/modules/behavior/draw.js +++ b/modules/behavior/draw.js @@ -14,7 +14,7 @@ import { behaviorTail } from './tail'; import { geoChooseEdge, - geoEuclideanDistance, + geoVecLength, geoViewportEdge } from '../geo'; @@ -81,7 +81,7 @@ export function behaviorDraw(context) { d3_select(window).on('mouseup.draw', function() { var t2 = +new Date(); var p2 = point(); - var dist = geoEuclideanDistance(p1, p2); + var dist = geoVecLength(p1, p2); element.on('mousemove.draw', mousemove); d3_select(window).on('mouseup.draw', null); @@ -157,7 +157,7 @@ export function behaviorDraw(context) { var currSpace = context.mouse(); if (_disableSpace && _lastSpace) { - var dist = geoEuclideanDistance(_lastSpace, currSpace); + var dist = geoVecLength(_lastSpace, currSpace); if (dist > tolerance) { _disableSpace = false; } diff --git a/modules/behavior/select.js b/modules/behavior/select.js index a77044133..98799e625 100644 --- a/modules/behavior/select.js +++ b/modules/behavior/select.js @@ -6,7 +6,7 @@ import { select as d3_select } from 'd3-selection'; -import { geoEuclideanDistance } from '../geo'; +import { geoVecLength } from '../geo'; import { modeBrowse, @@ -103,7 +103,7 @@ export function behaviorSelect(context) { if (!p1) return; var p2 = point(); - var dist = geoEuclideanDistance(p1, p2); + var dist = geoVecLength(p1, p2); p1 = null; if (dist > tolerance) { diff --git a/modules/geo/geo.js b/modules/geo/geo.js index 7a4fe121a..0c5a27778 100644 --- a/modules/geo/geo.js +++ b/modules/geo/geo.js @@ -1,6 +1,15 @@ import _every from 'lodash-es/every'; import _some from 'lodash-es/some'; +import { + geoVecAngle, + geoVecCross, + geoVecDot, + geoVecInterp, + geoVecLength, + geoVecSubtract +} from './vector.js'; + // constants var TAU = 2 * Math.PI; @@ -8,66 +17,6 @@ var EQUATORIAL_RADIUS = 6356752.314245179; var POLAR_RADIUS = 6378137.0; -// vector addition -export function geoVecEquals(a, b) { - return (a[0] === b[0]) && (a[1] === b[1]); -} - -// vector addition -export function geoVecAdd(a, b) { - return [ a[0] + b[0], a[1] + b[1] ]; -} - -// vector subtraction -export function geoVecSubtract(a, b) { - return [ a[0] - b[0], a[1] - b[1] ]; -} - -// vector multiplication -export function geoVecScale(a, b) { - return [ a[0] * b, a[1] * b ]; -} - -// vector rounding (was: geoRoundCoordinates) -export function geoVecFloor(a) { - return [ Math.floor(a[0]), Math.floor(a[1]) ]; -} - -// linear interpolation -export function geoInterp(p1, p2, t) { - return [ - p1[0] + (p2[0] - p1[0]) * t, - p1[1] + (p2[1] - p1[1]) * t - ]; -} - - -// dot product -export function geoDot(a, b, origin) { - origin = origin || [0, 0]; - return (a[0] - origin[0]) * (b[0] - origin[0]) + - (a[1] - origin[1]) * (b[1] - origin[1]); -} - - -// 2D cross product of OA and OB vectors, returns magnitude of Z vector -// Returns a positive value, if OAB makes a counter-clockwise turn, -// negative for clockwise turn, and zero if the points are collinear. -export function geoCross(a, b, origin) { - origin = origin || [0, 0]; - return (a[0] - origin[0]) * (b[1] - origin[1]) - - (a[1] - origin[1]) * (b[0] - origin[0]); -} - - -// http://jsperf.com/id-dist-optimization -export function geoEuclideanDistance(a, b) { - var x = a[0] - b[0]; - var y = a[1] - b[1]; - return Math.sqrt((x * x) + (y * y)); -} - - export function geoLatToMeters(dLat) { return dLat * (TAU * POLAR_RADIUS / 360); } @@ -140,9 +89,7 @@ export function geoEdgeEqual(a, b) { // Return the counterclockwise angle in the range (-pi, pi) // between the positive X axis and the line intersecting a and b. export function geoAngle(a, b, projection) { - a = projection(a.loc); - b = projection(b.loc); - return Math.atan2(b[1] - a[1], b[0] - a[0]); + return geoVecAngle(projection(a.loc), projection(b.loc)); } @@ -163,7 +110,7 @@ export function geoRotate(points, angle, around) { // the closest vertex on that edge. Returns an object with the `index` of the // chosen edge, the chosen `loc` on that edge, and the `distance` to to it. export function geoChooseEdge(nodes, point, projection, skipID) { - var dist = geoEuclideanDistance; + var dist = geoVecLength; var points = nodes.map(function(n) { return projection(n.loc); }); var ids = nodes.map(function(n) { return n.id; }); var min = Infinity; @@ -176,7 +123,7 @@ export function geoChooseEdge(nodes, point, projection, skipID) { var o = points[i]; var s = geoVecSubtract(points[i + 1], o); var v = geoVecSubtract(point, o); - var proj = geoDot(v, s) / geoDot(s, s); + var proj = geoVecDot(v, s) / geoVecDot(s, s); var p; if (proj < 0) { @@ -214,15 +161,15 @@ export function geoLineIntersection(a, b) { var q2 = [b[1][0], b[1][1]]; var r = geoVecSubtract(p2, p); var s = geoVecSubtract(q2, q); - var uNumerator = geoCross(geoVecSubtract(q, p), r); - var denominator = geoCross(r, s); + var uNumerator = geoVecCross(geoVecSubtract(q, p), r); + var denominator = geoVecCross(r, s); if (uNumerator && denominator) { var u = uNumerator / denominator; - var t = geoCross(geoVecSubtract(q, p), s) / denominator; + var t = geoVecCross(geoVecSubtract(q, p), s) / denominator; if ((t >= 0) && (t <= 1) && (u >= 0) && (u <= 1)) { - return geoInterp(p, p2, t); + return geoVecInterp(p, p2, t); } } @@ -284,7 +231,7 @@ export function geoPolygonIntersectsPolygon(outer, inner, checkSegments) { function testSegments(outer, inner) { for (var i = 0; i < outer.length - 1; i++) { for (var j = 0; j < inner.length - 1; j++) { - var a = [ outer[i], outer[i +1 ] ]; + var a = [ outer[i], outer[i + 1] ]; var b = [ inner[j], inner[j + 1] ]; if (geoLineIntersection(a, b)) return true; } @@ -305,7 +252,7 @@ export function geoPolygonIntersectsPolygon(outer, inner, checkSegments) { export function geoPathLength(path) { var length = 0; for (var i = 0; i < path.length - 1; i++) { - length += geoEuclideanDistance(path[i], path[i + 1]); + length += geoVecLength(path[i], path[i + 1]); } return length; } diff --git a/modules/geo/index.js b/modules/geo/index.js index 95bba82c5..dca2f31dd 100644 --- a/modules/geo/index.js +++ b/modules/geo/index.js @@ -1,11 +1,7 @@ export { geoAngle } from './geo.js'; export { geoChooseEdge } from './geo.js'; -export { geoCross } from './geo.js'; -export { geoDot } from './geo.js'; export { geoEdgeEqual } from './geo.js'; -export { geoEuclideanDistance } from './geo.js'; export { geoExtent } from './extent.js'; -export { geoInterp } from './geo.js'; export { geoRawMercator } from './raw_mercator.js'; export { geoRotate } from './geo.js'; export { geoLatToMeters } from './geo.js'; @@ -22,10 +18,15 @@ export { geoPolygonContainsPolygon } from './geo.js'; export { geoPolygonIntersectsPolygon } from './geo.js'; export { geoScaleToZoom } from './geo.js'; export { geoSphericalDistance } from './geo.js'; -export { geoVecAdd } from './geo.js'; -export { geoVecEquals } from './geo.js'; -export { geoVecFloor } from './geo.js'; -export { geoVecSubtract } from './geo.js'; -export { geoVecScale } from './geo.js'; +export { geoVecAdd } from './vector.js'; +export { geoVecAngle } from './vector.js'; +export { geoVecCross } from './vector.js'; +export { geoVecDot } from './vector.js'; +export { geoVecEqual } from './vector.js'; +export { geoVecFloor } from './vector.js'; +export { geoVecInterp } from './vector.js'; +export { geoVecLength } from './vector.js'; +export { geoVecSubtract } from './vector.js'; +export { geoVecScale } from './vector.js'; export { geoZoomToScale } from './geo.js'; export { geoViewportEdge } from './geo.js'; diff --git a/modules/geo/vector.js b/modules/geo/vector.js new file mode 100644 index 000000000..0e7929b3b --- /dev/null +++ b/modules/geo/vector.js @@ -0,0 +1,62 @@ +// vector equals +export function geoVecEqual(a, b) { + return (a[0] === b[0]) && (a[1] === b[1]); +} + +// vector addition +export function geoVecAdd(a, b) { + return [ a[0] + b[0], a[1] + b[1] ]; +} + +// vector subtraction +export function geoVecSubtract(a, b) { + return [ a[0] - b[0], a[1] - b[1] ]; +} + +// vector multiplication +export function geoVecScale(a, b) { + return [ a[0] * b, a[1] * b ]; +} + +// vector rounding (was: geoRoundCoordinates) +export function geoVecFloor(a) { + return [ Math.floor(a[0]), Math.floor(a[1]) ]; +} + +// linear interpolation +export function geoVecInterp(a, b, t) { + return [ + a[0] + (b[0] - a[0]) * t, + a[1] + (b[1] - a[1]) * t + ]; +} + +// http://jsperf.com/id-dist-optimization +export function geoVecLength(a, b) { + var x = a[0] - b[0]; + var y = a[1] - b[1]; + return Math.sqrt((x * x) + (y * y)); +} + +// 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) { + return Math.atan2(b[1] - a[1], b[0] - a[0]); +} + +// 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]); +} + +// 2D cross product of OA and OB vectors, returns magnitude of Z vector +// Returns a positive value, if OAB makes a counter-clockwise turn, +// 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]); +} + diff --git a/modules/index.js b/modules/index.js index 981a047db..dcd7f276e 100644 --- a/modules/index.js +++ b/modules/index.js @@ -29,6 +29,10 @@ export { coreDifference as Difference } from './core/difference'; export { coreGraph as Graph } from './core/graph'; export { coreHistory as History } from './core/history'; export { coreTree as Tree } from './core/tree'; +export { geoVecCross as geoCross } from './geo/vector'; +export { geoVecInterp as geoInterp } from './geo/vector'; +export { geoVecFloor as geoRoundCoordinates } from './geo/vector'; +export { geoVecLength as geoEuclideanDistance } from './geo/vector'; export { osmEntity as Entity } from './osm/entity'; export { osmNode as Node } from './osm/node'; export { osmRelation as Relation } from './osm/relation'; diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index d165949d5..3809a352f 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -21,7 +21,7 @@ import { import { geoChooseEdge, geoLineIntersection, - geoVecEquals, + geoVecEqual, geoVecSubtract, geoViewportEdge } from '../geo'; @@ -203,8 +203,8 @@ export function modeDragNode(context) { var p = actives[j]; var q = inactives[k]; // skip if segments share an endpoint - if (geoVecEquals(p[1], q[0]) || geoVecEquals(p[0], q[1]) || - geoVecEquals(p[0], q[0]) || geoVecEquals(p[1], q[1]) ) { + if (geoVecEqual(p[1], q[0]) || geoVecEqual(p[0], q[1]) || + geoVecEqual(p[0], q[0]) || geoVecEqual(p[1], q[1]) ) { continue; } else if (geoLineIntersection(p, q)) { return true; diff --git a/modules/modes/rotate.js b/modules/modes/rotate.js index d889a174b..4addaed88 100644 --- a/modules/modes/rotate.js +++ b/modules/modes/rotate.js @@ -13,7 +13,7 @@ import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js'; import { t } from '../util/locale'; import { actionRotate } from '../actions'; import { behaviorEdit } from '../behavior'; -import { geoInterp } from '../geo'; +import { geoVecInterp } from '../geo'; import { modeBrowse, @@ -80,7 +80,7 @@ export function modeRotate(context, entityIDs) { if (points.length === 1) { // degenerate case _pivot = points[0]; } else if (points.length === 2) { - _pivot = geoInterp(points[0], points[1], 0.5); + _pivot = geoVecInterp(points[0], points[1], 0.5); } else { _pivot = d3_polygonCentroid(d3_polygonHull(points)); } diff --git a/modules/osm/way.js b/modules/osm/way.js index c93224146..14e4e410c 100644 --- a/modules/osm/way.js +++ b/modules/osm/way.js @@ -4,7 +4,7 @@ import _uniq from 'lodash-es/uniq'; import { geoArea as d3_geoArea } from 'd3-geo'; -import { geoExtent, geoCross } from '../geo'; +import { geoExtent, geoVecCross } from '../geo'; import { osmEntity } from './entity'; import { osmLanes } from './lanes'; import { osmOneWayTags } from './tags'; @@ -142,7 +142,7 @@ _extend(osmWay.prototype, { var o = coords[(i+1) % coords.length]; var a = coords[i]; var b = coords[(i+2) % coords.length]; - var res = geoCross(a, b, o); + var res = geoVecCross(a, b, o); curr = (res > 0) ? 1 : (res < 0) ? -1 : 0; if (curr === 0) { diff --git a/modules/renderer/tile_layer.js b/modules/renderer/tile_layer.js index 9d143f045..b0fb42aa3 100644 --- a/modules/renderer/tile_layer.js +++ b/modules/renderer/tile_layer.js @@ -2,7 +2,7 @@ import { select as d3_select } from 'd3-selection'; import { t } from '../util/locale'; import { d3geoTile as d3_geoTile } from '../lib/d3.geo.tile'; -import { geoEuclideanDistance, geoScaleToZoom } from '../geo'; +import { geoScaleToZoom, geoVecLength } from '../geo'; import { utilPrefixCSSProperty } from '../util'; @@ -187,7 +187,7 @@ export function rendererTileLayer(context) { requests.forEach(function(d) { var c = tileCenter(d); - var dist = geoEuclideanDistance(c, mapCenter); + var dist = geoVecLength(c, mapCenter); if (dist < minDist) { minDist = dist; nearCenter = d; diff --git a/modules/svg/helpers.js b/modules/svg/helpers.js index b4d00264e..40f28d0a6 100644 --- a/modules/svg/helpers.js +++ b/modules/svg/helpers.js @@ -6,7 +6,7 @@ import { geoStream as d3_geoStream } from 'd3-geo'; -import { geoEuclideanDistance } from '../geo'; +import { geoVecLength } from '../geo'; // Touch targets control which other vertices we can drag a vertex onto. @@ -82,7 +82,7 @@ export function svgOneWaySegments(projection, graph, dt) { b = [x, y]; if (a) { - var span = geoEuclideanDistance(a, b) - offset; + var span = geoVecLength(a, b) - offset; if (span >= 0) { var angle = Math.atan2(b[1] - a[1], b[0] - a[0]); diff --git a/modules/svg/labels.js b/modules/svg/labels.js index 15c3d2b08..c79448d75 100644 --- a/modules/svg/labels.js +++ b/modules/svg/labels.js @@ -10,11 +10,11 @@ import { textDirection } from '../util/locale'; import { geoExtent, - geoEuclideanDistance, - geoInterp, geoPolygonIntersectsPolygon, geoPathLength, - geoScaleToZoom + geoScaleToZoom, + geoVecInterp, + geoVecLength } from '../geo'; import { osmEntity } from '../osm'; @@ -489,10 +489,10 @@ export function svgLabels(projection, context) { var b = sub[j + 1]; // split up the text into small collision boxes - var num = Math.max(1, Math.floor(geoEuclideanDistance(a, b) / boxsize / 2)); + var num = Math.max(1, Math.floor(geoVecLength(a, b) / boxsize / 2)); for (var box = 0; box < num; box++) { - var p = geoInterp(a, b, box / num); + var p = geoVecInterp(a, b, box / num); var x0 = p[0] - boxsize - padding; var y0 = p[1] - boxsize - padding; var x1 = p[0] + boxsize + padding; @@ -532,7 +532,7 @@ export function svgLabels(projection, context) { for (var i = 0; i < points.length - 1; i++) { var a = points[i]; var b = points[i + 1]; - var current = geoEuclideanDistance(a, b); + var current = geoVecLength(a, b); var portion; if (!start && sofar + current >= from) { portion = (from - sofar) / current; diff --git a/modules/svg/midpoints.js b/modules/svg/midpoints.js index ed629c6c6..bcf9495b2 100644 --- a/modules/svg/midpoints.js +++ b/modules/svg/midpoints.js @@ -7,9 +7,9 @@ import { import { geoAngle, - geoEuclideanDistance, - geoInterp, - geoLineIntersection + geoLineIntersection, + geoVecInterp, + geoVecLength } from '../geo'; @@ -73,8 +73,8 @@ export function svgMidpoints(projection, context) { if (midpoints[id]) { midpoints[id].parents.push(entity); } else { - if (geoEuclideanDistance(projection(a.loc), projection(b.loc)) > 40) { - var point = geoInterp(a.loc, b.loc, 0.5); + if (geoVecLength(projection(a.loc), projection(b.loc)) > 40) { + var point = geoVecInterp(a.loc, b.loc, 0.5); var loc = null; if (extent.intersects(point)) { @@ -83,8 +83,8 @@ export function svgMidpoints(projection, context) { for (var k = 0; k < 4; k++) { point = geoLineIntersection([a.loc, b.loc], [poly[k], poly[k + 1]]); if (point && - geoEuclideanDistance(projection(a.loc), projection(point)) > 20 && - geoEuclideanDistance(projection(b.loc), projection(point)) > 20) + geoVecLength(projection(a.loc), projection(point)) > 20 && + geoVecLength(projection(b.loc), projection(point)) > 20) { loc = point; break; diff --git a/test/index.html b/test/index.html index 18092851b..ed2cb0c54 100644 --- a/test/index.html +++ b/test/index.html @@ -74,6 +74,7 @@ + diff --git a/test/spec/actions/circularize.js b/test/spec/actions/circularize.js index fb54bbd05..5d7e4c73c 100644 --- a/test/spec/actions/circularize.js +++ b/test/spec/actions/circularize.js @@ -5,7 +5,7 @@ describe('iD.actionCircularize', function () { var points = graph.childNodes(graph.entity(id)) .map(function (n) { return projection(n.loc); }), centroid = d3.polygonCentroid(points), - radius = iD.geoEuclideanDistance(centroid, points[0]), + radius = iD.geoVecLength(centroid, points[0]), estArea = Math.PI * radius * radius, trueArea = Math.abs(d3.polygonArea(points)), pctDiff = (estArea - trueArea) / estArea; @@ -31,10 +31,10 @@ describe('iD.actionCircularize', function () { vector2 = [point2[0] - center[0], point2[1] - center[1]], distance; - distance = iD.geoEuclideanDistance(vector1, [0, 0]); + distance = iD.geoVecLength(vector1, [0, 0]); vector1 = [vector1[0] / distance, vector1[1] / distance]; - distance = iD.geoEuclideanDistance(vector2, [0, 0]); + distance = iD.geoVecLength(vector2, [0, 0]); vector2 = [vector2[0] / distance, vector2[1] / distance]; return 180 / Math.PI * Math.acos(vector1[0] * vector2[0] + vector1[1] * vector2[1]); @@ -106,7 +106,7 @@ describe('iD.actionCircularize', function () { graph = iD.actionCircularize('-', projection)(graph); expect(isCircular('-', graph)).to.be.ok; - expect(iD.geoEuclideanDistance(graph.entity('d').loc, [2, -2])).to.be.lt(0.5); + expect(iD.geoVecLength(graph.entity('d').loc, [2, -2])).to.be.lt(0.5); }); it('creates circle respecting min-angle limit', function() { diff --git a/test/spec/geo/geo.js b/test/spec/geo/geo.js index 585e7a2e7..9f31f1d27 100644 --- a/test/spec/geo/geo.js +++ b/test/spec/geo/geo.js @@ -1,108 +1,5 @@ describe('iD.geo', function() { - describe('geoVecEquals', function() { - it('tests vectors for equality', function() { - expect(iD.geoVecEquals([1, 2], [1, 2])).to.be.true; - expect(iD.geoVecEquals([1, 2], [1, 0])).to.be.false; - expect(iD.geoVecEquals([1, 2], [2, 1])).to.be.false; - }); - }); - - describe('geoVecAdd', function() { - it('adds vectors', function() { - expect(iD.geoVecAdd([1, 2], [3, 4])).to.eql([4, 6]); - expect(iD.geoVecAdd([1, 2], [0, 0])).to.eql([1, 2]); - expect(iD.geoVecAdd([1, 2], [-3, -4])).to.eql([-2, -2]); - }); - }); - - describe('geoVecSubtract', function() { - it('subtracts vectors', function() { - expect(iD.geoVecSubtract([1, 2], [3, 4])).to.eql([-2, -2]); - expect(iD.geoVecSubtract([1, 2], [0, 0])).to.eql([1, 2]); - expect(iD.geoVecSubtract([1, 2], [-3, -4])).to.eql([4, 6]); - }); - }); - - describe('geoVecScale', function() { - it('multiplies vectors', function() { - expect(iD.geoVecScale([1, 2], 0)).to.eql([0, 0]); - expect(iD.geoVecScale([1, 2], 1)).to.eql([1, 2]); - expect(iD.geoVecScale([1, 2], 2)).to.eql([2, 4]); - expect(iD.geoVecScale([1, 2], 0.5)).to.eql([0.5, 1]); - }); - }); - - describe('geoVecFloor (was: geoRoundCoordinates)', function() { - it('rounds vectors', function() { - expect(iD.geoVecFloor([0.1, 1])).to.eql([0, 1]); - expect(iD.geoVecFloor([0, 1])).to.eql([0, 1]); - expect(iD.geoVecFloor([0, 1.1])).to.eql([0, 1]); - }); - }); - - describe('geoInterp', function() { - it('interpolates halfway', function() { - var a = [0, 0]; - var b = [10, 10]; - expect(iD.geoInterp(a, b, 0.5)).to.eql([5, 5]); - }); - it('interpolates to one side', function() { - var a = [0, 0]; - var b = [10, 10]; - expect(iD.geoInterp(a, b, 0)).to.eql([0, 0]); - }); - }); - - describe('geoDot', function() { - it('dot product of right angle is zero', function() { - var a = [1, 0]; - var b = [0, 1]; - expect(iD.geoDot(a, b)).to.eql(0); - }); - it('dot product of same vector multiplies', function() { - var a = [2, 0]; - var b = [2, 0]; - expect(iD.geoDot(a, b)).to.eql(4); - }); - }); - - describe('geoCross', function() { - it('2D cross product of right hand turn is positive', function() { - var a = [2, 0]; - var b = [0, 2]; - expect(iD.geoCross(a, b)).to.eql(4); - }); - it('2D cross product of left hand turn is negative', function() { - var a = [2, 0]; - var b = [0, -2]; - expect(iD.geoCross(a, b)).to.eql(-4); - }); - it('2D cross product of colinear points is zero', function() { - var a = [-2, 0]; - var b = [2, 0]; - expect(iD.geoCross(a, b)).to.equal(0); - }); - }); - - describe('geoEuclideanDistance', function() { - it('distance between two same points is zero', function() { - var a = [0, 0]; - var b = [0, 0]; - expect(iD.geoEuclideanDistance(a, b)).to.eql(0); - }); - it('a straight 10 unit line is 10', function() { - var a = [0, 0]; - var b = [10, 0]; - expect(iD.geoEuclideanDistance(a, b)).to.eql(10); - }); - it('a pythagorean triangle is right', function() { - var a = [0, 0]; - var b = [4, 3]; - expect(iD.geoEuclideanDistance(a, b)).to.eql(5); - }); - }); - describe('geoLatToMeters', function() { it('0 degrees latitude is 0 meters', function() { expect(iD.geoLatToMeters(0)).to.eql(0); diff --git a/test/spec/geo/vector.js b/test/spec/geo/vector.js new file mode 100644 index 000000000..21968bb5a --- /dev/null +++ b/test/spec/geo/vector.js @@ -0,0 +1,115 @@ +describe('iD.geo vector', function() { + + describe('geoVecEqual', function() { + it('tests vectors for equality', function() { + expect(iD.geoVecEqual([1, 2], [1, 2])).to.be.true; + expect(iD.geoVecEqual([1, 2], [1, 0])).to.be.false; + expect(iD.geoVecEqual([1, 2], [2, 1])).to.be.false; + }); + }); + + describe('geoVecAdd', function() { + it('adds vectors', function() { + expect(iD.geoVecAdd([1, 2], [3, 4])).to.eql([4, 6]); + expect(iD.geoVecAdd([1, 2], [0, 0])).to.eql([1, 2]); + expect(iD.geoVecAdd([1, 2], [-3, -4])).to.eql([-2, -2]); + }); + }); + + describe('geoVecSubtract', function() { + it('subtracts vectors', function() { + expect(iD.geoVecSubtract([1, 2], [3, 4])).to.eql([-2, -2]); + expect(iD.geoVecSubtract([1, 2], [0, 0])).to.eql([1, 2]); + expect(iD.geoVecSubtract([1, 2], [-3, -4])).to.eql([4, 6]); + }); + }); + + describe('geoVecScale', function() { + it('multiplies vectors', function() { + expect(iD.geoVecScale([1, 2], 0)).to.eql([0, 0]); + expect(iD.geoVecScale([1, 2], 1)).to.eql([1, 2]); + expect(iD.geoVecScale([1, 2], 2)).to.eql([2, 4]); + expect(iD.geoVecScale([1, 2], 0.5)).to.eql([0.5, 1]); + }); + }); + + describe('geoVecFloor (was: geoRoundCoordinates)', function() { + it('rounds vectors', function() { + expect(iD.geoVecFloor([0.1, 1])).to.eql([0, 1]); + expect(iD.geoVecFloor([0, 1])).to.eql([0, 1]); + expect(iD.geoVecFloor([0, 1.1])).to.eql([0, 1]); + }); + }); + + describe('geoVecInterp', function() { + it('interpolates halfway', function() { + var a = [0, 0]; + var b = [10, 10]; + expect(iD.geoVecInterp(a, b, 0.5)).to.eql([5, 5]); + }); + it('interpolates to one side', function() { + var a = [0, 0]; + var b = [10, 10]; + expect(iD.geoVecInterp(a, b, 0)).to.eql([0, 0]); + }); + }); + + describe('geoVecLength (was: geoEuclideanDistance)', function() { + it('distance between two same points is zero', function() { + var a = [0, 0]; + var b = [0, 0]; + expect(iD.geoVecLength(a, b)).to.eql(0); + }); + it('a straight 10 unit line is 10', function() { + var a = [0, 0]; + var b = [10, 0]; + expect(iD.geoVecLength(a, b)).to.eql(10); + }); + it('a pythagorean triangle is right', function() { + var a = [0, 0]; + var b = [4, 3]; + expect(iD.geoVecLength(a, b)).to.eql(5); + }); + }); + + describe('geoVecAngle', function() { + it('returns angle between a and b', function() { + expect(iD.geoVecAngle([0, 0], [1, 0])).to.be.closeTo(0, 1e-6); + expect(iD.geoVecAngle([0, 0], [0, 1])).to.be.closeTo(Math.PI / 2, 1e-6); + expect(iD.geoVecAngle([0, 0], [-1, 0])).to.be.closeTo(Math.PI, 1e-6); + expect(iD.geoVecAngle([0, 0], [0, -1])).to.be.closeTo(-Math.PI / 2, 1e-6); + }); + }); + + describe('geoVecDot', function() { + it('dot product of right angle is zero', function() { + var a = [1, 0]; + var b = [0, 1]; + expect(iD.geoVecDot(a, b)).to.eql(0); + }); + it('dot product of same vector multiplies', function() { + var a = [2, 0]; + var b = [2, 0]; + expect(iD.geoVecDot(a, b)).to.eql(4); + }); + }); + + describe('geoVecCross', function() { + it('2D cross product of right hand turn is positive', function() { + var a = [2, 0]; + var b = [0, 2]; + expect(iD.geoVecCross(a, b)).to.eql(4); + }); + it('2D cross product of left hand turn is negative', function() { + var a = [2, 0]; + var b = [0, -2]; + expect(iD.geoVecCross(a, b)).to.eql(-4); + }); + it('2D cross product of colinear points is zero', function() { + var a = [-2, 0]; + var b = [2, 0]; + expect(iD.geoVecCross(a, b)).to.equal(0); + }); + }); + +}); From 7af73c10ef70c7f69fd38632bbac06f7ccc7cd81 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 28 Dec 2017 01:28:38 -0500 Subject: [PATCH 62/70] Refactor geometric math functions from geo.js to geom.js --- modules/geo/geo.js | 225 ++---------------------------- modules/geo/geom.js | 213 ++++++++++++++++++++++++++++ modules/geo/index.js | 31 +++-- test/index.html | 1 + test/spec/geo/geo.js | 296 +-------------------------------------- test/spec/geo/geom.js | 297 ++++++++++++++++++++++++++++++++++++++++ test/spec/geo/vector.js | 2 +- 7 files changed, 544 insertions(+), 521 deletions(-) create mode 100644 modules/geo/geom.js create mode 100644 test/spec/geo/geom.js diff --git a/modules/geo/geo.js b/modules/geo/geo.js index 0c5a27778..e0f7aa436 100644 --- a/modules/geo/geo.js +++ b/modules/geo/geo.js @@ -39,15 +39,6 @@ export function geoMetersToLon(m, atLat) { } -export function geoOffsetToMeters(offset, tileSize) { - tileSize = tileSize || 256; - return [ - offset[0] * TAU * EQUATORIAL_RADIUS / tileSize, - -offset[1] * TAU * POLAR_RADIUS / tileSize - ]; -} - - export function geoMetersToOffset(meters, tileSize) { tileSize = tileSize || 256; return [ @@ -57,6 +48,15 @@ export function geoMetersToOffset(meters, tileSize) { } +export function geoOffsetToMeters(offset, tileSize) { + tileSize = tileSize || 256; + return [ + offset[0] * TAU * EQUATORIAL_RADIUS / tileSize, + -offset[1] * TAU * POLAR_RADIUS / tileSize + ]; +} + + // Equirectangular approximation of spherical distances on Earth export function geoSphericalDistance(a, b) { var x = geoLonToMeters(a[0] - b[0], (a[1] + b[1]) / 2); @@ -65,13 +65,6 @@ export function geoSphericalDistance(a, b) { } -// zoom to scale -export function geoZoomToScale(z, tileSize) { - tileSize = tileSize || 256; - return tileSize * Math.pow(2, z) / TAU; -} - - // scale to zoom export function geoScaleToZoom(k, tileSize) { tileSize = tileSize || 256; @@ -80,203 +73,11 @@ export function geoScaleToZoom(k, tileSize) { } -export function geoEdgeEqual(a, b) { - return (a[0] === b[0] && a[1] === b[1]) || - (a[0] === b[1] && a[1] === b[0]); +// zoom to scale +export function geoZoomToScale(z, tileSize) { + tileSize = tileSize || 256; + return tileSize * Math.pow(2, z) / TAU; } -// Return the counterclockwise angle in the range (-pi, pi) -// between the positive X axis and the line intersecting a and b. -export function geoAngle(a, b, projection) { - return geoVecAngle(projection(a.loc), projection(b.loc)); -} - -// Rotate all points counterclockwise around a pivot point by given angle -export function geoRotate(points, angle, around) { - return points.map(function(point) { - var radial = [point[0] - around[0], point[1] - around[1]]; - return [ - radial[0] * Math.cos(angle) - radial[1] * Math.sin(angle) + around[0], - radial[0] * Math.sin(angle) + radial[1] * Math.cos(angle) + around[1] - ]; - }); -} - - -// Choose the edge with the minimal distance from `point` to its orthogonal -// projection onto that edge, if such a projection exists, or the distance to -// the closest vertex on that edge. Returns an object with the `index` of the -// chosen edge, the chosen `loc` on that edge, and the `distance` to to it. -export function geoChooseEdge(nodes, point, projection, skipID) { - var dist = geoVecLength; - var points = nodes.map(function(n) { return projection(n.loc); }); - var ids = nodes.map(function(n) { return n.id; }); - var min = Infinity; - var idx; - var loc; - - for (var i = 0; i < points.length - 1; i++) { - if (ids[i] === skipID || ids[i + 1] === skipID) continue; - - var o = points[i]; - var s = geoVecSubtract(points[i + 1], o); - var v = geoVecSubtract(point, 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 d = dist(p, point); - if (d < min) { - min = d; - idx = i + 1; - loc = projection.invert(p); - } - } - - if (idx !== undefined) { - return { index: idx, distance: min, loc: loc }; - } else { - return null; - } -} - - -// Return the intersection point of 2 line segments. -// From https://github.com/pgkelley4/line-segments-intersect -// This uses the vector cross product approach described below: -// http://stackoverflow.com/a/565282/786339 -export function geoLineIntersection(a, b) { - var p = [a[0][0], a[0][1]]; - var p2 = [a[1][0], a[1][1]]; - var q = [b[0][0], b[0][1]]; - var q2 = [b[1][0], b[1][1]]; - var r = geoVecSubtract(p2, p); - var s = geoVecSubtract(q2, q); - var uNumerator = geoVecCross(geoVecSubtract(q, p), r); - var denominator = geoVecCross(r, s); - - if (uNumerator && denominator) { - var u = uNumerator / denominator; - var t = geoVecCross(geoVecSubtract(q, p), s) / denominator; - - if ((t >= 0) && (t <= 1) && (u >= 0) && (u <= 1)) { - return geoVecInterp(p, p2, t); - } - } - - return null; -} - - -export function geoPathIntersections(path1, path2) { - var intersections = []; - for (var i = 0; i < path1.length - 1; i++) { - for (var j = 0; j < path2.length - 1; j++) { - var a = [ path1[i], path1[i+1] ]; - var b = [ path2[j], path2[j+1] ]; - var hit = geoLineIntersection(a, b); - if (hit) { - intersections.push(hit); - } - } - } - return intersections; -} - - -// Return whether point is contained in polygon. -// -// `point` should be a 2-item array of coordinates. -// `polygon` should be an array of 2-item arrays of coordinates. -// -// From https://github.com/substack/point-in-polygon. -// ray-casting algorithm based on -// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html -// -export function geoPointInPolygon(point, polygon) { - var x = point[0]; - var y = point[1]; - var inside = false; - - for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { - var xi = polygon[i][0], yi = polygon[i][1]; - var xj = polygon[j][0], yj = polygon[j][1]; - - var intersect = ((yi > y) !== (yj > y)) && - (x < (xj - xi) * (y - yi) / (yj - yi) + xi); - if (intersect) inside = !inside; - } - - return inside; -} - - -export function geoPolygonContainsPolygon(outer, inner) { - return _every(inner, function(point) { - return geoPointInPolygon(point, outer); - }); -} - - -export function geoPolygonIntersectsPolygon(outer, inner, checkSegments) { - function testSegments(outer, inner) { - for (var i = 0; i < outer.length - 1; i++) { - for (var j = 0; j < inner.length - 1; j++) { - var a = [ outer[i], outer[i + 1] ]; - var b = [ inner[j], inner[j + 1] ]; - if (geoLineIntersection(a, b)) return true; - } - } - return false; - } - - function testPoints(outer, inner) { - return _some(inner, function(point) { - return geoPointInPolygon(point, outer); - }); - } - - return testPoints(outer, inner) || (!!checkSegments && testSegments(outer, inner)); -} - - -export function geoPathLength(path) { - var length = 0; - for (var i = 0; i < path.length - 1; i++) { - length += geoVecLength(path[i], path[i + 1]); - } - return length; -} - - -// If the given point is at the edge of the padded viewport, -// return a vector that will nudge the viewport in that direction -export function geoViewportEdge(point, dimensions) { - var pad = [80, 20, 50, 20]; // top, right, bottom, left - var x = 0; - var y = 0; - - if (point[0] > dimensions[0] - pad[1]) - x = -10; - if (point[0] < pad[3]) - x = 10; - if (point[1] > dimensions[1] - pad[2]) - y = -10; - if (point[1] < pad[0]) - y = 10; - - if (x || y) { - return [x, y]; - } else { - return null; - } -} diff --git a/modules/geo/geom.js b/modules/geo/geom.js new file mode 100644 index 000000000..fef500eb6 --- /dev/null +++ b/modules/geo/geom.js @@ -0,0 +1,213 @@ +import _every from 'lodash-es/every'; +import _some from 'lodash-es/some'; + +import { + geoVecAngle, + geoVecCross, + geoVecDot, + geoVecInterp, + geoVecLength, + geoVecSubtract +} from './vector.js'; + + +// Return the counterclockwise angle in the range (-pi, pi) +// between the positive X axis and the line intersecting a and b. +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) { + var radial = [point[0] - around[0], point[1] - around[1]]; + return [ + radial[0] * Math.cos(angle) - radial[1] * Math.sin(angle) + around[0], + radial[0] * Math.sin(angle) + radial[1] * Math.cos(angle) + around[1] + ]; + }); +} + + +// Choose the edge with the minimal distance from `point` to its orthogonal +// projection onto that edge, if such a projection exists, or the distance to +// the closest vertex on that edge. Returns an object with the `index` of the +// chosen edge, the chosen `loc` on that edge, and the `distance` to to it. +export function geoChooseEdge(nodes, point, projection, skipID) { + var dist = geoVecLength; + var points = nodes.map(function(n) { return projection(n.loc); }); + var ids = nodes.map(function(n) { return n.id; }); + var min = Infinity; + var idx; + var loc; + + for (var i = 0; i < points.length - 1; i++) { + if (ids[i] === skipID || ids[i + 1] === skipID) continue; + + var o = points[i]; + var s = geoVecSubtract(points[i + 1], o); + var v = geoVecSubtract(point, 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 d = dist(p, point); + if (d < min) { + min = d; + idx = i + 1; + loc = projection.invert(p); + } + } + + if (idx !== undefined) { + return { index: idx, distance: min, loc: loc }; + } else { + return null; + } +} + + +// Return the intersection point of 2 line segments. +// From https://github.com/pgkelley4/line-segments-intersect +// This uses the vector cross product approach described below: +// http://stackoverflow.com/a/565282/786339 +export function geoLineIntersection(a, b) { + var p = [a[0][0], a[0][1]]; + var p2 = [a[1][0], a[1][1]]; + var q = [b[0][0], b[0][1]]; + var q2 = [b[1][0], b[1][1]]; + var r = geoVecSubtract(p2, p); + var s = geoVecSubtract(q2, q); + var uNumerator = geoVecCross(geoVecSubtract(q, p), r); + var denominator = geoVecCross(r, s); + + if (uNumerator && denominator) { + var u = uNumerator / denominator; + var t = geoVecCross(geoVecSubtract(q, p), s) / denominator; + + if ((t >= 0) && (t <= 1) && (u >= 0) && (u <= 1)) { + return geoVecInterp(p, p2, t); + } + } + + return null; +} + + +export function geoPathIntersections(path1, path2) { + var intersections = []; + for (var i = 0; i < path1.length - 1; i++) { + for (var j = 0; j < path2.length - 1; j++) { + var a = [ path1[i], path1[i+1] ]; + var b = [ path2[j], path2[j+1] ]; + var hit = geoLineIntersection(a, b); + if (hit) { + intersections.push(hit); + } + } + } + return intersections; +} + + +// Return whether point is contained in polygon. +// +// `point` should be a 2-item array of coordinates. +// `polygon` should be an array of 2-item arrays of coordinates. +// +// From https://github.com/substack/point-in-polygon. +// ray-casting algorithm based on +// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html +// +export function geoPointInPolygon(point, polygon) { + var x = point[0]; + var y = point[1]; + var inside = false; + + for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { + var xi = polygon[i][0]; + var yi = polygon[i][1]; + var xj = polygon[j][0]; + var yj = polygon[j][1]; + + var intersect = ((yi > y) !== (yj > y)) && + (x < (xj - xi) * (y - yi) / (yj - yi) + xi); + if (intersect) inside = !inside; + } + + return inside; +} + + +export function geoPolygonContainsPolygon(outer, inner) { + return _every(inner, function(point) { + return geoPointInPolygon(point, outer); + }); +} + + +export function geoPolygonIntersectsPolygon(outer, inner, checkSegments) { + function testSegments(outer, inner) { + for (var i = 0; i < outer.length - 1; i++) { + for (var j = 0; j < inner.length - 1; j++) { + var a = [ outer[i], outer[i + 1] ]; + var b = [ inner[j], inner[j + 1] ]; + if (geoLineIntersection(a, b)) return true; + } + } + return false; + } + + function testPoints(outer, inner) { + return _some(inner, function(point) { + return geoPointInPolygon(point, outer); + }); + } + + return testPoints(outer, inner) || (!!checkSegments && testSegments(outer, inner)); +} + + +export function geoPathLength(path) { + var length = 0; + for (var i = 0; i < path.length - 1; i++) { + length += geoVecLength(path[i], path[i + 1]); + } + return length; +} + + +// If the given point is at the edge of the padded viewport, +// return a vector that will nudge the viewport in that direction +export function geoViewportEdge(point, dimensions) { + var pad = [80, 20, 50, 20]; // top, right, bottom, left + var x = 0; + var y = 0; + + if (point[0] > dimensions[0] - pad[1]) + x = -10; + if (point[0] < pad[3]) + x = 10; + if (point[1] > dimensions[1] - pad[2]) + y = -10; + if (point[1] < pad[0]) + y = 10; + + if (x || y) { + return [x, y]; + } else { + return null; + } +} diff --git a/modules/geo/index.js b/modules/geo/index.js index dca2f31dd..fe801aff6 100644 --- a/modules/geo/index.js +++ b/modules/geo/index.js @@ -1,23 +1,29 @@ -export { geoAngle } from './geo.js'; -export { geoChooseEdge } from './geo.js'; -export { geoEdgeEqual } from './geo.js'; export { geoExtent } from './extent.js'; -export { geoRawMercator } from './raw_mercator.js'; -export { geoRotate } from './geo.js'; + export { geoLatToMeters } from './geo.js'; -export { geoLineIntersection } from './geo.js'; export { geoLonToMeters } from './geo.js'; export { geoMetersToLat } from './geo.js'; export { geoMetersToLon } from './geo.js'; export { geoMetersToOffset } from './geo.js'; export { geoOffsetToMeters } from './geo.js'; -export { geoPathIntersections } from './geo.js'; -export { geoPathLength } from './geo.js'; -export { geoPointInPolygon } from './geo.js'; -export { geoPolygonContainsPolygon } from './geo.js'; -export { geoPolygonIntersectsPolygon } from './geo.js'; export { geoScaleToZoom } from './geo.js'; export { geoSphericalDistance } from './geo.js'; +export { geoZoomToScale } from './geo.js'; + +export { geoAngle } from './geom.js'; +export { geoChooseEdge } from './geom.js'; +export { geoEdgeEqual } from './geom.js'; +export { geoRotate } from './geom.js'; +export { geoLineIntersection } from './geom.js'; +export { geoPathIntersections } from './geom.js'; +export { geoPathLength } from './geom.js'; +export { geoPointInPolygon } from './geom.js'; +export { geoPolygonContainsPolygon } from './geom.js'; +export { geoPolygonIntersectsPolygon } from './geom.js'; +export { geoViewportEdge } from './geom.js'; + +export { geoRawMercator } from './raw_mercator.js'; + export { geoVecAdd } from './vector.js'; export { geoVecAngle } from './vector.js'; export { geoVecCross } from './vector.js'; @@ -28,5 +34,4 @@ export { geoVecInterp } from './vector.js'; export { geoVecLength } from './vector.js'; export { geoVecSubtract } from './vector.js'; export { geoVecScale } from './vector.js'; -export { geoZoomToScale } from './geo.js'; -export { geoViewportEdge } from './geo.js'; + diff --git a/test/index.html b/test/index.html index ed2cb0c54..2357d5ba1 100644 --- a/test/index.html +++ b/test/index.html @@ -74,6 +74,7 @@ + diff --git a/test/spec/geo/geo.js b/test/spec/geo/geo.js index 9f31f1d27..76c77ca09 100644 --- a/test/spec/geo/geo.js +++ b/test/spec/geo/geo.js @@ -1,4 +1,4 @@ -describe('iD.geo', function() { +describe('iD.geo - geography', function() { describe('geoLatToMeters', function() { it('0 degrees latitude is 0 meters', function() { @@ -144,298 +144,4 @@ describe('iD.geo', function() { }); }); - describe('geoEdgeEqual', function() { - it('returns false for inequal edges', function() { - expect(iD.geoEdgeEqual(['a', 'b'], ['a', 'c'])).to.be.false; - }); - - it('returns true for equal edges along same direction', function() { - expect(iD.geoEdgeEqual(['a', 'b'], ['a', 'b'])).to.be.true; - }); - - it('returns true for equal edges along opposite direction', function() { - expect(iD.geoEdgeEqual(['a', 'b'], ['b', 'a'])).to.be.true; - }); - }); - - describe('geoAngle', function() { - it('returns angle between a and b', function() { - var projection = function (_) { return _; }; - expect(iD.geoAngle({loc:[0, 0]}, {loc:[1, 0]}, projection)).to.be.closeTo(0, 1e-6); - expect(iD.geoAngle({loc:[0, 0]}, {loc:[0, 1]}, projection)).to.be.closeTo(Math.PI / 2, 1e-6); - expect(iD.geoAngle({loc:[0, 0]}, {loc:[-1, 0]}, projection)).to.be.closeTo(Math.PI, 1e-6); - expect(iD.geoAngle({loc:[0, 0]}, {loc:[0, -1]}, projection)).to.be.closeTo(-Math.PI / 2, 1e-6); - }); - }); - - describe('geoRotate', function() { - it('rotates points around [0, 0]', function() { - var points = [[5, 0], [5, 1]]; - var angle = Math.PI; - var around = [0, 0]; - var result = iD.geoRotate(points, angle, around); - expect(result[0][0]).to.be.closeTo(-5, 1e-6); - expect(result[0][1]).to.be.closeTo(0, 1e-6); - expect(result[1][0]).to.be.closeTo(-5, 1e-6); - expect(result[1][1]).to.be.closeTo(-1, 1e-6); - }); - - it('rotates points around [3, 0]', function() { - var points = [[5, 0], [5, 1]]; - var angle = Math.PI; - var around = [3, 0]; - var result = iD.geoRotate(points, angle, around); - expect(result[0][0]).to.be.closeTo(1, 1e-6); - expect(result[0][1]).to.be.closeTo(0, 1e-6); - expect(result[1][0]).to.be.closeTo(1, 1e-6); - expect(result[1][1]).to.be.closeTo(-1, 1e-6); - }); - }); - - describe('geoChooseEdge', function() { - var projection = function (l) { return l; }; - projection.invert = projection; - - it('returns null for a degenerate way (no nodes)', function() { - expect(iD.geoChooseEdge([], [0, 0], projection)).to.be.null; - }); - - it('returns null for a degenerate way (single node)', function() { - expect(iD.geoChooseEdge([iD.osmNode({loc: [0, 0]})], [0, 0], projection)).to.be.null; - }); - - it('calculates the orthogonal projection of a point onto a segment', function() { - // a --*--- b - // | - // c - // - // * = [2, 0] - var a = [0, 0]; - var b = [5, 0]; - var c = [2, 1]; - var nodes = [ iD.osmNode({loc: a}), iD.osmNode({loc: b}) ]; - var choice = iD.geoChooseEdge(nodes, c, projection); - expect(choice.index).to.eql(1); - expect(choice.distance).to.eql(1); - expect(choice.loc).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 nodes = [ iD.osmNode({loc: a}), iD.osmNode({loc: b}) ]; - var choice = iD.geoChooseEdge(nodes, c, projection); - expect(choice.index).to.eql(1); - expect(choice.distance).to.eql(5); - expect(choice.loc).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 nodes = [ iD.osmNode({loc: a}), iD.osmNode({loc: b}) ]; - var choice = iD.geoChooseEdge(nodes, c, projection); - expect(choice.index).to.eql(1); - expect(choice.distance).to.eql(5); - expect(choice.loc).to.eql([5, 0]); - }); - - it('skips the given nodeID at end of way', function() { - // - // a --*-- b - // e | - // | | - // d - c - // - // * = [2, 0] - var a = [0, 0]; - var b = [5, 0]; - var c = [5, 5]; - var d = [2, 5]; - var e = [2, 0.1]; // e.g. user is dragging e onto ab - var nodes = [ - iD.osmNode({id: 'a', loc: a}), - iD.osmNode({id: 'b', loc: b}), - iD.osmNode({id: 'c', loc: c}), - iD.osmNode({id: 'd', loc: d}), - iD.osmNode({id: 'e', loc: e}) - ]; - var choice = iD.geoChooseEdge(nodes, e, projection, 'e'); - expect(choice.index).to.eql(1); - expect(choice.distance).to.eql(0.1); - expect(choice.loc).to.eql([2, 0]); - }); - - it('skips the given nodeID in middle of way', function() { - // - // a --*-- b - // d | - // / \ | - // e c - // - // * = [2, 0] - var a = [0, 0]; - var b = [5, 0]; - var c = [5, 5]; - var d = [2, 0.1]; // e.g. user is dragging d onto ab - var e = [0, 5]; - var nodes = [ - iD.osmNode({id: 'a', loc: a}), - iD.osmNode({id: 'b', loc: b}), - iD.osmNode({id: 'c', loc: c}), - iD.osmNode({id: 'd', loc: d}), - iD.osmNode({id: 'e', loc: e}) - ]; - var choice = iD.geoChooseEdge(nodes, d, projection, 'd'); - expect(choice.index).to.eql(1); - expect(choice.distance).to.eql(0.1); - expect(choice.loc).to.eql([2, 0]); - }); - - it('returns null if all nodes are skipped', function() { - var nodes = [ - iD.osmNode({id: 'a', loc: [0, 0]}), - iD.osmNode({id: 'b', loc: [5, 0]}), - ]; - var choice = iD.geoChooseEdge(nodes, [2, 2], projection, 'a'); - expect(choice).to.be.null; - }); - }); - - describe('geoLineIntersection', function() { - it('returns null if lines are colinear with overlap', function() { - var a = [[0, 0], [10, 0]]; - var b = [[-5, 0], [5, 0]]; - expect(iD.geoLineIntersection(a, b)).to.be.null; - }); - it('returns null if lines are colinear but disjoint', function() { - var a = [[5, 0], [10, 0]]; - var b = [[-10, 0], [-5, 0]]; - expect(iD.geoLineIntersection(a, b)).to.be.null; - }); - it('returns null if lines are parallel', function() { - var a = [[0, 0], [10, 0]]; - var b = [[0, 5], [10, 5]]; - expect(iD.geoLineIntersection(a, b)).to.be.null; - }); - it('returns the intersection point between 2 lines', function() { - var a = [[0, 0], [10, 0]]; - var b = [[5, 10], [5, -10]]; - expect(iD.geoLineIntersection(a, b)).to.eql([5, 0]); - }); - it('returns null if lines are not parallel but not intersecting', function() { - var a = [[0, 0], [10, 0]]; - var b = [[-5, 10], [-5, -10]]; - expect(iD.geoLineIntersection(a, b)).to.be.null; - }); - }); - - describe('geoPointInPolygon', function() { - it('says a point in a polygon is on a polygon', function() { - var poly = [[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]; - var point = [0.5, 0.5]; - expect(iD.geoPointInPolygon(point, poly)).to.be.true; - }); - it('says a point outside of a polygon is outside', function() { - var poly = [[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]; - var point = [0.5, 1.5]; - expect(iD.geoPointInPolygon(point, poly)).to.be.false; - }); - }); - - describe('geoPolygonContainsPolygon', function() { - it('says a polygon in a polygon is in', function() { - var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; - var inner = [[1, 1], [1, 2], [2, 2], [2, 1], [1, 1]]; - expect(iD.geoPolygonContainsPolygon(outer, inner)).to.be.true; - }); - it('says a polygon outside of a polygon is out', function() { - var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; - var inner = [[1, 1], [1, 9], [2, 2], [2, 1], [1, 1]]; - expect(iD.geoPolygonContainsPolygon(outer, inner)).to.be.false; - }); - }); - - describe('geoPolygonIntersectsPolygon', function() { - it('returns true when outer polygon fully contains inner', function() { - var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; - var inner = [[1, 1], [1, 2], [2, 2], [2, 1], [1, 1]]; - expect(iD.geoPolygonIntersectsPolygon(outer, inner)).to.be.true; - }); - - it('returns true when outer polygon partially contains inner (some vertices contained)', function() { - var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; - var inner = [[-1, -1], [1, 2], [2, 2], [2, 1], [1, 1]]; - expect(iD.geoPolygonIntersectsPolygon(outer, inner)).to.be.true; - }); - - it('returns false when outer polygon partially contains inner (no vertices contained - lax test)', function() { - var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; - var inner = [[1, -1], [1, 4], [2, 4], [2, -1], [1, -1]]; - expect(iD.geoPolygonIntersectsPolygon(outer, inner)).to.be.false; - }); - - it('returns true when outer polygon partially contains inner (no vertices contained - strict test)', function() { - var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; - var inner = [[1, -1], [1, 4], [2, 4], [2, -1], [1, -1]]; - expect(iD.geoPolygonIntersectsPolygon(outer, inner, true)).to.be.true; - }); - - it('returns false when outer and inner are fully disjoint', function() { - var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; - var inner = [[-1, -1], [-1, -2], [-2, -2], [-2, -1], [-1, -1]]; - expect(iD.geoPolygonIntersectsPolygon(outer, inner)).to.be.false; - }); - }); - - describe('geoPathLength', function() { - it('calculates a simple path length', function() { - var path = [[0, 0], [0, 1], [3, 5]]; - expect(iD.geoPathLength(path)).to.eql(6); - }); - - it('does not fail on single-point path', function() { - var path = [[0, 0]]; - expect(iD.geoPathLength(path)).to.eql(0); - }); - - it('estimates zero-length edges', function() { - var path = [[0, 0], [0, 0]]; - expect(iD.geoPathLength(path)).to.eql(0); - }); - }); - - describe('geoViewportEdge', function() { - var dimensions = [1000, 1000]; - it('returns null if the point is not at the edge', function() { - expect(iD.geoViewportEdge([500, 500], dimensions)).to.be.null; - }); - it('nudges top edge', function() { - expect(iD.geoViewportEdge([500, 5], dimensions)).to.eql([0, 10]); - }); - it('nudges top-right corner', function() { - expect(iD.geoViewportEdge([995, 5], dimensions)).to.eql([-10, 10]); - }); - it('nudges right edge', function() { - expect(iD.geoViewportEdge([995, 500], dimensions)).to.eql([-10, 0]); - }); - it('nudges bottom-right corner', function() { - expect(iD.geoViewportEdge([995, 995], dimensions)).to.eql([-10, -10]); - }); - it('nudges bottom edge', function() { - expect(iD.geoViewportEdge([500, 995], dimensions)).to.eql([0, -10]); - }); - it('nudges bottom-left corner', function() { - expect(iD.geoViewportEdge([5, 995], dimensions)).to.eql([10, -10]); - }); - it('nudges left edge', function() { - expect(iD.geoViewportEdge([5, 500], dimensions)).to.eql([10, 0]); - }); - it('nudges top-left corner', function() { - expect(iD.geoViewportEdge([5, 5], dimensions)).to.eql([10, 10]); - }); - }); - }); diff --git a/test/spec/geo/geom.js b/test/spec/geo/geom.js new file mode 100644 index 000000000..11b37c35a --- /dev/null +++ b/test/spec/geo/geom.js @@ -0,0 +1,297 @@ +describe('iD.geo - geometry', function() { + + describe('geoAngle', function() { + it('returns angle between a and b', function() { + var projection = function (_) { return _; }; + expect(iD.geoAngle({loc:[0, 0]}, {loc:[1, 0]}, projection)).to.be.closeTo(0, 1e-6); + expect(iD.geoAngle({loc:[0, 0]}, {loc:[0, 1]}, projection)).to.be.closeTo(Math.PI / 2, 1e-6); + expect(iD.geoAngle({loc:[0, 0]}, {loc:[-1, 0]}, projection)).to.be.closeTo(Math.PI, 1e-6); + expect(iD.geoAngle({loc:[0, 0]}, {loc:[0, -1]}, projection)).to.be.closeTo(-Math.PI / 2, 1e-6); + }); + }); + + describe('geoEdgeEqual', function() { + it('returns false for inequal edges', function() { + expect(iD.geoEdgeEqual(['a', 'b'], ['a', 'c'])).to.be.false; + }); + + it('returns true for equal edges along same direction', function() { + expect(iD.geoEdgeEqual(['a', 'b'], ['a', 'b'])).to.be.true; + }); + + it('returns true for equal edges along opposite direction', function() { + expect(iD.geoEdgeEqual(['a', 'b'], ['b', 'a'])).to.be.true; + }); + }); + + describe('geoRotate', function() { + it('rotates points around [0, 0]', function() { + var points = [[5, 0], [5, 1]]; + var angle = Math.PI; + var around = [0, 0]; + var result = iD.geoRotate(points, angle, around); + expect(result[0][0]).to.be.closeTo(-5, 1e-6); + expect(result[0][1]).to.be.closeTo(0, 1e-6); + expect(result[1][0]).to.be.closeTo(-5, 1e-6); + expect(result[1][1]).to.be.closeTo(-1, 1e-6); + }); + + it('rotates points around [3, 0]', function() { + var points = [[5, 0], [5, 1]]; + var angle = Math.PI; + var around = [3, 0]; + var result = iD.geoRotate(points, angle, around); + expect(result[0][0]).to.be.closeTo(1, 1e-6); + expect(result[0][1]).to.be.closeTo(0, 1e-6); + expect(result[1][0]).to.be.closeTo(1, 1e-6); + expect(result[1][1]).to.be.closeTo(-1, 1e-6); + }); + }); + + describe('geoChooseEdge', function() { + var projection = function (l) { return l; }; + projection.invert = projection; + + it('returns null for a degenerate way (no nodes)', function() { + expect(iD.geoChooseEdge([], [0, 0], projection)).to.be.null; + }); + + it('returns null for a degenerate way (single node)', function() { + expect(iD.geoChooseEdge([iD.osmNode({loc: [0, 0]})], [0, 0], projection)).to.be.null; + }); + + it('calculates the orthogonal projection of a point onto a segment', function() { + // a --*--- b + // | + // c + // + // * = [2, 0] + var a = [0, 0]; + var b = [5, 0]; + var c = [2, 1]; + var nodes = [ iD.osmNode({loc: a}), iD.osmNode({loc: b}) ]; + var choice = iD.geoChooseEdge(nodes, c, projection); + expect(choice.index).to.eql(1); + expect(choice.distance).to.eql(1); + expect(choice.loc).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 nodes = [ iD.osmNode({loc: a}), iD.osmNode({loc: b}) ]; + var choice = iD.geoChooseEdge(nodes, c, projection); + expect(choice.index).to.eql(1); + expect(choice.distance).to.eql(5); + expect(choice.loc).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 nodes = [ iD.osmNode({loc: a}), iD.osmNode({loc: b}) ]; + var choice = iD.geoChooseEdge(nodes, c, projection); + expect(choice.index).to.eql(1); + expect(choice.distance).to.eql(5); + expect(choice.loc).to.eql([5, 0]); + }); + + it('skips the given nodeID at end of way', function() { + // + // a --*-- b + // e | + // | | + // d - c + // + // * = [2, 0] + var a = [0, 0]; + var b = [5, 0]; + var c = [5, 5]; + var d = [2, 5]; + var e = [2, 0.1]; // e.g. user is dragging e onto ab + var nodes = [ + iD.osmNode({id: 'a', loc: a}), + iD.osmNode({id: 'b', loc: b}), + iD.osmNode({id: 'c', loc: c}), + iD.osmNode({id: 'd', loc: d}), + iD.osmNode({id: 'e', loc: e}) + ]; + var choice = iD.geoChooseEdge(nodes, e, projection, 'e'); + expect(choice.index).to.eql(1); + expect(choice.distance).to.eql(0.1); + expect(choice.loc).to.eql([2, 0]); + }); + + it('skips the given nodeID in middle of way', function() { + // + // a --*-- b + // d | + // / \ | + // e c + // + // * = [2, 0] + var a = [0, 0]; + var b = [5, 0]; + var c = [5, 5]; + var d = [2, 0.1]; // e.g. user is dragging d onto ab + var e = [0, 5]; + var nodes = [ + iD.osmNode({id: 'a', loc: a}), + iD.osmNode({id: 'b', loc: b}), + iD.osmNode({id: 'c', loc: c}), + iD.osmNode({id: 'd', loc: d}), + iD.osmNode({id: 'e', loc: e}) + ]; + var choice = iD.geoChooseEdge(nodes, d, projection, 'd'); + expect(choice.index).to.eql(1); + expect(choice.distance).to.eql(0.1); + expect(choice.loc).to.eql([2, 0]); + }); + + it('returns null if all nodes are skipped', function() { + var nodes = [ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [5, 0]}), + ]; + var choice = iD.geoChooseEdge(nodes, [2, 2], projection, 'a'); + expect(choice).to.be.null; + }); + }); + + describe('geoLineIntersection', function() { + it('returns null if lines are colinear with overlap', function() { + var a = [[0, 0], [10, 0]]; + var b = [[-5, 0], [5, 0]]; + expect(iD.geoLineIntersection(a, b)).to.be.null; + }); + it('returns null if lines are colinear but disjoint', function() { + var a = [[5, 0], [10, 0]]; + var b = [[-10, 0], [-5, 0]]; + expect(iD.geoLineIntersection(a, b)).to.be.null; + }); + it('returns null if lines are parallel', function() { + var a = [[0, 0], [10, 0]]; + var b = [[0, 5], [10, 5]]; + expect(iD.geoLineIntersection(a, b)).to.be.null; + }); + it('returns the intersection point between 2 lines', function() { + var a = [[0, 0], [10, 0]]; + var b = [[5, 10], [5, -10]]; + expect(iD.geoLineIntersection(a, b)).to.eql([5, 0]); + }); + it('returns null if lines are not parallel but not intersecting', function() { + var a = [[0, 0], [10, 0]]; + var b = [[-5, 10], [-5, -10]]; + expect(iD.geoLineIntersection(a, b)).to.be.null; + }); + }); + + describe('geoPointInPolygon', function() { + it('says a point in a polygon is on a polygon', function() { + var poly = [[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]; + var point = [0.5, 0.5]; + expect(iD.geoPointInPolygon(point, poly)).to.be.true; + }); + it('says a point outside of a polygon is outside', function() { + var poly = [[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]; + var point = [0.5, 1.5]; + expect(iD.geoPointInPolygon(point, poly)).to.be.false; + }); + }); + + describe('geoPolygonContainsPolygon', function() { + it('says a polygon in a polygon is in', function() { + var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; + var inner = [[1, 1], [1, 2], [2, 2], [2, 1], [1, 1]]; + expect(iD.geoPolygonContainsPolygon(outer, inner)).to.be.true; + }); + it('says a polygon outside of a polygon is out', function() { + var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; + var inner = [[1, 1], [1, 9], [2, 2], [2, 1], [1, 1]]; + expect(iD.geoPolygonContainsPolygon(outer, inner)).to.be.false; + }); + }); + + describe('geoPolygonIntersectsPolygon', function() { + it('returns true when outer polygon fully contains inner', function() { + var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; + var inner = [[1, 1], [1, 2], [2, 2], [2, 1], [1, 1]]; + expect(iD.geoPolygonIntersectsPolygon(outer, inner)).to.be.true; + }); + + it('returns true when outer polygon partially contains inner (some vertices contained)', function() { + var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; + var inner = [[-1, -1], [1, 2], [2, 2], [2, 1], [1, 1]]; + expect(iD.geoPolygonIntersectsPolygon(outer, inner)).to.be.true; + }); + + it('returns false when outer polygon partially contains inner (no vertices contained - lax test)', function() { + var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; + var inner = [[1, -1], [1, 4], [2, 4], [2, -1], [1, -1]]; + expect(iD.geoPolygonIntersectsPolygon(outer, inner)).to.be.false; + }); + + it('returns true when outer polygon partially contains inner (no vertices contained - strict test)', function() { + var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; + var inner = [[1, -1], [1, 4], [2, 4], [2, -1], [1, -1]]; + expect(iD.geoPolygonIntersectsPolygon(outer, inner, true)).to.be.true; + }); + + it('returns false when outer and inner are fully disjoint', function() { + var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; + var inner = [[-1, -1], [-1, -2], [-2, -2], [-2, -1], [-1, -1]]; + expect(iD.geoPolygonIntersectsPolygon(outer, inner)).to.be.false; + }); + }); + + describe('geoPathLength', function() { + it('calculates a simple path length', function() { + var path = [[0, 0], [0, 1], [3, 5]]; + expect(iD.geoPathLength(path)).to.eql(6); + }); + + it('does not fail on single-point path', function() { + var path = [[0, 0]]; + expect(iD.geoPathLength(path)).to.eql(0); + }); + + it('estimates zero-length edges', function() { + var path = [[0, 0], [0, 0]]; + expect(iD.geoPathLength(path)).to.eql(0); + }); + }); + + describe('geoViewportEdge', function() { + var dimensions = [1000, 1000]; + it('returns null if the point is not at the edge', function() { + expect(iD.geoViewportEdge([500, 500], dimensions)).to.be.null; + }); + it('nudges top edge', function() { + expect(iD.geoViewportEdge([500, 5], dimensions)).to.eql([0, 10]); + }); + it('nudges top-right corner', function() { + expect(iD.geoViewportEdge([995, 5], dimensions)).to.eql([-10, 10]); + }); + it('nudges right edge', function() { + expect(iD.geoViewportEdge([995, 500], dimensions)).to.eql([-10, 0]); + }); + it('nudges bottom-right corner', function() { + expect(iD.geoViewportEdge([995, 995], dimensions)).to.eql([-10, -10]); + }); + it('nudges bottom edge', function() { + expect(iD.geoViewportEdge([500, 995], dimensions)).to.eql([0, -10]); + }); + it('nudges bottom-left corner', function() { + expect(iD.geoViewportEdge([5, 995], dimensions)).to.eql([10, -10]); + }); + it('nudges left edge', function() { + expect(iD.geoViewportEdge([5, 500], dimensions)).to.eql([10, 0]); + }); + it('nudges top-left corner', function() { + expect(iD.geoViewportEdge([5, 5], dimensions)).to.eql([10, 10]); + }); + }); + +}); diff --git a/test/spec/geo/vector.js b/test/spec/geo/vector.js index 21968bb5a..7b69aab00 100644 --- a/test/spec/geo/vector.js +++ b/test/spec/geo/vector.js @@ -1,4 +1,4 @@ -describe('iD.geo vector', function() { +describe('iD.geo - vector', function() { describe('geoVecEqual', function() { it('tests vectors for equality', function() { From 462fef148dcf65fdb334a3a0dafad817f71b7e15 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 28 Dec 2017 21:20:14 -0500 Subject: [PATCH 63/70] Remove unused imports (eslint warning) --- modules/geo/geo.js | 13 ------------- modules/geo/geom.js | 2 +- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/modules/geo/geo.js b/modules/geo/geo.js index e0f7aa436..639887bac 100644 --- a/modules/geo/geo.js +++ b/modules/geo/geo.js @@ -1,16 +1,3 @@ -import _every from 'lodash-es/every'; -import _some from 'lodash-es/some'; - -import { - geoVecAngle, - geoVecCross, - geoVecDot, - geoVecInterp, - geoVecLength, - geoVecSubtract -} from './vector.js'; - - // constants var TAU = 2 * Math.PI; var EQUATORIAL_RADIUS = 6356752.314245179; diff --git a/modules/geo/geom.js b/modules/geo/geom.js index fef500eb6..e8b37c6fe 100644 --- a/modules/geo/geom.js +++ b/modules/geo/geom.js @@ -25,7 +25,7 @@ export function geoEdgeEqual(a, b) { // Rotate all points counterclockwise around a pivot point by given angle export function geoRotate(points, angle, around) { return points.map(function(point) { - var radial = [point[0] - around[0], point[1] - around[1]]; + var radial = geoVecSubtract(point, around); return [ radial[0] * Math.cos(angle) - radial[1] * Math.sin(angle) + around[0], radial[0] * Math.sin(angle) + radial[1] * Math.cos(angle) + around[1] From 4f02340374e2154dd54156fa756ed24b3650e2d8 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 28 Dec 2017 23:10:52 -0500 Subject: [PATCH 64/70] Extract self-intersection code to geoHasSelfIntersections Test for self-intersecting areas in both drag_node and draw_way --- css/55_cursors.css | 3 +- modules/behavior/draw_way.js | 69 ++++++++++++++++++++++++++++-------- modules/geo/geom.js | 40 +++++++++++++++++++-- modules/geo/index.js | 1 + modules/modes/drag_node.js | 56 +++++------------------------ 5 files changed, 105 insertions(+), 64 deletions(-) diff --git a/css/55_cursors.css b/css/55_cursors.css index 0e4732b58..854e2f88b 100644 --- a/css/55_cursors.css +++ b/css/55_cursors.css @@ -1,6 +1,7 @@ /* Cursors */ -.nope { +.nope, +.nope * { cursor: not-allowed !important; } diff --git a/modules/behavior/draw_way.js b/modules/behavior/draw_way.js index 18e270c7e..4a49856e9 100644 --- a/modules/behavior/draw_way.js +++ b/modules/behavior/draw_way.js @@ -7,7 +7,7 @@ import { } from '../actions'; import { behaviorDraw } from './draw'; -import { geoChooseEdge } from '../geo'; +import { geoChooseEdge, geoHasSelfIntersections } from '../geo'; import { modeBrowse, modeSelect } from '../modes'; import { osmNode } from '../osm'; @@ -39,25 +39,48 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // - `behavior/draw.js` `click()` // - `behavior/draw_way.js` `move()` function move(datum) { - var loc; - var target = datum && datum.id && context.hasEntity(datum.id); + var nodeGroups = datum && datum.properties && datum.properties.nodes; + var loc = context.map().mouseCoordinates(); + if (datum.loc) { // snap to node/vertex - a real entity or a nope target with a `loc` loc = datum.loc; - } else if (target && target.type === 'way') { // snap to way - var choice = geoChooseEdge( - context.childNodes(target), context.mouse(), context.projection, end.id - ); - if (choice) { - loc = choice.loc; - } - } - if (!loc) { - loc = context.map().mouseCoordinates(); + } else if (nodeGroups) { // snap to way - a line touch target or nope target with nodes + var best = Infinity; + for (var i = 0; i < nodeGroups.length; i++) { + var childNodes = nodeGroups[i].map(function(id) { return context.entity(id); }); + var choice = geoChooseEdge(childNodes, context.mouse(), context.projection, end.id); + if (choice && choice.distance < best) { + best = choice.distance; + loc = choice.loc; + } + } } context.replace(actionMoveNode(end.id, loc)); end = context.entity(end.id); + + // check if this movement causes the geometry to break + var doBlock = invalidGeometry(end, context.graph()); + context.surface() + .classed('nope', doBlock); + } + + + function invalidGeometry(entity, graph) { + var parents = graph.parentWays(entity); + + for (var i = 0; i < parents.length; i++) { + var parent = parents[i]; + var nodes = parent.nodes.map(function(nodeID) { return graph.entity(nodeID); }); + if (parent.isClosed()) { + if (geoHasSelfIntersections(nodes, entity.id)) { + return true; + } + } + } + + return false; } @@ -147,7 +170,10 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // Accept the current position of the drawing node and continue drawing. drawWay.add = function(loc, datum) { - if (datum && datum.id && /-nope/.test(datum.id)) return; // can't click here + if ((datum && datum.id && /-nope$/.test(datum.id)) || + context.surface().classed('nope')) { + return; // can't click here + } context.pop(_tempEdits); _tempEdits = 0; @@ -163,6 +189,10 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // Connect the way to an existing way. drawWay.addWay = function(loc, edge) { + if (context.surface().classed('nope')) { + return; // can't click here + } + context.pop(_tempEdits); _tempEdits = 0; @@ -178,6 +208,10 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // Connect the way to an existing node and continue drawing. drawWay.addNode = function(node) { + if (context.surface().classed('nope')) { + return; // can't click here + } + context.pop(_tempEdits); _tempEdits = 0; @@ -194,6 +228,10 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // If the way has enough nodes to be valid, it's selected. // Otherwise, delete everything and return to browse mode. drawWay.finish = function() { + if (context.surface().classed('nope')) { + return; // can't click here + } + context.pop(_tempEdits); _tempEdits = 0; @@ -224,6 +262,9 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { context.map().dblclickEnable(true); }, 1000); + context.surface() + .classed('nope', false); + context.enter(modeBrowse(context)); }; diff --git a/modules/geo/geom.js b/modules/geo/geom.js index e8b37c6fe..dbef35800 100644 --- a/modules/geo/geom.js +++ b/modules/geo/geom.js @@ -5,6 +5,7 @@ import { geoVecAngle, geoVecCross, geoVecDot, + geoVecEqual, geoVecInterp, geoVecLength, geoVecSubtract @@ -38,7 +39,7 @@ export function geoRotate(points, angle, around) { // projection onto that edge, if such a projection exists, or the distance to // the closest vertex on that edge. Returns an object with the `index` of the // chosen edge, the chosen `loc` on that edge, and the `distance` to to it. -export function geoChooseEdge(nodes, point, projection, skipID) { +export function geoChooseEdge(nodes, point, projection, activeID) { var dist = geoVecLength; var points = nodes.map(function(n) { return projection(n.loc); }); var ids = nodes.map(function(n) { return n.id; }); @@ -47,7 +48,7 @@ export function geoChooseEdge(nodes, point, projection, skipID) { var loc; for (var i = 0; i < points.length - 1; i++) { - if (ids[i] === skipID || ids[i + 1] === skipID) continue; + if (ids[i] === activeID || ids[i + 1] === activeID) continue; var o = points[i]; var s = geoVecSubtract(points[i + 1], o); @@ -79,6 +80,41 @@ export function geoChooseEdge(nodes, point, projection, skipID) { } +// check active (dragged or drawing) segments against inactive segments +export function geoHasSelfIntersections(nodes, activeID) { + var actives = []; + var inactives = []; + var j, k; + + for (j = 0; j < nodes.length - 1; j++) { + var n1 = nodes[j]; + var n2 = nodes[j+1]; + var segment = [n1.loc, n2.loc]; + if (n1.id === activeID || n2.id === activeID) { + actives.push(segment); + } else { + inactives.push(segment); + } + } + + for (j = 0; j < actives.length; j++) { + for (k = 0; k < inactives.length; k++) { + var p = actives[j]; + var q = inactives[k]; + // skip if segments share an endpoint + if (geoVecEqual(p[1], q[0]) || geoVecEqual(p[0], q[1]) || + geoVecEqual(p[0], q[0]) || geoVecEqual(p[1], q[1]) ) { + continue; + } else if (geoLineIntersection(p, q)) { + return true; + } + } + } + + return false; +} + + // Return the intersection point of 2 line segments. // From https://github.com/pgkelley4/line-segments-intersect // This uses the vector cross product approach described below: diff --git a/modules/geo/index.js b/modules/geo/index.js index fe801aff6..982596f04 100644 --- a/modules/geo/index.js +++ b/modules/geo/index.js @@ -13,6 +13,7 @@ export { geoZoomToScale } from './geo.js'; export { geoAngle } from './geom.js'; export { geoChooseEdge } from './geom.js'; export { geoEdgeEqual } from './geom.js'; +export { geoHasSelfIntersections } from './geom.js'; export { geoRotate } from './geom.js'; export { geoLineIntersection } from './geom.js'; export { geoPathIntersections } from './geom.js'; diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index 3809a352f..4251fb7a8 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -20,8 +20,7 @@ import { import { geoChooseEdge, - geoLineIntersection, - geoVecEqual, + geoHasSelfIntersections, geoVecSubtract, geoViewportEdge } from '../geo'; @@ -133,7 +132,6 @@ export function modeDragNode(context) { var currPoint = (d3_event && d3_event.point) || context.projection(_lastLoc); var currMouse = geoVecSubtract(currPoint, nudge); var loc = context.projection.invert(currMouse); - var didSnap = false; if (!_nudgeInterval) { // If not nudging at the edge of the viewport, try to snap.. // related code @@ -141,21 +139,19 @@ export function modeDragNode(context) { // - `behavior/draw.js` `click()` // - `behavior/draw_way.js` `move()` var d = datum(); - var nodegroups = d && d.properties && d.properties.nodes; + var nodeGroups = d && d.properties && d.properties.nodes; if (d.loc) { // snap to node/vertex - a real entity or a nope target with a `loc` loc = d.loc; - didSnap = true; - } else if (nodegroups) { // snap to way - a line touch target or nope target with nodes + } else if (nodeGroups) { // snap to way - a line touch target or nope target with nodes var best = Infinity; - for (var i = 0; i < nodegroups.length; i++) { - var childNodes = nodegroups[i].map(function(id) { return context.entity(id); }); + for (var i = 0; i < nodeGroups.length; i++) { + var childNodes = nodeGroups[i].map(function(id) { return context.entity(id); }); var choice = geoChooseEdge(childNodes, context.mouse(), context.projection, entity.id); if (choice && choice.distance < best) { best = choice.distance; loc = choice.loc; - didSnap = true; } } } @@ -168,11 +164,7 @@ export function modeDragNode(context) { // check if this movement causes the geometry to break - var doBlock = false; - if (!didSnap) { - doBlock = invalidGeometry(entity, context.graph()); - } - + var doBlock = invalidGeometry(entity, context.graph()); context.surface() .classed('nope', doBlock); @@ -183,41 +175,11 @@ export function modeDragNode(context) { function invalidGeometry(entity, graph) { var parents = graph.parentWays(entity); - function hasSelfIntersections(way, activeID) { - // check active (dragged) segments against inactive segments - var actives = []; - var inactives = []; - var j, k; - for (j = 0; j < way.nodes.length - 1; j++) { - var n1 = graph.entity(way.nodes[j]); - var n2 = graph.entity(way.nodes[j+1]); - var segment = [n1.loc, n2.loc]; - if (n1.id === activeID || n2.id === activeID) { - actives.push(segment); - } else { - inactives.push(segment); - } - } - for (j = 0; j < actives.length; j++) { - for (k = 0; k < inactives.length; k++) { - var p = actives[j]; - var q = inactives[k]; - // skip if segments share an endpoint - if (geoVecEqual(p[1], q[0]) || geoVecEqual(p[0], q[1]) || - geoVecEqual(p[0], q[0]) || geoVecEqual(p[1], q[1]) ) { - continue; - } else if (geoLineIntersection(p, q)) { - return true; - } - } - } - return false; - } - for (var i = 0; i < parents.length; i++) { var parent = parents[i]; - if (parent.isClosed()) { // check for self intersections - if (hasSelfIntersections(parent, entity.id)) { + var nodes = parent.nodes.map(function(nodeID) { return graph.entity(nodeID); }); + if (parent.isClosed()) { + if (geoHasSelfIntersections(nodes, entity.id)) { return true; } } From 08cd2c7325a55e0eb9e5ccb7ab0bed9e7855901c Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 28 Dec 2017 23:46:08 -0500 Subject: [PATCH 65/70] Tests for geoHasSelfIntersections --- test/spec/geo/geom.js | 121 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/test/spec/geo/geom.js b/test/spec/geo/geom.js index 11b37c35a..3acf7f940 100644 --- a/test/spec/geo/geom.js +++ b/test/spec/geo/geom.js @@ -160,6 +160,127 @@ describe('iD.geo - geometry', function() { }); }); + describe('geoHasSelfIntersections', function() { + it('returns false for a degenerate way (no nodes)', function() { + expect(iD.geoHasSelfIntersections([], '')).to.be.false; + }); + + it('returns false if no activeID', function() { + var a = iD.osmNode({id: 'a', loc: [0, 0]}); + var b = iD.osmNode({id: 'b', loc: [2, 0]}); + var c = iD.osmNode({id: 'c', loc: [2, 2]}); + var d = iD.osmNode({id: 'd', loc: [0, 2]}); + var nodes = [a, b, c, d, a]; + expect(iD.geoHasSelfIntersections(nodes, '')).to.be.false; + }); + + it('returns false if there are no self intersections (closed way)', function() { + // a --- b + // | | + // | | + // d --- c + var a = iD.osmNode({id: 'a', loc: [0, 0]}); + var b = iD.osmNode({id: 'b', loc: [2, 0]}); + var c = iD.osmNode({id: 'c', loc: [2, 2]}); + var d = iD.osmNode({id: 'd', loc: [0, 2]}); + var nodes = [a, b, c, d, a]; + expect(iD.geoHasSelfIntersections(nodes, 'a')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'b')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'c')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'd')).to.be.false; + }); + + it('returns true if there are self intersections without a junction (closed way)', function() { + // a c + // | \ / | + // | / | + // | / \ | + // d b + var a = iD.osmNode({id: 'a', loc: [0, 0]}); + var b = iD.osmNode({id: 'b', loc: [2, 2]}); + var c = iD.osmNode({id: 'c', loc: [2, 0]}); + var d = iD.osmNode({id: 'd', loc: [0, 2]}); + var nodes = [a, b, c, d, a]; + expect(iD.geoHasSelfIntersections(nodes, 'a')).to.be.true; + expect(iD.geoHasSelfIntersections(nodes, 'b')).to.be.true; + expect(iD.geoHasSelfIntersections(nodes, 'c')).to.be.true; + expect(iD.geoHasSelfIntersections(nodes, 'd')).to.be.true; + }); + + it('returns false if there are self intersections with a junction (closed way)', function() { + // a c + // | \ / | + // | x | + // | / \ | + // d b + var a = iD.osmNode({id: 'a', loc: [0, 0]}); + var b = iD.osmNode({id: 'b', loc: [2, 2]}); + var c = iD.osmNode({id: 'c', loc: [2, 0]}); + var d = iD.osmNode({id: 'd', loc: [0, 2]}); + var x = iD.osmNode({id: 'x', loc: [1, 1]}); + var nodes = [a, x, b, c, x, d, a]; + expect(iD.geoHasSelfIntersections(nodes, 'a')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'b')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'c')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'd')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'x')).to.be.false; + }); + + it('returns false if there are no self intersections (open way)', function() { + // a --- b + // | + // | + // d --- c + var a = iD.osmNode({id: 'a', loc: [0, 0]}); + var b = iD.osmNode({id: 'b', loc: [2, 0]}); + var c = iD.osmNode({id: 'c', loc: [2, 2]}); + var d = iD.osmNode({id: 'd', loc: [0, 2]}); + var nodes = [a, b, c, d]; + expect(iD.geoHasSelfIntersections(nodes, 'a')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'b')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'c')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'd')).to.be.false; + }); + + it('returns true if there are self intersections without a junction (open way)', function() { + // a c + // \ / | + // / | + // / \ | + // d b + var a = iD.osmNode({id: 'a', loc: [0, 0]}); + var b = iD.osmNode({id: 'b', loc: [2, 2]}); + var c = iD.osmNode({id: 'c', loc: [2, 0]}); + var d = iD.osmNode({id: 'd', loc: [0, 2]}); + var nodes = [a, b, c, d]; + expect(iD.geoHasSelfIntersections(nodes, 'a')).to.be.true; + expect(iD.geoHasSelfIntersections(nodes, 'b')).to.be.true; + expect(iD.geoHasSelfIntersections(nodes, 'c')).to.be.true; + expect(iD.geoHasSelfIntersections(nodes, 'd')).to.be.true; + }); + + it('returns false if there are self intersections with a junction (open way)', function() { + // a c + // \ / | + // x | + // / \ | + // d b + var a = iD.osmNode({id: 'a', loc: [0, 0]}); + var b = iD.osmNode({id: 'b', loc: [2, 2]}); + var c = iD.osmNode({id: 'c', loc: [2, 0]}); + var d = iD.osmNode({id: 'd', loc: [0, 2]}); + var x = iD.osmNode({id: 'x', loc: [1, 1]}); + var nodes = [a, x, b, c, x, d]; + expect(iD.geoHasSelfIntersections(nodes, 'a')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'b')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'c')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'd')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'x')).to.be.false; + }); + + }); + + describe('geoLineIntersection', function() { it('returns null if lines are colinear with overlap', function() { var a = [[0, 0], [10, 0]]; From 35f8d562493a1ed3223d890c3c3056dabfbdf6c4 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 29 Dec 2017 01:25:45 -0500 Subject: [PATCH 66/70] Update direction fields and add to several presets --- data/presets.yaml | 141 +++++++++++------- data/presets/fields.json | 137 ++++++++++------- data/presets/fields/direction.json | 6 + ...direction.json => direction_cardinal.json} | 0 ...ck_direction.json => direction_clock.json} | 0 ...l_direction.json => direction_vertex.json} | 3 +- .../position.json} | 0 .../fields/railway/signal/direction.json | 12 ++ .../fields/traffic_signals/direction.json | 12 ++ data/presets/presets.json | 57 ++++--- .../presets/advertising/billboard.json | 2 +- data/presets/presets/highway/give_way.json | 2 +- .../presets/highway/mini_roundabout.json | 2 +- .../presets/presets/highway/speed_camera.json | 1 + data/presets/presets/highway/stop.json | 2 +- data/presets/presets/highway/street_lamp.json | 1 + .../presets/highway/traffic_mirror.json | 3 + .../presets/highway/traffic_signals.json | 3 +- data/presets/presets/man_made/adit.json | 3 +- .../presets/man_made/surveillance.json | 3 +- .../presets/natural/cave_entrance.json | 3 +- data/presets/presets/railway/milestone.json | 2 +- data/presets/presets/railway/signal.json | 5 + .../presets/tourism/information/board.json | 3 +- .../presets/tourism/information/map.json | 3 +- data/presets/presets/tourism/viewpoint.json | 3 + data/presets/presets/traffic_calming.json | 2 +- .../presets/presets/traffic_calming/bump.json | 2 +- .../presets/traffic_calming/chicane.json | 2 +- .../presets/traffic_calming/choker.json | 2 +- .../presets/traffic_calming/cushion.json | 2 +- data/presets/presets/traffic_calming/dip.json | 2 +- .../presets/presets/traffic_calming/hump.json | 2 +- .../presets/traffic_calming/rumble_strip.json | 2 +- dist/locales/en.json | 99 +++++++----- 35 files changed, 336 insertions(+), 188 deletions(-) create mode 100644 data/presets/fields/direction.json rename data/presets/fields/{cardinal_direction.json => direction_cardinal.json} (100%) rename data/presets/fields/{clock_direction.json => direction_clock.json} (100%) rename data/presets/fields/{parallel_direction.json => direction_vertex.json} (71%) rename data/presets/fields/{milestone_position.json => railway/position.json} (100%) create mode 100644 data/presets/fields/railway/signal/direction.json create mode 100644 data/presets/fields/traffic_signals/direction.json diff --git a/data/presets.yaml b/data/presets.yaml index c7ee602ec..b386ab42a 100644 --- a/data/presets.yaml +++ b/data/presets.yaml @@ -292,53 +292,9 @@ en: label: Capacity # capacity field placeholder placeholder: '50, 100, 200...' - cardinal_direction: - # direction=* - label: Direction - options: - # direction=E - E: East - # direction=ENE - ENE: East-northeast - # direction=ESE - ESE: East-southeast - # direction=N - 'N': North - # direction=NE - NE: Northeast - # direction=NNE - NNE: North-northeast - # direction=NNW - NNW: North-northwest - # direction=NW - NW: Northwest - # direction=S - S: South - # direction=SE - SE: Southeast - # direction=SSE - SSE: South-southeast - # direction=SSW - SSW: South-southwest - # direction=SW - SW: Southwest - # direction=W - W: West - # direction=WNW - WNW: West-northwest - # direction=WSW - WSW: West-southwest castle_type: # castle_type=* label: Type - clock_direction: - # direction=* - label: Direction - options: - # direction=anticlockwise - anticlockwise: Counterclockwise - # direction=clockwise - clockwise: Clockwise clothes: # clothes=* label: Clothes @@ -466,6 +422,65 @@ en: diaper: # diaper=* label: Diaper Changing Available + direction: + # direction=* + label: Direction (Degrees Clockwise) + # direction field placeholder + placeholder: '45, 90, 180, 270' + direction_cardinal: + # direction=* + label: Direction + options: + # direction=E + E: East + # direction=ENE + ENE: East-northeast + # direction=ESE + ESE: East-southeast + # direction=N + 'N': North + # direction=NE + NE: Northeast + # direction=NNE + NNE: North-northeast + # direction=NNW + NNW: North-northwest + # direction=NW + NW: Northwest + # direction=S + S: South + # direction=SE + SE: Southeast + # direction=SSE + SSE: South-southeast + # direction=SSW + SSW: South-southwest + # direction=SW + SW: Southwest + # direction=W + W: West + # direction=WNW + WNW: West-northwest + # direction=WSW + WSW: West-southwest + direction_clock: + # direction=* + label: Direction + options: + # direction=anticlockwise + anticlockwise: Counterclockwise + # direction=clockwise + clockwise: Clockwise + direction_vertex: + # direction=* + label: Direction + options: + # direction=backward + backward: Backward + # direction=both + both: Both / All + # direction=forward + forward: Forward display: # display=* label: Display @@ -812,11 +827,6 @@ en: memorial: # memorial=* label: Type - milestone_position: - # 'railway:position=*' - label: Milestone Position - # milestone_position field placeholder - placeholder: Distance to one decimal (123.4) monitoring_multi: # 'monitoring:=*' label: Monitoring @@ -970,14 +980,6 @@ en: label: Par # par field placeholder placeholder: '3, 4, 5...' - parallel_direction: - # direction=* - label: Direction - options: - # direction=backward - backward: Backward - # direction=forward - forward: Forward park_ride: # park_ride=* label: Park and Ride @@ -1108,6 +1110,21 @@ en: railway: # railway=* label: Type + railway/position: + # 'railway:position=*' + label: Milestone Position + # railway/position field placeholder + placeholder: Distance to one decimal (123.4) + railway/signal/direction: + # 'railway:signal:direction=*' + label: Direction + options: + # 'railway:signal:direction=backward' + backward: Backward + # 'railway:signal:direction=both' + both: Both / All + # 'railway:signal:direction=forward' + forward: Forward rating: # rating=* label: Power Rating @@ -1462,6 +1479,16 @@ en: traffic_signals: # traffic_signals=* label: Type + traffic_signals/direction: + # 'traffic_signals:direction=*' + label: Direction + options: + # 'traffic_signals:direction=backward' + backward: Backward + # 'traffic_signals:direction=both' + both: Both / All + # 'traffic_signals:direction=forward' + forward: Forward trail_visibility: # trail_visibility=* label: Trail Visibility diff --git a/data/presets/fields.json b/data/presets/fields.json index d3c833ac9..4f20de15a 100644 --- a/data/presets/fields.json +++ b/data/presets/fields.json @@ -389,47 +389,11 @@ "label": "Capacity", "placeholder": "50, 100, 200..." }, - "cardinal_direction": { - "key": "direction", - "type": "combo", - "label": "Direction", - "strings": { - "options": { - "N": "North", - "E": "East", - "S": "South", - "W": "West", - "NE": "Northeast", - "SE": "Southeast", - "SW": "Southwest", - "NW": "Northwest", - "NNE": "North-northeast", - "ENE": "East-northeast", - "ESE": "East-southeast", - "SSE": "South-southeast", - "SSW": "South-southwest", - "WSW": "West-southwest", - "WNW": "West-northwest", - "NNW": "North-northwest" - } - } - }, "castle_type": { "key": "castle_type", "type": "combo", "label": "Type" }, - "clock_direction": { - "key": "direction", - "type": "combo", - "label": "Direction", - "strings": { - "options": { - "clockwise": "Clockwise", - "anticlockwise": "Counterclockwise" - } - } - }, "clothes": { "key": "clothes", "type": "semiCombo", @@ -626,6 +590,60 @@ "5" ] }, + "direction_cardinal": { + "key": "direction", + "type": "combo", + "label": "Direction", + "strings": { + "options": { + "N": "North", + "E": "East", + "S": "South", + "W": "West", + "NE": "Northeast", + "SE": "Southeast", + "SW": "Southwest", + "NW": "Northwest", + "NNE": "North-northeast", + "ENE": "East-northeast", + "ESE": "East-southeast", + "SSE": "South-southeast", + "SSW": "South-southwest", + "WSW": "West-southwest", + "WNW": "West-northwest", + "NNW": "North-northwest" + } + } + }, + "direction_clock": { + "key": "direction", + "type": "combo", + "label": "Direction", + "strings": { + "options": { + "clockwise": "Clockwise", + "anticlockwise": "Counterclockwise" + } + } + }, + "direction_vertex": { + "key": "direction", + "type": "combo", + "label": "Direction", + "strings": { + "options": { + "forward": "Forward", + "backward": "Backward", + "both": "Both / All" + } + } + }, + "direction": { + "key": "direction", + "type": "number", + "label": "Direction (Degrees Clockwise)", + "placeholder": "45, 90, 180, 270" + }, "display": { "key": "display", "type": "combo", @@ -1135,12 +1153,6 @@ "type": "typeCombo", "label": "Type" }, - "milestone_position": { - "key": "railway:position", - "type": "text", - "placeholder": "Distance to one decimal (123.4)", - "label": "Milestone Position" - }, "monitoring_multi": { "key": "monitoring:", "type": "multiCombo", @@ -1316,17 +1328,6 @@ "label": "Par", "placeholder": "3, 4, 5..." }, - "parallel_direction": { - "key": "direction", - "type": "combo", - "label": "Direction", - "strings": { - "options": { - "forward": "Forward", - "backward": "Backward" - } - } - }, "park_ride": { "key": "park_ride", "type": "check", @@ -1478,6 +1479,24 @@ "type": "typeCombo", "label": "Type" }, + "railway/position": { + "key": "railway:position", + "type": "text", + "placeholder": "Distance to one decimal (123.4)", + "label": "Milestone Position" + }, + "railway/signal/direction": { + "key": "railway:signal:direction", + "type": "combo", + "label": "Direction", + "strings": { + "options": { + "forward": "Forward", + "backward": "Backward", + "both": "Both / All" + } + } + }, "rating": { "key": "rating", "type": "combo", @@ -2000,6 +2019,18 @@ "label": "Type", "default": "signal" }, + "traffic_signals/direction": { + "key": "traffic_signals:direction", + "type": "combo", + "label": "Direction", + "strings": { + "options": { + "forward": "Forward", + "backward": "Backward", + "both": "Both / All" + } + } + }, "trail_visibility": { "key": "trail_visibility", "type": "combo", diff --git a/data/presets/fields/direction.json b/data/presets/fields/direction.json new file mode 100644 index 000000000..c4324ce8e --- /dev/null +++ b/data/presets/fields/direction.json @@ -0,0 +1,6 @@ +{ + "key": "direction", + "type": "number", + "label": "Direction (Degrees Clockwise)", + "placeholder": "45, 90, 180, 270" +} diff --git a/data/presets/fields/cardinal_direction.json b/data/presets/fields/direction_cardinal.json similarity index 100% rename from data/presets/fields/cardinal_direction.json rename to data/presets/fields/direction_cardinal.json diff --git a/data/presets/fields/clock_direction.json b/data/presets/fields/direction_clock.json similarity index 100% rename from data/presets/fields/clock_direction.json rename to data/presets/fields/direction_clock.json diff --git a/data/presets/fields/parallel_direction.json b/data/presets/fields/direction_vertex.json similarity index 71% rename from data/presets/fields/parallel_direction.json rename to data/presets/fields/direction_vertex.json index 03801f0f3..9b0d7ebc0 100644 --- a/data/presets/fields/parallel_direction.json +++ b/data/presets/fields/direction_vertex.json @@ -5,7 +5,8 @@ "strings": { "options": { "forward": "Forward", - "backward": "Backward" + "backward": "Backward", + "both": "Both / All" } } } diff --git a/data/presets/fields/milestone_position.json b/data/presets/fields/railway/position.json similarity index 100% rename from data/presets/fields/milestone_position.json rename to data/presets/fields/railway/position.json diff --git a/data/presets/fields/railway/signal/direction.json b/data/presets/fields/railway/signal/direction.json new file mode 100644 index 000000000..3034345b8 --- /dev/null +++ b/data/presets/fields/railway/signal/direction.json @@ -0,0 +1,12 @@ +{ + "key": "railway:signal:direction", + "type": "combo", + "label": "Direction", + "strings": { + "options": { + "forward": "Forward", + "backward": "Backward", + "both": "Both / All" + } + } +} diff --git a/data/presets/fields/traffic_signals/direction.json b/data/presets/fields/traffic_signals/direction.json new file mode 100644 index 000000000..079c2a133 --- /dev/null +++ b/data/presets/fields/traffic_signals/direction.json @@ -0,0 +1,12 @@ +{ + "key": "traffic_signals:direction", + "type": "combo", + "label": "Direction", + "strings": { + "options": { + "forward": "Forward", + "backward": "Backward", + "both": "Both / All" + } + } +} diff --git a/data/presets/presets.json b/data/presets/presets.json index 8cc93e1d7..fa822573e 100644 --- a/data/presets/presets.json +++ b/data/presets/presets.json @@ -170,7 +170,7 @@ }, "advertising/billboard": { "fields": [ - "parallel_direction", + "direction", "lit" ], "geometry": [ @@ -6896,7 +6896,7 @@ "highway/give_way": { "icon": "poi-yield", "fields": [ - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex" @@ -6941,7 +6941,7 @@ "highway": "mini_roundabout" }, "fields": [ - "clock_direction" + "direction_clock" ], "name": "Mini-Roundabout" }, @@ -7474,6 +7474,7 @@ "vertex" ], "fields": [ + "direction", "ref" ], "tags": { @@ -7508,7 +7509,7 @@ "icon": "poi-stop", "fields": [ "stop", - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex" @@ -7534,6 +7535,7 @@ }, "fields": [ "lamp_type", + "direction", "ref" ], "terms": [ @@ -7645,6 +7647,9 @@ "point", "vertex" ], + "fields": [ + "direction" + ], "tags": { "highway": "traffic_mirror" }, @@ -7670,7 +7675,8 @@ "highway": "traffic_signals" }, "fields": [ - "traffic_signals" + "traffic_signals", + "traffic_signals/direction" ], "terms": [ "light", @@ -10468,7 +10474,8 @@ "area" ], "fields": [ - "operator" + "operator", + "direction" ], "terms": [ "entrance", @@ -10801,7 +10808,8 @@ "fields": [ "surveillance", "surveillance/type", - "surveillance/zone" + "surveillance/zone", + "direction" ], "terms": [ "anpr", @@ -11128,7 +11136,8 @@ ], "fields": [ "fee", - "access_simple" + "access_simple", + "direction" ], "tags": { "natural": "cave_entrance" @@ -15038,7 +15047,7 @@ "vertex" ], "fields": [ - "milestone_position" + "railway/position" ], "tags": { "railway": "milestone" @@ -15134,6 +15143,11 @@ "point", "vertex" ], + "fields": [ + "railway/position", + "railway/signal/direction", + "ref" + ], "tags": { "railway": "signal" }, @@ -18134,7 +18148,8 @@ "fields": [ "name", "operator", - "board_type" + "board_type", + "direction" ], "geometry": [ "point", @@ -18178,7 +18193,8 @@ "fields": [ "operator", "map_type", - "map_size" + "map_size", + "direction" ], "geometry": [ "point", @@ -18312,6 +18328,9 @@ "point", "vertex" ], + "fields": [ + "direction" + ], "tags": { "tourism": "viewpoint" }, @@ -18363,7 +18382,7 @@ "icon": "poi-warning", "fields": [ "traffic_calming", - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", @@ -18384,7 +18403,7 @@ "icon": "poi-warning", "fields": [ "surface", - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", @@ -18403,7 +18422,7 @@ "traffic_calming/chicane": { "icon": "poi-warning", "fields": [ - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", @@ -18422,7 +18441,7 @@ "traffic_calming/choker": { "icon": "poi-warning", "fields": [ - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", @@ -18441,7 +18460,7 @@ "icon": "poi-warning", "fields": [ "surface", - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", @@ -18462,7 +18481,7 @@ "icon": "poi-warning", "fields": [ "surface", - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", @@ -18481,7 +18500,7 @@ "icon": "poi-warning", "fields": [ "surface", - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", @@ -18515,7 +18534,7 @@ "traffic_calming/rumble_strip": { "icon": "poi-warning", "fields": [ - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", diff --git a/data/presets/presets/advertising/billboard.json b/data/presets/presets/advertising/billboard.json index e3cc660ba..1259e8888 100644 --- a/data/presets/presets/advertising/billboard.json +++ b/data/presets/presets/advertising/billboard.json @@ -1,6 +1,6 @@ { "fields": [ - "parallel_direction", + "direction", "lit" ], "geometry": [ diff --git a/data/presets/presets/highway/give_way.json b/data/presets/presets/highway/give_way.json index 232c27d25..3477820fc 100644 --- a/data/presets/presets/highway/give_way.json +++ b/data/presets/presets/highway/give_way.json @@ -1,7 +1,7 @@ { "icon": "poi-yield", "fields": [ - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex" diff --git a/data/presets/presets/highway/mini_roundabout.json b/data/presets/presets/highway/mini_roundabout.json index f6ec98f34..b290cbb18 100644 --- a/data/presets/presets/highway/mini_roundabout.json +++ b/data/presets/presets/highway/mini_roundabout.json @@ -7,7 +7,7 @@ "highway": "mini_roundabout" }, "fields": [ - "clock_direction" + "direction_clock" ], "name": "Mini-Roundabout" } diff --git a/data/presets/presets/highway/speed_camera.json b/data/presets/presets/highway/speed_camera.json index f0bea39da..c3f9afc32 100644 --- a/data/presets/presets/highway/speed_camera.json +++ b/data/presets/presets/highway/speed_camera.json @@ -5,6 +5,7 @@ "vertex" ], "fields": [ + "direction", "ref" ], "tags": { diff --git a/data/presets/presets/highway/stop.json b/data/presets/presets/highway/stop.json index 47b5e35ed..ec4576d12 100644 --- a/data/presets/presets/highway/stop.json +++ b/data/presets/presets/highway/stop.json @@ -2,7 +2,7 @@ "icon": "poi-stop", "fields": [ "stop", - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex" diff --git a/data/presets/presets/highway/street_lamp.json b/data/presets/presets/highway/street_lamp.json index d72d74bc8..8c48d005f 100644 --- a/data/presets/presets/highway/street_lamp.json +++ b/data/presets/presets/highway/street_lamp.json @@ -9,6 +9,7 @@ }, "fields": [ "lamp_type", + "direction", "ref" ], "terms": [ diff --git a/data/presets/presets/highway/traffic_mirror.json b/data/presets/presets/highway/traffic_mirror.json index 29ca4d78f..f2d3f066d 100644 --- a/data/presets/presets/highway/traffic_mirror.json +++ b/data/presets/presets/highway/traffic_mirror.json @@ -3,6 +3,9 @@ "point", "vertex" ], + "fields": [ + "direction" + ], "tags": { "highway": "traffic_mirror" }, diff --git a/data/presets/presets/highway/traffic_signals.json b/data/presets/presets/highway/traffic_signals.json index 3dee65640..2350052fd 100644 --- a/data/presets/presets/highway/traffic_signals.json +++ b/data/presets/presets/highway/traffic_signals.json @@ -7,7 +7,8 @@ "highway": "traffic_signals" }, "fields": [ - "traffic_signals" + "traffic_signals", + "traffic_signals/direction" ], "terms": [ "light", diff --git a/data/presets/presets/man_made/adit.json b/data/presets/presets/man_made/adit.json index fdc535208..b71decf24 100644 --- a/data/presets/presets/man_made/adit.json +++ b/data/presets/presets/man_made/adit.json @@ -5,7 +5,8 @@ "area" ], "fields": [ - "operator" + "operator", + "direction" ], "terms": [ "entrance", diff --git a/data/presets/presets/man_made/surveillance.json b/data/presets/presets/man_made/surveillance.json index 98ec69379..2bb824dc4 100644 --- a/data/presets/presets/man_made/surveillance.json +++ b/data/presets/presets/man_made/surveillance.json @@ -7,7 +7,8 @@ "fields": [ "surveillance", "surveillance/type", - "surveillance/zone" + "surveillance/zone", + "direction" ], "terms": [ "anpr", diff --git a/data/presets/presets/natural/cave_entrance.json b/data/presets/presets/natural/cave_entrance.json index 6dba4ed02..b8fa4023a 100644 --- a/data/presets/presets/natural/cave_entrance.json +++ b/data/presets/presets/natural/cave_entrance.json @@ -6,7 +6,8 @@ ], "fields": [ "fee", - "access_simple" + "access_simple", + "direction" ], "tags": { "natural": "cave_entrance" diff --git a/data/presets/presets/railway/milestone.json b/data/presets/presets/railway/milestone.json index f3bd6de5e..328eef562 100644 --- a/data/presets/presets/railway/milestone.json +++ b/data/presets/presets/railway/milestone.json @@ -5,7 +5,7 @@ "vertex" ], "fields": [ - "milestone_position" + "railway/position" ], "tags": { "railway": "milestone" diff --git a/data/presets/presets/railway/signal.json b/data/presets/presets/railway/signal.json index d4fe389fd..9c62f5276 100644 --- a/data/presets/presets/railway/signal.json +++ b/data/presets/presets/railway/signal.json @@ -4,6 +4,11 @@ "point", "vertex" ], + "fields": [ + "railway/position", + "railway/signal/direction", + "ref" + ], "tags": { "railway": "signal" }, diff --git a/data/presets/presets/tourism/information/board.json b/data/presets/presets/tourism/information/board.json index 40b2e504c..0acc5a545 100644 --- a/data/presets/presets/tourism/information/board.json +++ b/data/presets/presets/tourism/information/board.json @@ -3,7 +3,8 @@ "fields": [ "name", "operator", - "board_type" + "board_type", + "direction" ], "geometry": [ "point", diff --git a/data/presets/presets/tourism/information/map.json b/data/presets/presets/tourism/information/map.json index 0527009f9..fce5d3418 100644 --- a/data/presets/presets/tourism/information/map.json +++ b/data/presets/presets/tourism/information/map.json @@ -3,7 +3,8 @@ "fields": [ "operator", "map_type", - "map_size" + "map_size", + "direction" ], "geometry": [ "point", diff --git a/data/presets/presets/tourism/viewpoint.json b/data/presets/presets/tourism/viewpoint.json index 8f6fc1baa..1d7ebb4eb 100644 --- a/data/presets/presets/tourism/viewpoint.json +++ b/data/presets/presets/tourism/viewpoint.json @@ -4,6 +4,9 @@ "point", "vertex" ], + "fields": [ + "direction" + ], "tags": { "tourism": "viewpoint" }, diff --git a/data/presets/presets/traffic_calming.json b/data/presets/presets/traffic_calming.json index 0cb5d8fca..5048a9e73 100644 --- a/data/presets/presets/traffic_calming.json +++ b/data/presets/presets/traffic_calming.json @@ -2,7 +2,7 @@ "icon": "poi-warning", "fields": [ "traffic_calming", - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", diff --git a/data/presets/presets/traffic_calming/bump.json b/data/presets/presets/traffic_calming/bump.json index 071ed8e2c..2d281beab 100644 --- a/data/presets/presets/traffic_calming/bump.json +++ b/data/presets/presets/traffic_calming/bump.json @@ -2,7 +2,7 @@ "icon": "poi-warning", "fields": [ "surface", - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", diff --git a/data/presets/presets/traffic_calming/chicane.json b/data/presets/presets/traffic_calming/chicane.json index bbdbfa068..3fbd53cb0 100644 --- a/data/presets/presets/traffic_calming/chicane.json +++ b/data/presets/presets/traffic_calming/chicane.json @@ -1,7 +1,7 @@ { "icon": "poi-warning", "fields": [ - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", diff --git a/data/presets/presets/traffic_calming/choker.json b/data/presets/presets/traffic_calming/choker.json index bee59649a..78688ef90 100644 --- a/data/presets/presets/traffic_calming/choker.json +++ b/data/presets/presets/traffic_calming/choker.json @@ -1,7 +1,7 @@ { "icon": "poi-warning", "fields": [ - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", diff --git a/data/presets/presets/traffic_calming/cushion.json b/data/presets/presets/traffic_calming/cushion.json index 788b2c4ba..47a3d5930 100644 --- a/data/presets/presets/traffic_calming/cushion.json +++ b/data/presets/presets/traffic_calming/cushion.json @@ -2,7 +2,7 @@ "icon": "poi-warning", "fields": [ "surface", - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", diff --git a/data/presets/presets/traffic_calming/dip.json b/data/presets/presets/traffic_calming/dip.json index 6098025ba..aecc88ab4 100644 --- a/data/presets/presets/traffic_calming/dip.json +++ b/data/presets/presets/traffic_calming/dip.json @@ -2,7 +2,7 @@ "icon": "poi-warning", "fields": [ "surface", - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", diff --git a/data/presets/presets/traffic_calming/hump.json b/data/presets/presets/traffic_calming/hump.json index fd724701f..3c17b96be 100644 --- a/data/presets/presets/traffic_calming/hump.json +++ b/data/presets/presets/traffic_calming/hump.json @@ -2,7 +2,7 @@ "icon": "poi-warning", "fields": [ "surface", - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", diff --git a/data/presets/presets/traffic_calming/rumble_strip.json b/data/presets/presets/traffic_calming/rumble_strip.json index f706db468..37a3ca57f 100644 --- a/data/presets/presets/traffic_calming/rumble_strip.json +++ b/data/presets/presets/traffic_calming/rumble_strip.json @@ -1,7 +1,7 @@ { "icon": "poi-warning", "fields": [ - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", diff --git a/dist/locales/en.json b/dist/locales/en.json index cf8eb15ba..034b67fa5 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -1498,37 +1498,9 @@ "label": "Capacity", "placeholder": "50, 100, 200..." }, - "cardinal_direction": { - "label": "Direction", - "options": { - "N": "North", - "E": "East", - "S": "South", - "W": "West", - "NE": "Northeast", - "SE": "Southeast", - "SW": "Southwest", - "NW": "Northwest", - "NNE": "North-northeast", - "ENE": "East-northeast", - "ESE": "East-southeast", - "SSE": "South-southeast", - "SSW": "South-southwest", - "WSW": "West-southwest", - "WNW": "West-northwest", - "NNW": "North-northwest" - } - }, "castle_type": { "label": "Type" }, - "clock_direction": { - "label": "Direction", - "options": { - "clockwise": "Clockwise", - "anticlockwise": "Counterclockwise" - } - }, "clothes": { "label": "Clothes" }, @@ -1651,6 +1623,46 @@ "diaper": { "label": "Diaper Changing Available" }, + "direction_cardinal": { + "label": "Direction", + "options": { + "N": "North", + "E": "East", + "S": "South", + "W": "West", + "NE": "Northeast", + "SE": "Southeast", + "SW": "Southwest", + "NW": "Northwest", + "NNE": "North-northeast", + "ENE": "East-northeast", + "ESE": "East-southeast", + "SSE": "South-southeast", + "SSW": "South-southwest", + "WSW": "West-southwest", + "WNW": "West-northwest", + "NNW": "North-northwest" + } + }, + "direction_clock": { + "label": "Direction", + "options": { + "clockwise": "Clockwise", + "anticlockwise": "Counterclockwise" + } + }, + "direction_vertex": { + "label": "Direction", + "options": { + "forward": "Forward", + "backward": "Backward", + "both": "Both / All" + } + }, + "direction": { + "label": "Direction (Degrees Clockwise)", + "placeholder": "45, 90, 180, 270" + }, "display": { "label": "Display" }, @@ -1953,10 +1965,6 @@ "memorial": { "label": "Type" }, - "milestone_position": { - "label": "Milestone Position", - "placeholder": "Distance to one decimal (123.4)" - }, "monitoring_multi": { "label": "Monitoring" }, @@ -2074,13 +2082,6 @@ "label": "Par", "placeholder": "3, 4, 5..." }, - "parallel_direction": { - "label": "Direction", - "options": { - "forward": "Forward", - "backward": "Backward" - } - }, "park_ride": { "label": "Park and Ride" }, @@ -2182,6 +2183,18 @@ "railway": { "label": "Type" }, + "railway/position": { + "label": "Milestone Position", + "placeholder": "Distance to one decimal (123.4)" + }, + "railway/signal/direction": { + "label": "Direction", + "options": { + "forward": "Forward", + "backward": "Backward", + "both": "Both / All" + } + }, "rating": { "label": "Power Rating" }, @@ -2484,6 +2497,14 @@ "traffic_signals": { "label": "Type" }, + "traffic_signals/direction": { + "label": "Direction", + "options": { + "forward": "Forward", + "backward": "Backward", + "both": "Both / All" + } + }, "trail_visibility": { "label": "Trail Visibility", "placeholder": "Excellent, Good, Bad...", From 1bd41b894c1be70474ecad9de171e69a58fc4b10 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 29 Dec 2017 16:04:03 -0500 Subject: [PATCH 67/70] Add direction tagging to intro graph for walkthrough --- data/intro_graph.json | 374 ++++++++++++++++++++++++++++++++---------- 1 file changed, 288 insertions(+), 86 deletions(-) diff --git a/data/intro_graph.json b/data/intro_graph.json index 1647a777e..fcf2b191e 100644 --- a/data/intro_graph.json +++ b/data/intro_graph.json @@ -2984,7 +2984,8 @@ "loc": [-85.643097, 41.942575], "tags": { "highway": "traffic_signals", - "traffic_signals": "signal" + "traffic_signals": "signal", + "traffic_signals:direction": "both" } }, "n1643": { @@ -3840,7 +3841,8 @@ "loc": [-85.63582, 41.942771], "tags": { "highway": "traffic_signals", - "traffic_signals": "emergency" + "traffic_signals": "emergency", + "traffic_signals:direction": "both" } }, "n1835": { @@ -8001,7 +8003,8 @@ "loc": [-85.632793, 41.94405], "tags": { "highway": "traffic_signals", - "traffic_signals": "signal" + "traffic_signals": "signal", + "traffic_signals:direction": "both" } }, "n2749": { @@ -12872,7 +12875,7 @@ }, "n3858": { "id": "n3858", - "loc": [-85.616755, 41.952231] + "loc": [-85.616762, 41.952222] }, "n3859": { "id": "n3859", @@ -12908,11 +12911,11 @@ }, "n3866": { "id": "n3866", - "loc": [-85.616572, 41.951992] + "loc": [-85.616557, 41.951997] }, "n3867": { "id": "n3867", - "loc": [-85.616583, 41.952076] + "loc": [-85.61658, 41.952093] }, "n3868": { "id": "n3868", @@ -12920,7 +12923,7 @@ }, "n3869": { "id": "n3869", - "loc": [-85.616916, 41.952279] + "loc": [-85.616918, 41.952276] }, "n387": { "id": "n387", @@ -12928,7 +12931,7 @@ }, "n3870": { "id": "n3870", - "loc": [-85.617088, 41.952254] + "loc": [-85.617098, 41.952235] }, "n3871": { "id": "n3871", @@ -13284,7 +13287,7 @@ }, "n3950": { "id": "n3950", - "loc": [-85.616494, 41.951959] + "loc": [-85.616502, 41.951946] }, "n3951": { "id": "n3951", @@ -13517,7 +13520,9 @@ "id": "n4", "loc": [-85.622764, 41.950892], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n40": { @@ -15769,7 +15774,8 @@ "loc": [-85.628201, 41.954694], "tags": { "highway": "stop", - "stop": "all" + "stop": "all", + "direction": "forward" } }, "n4501": { @@ -15777,7 +15783,8 @@ "loc": [-85.627921, 41.954783], "tags": { "highway": "stop", - "stop": "all" + "stop": "all", + "direction": "backward" } }, "n4502": { @@ -15785,7 +15792,8 @@ "loc": [-85.62775, 41.954696], "tags": { "highway": "stop", - "stop": "all" + "stop": "all", + "direction": "backward" } }, "n4503": { @@ -15793,35 +15801,44 @@ "loc": [-85.628046, 41.954591], "tags": { "highway": "stop", - "stop": "all" + "stop": "all", + "direction": "forward" } }, "n4504": { "id": "n4504", "loc": [-85.631074, 41.957428], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n4505": { "id": "n4505", "loc": [-85.630768, 41.957429], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4506": { "id": "n4506", "loc": [-85.629888, 41.957432], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n4507": { "id": "n4507", "loc": [-85.629565, 41.957433], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "forward" } }, "n4508": { @@ -15931,7 +15948,9 @@ "id": "n4528", "loc": [-85.631073, 41.955913], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "backward" } }, "n4529": { @@ -15969,21 +15988,27 @@ "id": "n4535", "loc": [-85.629675, 41.954564], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4536": { "id": "n4536", "loc": [-85.630881, 41.954806], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "backward" } }, "n4537": { "id": "n4537", "loc": [-85.630879, 41.954564], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "forward" } }, "n4538": { @@ -16023,49 +16048,61 @@ "id": "n4543", "loc": [-85.631045, 41.959036], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "backward" } }, "n4544": { "id": "n4544", "loc": [-85.632071, 41.959029], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "forward" } }, "n4545": { "id": "n4545", "loc": [-85.632257, 41.959027], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "backward" } }, "n4546": { "id": "n4546", "loc": [-85.631966, 41.957427], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4547": { "id": "n4547", "loc": [-85.632297, 41.957426], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n4548": { "id": "n4548", "loc": [-85.631976, 41.955911], "tags": { - "highway": "give_way" + "highway": "give_way", + "direction": "forward" } }, "n4549": { "id": "n4549", "loc": [-85.632272, 41.955911], "tags": { - "highway": "give_way" + "highway": "give_way", + "direction": "backward" } }, "n455": { @@ -16076,14 +16113,18 @@ "id": "n4550", "loc": [-85.632097, 41.954805], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "backward" } }, "n4551": { "id": "n4551", "loc": [-85.632094, 41.954566], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "forward" } }, "n4552": { @@ -16126,7 +16167,9 @@ "id": "n4560", "loc": [-85.622763, 41.95109], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n4561": { @@ -16148,7 +16191,9 @@ "id": "n4564", "loc": [-85.624599, 41.950984], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4565": { @@ -16250,7 +16295,9 @@ "id": "n4583", "loc": [-85.617856, 41.954642], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4584": { @@ -16271,7 +16318,9 @@ "id": "n4586", "loc": [-85.620352, 41.951894], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4587": { @@ -16285,14 +16334,18 @@ "id": "n4588", "loc": [-85.620316, 41.950999], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n4589": { "id": "n4589", "loc": [-85.620311, 41.950131], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n459": { @@ -16310,21 +16363,27 @@ "id": "n4591", "loc": [-85.620301, 41.949239], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n4592": { "id": "n4592", "loc": [-85.620278, 41.947443], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n4593": { "id": "n4593", "loc": [-85.619844, 41.947444], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4594": { @@ -16345,14 +16404,18 @@ "id": "n4596", "loc": [-85.622744, 41.947541], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "backward" } }, "n4597": { "id": "n4597", "loc": [-85.622739, 41.947316], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4598": { @@ -16388,14 +16451,18 @@ "id": "n4601", "loc": [-85.622768, 41.949125], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4602": { "id": "n4602", "loc": [-85.622769, 41.949325], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "backward" } }, "n4603": { @@ -16409,14 +16476,17 @@ "id": "n4604", "loc": [-85.622614, 41.950113], "tags": { - "highway": "give_way" + "highway": "give_way", + "direction": "forward" } }, "n4605": { "id": "n4605", "loc": [-85.624777, 41.949219], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4606": { @@ -16462,7 +16532,9 @@ "id": "n4611", "loc": [-85.62476, 41.947428], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "forward" } }, "n4612": { @@ -16483,7 +16555,7 @@ }, "n4616": { "id": "n4616", - "loc": [-85.61823, 41.9499] + "loc": [-85.618232, 41.949913] }, "n4617": { "id": "n4617", @@ -16628,7 +16700,9 @@ "id": "n4645", "loc": [-85.635815, 41.942638], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4646": { @@ -16863,14 +16937,18 @@ "id": "n4684", "loc": [-85.635566, 41.940102], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4685": { "id": "n4685", "loc": [-85.635961, 41.940125], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "backward" } }, "n4686": { @@ -16934,14 +17012,18 @@ "id": "n4694", "loc": [-85.637038, 41.942513], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4695": { "id": "n4695", "loc": [-85.637174, 41.941354], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n4696": { @@ -16955,14 +17037,16 @@ "id": "n4697", "loc": [-85.638058, 41.941346], "tags": { - "highway": "give_way" + "highway": "give_way", + "direction": "forward" } }, "n4698": { "id": "n4698", "loc": [-85.638359, 41.941344], "tags": { - "highway": "give_way" + "highway": "give_way", + "direction": "backward" } }, "n4699": { @@ -16991,14 +17075,16 @@ "id": "n4701", "loc": [-85.639277, 41.941337], "tags": { - "highway": "give_way" + "highway": "give_way", + "direction": "forward" } }, "n4702": { "id": "n4702", "loc": [-85.639548, 41.941334], "tags": { - "highway": "give_way" + "highway": "give_way", + "direction": "backward" } }, "n4703": { @@ -17016,28 +17102,36 @@ "id": "n4705", "loc": [-85.64049, 41.941327], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4706": { "id": "n4706", "loc": [-85.640803, 41.941324], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n4707": { "id": "n4707", "loc": [-85.641717, 41.941317], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "all" } }, "n4708": { "id": "n4708", "loc": [-85.641846, 41.941415], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "all" } }, "n4709": { @@ -17058,21 +17152,27 @@ "id": "n4710", "loc": [-85.642014, 41.941313], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "all" } }, "n4711": { "id": "n4711", "loc": [-85.641854, 41.942455], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4712": { "id": "n4712", "loc": [-85.641859, 41.942739], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n4713": { @@ -17086,14 +17186,18 @@ "id": "n4714", "loc": [-85.640669, 41.942716], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n4715": { "id": "n4715", "loc": [-85.640664, 41.942478], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4716": { @@ -17107,14 +17211,18 @@ "id": "n4717", "loc": [-85.639455, 41.942731], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n4718": { "id": "n4718", "loc": [-85.63945, 41.942492], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4719": { @@ -17132,14 +17240,18 @@ "id": "n4720", "loc": [-85.638238, 41.942745], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n4721": { "id": "n4721", "loc": [-85.638233, 41.942511], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4722": { @@ -17167,21 +17279,27 @@ "id": "n4725", "loc": [-85.63704, 41.942741], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4726": { "id": "n4726", "loc": [-85.633467, 41.943818], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "backward" } }, "n4727": { "id": "n4727", "loc": [-85.633987, 41.943531], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4728": { @@ -17288,7 +17406,9 @@ "id": "n4741", "loc": [-85.63481, 41.946056], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "backward" } }, "n4742": { @@ -17313,14 +17433,18 @@ "id": "n4745", "loc": [-85.639487, 41.945042], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4746": { "id": "n4746", "loc": [-85.639635, 41.94387], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "backward" } }, "n4747": { @@ -17334,14 +17458,18 @@ "id": "n4748", "loc": [-85.64055, 41.943862], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4749": { "id": "n4749", "loc": [-85.640864, 41.943859], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "backward" } }, "n475": { @@ -17352,7 +17480,9 @@ "id": "n4750", "loc": [-85.640718, 41.945022], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4751": { @@ -17366,7 +17496,9 @@ "id": "n4752", "loc": [-85.641913, 41.94502], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4753": { @@ -17380,21 +17512,25 @@ "id": "n4754", "loc": [-85.642045, 41.94385], "tags": { - "highway": "give_way" + "highway": "give_way", + "direction": "backward" } }, "n4755": { "id": "n4755", "loc": [-85.641738, 41.943852], "tags": { - "highway": "give_way" + "highway": "give_way", + "direction": "forward" } }, "n4756": { "id": "n4756", "loc": [-85.642928, 41.943843], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "forward" } }, "n4757": { @@ -17408,14 +17544,18 @@ "id": "n4758", "loc": [-85.642986, 41.945105], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "all" } }, "n4759": { "id": "n4759", "loc": [-85.643136, 41.94502], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "all", + "direction": "forward" } }, "n476": { @@ -27090,7 +27230,25 @@ }, "w660": { "id": "w660", - "nodes": ["n3982", "n3842", "n3864", "n3865", "n3866", "n3867", "n3868", "n3858", "n3869", "n3870", "n3862"], + "nodes": [ + "n3982", + "n3842", + "n3864", + "n3865", + "n2938", + "n3866", + "n2939", + "n3867", + "n3868", + "n3858", + "n2937", + "n3869", + "n2935", + "n2934", + "n3870", + "n3348", + "n3862" + ], "tags": { "highway": "service" } @@ -27299,8 +27457,12 @@ "n4002", "n4003", "n3949", + "n3351", "n3950", + "n3354", + "n3350", "n3951", + "n3349", "n3952", "n3953", "n3954", @@ -29256,6 +29418,46 @@ "tags": { "amenity": "parking" } + }, + "n2934": { + "id": "n2934", + "loc": [-85.617051, 41.952263] + }, + "n2935": { + "id": "n2935", + "loc": [-85.61699, 41.952276] + }, + "n2937": { + "id": "n2937", + "loc": [-85.616847, 41.952262] + }, + "n2938": { + "id": "n2938", + "loc": [-85.616577, 41.951956] + }, + "n2939": { + "id": "n2939", + "loc": [-85.61656, 41.952044] + }, + "n3348": { + "id": "n3348", + "loc": [-85.61714, 41.9522] + }, + "n3349": { + "id": "n3349", + "loc": [-85.616517, 41.95212] + }, + "n3350": { + "id": "n3350", + "loc": [-85.616489, 41.952033] + }, + "n3351": { + "id": "n3351", + "loc": [-85.616529, 41.951907] + }, + "n3354": { + "id": "n3354", + "loc": [-85.616488, 41.951994] } } } From 9c27893748efb7dafaaa08a7c8dc1fe88dff50c8 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sun, 31 Dec 2017 02:26:19 -0500 Subject: [PATCH 68/70] Check for valid multipolygon geometry when dragging nodes (this can get a bit expensive for large/complex multipolygons) --- modules/geo/geom.js | 27 +++++++++++--------- modules/geo/index.js | 1 + modules/modes/drag_node.js | 51 ++++++++++++++++++++++++++++++++++---- test/spec/geo/geom.js | 6 +++++ 4 files changed, 68 insertions(+), 17 deletions(-) diff --git a/modules/geo/geom.js b/modules/geo/geom.js index dbef35800..da76cb607 100644 --- a/modules/geo/geom.js +++ b/modules/geo/geom.js @@ -157,6 +157,20 @@ export function geoPathIntersections(path1, path2) { return intersections; } +export function geoPathHasIntersections(path1, path2) { + for (var i = 0; i < path1.length - 1; i++) { + for (var j = 0; j < path2.length - 1; j++) { + var a = [ path1[i], path1[i+1] ]; + var b = [ path2[j], path2[j+1] ]; + var hit = geoLineIntersection(a, b); + if (hit) { + return true; + } + } + } + return false; +} + // Return whether point is contained in polygon. // @@ -195,24 +209,13 @@ export function geoPolygonContainsPolygon(outer, inner) { export function geoPolygonIntersectsPolygon(outer, inner, checkSegments) { - function testSegments(outer, inner) { - for (var i = 0; i < outer.length - 1; i++) { - for (var j = 0; j < inner.length - 1; j++) { - var a = [ outer[i], outer[i + 1] ]; - var b = [ inner[j], inner[j + 1] ]; - if (geoLineIntersection(a, b)) return true; - } - } - return false; - } - function testPoints(outer, inner) { return _some(inner, function(point) { return geoPointInPolygon(point, outer); }); } - return testPoints(outer, inner) || (!!checkSegments && testSegments(outer, inner)); + return testPoints(outer, inner) || (!!checkSegments && geoPathHasIntersections(outer, inner)); } diff --git a/modules/geo/index.js b/modules/geo/index.js index 982596f04..0c80b12d6 100644 --- a/modules/geo/index.js +++ b/modules/geo/index.js @@ -16,6 +16,7 @@ export { geoEdgeEqual } from './geom.js'; export { geoHasSelfIntersections } from './geom.js'; export { geoRotate } from './geom.js'; export { geoLineIntersection } from './geom.js'; +export { geoPathHasIntersections } from './geom.js'; export { geoPathIntersections } from './geom.js'; export { geoPathLength } from './geom.js'; export { geoPointInPolygon } from './geom.js'; diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index 4251fb7a8..fff0f08a0 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -1,3 +1,5 @@ +import _find from 'lodash-es/find'; + import { event as d3_event, select as d3_select @@ -21,12 +23,13 @@ import { import { geoChooseEdge, geoHasSelfIntersections, + geoPathHasIntersections, geoVecSubtract, geoViewportEdge } from '../geo'; import { modeBrowse, modeSelect } from './index'; -import { osmNode } from '../osm'; +import { osmJoinWays, osmNode } from '../osm'; import { uiFlash } from '../ui'; @@ -174,15 +177,53 @@ export function modeDragNode(context) { function invalidGeometry(entity, graph) { var parents = graph.parentWays(entity); + var i, j, k; - for (var i = 0; i < parents.length; i++) { + for (i = 0; i < parents.length; i++) { var parent = parents[i]; - var nodes = parent.nodes.map(function(nodeID) { return graph.entity(nodeID); }); - if (parent.isClosed()) { - if (geoHasSelfIntersections(nodes, entity.id)) { + var nodes = []; + var activeIndex = null; // which multipolygon ring contains node being dragged + + // test any parent multipolygons for valid geometry + var relations = graph.parentRelations(parent); + for (j = 0; j < relations.length; j++) { + if (!relations[j].isMultipolygon()) continue; + + var rings = osmJoinWays(relations[j].members, graph); + + // find active ring and test it for self intersections + for (k = 0; k < rings.length; k++) { + nodes = rings[k].nodes; + if (_find(nodes, function(n) { return n.id === entity.id; })) { + activeIndex = k; + if (geoHasSelfIntersections(nodes, entity.id)) { + return true; + } + } + rings[k].coords = nodes.map(function(n) { return n.loc; }); + } + + // test active ring for intersections with other rings in the multipolygon + for (k = 0; k < rings.length; k++) { + if (k === activeIndex) continue; + + // make sure active ring doesnt cross passive rings + if (geoPathHasIntersections(rings[activeIndex].coords, rings[k].coords)) { + return true; + } + } + } + + + // If we still haven't tested this node's parent way for self-intersections. + // (because it's not a member of a multipolygon), test it now. + if (activeIndex !== null && parent.isClosed()) { + nodes = parent.nodes.map(function(nodeID) { return graph.entity(nodeID); }); + if (nodes.length && geoHasSelfIntersections(nodes, entity.id)) { return true; } } + } return false; diff --git a/test/spec/geo/geom.js b/test/spec/geo/geom.js index 3acf7f940..188c738fd 100644 --- a/test/spec/geo/geom.js +++ b/test/spec/geo/geom.js @@ -342,6 +342,12 @@ describe('iD.geo - geometry', function() { expect(iD.geoPolygonIntersectsPolygon(outer, inner)).to.be.true; }); + it('returns false when inner polygon fully contains outer', function() { + var inner = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; + var outer = [[1, 1], [1, 2], [2, 2], [2, 1], [1, 1]]; + expect(iD.geoPolygonIntersectsPolygon(outer, inner)).to.be.false; + }); + it('returns true when outer polygon partially contains inner (some vertices contained)', function() { var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; var inner = [[-1, -1], [1, 2], [2, 2], [2, 1], [1, 1]]; From 6881205d43a56a6446e55182191a75c4ae23a78a Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 1 Jan 2018 22:37:10 -0500 Subject: [PATCH 69/70] All touch targets are GeoJSON now This makes the code a bit more consistent and lets us avoid some hacky and probably non-performant things: - abusing CSS classes in the draw/drag datum functions (classed `.target`) (is this thing target? just check d.properties) - regexing the id for `-nope$` (is this thing a nope target? just check d.properties) - using context.hasEntity to get a the real entity (is this thing a real osmEntity? just check d.properties) - fixes code like the restriction editor which uses fake ids for split ways --- css/20_map.css | 1 + modules/behavior/drag.js | 15 +++-------- modules/behavior/draw.js | 9 ++++--- modules/behavior/draw_way.js | 12 ++++----- modules/behavior/hover.js | 16 +++++------ modules/behavior/select.js | 2 +- modules/modes/drag_node.js | 22 +++++++++------ modules/svg/areas.js | 4 +-- modules/svg/helpers.js | 45 +++++++++++++++++++------------ modules/svg/lines.js | 4 +-- modules/svg/midpoints.js | 23 +++++++++++++--- modules/svg/points.js | 24 +++++++++++++---- modules/svg/vertices.js | 37 +++++++++++++++++-------- modules/ui/fields/restrictions.js | 4 +++ 14 files changed, 140 insertions(+), 78 deletions(-) diff --git a/css/20_map.css b/css/20_map.css index 0c0905799..d162eef12 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -240,6 +240,7 @@ g.turn circle { .form-field-restrictions .vertex { cursor: auto !important; + pointer-events: none; } .lasso #map { diff --git a/modules/behavior/drag.js b/modules/behavior/drag.js index b9ea9c0af..f32c952ce 100644 --- a/modules/behavior/drag.js +++ b/modules/behavior/drag.js @@ -36,7 +36,6 @@ export function behaviorDrag() { var dispatch = d3_dispatch('start', 'move', 'end'); var _origin = null; var _selector = ''; - var _filter = null; var _event; var _target; var _surface; @@ -162,9 +161,10 @@ export function behaviorDrag() { var root = this; var target = d3_event.target; for (; target && target !== root; target = target.parentNode) { - if (target[matchesSelector](_selector) && - (!_filter || _filter(target.__data__))) { - return dragstart.call(target, target.__data__); + var datum = target.__data__; + var entity = datum && datum.properties && datum.properties.entity; + if (entity && target[matchesSelector](_selector)) { + return dragstart.call(target, entity); } } }; @@ -190,13 +190,6 @@ export function behaviorDrag() { }; - drag.filter = function(_) { - if (!arguments.length) return _filter; - _filter = _; - return drag; - }; - - drag.origin = function (_) { if (!arguments.length) return _origin; _origin = _; diff --git a/modules/behavior/draw.js b/modules/behavior/draw.js index 77315e03b..5866d3c26 100644 --- a/modules/behavior/draw.js +++ b/modules/behavior/draw.js @@ -44,6 +44,8 @@ export function behaviorDraw(context) { var _lastMouse = null; + // related code + // - `mode/drag_node.js` `datum()` function datum() { if (d3_event.altKey) return {}; @@ -54,11 +56,10 @@ export function behaviorDraw(context) { element = d3_event.target; } - // When drawing, connect only to things classed as targets.. + // When drawing, snap only to touch targets.. // (this excludes area fills and active drawing elements) - var selection = d3_select(element); - if (!selection.classed('target')) return {}; - return selection.datum(); + var d = element.__data__; + return (d && d.properties && d.properties.target) ? d : {}; } diff --git a/modules/behavior/draw_way.js b/modules/behavior/draw_way.js index 4a49856e9..6c43c6ea9 100644 --- a/modules/behavior/draw_way.js +++ b/modules/behavior/draw_way.js @@ -39,13 +39,14 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // - `behavior/draw.js` `click()` // - `behavior/draw_way.js` `move()` function move(datum) { + var nodeLoc = datum && datum.properties && datum.properties.entity && datum.properties.entity.loc; var nodeGroups = datum && datum.properties && datum.properties.nodes; var loc = context.map().mouseCoordinates(); - if (datum.loc) { // snap to node/vertex - a real entity or a nope target with a `loc` - loc = datum.loc; + if (nodeLoc) { // snap to node/vertex - a point target with `.loc` + loc = nodeLoc; - } else if (nodeGroups) { // snap to way - a line touch target or nope target with nodes + } else if (nodeGroups) { // snap to way - a line target with `.nodes` var best = Infinity; for (var i = 0; i < nodeGroups.length; i++) { var childNodes = nodeGroups[i].map(function(id) { return context.entity(id); }); @@ -169,9 +170,8 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // Accept the current position of the drawing node and continue drawing. - drawWay.add = function(loc, datum) { - if ((datum && datum.id && /-nope$/.test(datum.id)) || - context.surface().classed('nope')) { + drawWay.add = function(loc, d) { + if ((d && d.properties && d.properties.nope) || context.surface().classed('nope')) { return; // can't click here } diff --git a/modules/behavior/hover.js b/modules/behavior/hover.js index 1a496ee2d..1dbe142d3 100644 --- a/modules/behavior/hover.js +++ b/modules/behavior/hover.js @@ -71,15 +71,15 @@ export function behaviorHover(context) { function mouseover() { if (_buttonDown) return; - var _target = d3_event.target; - enter(_target ? _target.__data__ : null); + var target = d3_event.target; + enter(target ? target.__data__ : null); } function mouseout() { if (_buttonDown) return; - var _target = d3_event.relatedTarget; - enter(_target ? _target.__data__ : null); + var target = d3_event.relatedTarget; + enter(target ? target.__data__ : null); } @@ -97,16 +97,16 @@ export function behaviorHover(context) { } - function enter(d) { - if (d === _target) return; - _target = d; + function enter(datum) { + if (datum === _target) return; + _target = datum; _selection.selectAll('.hover') .classed('hover', false); _selection.selectAll('.hover-suppressed') .classed('hover-suppressed', false); - var entity = _target && _target.id && context.hasEntity(_target.id); + var entity = datum && datum.properties && datum.properties.entity; if (entity && entity.id !== _newId) { // If drawing a way, don't hover on a node that was just placed. #3974 diff --git a/modules/behavior/select.js b/modules/behavior/select.js index 98799e625..9a9c0b34d 100644 --- a/modules/behavior/select.js +++ b/modules/behavior/select.js @@ -115,7 +115,7 @@ export function behaviorSelect(context) { var datum = d3_event.target.__data__ || (lastMouse && lastMouse.target.__data__); var mode = context.mode(); - var entity = datum && datum.id && context.hasEntity(datum.id); + var entity = datum && datum.properties && datum.properties.entity; if (entity) datum = entity; if (datum && datum.type === 'midpoint') { diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index fff0f08a0..faf603f66 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -100,7 +100,7 @@ export function modeDragNode(context) { var midpoint = entity; entity = osmNode(); context.perform(actionAddMidpoint(midpoint, entity)); - entity = context.entity(entity.id); // get post-action entity + entity = context.entity(entity.id); // get post-action entity var vertex = context.surface().selectAll('.' + entity.id); drag.target(vertex.node(), entity); @@ -119,12 +119,17 @@ export function modeDragNode(context) { } + // related code + // - `behavior/draw.js` `datum()` function datum() { var event = d3_event && d3_event.sourceEvent; - if (!event || event.altKey || !d3_select(event.target).classed('target')) { + if (!event || event.altKey) { return {}; } else { - return event.target.__data__ || {}; + // When dragging, snap only to touch targets.. + // (this excludes area fills and active drawing elements) + var d = event.target.__data__; + return (d && d.properties && d.properties.target) ? d : {}; } } @@ -142,12 +147,13 @@ export function modeDragNode(context) { // - `behavior/draw.js` `click()` // - `behavior/draw_way.js` `move()` var d = datum(); + var nodeLoc = d && d.properties && d.properties.entity && d.properties.entity.loc; var nodeGroups = d && d.properties && d.properties.nodes; - if (d.loc) { // snap to node/vertex - a real entity or a nope target with a `loc` - loc = d.loc; + if (nodeLoc) { // snap to node/vertex - a point target with `.loc` + loc = nodeLoc; - } else if (nodeGroups) { // snap to way - a line touch target or nope target with nodes + } else if (nodeGroups) { // snap to way - a line target with `.nodes` var best = Infinity; for (var i = 0; i < nodeGroups.length; i++) { var childNodes = nodeGroups[i].map(function(id) { return context.entity(id); }); @@ -250,8 +256,8 @@ export function modeDragNode(context) { if (_isCancelled) return; var d = datum(); - var nope = (d && d.id && /-nope$/.test(d.id)) || context.surface().classed('nope'); - var target = d && d.id && context.hasEntity(d.id); // entity to snap to + var nope = (d && d.properties && d.properties.nope) || context.surface().classed('nope'); + var target = d && d.properties && d.properties.entity; // entity to snap to if (nope) { // bounce back context.perform( diff --git a/modules/svg/areas.js b/modules/svg/areas.js index 90f35eb57..8826ddcad 100644 --- a/modules/svg/areas.js +++ b/modules/svg/areas.js @@ -59,7 +59,7 @@ export function svgAreas(projection, context) { // Targets allow hover and vertex snapping var targets = selection.selectAll('.area.target-allowed') - .filter(filter) + .filter(function(d) { return filter(d.properties.entity); }) .data(data.targets, function key(d) { return d.id; }); // exit @@ -76,7 +76,7 @@ export function svgAreas(projection, context) { // NOPE var nopes = selection.selectAll('.area.target-nope') - .filter(function(d) { return filter({ id: d.properties.originalID }); }) + .filter(function(d) { return filter(d.properties.entity); }) .data(data.nopes, function key(d) { return d.id; }); // exit diff --git a/modules/svg/helpers.js b/modules/svg/helpers.js index 40f28d0a6..3ca51ea8d 100644 --- a/modules/svg/helpers.js +++ b/modules/svg/helpers.js @@ -155,11 +155,17 @@ export function svgPath(projection, graph, isArea) { export function svgPointTransform(projection) { - return function(entity) { + var svgpoint = function(entity) { // http://jsperf.com/short-array-join var pt = projection(entity.loc); return 'translate(' + pt[0] + ',' + pt[1] + ')'; }; + + svgpoint.geojson = function(d) { + return svgpoint(d.properties.entity); + }; + + return svgpoint; } @@ -184,7 +190,7 @@ export function svgSegmentWay(way, graph, activeID) { var coords = []; var nodes = []; var startType = null; // 0 = active, 1 = passive, 2 = adjacent - var currType = null; + var currType = null; // 0 = active, 1 = passive, 2 = adjacent var node; for (var i = 0; i < way.nodes.length; i++) { @@ -244,29 +250,34 @@ export function svgSegmentWay(way, graph, activeID) { if (coordGroups.passive.length) { features.passive.push({ - 'type': 'Feature', - 'id': way.id, - 'properties': { - 'nodes': nodeGroups.passive + type: 'Feature', + id: way.id, + properties: { + target: true, + entity: way, + nodes: nodeGroups.passive }, - 'geometry': { - 'type': 'MultiLineString', - 'coordinates': coordGroups.passive + geometry: { + type: 'MultiLineString', + coordinates: coordGroups.passive } }); } if (coordGroups.active.length) { features.active.push({ - 'type': 'Feature', - 'id': way.id + '-nope', // break the ids on purpose - 'properties': { - 'originalID': way.id, - 'nodes': nodeGroups.active + type: 'Feature', + id: way.id + '-nope', // break the ids on purpose + properties: { + target: true, + entity: way, + nodes: nodeGroups.active, + nope: true, + originalID: way.id }, - 'geometry': { - 'type': 'MultiLineString', - 'coordinates': coordGroups.active + geometry: { + type: 'MultiLineString', + coordinates: coordGroups.active } }); } diff --git a/modules/svg/lines.js b/modules/svg/lines.js index 67e433ab8..727d0178b 100644 --- a/modules/svg/lines.js +++ b/modules/svg/lines.js @@ -55,7 +55,7 @@ export function svgLines(projection, context) { // Targets allow hover and vertex snapping var targets = selection.selectAll('.line.target-allowed') - .filter(filter) + .filter(function(d) { return filter(d.properties.entity); }) .data(data.targets, function key(d) { return d.id; }); // exit @@ -72,7 +72,7 @@ export function svgLines(projection, context) { // NOPE var nopes = selection.selectAll('.line.target-nope') - .filter(function(d) { return filter({ id: d.properties.originalID }); }) + .filter(function(d) { return filter(d.properties.entity); }) .data(data.nopes, function key(d) { return d.id; }); // exit diff --git a/modules/svg/midpoints.js b/modules/svg/midpoints.js index bcf9495b2..0f68d949c 100644 --- a/modules/svg/midpoints.js +++ b/modules/svg/midpoints.js @@ -18,9 +18,26 @@ export function svgMidpoints(projection, context) { function drawTargets(selection, graph, entities, filter) { var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; + var getTransform = svgPointTransform(projection).geojson; + + var data = entities.map(function(midpoint) { + return { + type: 'Feature', + id: midpoint.id, + properties: { + target: true, + entity: midpoint + }, + geometry: { + type: 'Point', + coordinates: midpoint.loc + } + }; + }); + var targets = selection.selectAll('.midpoint.target') - .filter(filter) - .data(entities, function key(d) { return d.id; }); + .filter(function(d) { return filter(d.properties.entity); }) + .data(data, function key(d) { return d.id; }); // exit targets.exit() @@ -32,7 +49,7 @@ export function svgMidpoints(projection, context) { .attr('r', targetRadius) .merge(targets) .attr('class', function(d) { return 'node midpoint target ' + fillClass + d.id; }) - .attr('transform', svgPointTransform(projection)); + .attr('transform', getTransform); } diff --git a/modules/svg/points.js b/modules/svg/points.js index 0b3366ab3..c2f1fc0eb 100644 --- a/modules/svg/points.js +++ b/modules/svg/points.js @@ -29,13 +29,27 @@ export function svgPoints(projection, context) { function drawTargets(selection, graph, entities, filter) { var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; - var passive = entities.filter(function(d) { - return d.id !== context.activeID(); + var getTransform = svgPointTransform(projection).geojson; + var activeID = context.activeID(); + var data = []; + + entities.forEach(function(node) { + if (activeID === node.id) return; // draw no target on the activeID + + data.push({ + type: 'Feature', + id: node.id, + properties: { + target: true, + entity: node + }, + geometry: node.asGeoJSON() + }); }); var targets = selection.selectAll('.point.target') - .filter(filter) - .data(passive, function key(d) { return d.id; }); + .filter(function(d) { return filter(d.properties.entity); }) + .data(data, function key(d) { return d.id; }); // exit targets.exit() @@ -50,7 +64,7 @@ export function svgPoints(projection, context) { .attr('height', 30) .merge(targets) .attr('class', function(d) { return 'node point target ' + fillClass + d.id; }) - .attr('transform', svgPointTransform(projection)); + .attr('transform', getTransform); } diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index 1a5ed0a88..c6843a3af 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -184,20 +184,35 @@ export function svgVertices(projection, context) { function drawTargets(selection, graph, entities, filter) { var targetClass = context.getDebug('target') ? 'pink ' : 'nocolor '; var nopeClass = context.getDebug('target') ? 'red ' : 'nocolor '; + var getTransform = svgPointTransform(projection).geojson; var activeID = context.activeID(); var data = { targets: [], nopes: [] }; entities.forEach(function(node) { if (activeID === node.id) return; // draw no target on the activeID - var currType = svgPassiveVertex(node, graph, activeID); - if (currType !== 0) { - data.targets.push(node); // passive or adjacent - allow to connect + var vertexType = svgPassiveVertex(node, graph, activeID); + if (vertexType !== 0) { // passive or adjacent - allow to connect + data.targets.push({ + type: 'Feature', + id: node.id, + properties: { + target: true, + entity: node + }, + geometry: node.asGeoJSON() + }); } else { data.nopes.push({ - id: node.id + '-nope', // not a real osmNode, break the id on purpose - originalID: node.id, - loc: node.loc + type: 'Feature', + id: node.id + '-nope', // break the ids on purpose + properties: { + target: true, + entity: node, + nope: true, + originalID: node.id + }, + geometry: node.asGeoJSON() }); } }); @@ -205,7 +220,7 @@ export function svgVertices(projection, context) { // Targets allow hover and vertex snapping var targets = selection.selectAll('.vertex.target-allowed') - .filter(filter) + .filter(function(d) { return filter(d.properties.entity); }) .data(data.targets, function key(d) { return d.id; }); // exit @@ -218,12 +233,12 @@ export function svgVertices(projection, context) { .attr('r', function(d) { return (_radii[d.id] || radiuses.shadow[3]); }) .merge(targets) .attr('class', function(d) { return 'node vertex target target-allowed ' + targetClass + d.id; }) - .attr('transform', svgPointTransform(projection)); + .attr('transform', getTransform); // NOPE var nopes = selection.selectAll('.vertex.target-nope') - .filter(function(d) { return filter({ id: d.originalID }); }) + .filter(function(d) { return filter(d.properties.entity); }) .data(data.nopes, function key(d) { return d.id; }); // exit @@ -233,10 +248,10 @@ export function svgVertices(projection, context) { // enter/update nopes.enter() .append('circle') - .attr('r', function(d) { return (_radii[d.id.replace('-nope','')] || radiuses.shadow[3]); }) + .attr('r', function(d) { return (_radii[d.properties.originalID] || radiuses.shadow[3]); }) .merge(nopes) .attr('class', function(d) { return 'node vertex target target-nope ' + nopeClass + d.id; }) - .attr('transform', svgPointTransform(projection)); + .attr('transform', getTransform); } diff --git a/modules/ui/fields/restrictions.js b/modules/ui/fields/restrictions.js index f56bc868d..dde654d45 100644 --- a/modules/ui/fields/restrictions.js +++ b/modules/ui/fields/restrictions.js @@ -154,9 +154,13 @@ export function uiFieldRestrictions(field, context) { .call(breathe); var datum = d3_event.target.__data__; + var entity = datum && datum.properties && datum.properties.entity; + if (entity) datum = entity; + if (datum instanceof osmEntity) { fromNodeID = intersection.adjacentNodeId(datum.id); render(); + } else if (datum instanceof osmTurn) { if (datum.restriction) { context.perform( From ddaa828ec99f79af4a510811f2b1b84acd5c8fdc Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 1 Jan 2018 22:53:40 -0500 Subject: [PATCH 70/70] Handle both target GeoJSON and osmEntity in hover (fixes test and allows hover on the area fills again) --- modules/behavior/hover.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/modules/behavior/hover.js b/modules/behavior/hover.js index 1dbe142d3..e57687369 100644 --- a/modules/behavior/hover.js +++ b/modules/behavior/hover.js @@ -6,6 +6,7 @@ import { } from 'd3-selection'; import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js'; +import { osmEntity } from '../osm'; import { utilRebind } from '../util/rebind'; @@ -106,9 +107,14 @@ export function behaviorHover(context) { _selection.selectAll('.hover-suppressed') .classed('hover-suppressed', false); - var entity = datum && datum.properties && datum.properties.entity; - if (entity && entity.id !== _newId) { + var entity; + if (datum instanceof osmEntity) { + entity = datum; + } else { + entity = datum && datum.properties && datum.properties.entity; + } + if (entity && entity.id !== _newId) { // If drawing a way, don't hover on a node that was just placed. #3974 var mode = context.mode() && context.mode().id; if ((mode === 'draw-line' || mode === 'draw-area') && !_newId && entity.type === 'node') {