diff --git a/js/id/id.js b/js/id/id.js index fd18f9a5a..20c2795e0 100644 --- a/js/id/id.js +++ b/js/id/id.js @@ -207,8 +207,9 @@ window.iD = function () { var features = iD.Features(context); context.features = function() { return features; }; context.hasHiddenConnections = function(id) { - var entity = history.graph().entity(id); - return features.hasHiddenConnections(entity); + var graph = history.graph(), + entity = graph.entity(id); + return features.hasHiddenConnections(entity, graph); }; /* Map */ diff --git a/js/id/renderer/features.js b/js/id/renderer/features.js index d1f32adda..f1fbd8fb2 100644 --- a/js/id/renderer/features.js +++ b/js/id/renderer/features.js @@ -42,6 +42,7 @@ iD.Features = function(context) { }; var dispatch = d3.dispatch('change', 'redraw'), + resolver = context.graph(), feature = {}, cullFactor = 1; @@ -59,31 +60,31 @@ iD.Features = function(context) { defaultMax: (max || Infinity), enable: function() { this.enabled = true; this.currentMax = this.defaultMax; }, disable: function() { this.enabled = false; this.currentMax = 0; }, - hidden: function() { return this.count >= this.currentMax * cullFactor; }, + hidden: function() { return this.count > this.currentMax * cullFactor; }, autoHidden: function() { return this.hidden() && this.currentMax > 0; } }; } defineFeature('points', function(entity) { - return entity.geometry(context.graph()) === 'point'; + return entity.geometry(resolver) === 'point'; }, 200); defineFeature('major_roads', function(entity) { - return entity.geometry(context.graph()) === 'line' && major_roads[entity.tags.highway]; + return entity.geometry(resolver) === 'line' && major_roads[entity.tags.highway]; }); defineFeature('minor_roads', function(entity) { - return entity.geometry(context.graph()) === 'line' && minor_roads[entity.tags.highway]; + return entity.geometry(resolver) === 'line' && minor_roads[entity.tags.highway]; }); defineFeature('paths', function(entity) { - return entity.geometry(context.graph()) === 'line' && paths[entity.tags.highway]; + return entity.geometry(resolver) === 'line' && paths[entity.tags.highway]; }); defineFeature('buildings', function(entity) { return ( - entity.geometry(context.graph()) === 'area' && ( + entity.geometry(resolver) === 'area' && ( (!!entity.tags.building && entity.tags.building !== 'no') || entity.tags.amenity === 'shelter' || entity.tags.parking === 'multi-storey' || @@ -95,7 +96,7 @@ iD.Features = function(context) { }, 250); defineFeature('landuse', function(entity) { - return entity.geometry(context.graph()) === 'area' && + return entity.geometry(resolver) === 'area' && !feature.buildings.filter(entity) && !feature.water.filter(entity); }); @@ -145,8 +146,8 @@ iD.Features = function(context) { // lines or areas that don't match another feature filter. defineFeature('others', function(entity) { return ( - entity.geometry(context.graph()) === 'line' || - entity.geometry(context.graph()) === 'area' + entity.geometry(resolver) === 'line' || + entity.geometry(resolver) === 'area' ) && _.reduce(_.omit(feature, 'others'), function(result, v) { return result && !v.filter(entity); @@ -213,20 +214,18 @@ iD.Features = function(context) { return feature[k] && feature[k].count; }; - features.resetStats = function() { - var dimensions = context.map().dimensions(); + + features.gatherStats = function(d, graph, dimensions) { + var hidden = features.hidden(), + keys = features.keys(); + resolver = graph || resolver; + _.each(feature, function(f) { f.count = 0; }); // adjust the threshold for point/building culling based on viewport size.. // a cullFactor of 1 corresponds to a 1000x1000px viewport.. cullFactor = dimensions[0] * dimensions[1] / 1000000; - }; - features.gatherStats = function(d) { - var hidden = features.hidden(), - keys = features.keys(); - - features.resetStats(); _.each(d, function(entity) { _.each(keys, function(k) { if (feature[k].filter(entity)) { @@ -248,43 +247,54 @@ iD.Features = function(context) { return stats; }; - features.isHiddenFeature = function(entity) { + features.isHiddenFeature = function(entity, graph) { + resolver = graph || resolver; return _.any(features.hidden(), function(k) { return feature[k].filter(entity); }); }; - features.isHiddenChild = function(entity) { - var g = context.graph(), - parents = _.union(g.parentWays(entity), g.parentRelations(entity)); - return parents.length ? _.all(parents, features.isHidden) : false; + features.isHiddenChild = function(entity, graph) { + var parents; + resolver = graph || resolver; + + parents = _.union(resolver.parentWays(entity), resolver.parentRelations(entity)); + return parents.length ? _.all(parents, function(e) { + return features.isHidden(e, resolver); + }) : false; }; - features.hasHiddenConnections = function(entity) { - var g = context.graph(), - childNodes, connections; + features.hasHiddenConnections = function(entity, graph) { + var childNodes, connections; + resolver = graph || resolver; if (entity.type === 'midpoint') { - childNodes = [context.entity(entity.edge[0]), context.entity(entity.edge[1])]; + childNodes = [resolver.entity(entity.edge[0]), resolver.entity(entity.edge[1])]; } else { - childNodes = g.childNodes(entity); + childNodes = resolver.childNodes(entity); } // gather parents.. - connections = _.union(g.parentWays(entity), g.parentRelations(entity)); + connections = _.union(resolver.parentWays(entity), resolver.parentRelations(entity)); // gather ways connected to child nodes.. connections = _.reduce(childNodes, function(result, e) { - return g.isShared(e) ? _.union(result, g.parentWays(e)) : result; + return resolver.isShared(e) ? _.union(result, resolver.parentWays(e)) : result; }, connections); - return connections.length ? _.any(connections, features.isHidden) : false; + return connections.length ? _.any(connections, function(e) { + return features.isHidden(e, resolver); + }) : false; }; - features.isHidden = function(entity) { + features.isHidden = function(entity, graph) { + resolver = graph || resolver; return !!entity.version && - (features.isHiddenFeature(entity) || features.isHiddenChild(entity)); + (features.isHiddenFeature(entity, resolver) || features.isHiddenChild(entity, resolver)); }; - features.filter = function(d) { - return features.hidden().length ? _.reject(d, features.isHidden) : d; + features.filter = function(d, graph) { + resolver = graph || resolver; + return features.hidden().length ? _.reject(d, function(e) { + return features.isHidden(e, resolver); + }) : d; }; return d3.rebind(features, dispatch, 'on'); diff --git a/js/id/renderer/map.js b/js/id/renderer/map.js index c33678418..e26b5cb0a 100644 --- a/js/id/renderer/map.js +++ b/js/id/renderer/map.js @@ -95,34 +95,36 @@ iD.Map = function(context) { function pxCenter() { return [dimensions[0] / 2, dimensions[1] / 2]; } function drawVector(difference, extent) { - var filter, all, - graph = context.graph(); + var graph = context.graph(), + features = context.features(), + all = context.intersects(map.extent()), + data, filter; if (difference) { var complete = difference.complete(map.extent()); - all = _.compact(_.values(complete)); + data = _.compact(_.values(complete)); filter = function(d) { return d.id in complete; }; } else if (extent) { - all = context.intersects(map.extent().intersection(extent)); - var set = d3.set(_.pluck(all, 'id')); + data = context.intersects(map.extent().intersection(extent)); + var set = d3.set(_.pluck(data, 'id')); filter = function(d) { return set.has(d.id); }; } else { - all = context.intersects(map.extent()); + data = all; filter = d3.functor(true); } - context.features().gatherStats(context.intersects(map.extent())); - all = context.features().filter(all); + features.gatherStats(all, graph, dimensions); + data = features.filter(data, graph); surface - .call(vertices, graph, all, filter, map.extent(), map.zoom()) - .call(lines, graph, all, filter) - .call(areas, graph, all, filter) - .call(midpoints, graph, all, filter, map.trimmedExtent()) - .call(labels, graph, all, filter, dimensions, !difference && !extent) - .call(points, all, filter); + .call(vertices, graph, data, filter, map.extent(), map.zoom()) + .call(lines, graph, data, filter) + .call(areas, graph, data, filter) + .call(midpoints, graph, data, filter, map.trimmedExtent()) + .call(labels, graph, data, filter, dimensions, !difference && !extent) + .call(points, data, filter); dispatch.drawn({full: true}); } diff --git a/test/spec/renderer/features.js b/test/spec/renderer/features.js index 1213501ca..b31c642d8 100644 --- a/test/spec/renderer/features.js +++ b/test/spec/renderer/features.js @@ -1,9 +1,9 @@ describe('iD.Features', function() { - var context, features; + var dimensions = [1000, 1000], + features; beforeEach(function() { - context = iD(); - features = context.features(); + features = iD().features(); }); it('returns feature keys', function() { @@ -121,23 +121,26 @@ describe('iD.Features', function() { // Others iD.Way({id: 'fence', tags: {barrier: 'fence'}, version: 1}), iD.Way({id: 'pipeline', tags: {man_made: 'pipeline'}, version: 1}) - ]); + ]), + all = _.values(graph.base().entities); + function doMatch(ids) { _.each(ids, function(id) { - expect(features.isHidden(graph.entity(id)), id).to.be.true; + expect(features.isHidden(graph.entity(id), graph), 'doMatch: ' + id).to.be.true; }); } function dontMatch(ids) { _.each(ids, function(id) { - expect(features.isHidden(graph.entity(id)), id).to.be.false; + expect(features.isHidden(graph.entity(id), graph), 'dontMatch: ' + id).to.be.false; }); } it("matches points", function () { features.disable('points'); + features.gatherStats(all, graph, dimensions); doMatch([ 'point_bar', 'point_dock', 'point_rail_station', @@ -156,6 +159,7 @@ describe('iD.Features', function() { it("matches major roads", function () { features.disable('major_roads'); + features.gatherStats(all, graph, dimensions); doMatch([ 'motorway', 'motorway_link', 'trunk', 'trunk_link', @@ -175,6 +179,7 @@ describe('iD.Features', function() { it("matches minor roads", function () { features.disable('minor_roads'); + features.gatherStats(all, graph, dimensions); doMatch([ 'service', 'living_street', 'road', 'unclassified', 'track' @@ -192,6 +197,7 @@ describe('iD.Features', function() { it("matches paths", function () { features.disable('paths'); + features.gatherStats(all, graph, dimensions); doMatch([ 'path', 'footway', 'cycleway', 'bridleway', @@ -210,6 +216,7 @@ describe('iD.Features', function() { it("matches buildings", function () { features.disable('buildings'); + features.gatherStats(all, graph, dimensions); doMatch([ 'building_yes', 'building_part', 'shelter', @@ -228,6 +235,7 @@ describe('iD.Features', function() { it("matches landuse", function () { features.disable('landuse'); + features.gatherStats(all, graph, dimensions); doMatch([ 'forest', 'scrub', 'industrial', 'parkinglot', 'building_no', @@ -246,6 +254,7 @@ describe('iD.Features', function() { it("matches boundaries", function () { features.disable('boundaries'); + features.gatherStats(all, graph, dimensions); doMatch([ 'boundary' @@ -263,6 +272,7 @@ describe('iD.Features', function() { it("matches water", function () { features.disable('water'); + features.gatherStats(all, graph, dimensions); doMatch([ 'point_dock', 'water', 'coastline', 'bay', 'pond', @@ -281,6 +291,7 @@ describe('iD.Features', function() { it("matches rail", function () { features.disable('rail'); + features.gatherStats(all, graph, dimensions); doMatch([ 'point_rail_station', 'point_old_rail_station', @@ -300,6 +311,7 @@ describe('iD.Features', function() { it("matches power", function () { features.disable('power'); + features.gatherStats(all, graph, dimensions); doMatch([ 'point_generator', 'power_line' @@ -317,6 +329,7 @@ describe('iD.Features', function() { it("matches past/future", function () { features.disable('past_future'); + features.gatherStats(all, graph, dimensions); doMatch([ 'point_old_rail_station', 'rail_disused', @@ -335,6 +348,7 @@ describe('iD.Features', function() { it("matches others", function () { features.disable('others'); + features.gatherStats(all, graph, dimensions); doMatch([ 'fence', 'pipeline' @@ -355,32 +369,44 @@ describe('iD.Features', function() { var a = iD.Node({id: 'a', version: 1}), b = iD.Node({id: 'b', version: 1}), w = iD.Way({id: 'w', nodes: [a.id, b.id], tags: {highway: 'path'}, version: 1}), - graph = iD.Graph([a, b, w]); + graph = iD.Graph([a, b, w]), + all = _.values(graph.base().entities); features.disable('paths'); + features.gatherStats(all, graph, dimensions); - expect(features.isHiddenChild(graph.entity('a'))).to.be.true; - expect(features.isHidden(graph.entity('a'))).to.be.true; + expect(features.isHiddenChild(graph.entity('a'), graph)).to.be.true; + expect(features.isHidden(graph.entity('a'), graph)).to.be.true; features.enable('paths'); }); it('hides child ways on a hidden relation', function() { - var a = iD.Node({id: 'a'}), - b = iD.Node({id: 'b'}), - c = iD.Node({id: 'c'}), - d = iD.Node({id: 'd'}), - e = iD.Node({id: 'e'}), - f = iD.Node({id: 'f'}), - outer = iD.Way({id: 'outer', nodes: [a.id, b.id, c.id, a.id], tags: {natural: 'wood'}}), - inner = iD.Way({id: 'inner', nodes: [d.id, e.id, f.id, d.id]}), - r = iD.Relation({members: [{id: outer.id, type: 'way'}, {id: inner.id, role: 'inner', type: 'way'}]}), - graph = iD.Graph([a, b, c, d, e, f, outer, inner, r]); + var a = iD.Node({id: 'a', version: 1}), + b = iD.Node({id: 'b', version: 1}), + c = iD.Node({id: 'c', version: 1}), + d = iD.Node({id: 'd', version: 1}), + e = iD.Node({id: 'e', version: 1}), + f = iD.Node({id: 'f', version: 1}), + outer = iD.Way({id: 'outer', nodes: [a.id, b.id, c.id, a.id], tags: {area: 'yes', natural: 'wood'}, version: 1}), + inner = iD.Way({id: 'inner', nodes: [d.id, e.id, f.id, d.id], version: 1}), + r = iD.Relation({ + id: 'r', + tags: {type: 'multipolygon'}, + members: [ + {id: outer.id, role: 'outer', type: 'way'}, + {id: inner.id, role: 'inner', type: 'way'} + ], + version: 1 + }), + graph = iD.Graph([a, b, c, d, e, f, outer, inner, r]), + all = _.values(graph.base().entities); features.disable('landuse'); + features.gatherStats(all, graph, dimensions); - expect(features.isHiddenChild(graph.entity('inner'))).to.be.true; - expect(features.isHidden(graph.entity('inner'))).to.be.true; + expect(features.isHiddenChild(graph.entity('inner'), graph)).to.be.true; + expect(features.isHidden(graph.entity('inner'), graph)).to.be.true; features.enable('landuse'); });