decoupled graph from context for better testing

fixed feature matching tests for vertices and relations
This commit is contained in:
Bryan Housel
2014-10-28 13:18:55 -04:00
parent 2b260f9a6c
commit 43f1cdd3eb
4 changed files with 110 additions and 71 deletions

View File

@@ -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 */

View File

@@ -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');

View File

@@ -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});
}

View File

@@ -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');
});