Merge pull request #2357 from bhousel/feature-filtering

Feature filtering
This commit is contained in:
John Firebaugh
2014-11-11 12:16:42 -08:00
42 changed files with 1982 additions and 413 deletions

View File

@@ -50,7 +50,15 @@ iD.Graph.prototype = {
},
parentWays: function(entity) {
return _.map(this._parentWays[entity.id], this.entity, this);
var parents = this._parentWays[entity.id],
result = [];
if (parents) {
for (var i = 0, imax = parents.length; i !== imax; i++) {
result.push(this.entity(parents[i]));
}
}
return result;
},
isPoi: function(entity) {
@@ -64,7 +72,15 @@ iD.Graph.prototype = {
},
parentRelations: function(entity) {
return _.map(this._parentRels[entity.id], this.entity, this);
var parents = this._parentRels[entity.id],
result = [];
if (parents) {
for (var i = 0, imax = parents.length; i !== imax; i++) {
result.push(this.entity(parents[i]));
}
}
return result;
},
childNodes: function(entity) {
@@ -72,8 +88,10 @@ iD.Graph.prototype = {
return this._childNodes[entity.id];
var nodes = [];
for (var i = 0, l = entity.nodes.length; i < l; i++) {
nodes[i] = this.entity(entity.nodes[i]);
if (entity.nodes) {
for (var i = 0, l = entity.nodes.length; i < l; i++) {
nodes[i] = this.entity(entity.nodes[i]);
}
}
if (iD.debug) Object.freeze(nodes);

View File

@@ -14,6 +14,13 @@ iD.geo.Extent = function geoExtent(min, max) {
iD.geo.Extent.prototype = new Array(2);
_.extend(iD.geo.Extent.prototype, {
equals: function (obj) {
return this[0][0] === obj[0][0] &&
this[0][1] === obj[0][1] &&
this[1][0] === obj[1][0] &&
this[1][1] === obj[1][1];
},
extend: function(obj) {
if (!(obj instanceof iD.geo.Extent)) obj = new iD.geo.Extent(obj);
return iD.geo.Extent([Math.min(obj[0][0], this[0][0]),

View File

@@ -97,6 +97,7 @@ window.iD = function () {
context.flush = function() {
connection.flush();
features.reset();
history.reset();
return context;
};
@@ -203,6 +204,15 @@ window.iD = function () {
var background = iD.Background(context);
context.background = function() { return background; };
/* Features */
var features = iD.Features();
context.features = function() { return features; };
context.hasHiddenConnections = function(id) {
var graph = history.graph(),
entity = graph.entity(id);
return features.hasHiddenConnections(entity, graph);
};
/* Map */
var map = iD.Map(context);
context.map = function() { return map; };

View File

@@ -48,7 +48,9 @@ iD.modes.DragNode = function(context) {
}
function start(entity) {
cancelled = d3.event.sourceEvent.shiftKey;
cancelled = d3.event.sourceEvent.shiftKey ||
context.features().hasHiddenConnections(entity, context.graph());
if (cancelled) return behavior.cancel();
wasMidpoint = entity.type === 'midpoint';

View File

@@ -137,9 +137,16 @@ iD.modes.Select = function(context, selectedIDs) {
.call(keybinding);
function selectElements() {
context.surface()
.selectAll(iD.util.entityOrMemberSelector(selectedIDs, context.graph()))
.classed('selected', true);
var selection = context.surface()
.selectAll(iD.util.entityOrMemberSelector(selectedIDs, context.graph()));
if (selection.empty()) {
// Exit mode if selected DOM elements have disappeared..
context.enter(iD.modes.Browse(context));
} else {
selection
.classed('selected', true);
}
}
context.map().on('drawn.select', selectElements);

View File

@@ -20,6 +20,8 @@ iD.operations.Circularize = function(selectedIDs, context) {
var reason;
if (extent.percentContainedIn(context.extent()) < 0.8) {
reason = 'too_large';
} else if (context.hasHiddenConnections(entityId)) {
reason = 'connected_to_hidden';
}
return action.disabled(context.graph()) || reason;
};

View File

@@ -23,7 +23,8 @@ iD.operations.Continue = function(selectedIDs, context) {
};
operation.available = function() {
return geometries.vertex.length === 1 && geometries.line.length <= 1;
return geometries.vertex.length === 1 && geometries.line.length <= 1 &&
!context.features().hasHiddenConnections(vertex, context.graph());
};
operation.disabled = function() {

View File

@@ -52,7 +52,11 @@ iD.operations.Delete = function(selectedIDs, context) {
};
operation.disabled = function() {
return action.disabled(context.graph());
var reason;
if (_.any(selectedIDs, context.hasHiddenConnections)) {
reason = 'connected_to_hidden';
}
return action.disabled(context.graph()) || reason;
};
operation.tooltip = function() {

View File

@@ -19,7 +19,11 @@ iD.operations.Disconnect = function(selectedIDs, context) {
};
operation.disabled = function() {
return action.disabled(context.graph());
var reason;
if (_.any(selectedIDs, context.hasHiddenConnections)) {
reason = 'connected_to_hidden';
}
return action.disabled(context.graph()) || reason;
};
operation.tooltip = function() {

View File

@@ -16,6 +16,8 @@ iD.operations.Move = function(selectedIDs, context) {
var reason;
if (extent.area() && extent.percentContainedIn(context.extent()) < 0.8) {
reason = 'too_large';
} else if (_.any(selectedIDs, context.hasHiddenConnections)) {
reason = 'connected_to_hidden';
}
return iD.actions.Move(selectedIDs).disabled(context.graph()) || reason;
};

View File

@@ -21,6 +21,8 @@ iD.operations.Orthogonalize = function(selectedIDs, context) {
var reason;
if (extent.percentContainedIn(context.extent()) < 0.8) {
reason = 'too_large';
} else if (context.hasHiddenConnections(entityId)) {
reason = 'connected_to_hidden';
}
return action.disabled(context.graph()) || reason;
};

View File

@@ -22,6 +22,8 @@ iD.operations.Rotate = function(selectedIDs, context) {
operation.disabled = function() {
if (extent.percentContainedIn(context.extent()) < 0.8) {
return 'too_large';
} else if (context.hasHiddenConnections(entityId)) {
return 'connected_to_hidden';
} else {
return false;
}

View File

@@ -29,7 +29,11 @@ iD.operations.Split = function(selectedIDs, context) {
};
operation.disabled = function() {
return action.disabled(context.graph());
var reason;
if (_.any(selectedIDs, context.hasHiddenConnections)) {
reason = 'connected_to_hidden';
}
return action.disabled(context.graph()) || reason;
};
operation.tooltip = function() {

View File

@@ -16,7 +16,11 @@ iD.operations.Straighten = function(selectedIDs, context) {
};
operation.disabled = function() {
return action.disabled(context.graph());
var reason;
if (context.hasHiddenConnections(entityId)) {
reason = 'connected_to_hidden';
}
return action.disabled(context.graph()) || reason;
};
operation.tooltip = function() {

View File

@@ -64,19 +64,11 @@ iD.Background = function(context) {
base.call(baseLayer);
var gpx = selection.selectAll('.gpx-layer')
.data([0]);
gpx.enter().insert('div', '.layer-data')
.attr('class', 'layer-layer gpx-layer');
gpx.call(gpxLayer);
var overlays = selection.selectAll('.overlay-layer')
var overlays = selection.selectAll('.layer-overlay')
.data(overlayLayers, function(d) { return d.source().name(); });
overlays.enter().insert('div', '.layer-data')
.attr('class', 'layer-layer overlay-layer');
.attr('class', 'layer-layer layer-overlay');
overlays.each(function(layer) {
d3.select(this).call(layer);
@@ -85,6 +77,14 @@ iD.Background = function(context) {
overlays.exit()
.remove();
var gpx = selection.selectAll('.layer-gpx')
.data([0]);
gpx.enter().insert('div')
.attr('class', 'layer-layer layer-gpx');
gpx.call(gpxLayer);
var mapillary = selection.selectAll('.layer-mapillary')
.data([0]);

396
js/id/renderer/features.js Normal file
View File

@@ -0,0 +1,396 @@
iD.Features = function() {
var major_roads = {
'motorway': true,
'motorway_link': true,
'trunk': true,
'trunk_link': true,
'primary': true,
'primary_link': true,
'secondary': true,
'secondary_link': true,
'tertiary': true,
'tertiary_link': true,
'residential': true
};
var minor_roads = {
'service': true,
'living_street': true,
'road': true,
'unclassified': true,
'track': true
};
var paths = {
'path': true,
'footway': true,
'cycleway': true,
'bridleway': true,
'steps': true,
'pedestrian': true
};
var past_futures = {
'proposed': true,
'construction': true,
'abandoned': true,
'dismantled': true,
'disused': true,
'razed': true,
'demolished': true,
'obliterated': true
};
var dispatch = d3.dispatch('change', 'redraw'),
_cullFactor = 1,
_cache = {},
_features = {},
_stats = {},
_keys = [],
_hidden = [];
function update() {
_hidden = features.hidden();
dispatch.change();
dispatch.redraw();
}
function defineFeature(k, filter, max) {
_keys.push(k);
_features[k] = {
filter: filter,
enabled: true, // whether the user wants it enabled..
count: 0,
currentMax: (max || Infinity),
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; },
autoHidden: function() { return this.hidden() && this.currentMax > 0; }
};
}
defineFeature('points', function isPoint(entity, resolver) {
return entity.geometry(resolver) === 'point';
}, 200);
defineFeature('major_roads', function isMajorRoad(entity) {
return major_roads[entity.tags.highway];
});
defineFeature('minor_roads', function isMinorRoad(entity) {
return minor_roads[entity.tags.highway];
});
defineFeature('paths', function isPath(entity) {
return paths[entity.tags.highway];
});
defineFeature('buildings', function isBuilding(entity) {
return (
!!entity.tags['building:part'] ||
(!!entity.tags.building && entity.tags.building !== 'no') ||
entity.tags.amenity === 'shelter' ||
entity.tags.parking === 'multi-storey' ||
entity.tags.parking === 'sheds' ||
entity.tags.parking === 'carports' ||
entity.tags.parking === 'garage_boxes'
);
}, 250);
defineFeature('landuse', function isLanduse(entity, resolver) {
return entity.geometry(resolver) === 'area' &&
!_features.buildings.filter(entity) &&
!_features.water.filter(entity);
});
defineFeature('boundaries', function isBoundary(entity) {
return !!entity.tags.boundary;
});
defineFeature('water', function isWater(entity) {
return (
!!entity.tags.waterway ||
entity.tags.natural === 'water' ||
entity.tags.natural === 'coastline' ||
entity.tags.natural === 'bay' ||
entity.tags.landuse === 'pond' ||
entity.tags.landuse === 'basin' ||
entity.tags.landuse === 'reservoir' ||
entity.tags.landuse === 'salt_pond'
);
});
defineFeature('rail', function isRail(entity) {
return (
!!entity.tags.railway ||
entity.tags.landuse === 'railway'
) && !(
major_roads[entity.tags.highway] ||
minor_roads[entity.tags.highway] ||
paths[entity.tags.highway]
);
});
defineFeature('power', function isPower(entity) {
return !!entity.tags.power;
});
// contains a past/future tag, but not in active use as a road/path/cycleway/etc..
defineFeature('past_future', function isPastFuture(entity) {
if (
major_roads[entity.tags.highway] ||
minor_roads[entity.tags.highway] ||
paths[entity.tags.highway]
) { return false; }
var strings = Object.keys(entity.tags);
for (var i = 0, imax = strings.length; i !== imax; i++) {
var s = strings[i];
if (past_futures[s] || past_futures[entity.tags[s]]) { return true; }
}
return false;
});
// lines or areas that don't match another feature filter.
defineFeature('others', function isOther(entity, resolver) {
var geom = entity.geometry(resolver);
return (geom === 'line' || geom === 'area') && !(
_features.major_roads.filter(entity, resolver) ||
_features.minor_roads.filter(entity, resolver) ||
_features.paths.filter(entity, resolver) ||
_features.buildings.filter(entity, resolver) ||
_features.landuse.filter(entity, resolver) ||
_features.boundaries.filter(entity, resolver) ||
_features.water.filter(entity, resolver) ||
_features.rail.filter(entity, resolver) ||
_features.power.filter(entity, resolver) ||
_features.past_future.filter(entity, resolver)
);
});
function features() {}
features.keys = function() {
return _keys;
};
features.enabled = function(k) {
if (!arguments.length) {
return _.filter(_keys, function(k) { return _features[k].enabled; });
}
return _features[k] && _features[k].enabled;
};
features.disabled = function(k) {
if (!arguments.length) {
return _.reject(_keys, function(k) { return _features[k].enabled; });
}
return _features[k] && !_features[k].enabled;
};
features.hidden = function(k) {
if (!arguments.length) {
return _.filter(_keys, function(k) { return _features[k].hidden(); });
}
return _features[k] && _features[k].hidden();
};
features.autoHidden = function(k) {
if (!arguments.length) {
return _.filter(_keys, function(k) { return _features[k].autoHidden(); });
}
return _features[k] && _features[k].autoHidden();
};
features.enable = function(k) {
if (_features[k] && !_features[k].enabled) {
_features[k].enable();
update();
}
};
features.disable = function(k) {
if (_features[k] && _features[k].enabled) {
_features[k].disable();
update();
}
};
features.toggle = function(k) {
if (_features[k]) {
(function(f) { return f.enabled ? f.disable() : f.enable(); }(_features[k]));
update();
}
};
features.resetStats = function() {
_.each(_features, function(f) { f.count = 0; });
dispatch.change();
};
features.gatherStats = function(d, resolver, dimensions) {
var needsRedraw = false,
currHidden, geometry, feats;
_.each(_features, 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;
for (var i = 0, imax = d.length; i !== imax; i++) {
geometry = d[i].geometry(resolver);
if (!(geometry === 'vertex' || geometry === 'relation')) {
feats = Object.keys(features.matchEntity(d[i], resolver));
for (var j = 0, jmax = feats.length; j !== jmax; j++) {
_features[feats[j]].count++;
}
}
}
currHidden = features.hidden();
if (currHidden !== _hidden) {
_hidden = currHidden;
needsRedraw = true;
dispatch.change();
}
return needsRedraw;
};
features.stats = function() {
_.each(_keys, function(k) { _stats[k] = _features[k].count; });
return _stats;
};
features.clear = function(d) {
for (var i = 0, imax = d.length; i !== imax; i++) {
features.clearEntity(d[i]);
}
};
features.clearEntity = function(entity) {
delete _cache[iD.Entity.key(entity)];
};
features.reset = function() {
_cache = {};
};
features.match = function(d) {
for (var i = 0, imax = d.length; i !== imax; i++) {
features.matchEntity(d[i]);
}
};
features.matchEntity = function(entity, resolver) {
var ent = iD.Entity.key(entity);
if (!_cache[ent]) {
var geometry = entity.geometry(resolver),
matches = {},
hasMatch = false;
if (!(geometry === 'vertex' || geometry === 'relation')) {
for (var i = 0, imax = _keys.length; i !== imax; i++) {
if (hasMatch && _keys[i] === 'others') {
continue;
}
if (_features[_keys[i]].filter(entity, resolver)) {
matches[_keys[i]] = hasMatch = true;
}
}
}
_cache[ent] = matches;
}
return _cache[ent];
};
features.isHiddenFeature = function(entity, resolver) {
var matches = features.matchEntity(entity, resolver);
if (!entity.version) return false;
for (var i = 0, imax = _hidden.length; i !== imax; i++) {
if (matches[_hidden[i]]) { return true; }
}
return false;
};
features.isHiddenChild = function(entity, resolver, geom) {
var geometry = geom || entity.geometry(resolver),
parents;
if (!entity.version || geometry === 'point') { return false; }
if (geometry === 'vertex') {
parents = resolver.parentWays(entity);
} else { // 'line', 'area', 'relation'
parents = resolver.parentRelations(entity);
}
if (!parents.length) { return false; }
for (var i = 0, imax = parents.length; i !== imax; i++) {
if (!features.isHidden(parents[i], resolver)) {
return false;
}
}
return true;
};
features.hasHiddenConnections = function(entity, resolver) {
var childNodes, connections;
if (entity.type === 'midpoint') {
childNodes = [resolver.entity(entity.edge[0]), resolver.entity(entity.edge[1])];
} else {
childNodes = resolver.childNodes(entity);
}
// gather parents..
connections = _.union(resolver.parentWays(entity), resolver.parentRelations(entity));
// gather ways connected to child nodes..
connections = _.reduce(childNodes, function(result, e) {
return resolver.isShared(e) ? _.union(result, resolver.parentWays(e)) : result;
}, connections);
return connections.length ? _.any(connections, function(e) {
return features.isHidden(e, resolver);
}) : false;
};
features.isHidden = function(entity, resolver) {
var geometry;
if (!entity.version) return false;
geometry = entity.geometry(resolver);
if (geometry === 'vertex') return features.isHiddenChild(entity, resolver, geometry);
if (geometry === 'point') return features.isHiddenFeature(entity, resolver);
return (features.isHiddenFeature(entity, resolver) ||
features.isHiddenChild(entity, resolver, geometry));
};
features.filter = function(d, resolver) {
var result = [];
if (!_hidden.length) {
return d;
} else {
for (var i = 0, imax = d.length; i !== imax; i++) {
if (!features.isHidden(d[i], resolver)) {
result.push(d[i]);
}
}
return result;
}
};
return d3.rebind(features, dispatch, 'on');
};

View File

@@ -27,6 +27,8 @@ iD.Map = function(context) {
.on('change.map', redraw);
context.background()
.on('change.map', redraw);
context.features()
.on('redraw.map', redraw);
selection.call(zoom);
@@ -77,6 +79,8 @@ iD.Map = function(context) {
var all = context.intersects(map.extent()),
filter = d3.functor(true),
graph = context.graph();
all = context.features().filter(all, graph);
surface.call(vertices, graph, all, filter, map.extent(), map.zoom());
surface.call(midpoints, graph, all, filter, map.trimmedExtent());
dispatch.drawn({full: false});
@@ -91,47 +95,58 @@ 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'));
filter = function(d) { return set.has(d.id); };
features.clear(data);
} else {
all = context.intersects(map.extent());
filter = d3.functor(true);
// force a full redraw if gatherStats detects that a feature
// should be auto-hidden (e.g. points or buildings)..
if (features.gatherStats(all, graph, dimensions)) {
extent = undefined;
}
if (extent) {
data = context.intersects(map.extent().intersection(extent));
var set = d3.set(_.pluck(data, 'id'));
filter = function(d) { return set.has(d.id); };
} else {
data = all;
filter = d3.functor(true);
}
}
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);
if (points.points(context.intersects(map.extent()), 100).length >= 100) {
surface.select('.layer-hit').selectAll('g.point').remove();
} else {
surface.call(points, points.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});
}
function editOff() {
var mode = context.mode();
context.features().resetStats();
surface.selectAll('.layer *').remove();
dispatch.drawn({full: true});
if (!(mode && mode.id === 'browse')) {
context.enter(iD.modes.Browse(context));
}
dispatch.drawn({full: true});
}
function zoomPan() {

View File

@@ -17,20 +17,14 @@ iD.svg.Areas = function(projection) {
var patternKeys = ['landuse', 'natural', 'amenity'];
var clipped = ['residential', 'commercial', 'retail', 'industrial'];
function clip(entity) {
return clipped.indexOf(entity.tags.landuse) !== -1;
}
function setPattern(d) {
for (var i = 0; i < patternKeys.length; i++) {
if (patterns.hasOwnProperty(d.tags[patternKeys[i]])) {
this.style.fill = 'url("#pattern-' + patterns[d.tags[patternKeys[i]]] + '")';
this.style.fill = this.style.stroke = 'url("#pattern-' + patterns[d.tags[patternKeys[i]]] + '")';
return;
}
}
this.style.fill = '';
this.style.fill = this.style.stroke = '';
}
return function drawAreas(surface, graph, entities, filter) {
@@ -65,7 +59,7 @@ iD.svg.Areas = function(projection) {
});
var data = {
clip: areas.filter(clip),
clip: areas,
shadow: strokes,
stroke: strokes,
fill: areas
@@ -125,11 +119,8 @@ iD.svg.Areas = function(projection) {
this.setAttribute('class', entity.type + ' area ' + layer + ' ' + entity.id);
if (layer === 'fill' && clip(entity)) {
this.setAttribute('clip-path', 'url(#' + entity.id + '-clippath)');
}
if (layer === 'fill') {
this.setAttribute('clip-path', 'url(#' + entity.id + '-clippath)');
setPattern.apply(this, arguments);
}
})

View File

@@ -10,7 +10,10 @@ iD.svg.Points = function(projection, context) {
return b.loc[1] - a.loc[1];
}
function drawPoints(surface, points, filter) {
return function drawPoints(surface, entities, filter) {
var graph = context.graph(),
points = _.filter(entities, function(e) { return e.geometry(graph) === 'point'; });
points.sort(sortY);
var groups = surface.select('.layer-hit').selectAll('g.point')
@@ -48,22 +51,5 @@ iD.svg.Points = function(projection, context) {
groups.exit()
.remove();
}
drawPoints.points = function(entities, limit) {
var graph = context.graph(),
points = [];
for (var i = 0; i < entities.length; i++) {
var entity = entities[i];
if (entity.geometry(graph) === 'point') {
points.push(entity);
if (limit && points.length >= limit) break;
}
}
return points;
};
return drawPoints;
};

View File

@@ -12,20 +12,22 @@ iD.svg.Vertices = function(projection, context) {
var vertices = {};
function addChildVertices(entity) {
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);
if (!context.features().isHiddenFeature(entity, 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;
}
} else if (entity.intersects(extent, graph)) {
vertices[entity.id] = entity;
}
}

View File

@@ -54,9 +54,6 @@ iD.ui = function(context) {
.attr('class', 'spinner')
.call(iD.ui.Spinner(context));
content
.call(iD.ui.Attribution(context));
content.append('div')
.style('display', 'none')
.attr('class', 'help-wrap map-overlay fillL col5 content');
@@ -76,11 +73,22 @@ iD.ui = function(context) {
.attr('class', 'map-control background-control')
.call(iD.ui.Background(context));
controls.append('div')
.attr('class', 'map-control map-data-control')
.call(iD.ui.MapData(context));
controls.append('div')
.attr('class', 'map-control help-control')
.call(iD.ui.Help(context));
var footer = content.append('div')
var about = content.append('div')
.attr('id', 'about');
about.append('div')
.attr('id', 'attrib')
.call(iD.ui.Attribution(context));
var footer = about.append('div')
.attr('id', 'footer')
.attr('class', 'fillD');
@@ -88,24 +96,23 @@ iD.ui = function(context) {
.attr('id', 'scale-block')
.call(iD.ui.Scale(context));
var linkList = footer.append('div')
var aboutList = footer.append('div')
.attr('id', 'info-block')
.append('ul')
.attr('id', 'about-list')
.attr('class', 'link-list');
.attr('id', 'about-list');
if (!context.embed()) {
linkList.call(iD.ui.Account(context));
aboutList.call(iD.ui.Account(context));
}
linkList.append('li')
aboutList.append('li')
.append('a')
.attr('target', '_blank')
.attr('tabindex', -1)
.attr('href', 'http://github.com/openstreetmap/iD')
.text(iD.version);
var bugReport = linkList.append('li')
var bugReport = aboutList.append('li')
.append('a')
.attr('target', '_blank')
.attr('tabindex', -1)
@@ -119,7 +126,12 @@ iD.ui = function(context) {
.placement('top')
);
linkList.append('li')
aboutList.append('li')
.attr('class', 'feature-warning')
.attr('tabindex', -1)
.call(iD.ui.FeatureInfo(context));
aboutList.append('li')
.attr('class', 'user-list')
.attr('tabindex', -1)
.call(iD.ui.Contributors(context));
@@ -191,5 +203,11 @@ iD.ui = function(context) {
};
iD.ui.tooltipHtml = function(text, key) {
return '<span>' + text + '</span>' + '<div class="keyhint-wrap">' + '<span> ' + (t('tooltip_keyhint')) + ' </span>' + '<span class="keyhint"> ' + key + '</span></div>';
var s = '<span>' + text + '</span>';
if (key) {
s += '<div class="keyhint-wrap">' +
'<span> ' + (t('tooltip_keyhint')) + ' </span>' +
'<span class="keyhint"> ' + key + '</span></div>';
}
return s;
};

View File

@@ -4,7 +4,7 @@ iD.ui.Account = function(context) {
function update(selection) {
if (!connection.authenticated()) {
selection.selectAll('#userLink, #logoutLink')
.style('display', 'none');
.classed('hide', true);
return;
}
@@ -18,7 +18,7 @@ iD.ui.Account = function(context) {
if (err) return;
selection.selectAll('#userLink, #logoutLink')
.style('display', 'list-item');
.classed('hide', false);
// Link
userLink.append('a')
@@ -54,11 +54,11 @@ iD.ui.Account = function(context) {
return function(selection) {
selection.append('li')
.attr('id', 'logoutLink')
.style('display', 'none');
.classed('hide', true);
selection.append('li')
.attr('id', 'userLink')
.style('display', 'none');
.classed('hide', true);
connection.on('auth.account', function() { update(selection); });
update(selection);

View File

@@ -1,5 +1,5 @@
iD.ui.Background = function(context) {
var key = 'b',
var key = 'B',
opacities = [1, 0.75, 0.5, 0.25],
directions = [
['left', [1, 0]],
@@ -72,16 +72,6 @@ iD.ui.Background = function(context) {
selectLayer();
}
function clickGpx() {
context.background().toggleGpxLayer();
update();
}
function clickMapillary() {
context.background().toggleMapillaryLayer();
update();
}
function drawList(layerList, type, change, filter) {
var sources = context.background()
.sources(context.map().extent())
@@ -120,22 +110,6 @@ iD.ui.Background = function(context) {
backgroundList.call(drawList, 'radio', clickSetSource, function(d) { return !d.overlay; });
overlayList.call(drawList, 'checkbox', clickSetOverlay, function(d) { return d.overlay; });
var hasGpx = context.background().hasGpxLayer(),
showsGpx = context.background().showsGpxLayer();
gpxLayerItem
.classed('active', showsGpx)
.selectAll('input')
.property('disabled', !hasGpx)
.property('checked', showsGpx);
var showsMapillary = context.background().showsMapillaryLayer();
mapillaryLayerItem
.classed('active', showsMapillary)
.selectAll('input')
.property('checked', showsMapillary);
selectLayer();
var source = context.background().baseLayerSource();
@@ -276,68 +250,6 @@ iD.ui.Background = function(context) {
var overlayList = content.append('ul')
.attr('class', 'layer-list');
var mapillaryLayerItem = overlayList.append('li');
label = mapillaryLayerItem.append('label')
.call(bootstrap.tooltip()
.title(t('mapillary.tooltip'))
.placement('top'));
label.append('input')
.attr('type', 'checkbox')
.on('change', clickMapillary);
label.append('span')
.text(t('mapillary.title'));
var gpxLayerItem = content.append('ul')
.style('display', iD.detect().filedrop ? 'block' : 'none')
.attr('class', 'layer-list')
.append('li')
.classed('layer-toggle-gpx', true);
gpxLayerItem.append('button')
.attr('class', 'layer-extent')
.call(bootstrap.tooltip()
.title(t('gpx.zoom'))
.placement('left'))
.on('click', function() {
d3.event.preventDefault();
d3.event.stopPropagation();
context.background().zoomToGpxLayer();
})
.append('span')
.attr('class', 'icon geolocate');
gpxLayerItem.append('button')
.attr('class', 'layer-browse')
.call(bootstrap.tooltip()
.title(t('gpx.browse'))
.placement('left'))
.on('click', function() {
d3.select(document.createElement('input'))
.attr('type', 'file')
.on('change', function() {
context.background().gpxLayerFiles(d3.event.target.files);
})
.node().click();
})
.append('span')
.attr('class', 'icon geocode');
label = gpxLayerItem.append('label')
.call(bootstrap.tooltip()
.title(t('gpx.drag_drop'))
.placement('top'));
label.append('input')
.attr('type', 'checkbox')
.property('disabled', true)
.on('change', clickGpx);
label.append('span')
.text(t('gpx.local_layer'));
var adjustments = content.append('div')
.attr('class', 'adjustments');
@@ -382,11 +294,10 @@ iD.ui.Background = function(context) {
update();
setOpacity(opacityDefault);
var keybinding = d3.keybinding('background');
keybinding.on(key, toggle);
keybinding.on('m', function() {
context.enter(iD.modes.SelectImage(context));
});
var keybinding = d3.keybinding('background')
.on(key, toggle)
.on('F', hide)
.on('H', hide);
d3.select(document)
.call(keybinding);

45
js/id/ui/feature_info.js Normal file
View File

@@ -0,0 +1,45 @@
iD.ui.FeatureInfo = function(context) {
function update(selection) {
var features = context.features(),
hidden = features.hidden();
selection.html('');
if (hidden.length) {
var stats = features.stats(),
count = 0,
hiddenList = _.map(hidden, function(k) {
count += stats[k];
return String(stats[k]) + ' ' + t('feature.' + k + '.description');
}),
tooltip = bootstrap.tooltip()
.placement('top')
.html(true)
.title(function() {
return iD.ui.tooltipHtml(hiddenList.join('<br/>'));
});
var warning = selection.append('a')
.attr('href', '#')
.attr('tabindex', -1)
.html(t('feature_info.hidden_warning', { count: count }))
.call(tooltip)
.on('click', function() {
tooltip.hide(warning);
// open map data panel?
d3.event.preventDefault();
});
}
selection
.classed('hide', !hidden.length);
}
return function(selection) {
update(selection);
context.features().on('change.feature_info', function() {
update(selection);
});
};
};

View File

@@ -1,5 +1,5 @@
iD.ui.Help = function(context) {
var key = 'h';
var key = 'H';
var docKeys = [
'help.help',
@@ -140,7 +140,9 @@ iD.ui.Help = function(context) {
clickHelp(docs[0], 0);
var keybinding = d3.keybinding('help')
.on(key, toggle);
.on(key, toggle)
.on('B', hide)
.on('F', hide);
d3.select(document)
.call(keybinding);

329
js/id/ui/map_data.js Normal file
View File

@@ -0,0 +1,329 @@
iD.ui.MapData = function(context) {
var key = 'F',
features = context.features().keys(),
fills = ['wireframe', 'partial', 'full'],
fillDefault = context.storage('area-fill') || 'partial',
fillSelected = fillDefault;
function map_data(selection) {
function showsFeature(d) {
return autoHiddenFeature(d) ? null : context.features().enabled(d);
}
function autoHiddenFeature(d) {
return context.features().autoHidden(d);
}
function clickFeature(d) {
context.features().toggle(d);
update();
}
function showsFill(d) {
return fillSelected === d;
}
function setFill(d) {
_.each(fills, function(opt) {
context.surface().classed('fill-' + opt, Boolean(opt === d));
});
fillSelected = d;
if (d !== 'wireframe') {
fillDefault = d;
context.storage('area-fill', d);
}
update();
}
function clickGpx() {
context.background().toggleGpxLayer();
update();
}
function clickMapillary() {
context.background().toggleMapillaryLayer();
update();
}
function drawList(selection, data, type, name, change, active) {
var items = selection.selectAll('li')
.data(data);
//enter
var enter = items.enter()
.append('li')
.attr('class', 'layer')
.call(bootstrap.tooltip()
.html(true)
.title(function(d) {
var tip = t(name + '.' + d + '.tooltip'),
key = (d === 'wireframe' ? 'W' : null);
if (name === 'feature' && autoHiddenFeature(d)) {
tip += '<div>' + t('map_data.autohidden') + '</div>';
}
return iD.ui.tooltipHtml(tip, key);
})
.placement('top')
);
var label = enter.append('label');
label.append('input')
.attr('type', type)
.attr('name', name)
.on('change', change);
label.append('span')
.text(function(d) { return t(name + '.' + d + '.description'); });
//update
items
.classed('active', active)
.selectAll('input')
.property('checked', active);
if (name === 'feature') {
items
.selectAll('input')
.property('indeterminate', autoHiddenFeature);
}
//exit
items.exit()
.remove();
}
function update() {
featureList.call(drawList, features, 'checkbox', 'feature', clickFeature, showsFeature);
fillList.call(drawList, fills, 'radio', 'area_fill', setFill, showsFill);
var hasGpx = context.background().hasGpxLayer(),
showsGpx = context.background().showsGpxLayer(),
showsMapillary = context.background().showsMapillaryLayer();
gpxLayerItem
.classed('active', showsGpx)
.selectAll('input')
.property('disabled', !hasGpx)
.property('checked', showsGpx);
mapillaryLayerItem
.classed('active', showsMapillary)
.selectAll('input')
.property('checked', showsMapillary);
}
var content = selection.append('div')
.attr('class', 'fillL map-overlay col3 content hide'),
tooltip = bootstrap.tooltip()
.placement('left')
.html(true)
.title(iD.ui.tooltipHtml(t('map_data.description'), key));
function hidePanel() { setVisible(false); }
function togglePanel() {
if (d3.event) d3.event.preventDefault();
tooltip.hide(button);
setVisible(!button.classed('active'));
}
function toggleWireframe() {
if (d3.event) {
d3.event.preventDefault();
d3.event.stopPropagation();
}
setFill((fillSelected === 'wireframe' ? fillDefault : 'wireframe'));
}
function setVisible(show) {
if (show !== shown) {
button.classed('active', show);
shown = show;
if (show) {
selection.on('mousedown.map_data-inside', function() {
return d3.event.stopPropagation();
});
content.style('display', 'block')
.style('right', '-300px')
.transition()
.duration(200)
.style('right', '0px');
} else {
content.style('display', 'block')
.style('right', '0px')
.transition()
.duration(200)
.style('right', '-300px')
.each('end', function() {
d3.select(this).style('display', 'none');
});
selection.on('mousedown.map_data-inside', null);
}
}
}
var button = selection.append('button')
.attr('tabindex', -1)
.on('click', togglePanel)
.call(tooltip),
shown = false;
button.append('span')
.attr('class', 'icon data light');
content.append('h4')
.text(t('map_data.title'));
// data layers
content.append('a')
.text(t('map_data.data_layers'))
.attr('href', '#')
.classed('hide-toggle', true)
.classed('expanded', true)
.on('click', function() {
var exp = d3.select(this).classed('expanded');
layerContainer.style('display', exp ? 'none' : 'block');
d3.select(this).classed('expanded', !exp);
d3.event.preventDefault();
});
var layerContainer = content.append('div')
.attr('class', 'filters')
.style('display', 'block');
// mapillary
var mapillaryLayerItem = layerContainer.append('ul')
.attr('class', 'layer-list')
.append('li');
var label = mapillaryLayerItem.append('label')
.call(bootstrap.tooltip()
.title(t('mapillary.tooltip'))
.placement('top'));
label.append('input')
.attr('type', 'checkbox')
.on('change', clickMapillary);
label.append('span')
.text(t('mapillary.title'));
// gpx
var gpxLayerItem = layerContainer.append('ul')
.style('display', iD.detect().filedrop ? 'block' : 'none')
.attr('class', 'layer-list')
.append('li')
.classed('layer-toggle-gpx', true);
gpxLayerItem.append('button')
.attr('class', 'layer-extent')
.call(bootstrap.tooltip()
.title(t('gpx.zoom'))
.placement('left'))
.on('click', function() {
d3.event.preventDefault();
d3.event.stopPropagation();
context.background().zoomToGpxLayer();
})
.append('span')
.attr('class', 'icon geolocate');
gpxLayerItem.append('button')
.attr('class', 'layer-browse')
.call(bootstrap.tooltip()
.title(t('gpx.browse'))
.placement('left'))
.on('click', function() {
d3.select(document.createElement('input'))
.attr('type', 'file')
.on('change', function() {
context.background().gpxLayerFiles(d3.event.target.files);
})
.node().click();
})
.append('span')
.attr('class', 'icon geocode');
label = gpxLayerItem.append('label')
.call(bootstrap.tooltip()
.title(t('gpx.drag_drop'))
.placement('top'));
label.append('input')
.attr('type', 'checkbox')
.property('disabled', true)
.on('change', clickGpx);
label.append('span')
.text(t('gpx.local_layer'));
// area fills
content.append('a')
.text(t('map_data.fill_area'))
.attr('href', '#')
.classed('hide-toggle', true)
.classed('expanded', false)
.on('click', function() {
var exp = d3.select(this).classed('expanded');
fillContainer.style('display', exp ? 'none' : 'block');
d3.select(this).classed('expanded', !exp);
d3.event.preventDefault();
});
var fillContainer = content.append('div')
.attr('class', 'filters')
.style('display', 'none');
var fillList = fillContainer.append('ul')
.attr('class', 'layer-list');
// feature filters
content.append('a')
.text(t('map_data.map_features'))
.attr('href', '#')
.classed('hide-toggle', true)
.classed('expanded', false)
.on('click', function() {
var exp = d3.select(this).classed('expanded');
featureContainer.style('display', exp ? 'none' : 'block');
d3.select(this).classed('expanded', !exp);
d3.event.preventDefault();
});
var featureContainer = content.append('div')
.attr('class', 'filters')
.style('display', 'none');
var featureList = featureContainer.append('ul')
.attr('class', 'layer-list');
context.features()
.on('change.map_data-update', update);
update();
setFill(fillDefault);
var keybinding = d3.keybinding('features')
.on(key, togglePanel)
.on('W', toggleWireframe)
.on('B', hidePanel)
.on('H', hidePanel);
d3.select(document)
.call(keybinding);
context.surface().on('mousedown.map_data-outside', hidePanel);
context.container().on('mousedown.map_data-outside', hidePanel);
}
return map_data;
};