@@ -84,22 +98,10 @@
image/svg+xml
-
+
-
-
-
+ d="M 720 237 L 717 240 L 717 245 L 742 270 L 717 295 L 717 300 L 720 303 L 725 303 L 750 278 L 775 303 L 780 303 L 783 300 L 783 295 L 758 270 L 783 245 L 783 240 L 780 237 L 775 237 L 750 262 L 725 237 L 720 237 z "
+ transform="translate(-270,-5.0000031)"
+ id="path4223" />
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dist/locales/en.json b/dist/locales/en.json
index 9578881e7..eb741328a 100644
--- a/dist/locales/en.json
+++ b/dist/locales/en.json
@@ -72,7 +72,8 @@
"area": "Made an area circular."
},
"not_closed": "This can't be made circular because it's not a loop.",
- "too_large": "This can't be made circular because not enough of it is currently visible."
+ "too_large": "This can't be made circular because not enough of it is currently visible.",
+ "connected_to_hidden": "This can't be made circular because it is connected to a hidden feature."
},
"orthogonalize": {
"title": "Square",
@@ -86,14 +87,16 @@
"area": "Squared the corners of an area."
},
"not_squarish": "This can't be made square because it is not squarish.",
- "too_large": "This can't be made square because not enough of it is currently visible."
+ "too_large": "This can't be made square because not enough of it is currently visible.",
+ "connected_to_hidden": "This can't be made square because it is connected to a hidden feature."
},
"straighten": {
"title": "Straighten",
"description": "Straighten this line.",
"key": "S",
"annotation": "Straightened a line.",
- "too_bendy": "This can't be straightened because it bends too much."
+ "too_bendy": "This can't be straightened because it bends too much.",
+ "connected_to_hidden": "This line can't be straightened because it is connected to a hidden feature."
},
"delete": {
"title": "Delete",
@@ -106,7 +109,8 @@
"relation": "Deleted a relation.",
"multiple": "Deleted {n} objects."
},
- "incomplete_relation": "This feature can't be deleted because it hasn't been fully downloaded."
+ "incomplete_relation": "This feature can't be deleted because it hasn't been fully downloaded.",
+ "connected_to_hidden": "This can't be deleted because it is connected to a hidden feature."
},
"add_member": {
"annotation": "Added a member to a relation."
@@ -127,7 +131,8 @@
"description": "Disconnect these lines/areas from each other.",
"key": "D",
"annotation": "Disconnected lines/areas.",
- "not_connected": "There aren't enough lines/areas here to disconnect."
+ "not_connected": "There aren't enough lines/areas here to disconnect.",
+ "connected_to_hidden": "This can't be disconnected because it is connected to a hidden feature."
},
"merge": {
"title": "Merge",
@@ -151,7 +156,8 @@
"multiple": "Moved multiple objects."
},
"incomplete_relation": "This feature can't be moved because it hasn't been fully downloaded.",
- "too_large": "This can't be moved because not enough of it is currently visible."
+ "too_large": "This can't be moved because not enough of it is currently visible.",
+ "connected_to_hidden": "This can't be moved because it is connected to a hidden feature."
},
"rotate": {
"title": "Rotate",
@@ -161,7 +167,8 @@
"line": "Rotated a line.",
"area": "Rotated an area."
},
- "too_large": "This can't be rotated because not enough of it is currently visible."
+ "too_large": "This can't be rotated because not enough of it is currently visible.",
+ "connected_to_hidden": "This can't be rotated because it is connected to a hidden feature."
},
"reverse": {
"title": "Reverse",
@@ -183,7 +190,8 @@
"multiple": "Split {n} lines/area boundaries."
},
"not_eligible": "Lines can't be split at their beginning or end.",
- "multiple_ways": "There are too many lines here to split."
+ "multiple_ways": "There are too many lines here to split.",
+ "connected_to_hidden": "This can't be split because it is connected to a hidden feature."
},
"restriction": {
"help": {
@@ -218,6 +226,10 @@
"logout": "logout",
"loading_auth": "Connecting to OpenStreetMap...",
"report_a_bug": "report a bug",
+ "feature_info": {
+ "hidden_warning": "{count} hidden features",
+ "hidden_details": "These features are currently hidden: {details}"
+ },
"status": {
"error": "Unable to connect to API.",
"offline": "The API is offline. Please try editing later.",
@@ -292,6 +304,78 @@
"fix_misalignment": "Fix alignment",
"reset": "reset"
},
+ "map_data": {
+ "title": "Map Data",
+ "description": "Map Data",
+ "data_layers": "Data Layers",
+ "fill_area": "Fill Areas",
+ "map_features": "Map Features",
+ "autohidden": "These features have been automatically hidden because too many would be shown on the screen. You can zoom in to edit them."
+ },
+ "feature": {
+ "points": {
+ "description": "Points",
+ "tooltip": "Points of Interest"
+ },
+ "major_roads": {
+ "description": "Major Roads",
+ "tooltip": "Highways, Streets, etc."
+ },
+ "minor_roads": {
+ "description": "Minor Roads",
+ "tooltip": "Service Roads, Parking Aisles, Tracks, etc."
+ },
+ "paths": {
+ "description": "Paths",
+ "tooltip": "Sidewalks, Foot Paths, Cycle Paths, etc."
+ },
+ "buildings": {
+ "description": "Buildings",
+ "tooltip": "Buildings, Shelters, Garages, etc."
+ },
+ "landuse": {
+ "description": "Landuse Features",
+ "tooltip": "Forests, Farmland, Parks, Residential, Commercial, etc."
+ },
+ "boundaries": {
+ "description": "Boundaries",
+ "tooltip": "Administrative Boundaries"
+ },
+ "water": {
+ "description": "Water Features",
+ "tooltip": "Rivers, Lakes, Ponds, Basins, etc."
+ },
+ "rail": {
+ "description": "Rail Features",
+ "tooltip": "Railways"
+ },
+ "power": {
+ "description": "Power Features",
+ "tooltip": "Power Lines, Power Plants, Substations, etc."
+ },
+ "past_future": {
+ "description": "Past/Future",
+ "tooltip": "Proposed, Construction, Abandoned, Demolished, etc."
+ },
+ "others": {
+ "description": "Others",
+ "tooltip": "Everything Else"
+ }
+ },
+ "area_fill": {
+ "wireframe": {
+ "description": "No Fill (Wireframe)",
+ "tooltip": "Enabling wireframe mode makes it easy to see the background imagery."
+ },
+ "partial": {
+ "description": "Partial Fill",
+ "tooltip": "Areas are drawn with fill only around their inner edges. (Recommended for beginner mappers)"
+ },
+ "full": {
+ "description": "Full Fill",
+ "tooltip": "Areas are drawn fully filled."
+ }
+ },
"restore": {
"heading": "You have unsaved changes",
"description": "Do you wish to restore unsaved changes from a previous editing session?",
@@ -367,7 +451,7 @@
"help": "# Help\n\nThis is an editor for [OpenStreetMap](http://www.openstreetmap.org/), the\nfree and editable map of the world. You can use it to add and update\ndata in your area, making an open-source and open-data map of the world\nbetter for everyone.\n\nEdits that you make on this map will be visible to everyone who uses\nOpenStreetMap. In order to make an edit, you'll need a\n[free OpenStreetMap account](https://www.openstreetmap.org/user/new).\n\nThe [iD editor](http://ideditor.com/) is a collaborative project with [source\ncode available on GitHub](https://github.com/openstreetmap/iD).\n",
"editing_saving": "# Editing & Saving\n\nThis editor is designed to work primarily online, and you're accessing\nit through a website right now.\n\n### Selecting Features\n\nTo select a map feature, like a road or point of interest, click\non it on the map. This will highlight the selected feature, open a panel with\ndetails about it, and show a menu of things you can do with the feature.\n\nTo select multiple features, hold down the 'Shift' key. Then either click\non the features you want to select, or drag on the map to draw a rectangle.\nThis will draw a box and select all the points within it.\n\n### Saving Edits\n\nWhen you make changes like editing roads, buildings, and places, these are\nstored locally until you save them to the server. Don't worry if you make\na mistake - you can undo changes by clicking the undo button, and redo\nchanges by clicking the redo button.\n\nClick 'Save' to finish a group of edits - for instance, if you've completed\nan area of town and would like to start on a new area. You'll have a chance\nto review what you've done, and the editor supplies helpful suggestions\nand warnings if something doesn't seem right about the changes.\n\nIf everything looks good, you can enter a short comment explaining the change\nyou made, and click 'Save' again to post the changes\nto [OpenStreetMap.org](http://www.openstreetmap.org/), where they are visible\nto all other users and available for others to build and improve upon.\n\nIf you can't finish your edits in one sitting, you can leave the editor\nwindow and come back (on the same browser and computer), and the\neditor application will offer to restore your work.\n",
"roads": "# Roads\n\nYou can create, fix, and delete roads with this editor. Roads can be all\nkinds: paths, highways, trails, cycleways, and more - any often-crossed\nsegment should be mappable.\n\n### Selecting\n\nClick on a road to select it. An outline should become visible, along\nwith a small tools menu on the map and a sidebar showing more information\nabout the road.\n\n### Modifying\n\nOften you'll see roads that aren't aligned to the imagery behind them\nor to a GPS track. You can adjust these roads so they are in the correct\nplace.\n\nFirst click on the road you want to change. This will highlight it and show\ncontrol points along it that you can drag to better locations. If\nyou want to add new control points for more detail, double-click a part\nof the road without a node, and one will be added.\n\nIf the road connects to another road, but doesn't properly connect on\nthe map, you can drag one of its control points onto the other road in\norder to join them. Having roads connect is important for the map\nand essential for providing driving directions.\n\nYou can also click the 'Move' tool or press the `M` shortcut key to move the entire road at\none time, and then click again to save that movement.\n\n### Deleting\n\nIf a road is entirely incorrect - you can see that it doesn't exist in satellite\nimagery and ideally have confirmed locally that it's not present - you can delete\nit, which removes it from the map. Be cautious when deleting features -\nlike any other edit, the results are seen by everyone and satellite imagery\nis often out of date, so the road could simply be newly built.\n\nYou can delete a road by clicking on it to select it, then clicking the\ntrash can icon or pressing the 'Delete' key.\n\n### Creating\n\nFound somewhere there should be a road but there isn't? Click the 'Line'\nicon in the top-left of the editor or press the shortcut key `2` to start drawing\na line.\n\nClick on the start of the road on the map to start drawing. If the road\nbranches off from an existing road, start by clicking on the place where they connect.\n\nThen click on points along the road so that it follows the right path, according\nto satellite imagery or GPS. If the road you are drawing crosses another road, connect\nit by clicking on the intersection point. When you're done drawing, double-click\nor press 'Return' or 'Enter' on your keyboard.\n",
- "gps": "# GPS\n\nGPS data is the most trusted source of data for OpenStreetMap. This editor\nsupports local traces - `.gpx` files on your local computer. You can collect\nthis kind of GPS trace with a number of smartphone applications as well as\npersonal GPS hardware.\n\nFor information on how to perform a GPS survey, read\n[Surveying with a GPS](http://learnosm.org/en/beginner/using-gps/).\n\nTo use a GPX track for mapping, drag and drop the GPX file onto the map\neditor. If it's recognized, it will be added to the map as a bright green\nline. Click on the 'Background Settings' menu on the right side to enable,\ndisable, or zoom to this new GPX-powered layer.\n\nThe GPX track isn't directly uploaded to OpenStreetMap - the best way to\nuse it is to draw on the map, using it as a guide for the new features that\nyou add, and also to [upload it to OpenStreetMap](http://www.openstreetmap.org/trace/create)\nfor other users to use.\n",
+ "gps": "# GPS\n\nGPS data is the most trusted source of data for OpenStreetMap. This editor\nsupports local traces - `.gpx` files on your local computer. You can collect\nthis kind of GPS trace with a number of smartphone applications as well as\npersonal GPS hardware.\n\nFor information on how to perform a GPS survey, read\n[Surveying with a GPS](http://learnosm.org/en/beginner/using-gps/).\n\nTo use a GPX track for mapping, drag and drop the GPX file onto the map\neditor. If it's recognized, it will be added to the map as a bright purple\nline. Click on the 'Map Data' menu on the right side to enable,\ndisable, or zoom to this new GPX-powered layer.\n\nThe GPX track isn't directly uploaded to OpenStreetMap - the best way to\nuse it is to draw on the map, using it as a guide for the new features that\nyou add, and also to [upload it to OpenStreetMap](http://www.openstreetmap.org/trace/create)\nfor other users to use.\n",
"imagery": "# Imagery\n\nAerial imagery is an important resource for mapping. A combination of\nairplane flyovers, satellite views, and freely-compiled sources are available\nin the editor under the 'Background Settings' menu on the right.\n\nBy default a [Bing Maps](http://www.bing.com/maps/) satellite layer is\npresented in the editor, but as you pan and zoom the map to new geographical\nareas, new sources will become available. Some countries, like the United\nStates, France, and Denmark have very high-quality imagery available for some areas.\n\nImagery is sometimes offset from the map data because of a mistake on the\nimagery provider's side. If you see a lot of roads shifted from the background,\ndon't immediately move them all to match the background. Instead you can adjust\nthe imagery so that it matches the existing data by clicking 'Fix alignment' at\nthe bottom of the Background Settings UI.\n",
"addresses": "# Addresses\n\nAddresses are some of the most useful information for the map.\n\nAlthough addresses are often represented as parts of streets, in OpenStreetMap\nthey're recorded as attributes of buildings and places along streets.\n\nYou can add address information to places mapped as building outlines\nas well as those mapped as single points. The optimal source of address\ndata is from an on-the-ground survey or personal knowledge - as with any\nother feature, copying from commercial sources like Google Maps is strictly\nforbidden.\n",
"inspector": "# Using the Inspector\n\nThe inspector is the section on the left side of the page that allows you to\nedit the details of the selected feature.\n\n### Selecting a Feature Type\n\nAfter you add a point, line, or area, you can choose what type of feature it\nis, like whether it's a highway or residential road, supermarket or cafe.\nThe inspector will display buttons for common feature types, and you can\nfind others by typing what you're looking for in the search box.\n\nClick the 'i' in the bottom-right-hand corner of a feature type button to\nlearn more about it. Click a button to choose that type.\n\n### Using Forms and Editing Tags\n\nAfter you choose a feature type, or when you select a feature that already\nhas a type assigned, the inspector will display fields with details about\nthe feature like its name and address.\n\nBelow the fields you see, you can click icons to add other details,\nlike [Wikipedia](http://www.wikipedia.org/) information, wheelchair\naccess, and more.\n\nAt the bottom of the inspector, click 'Additional tags' to add arbitrary\nother tags to the element. [Taginfo](http://taginfo.openstreetmap.org/) is a\ngreat resource for learn more about popular tag combinations.\n\nChanges you make in the inspector are automatically applied to the map.\nYou can undo them at any time by clicking the 'Undo' button.\n",
diff --git a/index.html b/index.html
index cdff162bf..f3129da2d 100644
--- a/index.html
+++ b/index.html
@@ -55,6 +55,7 @@
+
@@ -85,12 +86,14 @@
+
+
@@ -238,7 +241,7 @@
d3.select('#id-container')
.call(id.ui());
- d3.select('#about').insert('li', '.user-list')
+ d3.select('#about-list').insert('li', '.user-list')
.attr('class', 'source-switch')
.call(iD.ui.SourceSwitch(id)
.keys([
diff --git a/js/id/core/graph.js b/js/id/core/graph.js
index 3f9ab7c9c..9422acc73 100644
--- a/js/id/core/graph.js
+++ b/js/id/core/graph.js
@@ -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);
diff --git a/js/id/geo/extent.js b/js/id/geo/extent.js
index b86d687b7..a32b5783c 100644
--- a/js/id/geo/extent.js
+++ b/js/id/geo/extent.js
@@ -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]),
diff --git a/js/id/id.js b/js/id/id.js
index 96d9c347d..2f52c2f9c 100644
--- a/js/id/id.js
+++ b/js/id/id.js
@@ -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; };
diff --git a/js/id/modes/drag_node.js b/js/id/modes/drag_node.js
index be2956181..3388eaa3c 100644
--- a/js/id/modes/drag_node.js
+++ b/js/id/modes/drag_node.js
@@ -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';
diff --git a/js/id/modes/select.js b/js/id/modes/select.js
index 5acee7a84..be2746ba8 100644
--- a/js/id/modes/select.js
+++ b/js/id/modes/select.js
@@ -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);
diff --git a/js/id/operations/circularize.js b/js/id/operations/circularize.js
index a786627c9..f15a11d8f 100644
--- a/js/id/operations/circularize.js
+++ b/js/id/operations/circularize.js
@@ -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;
};
diff --git a/js/id/operations/continue.js b/js/id/operations/continue.js
index 9ebcdac1e..642458721 100644
--- a/js/id/operations/continue.js
+++ b/js/id/operations/continue.js
@@ -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() {
diff --git a/js/id/operations/delete.js b/js/id/operations/delete.js
index 15c41c067..bdd1483a4 100644
--- a/js/id/operations/delete.js
+++ b/js/id/operations/delete.js
@@ -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() {
diff --git a/js/id/operations/disconnect.js b/js/id/operations/disconnect.js
index ea12b0419..baa1ff1d4 100644
--- a/js/id/operations/disconnect.js
+++ b/js/id/operations/disconnect.js
@@ -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() {
diff --git a/js/id/operations/move.js b/js/id/operations/move.js
index 525ee7047..6f877a0cc 100644
--- a/js/id/operations/move.js
+++ b/js/id/operations/move.js
@@ -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;
};
diff --git a/js/id/operations/orthogonalize.js b/js/id/operations/orthogonalize.js
index 851e7e248..851f3b7a9 100644
--- a/js/id/operations/orthogonalize.js
+++ b/js/id/operations/orthogonalize.js
@@ -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;
};
diff --git a/js/id/operations/rotate.js b/js/id/operations/rotate.js
index 3edc3e4d9..485d05d24 100644
--- a/js/id/operations/rotate.js
+++ b/js/id/operations/rotate.js
@@ -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;
}
diff --git a/js/id/operations/split.js b/js/id/operations/split.js
index 9a9130df1..43bc72946 100644
--- a/js/id/operations/split.js
+++ b/js/id/operations/split.js
@@ -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() {
diff --git a/js/id/operations/straighten.js b/js/id/operations/straighten.js
index 85e9bcd17..fd0a5e823 100644
--- a/js/id/operations/straighten.js
+++ b/js/id/operations/straighten.js
@@ -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() {
diff --git a/js/id/renderer/background.js b/js/id/renderer/background.js
index bb441592c..8b74795e7 100644
--- a/js/id/renderer/background.js
+++ b/js/id/renderer/background.js
@@ -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]);
diff --git a/js/id/renderer/features.js b/js/id/renderer/features.js
new file mode 100644
index 000000000..400932594
--- /dev/null
+++ b/js/id/renderer/features.js
@@ -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');
+};
diff --git a/js/id/renderer/map.js b/js/id/renderer/map.js
index 989c7188b..d5826faf5 100644
--- a/js/id/renderer/map.js
+++ b/js/id/renderer/map.js
@@ -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() {
diff --git a/js/id/svg/areas.js b/js/id/svg/areas.js
index 12070f993..ebc77101f 100644
--- a/js/id/svg/areas.js
+++ b/js/id/svg/areas.js
@@ -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);
}
})
diff --git a/js/id/svg/points.js b/js/id/svg/points.js
index cd3de9d8a..33d563208 100644
--- a/js/id/svg/points.js
+++ b/js/id/svg/points.js
@@ -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;
};
diff --git a/js/id/svg/vertices.js b/js/id/svg/vertices.js
index 211c49721..d70d751a8 100644
--- a/js/id/svg/vertices.js
+++ b/js/id/svg/vertices.js
@@ -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;
}
}
diff --git a/js/id/ui.js b/js/id/ui.js
index eacf19f8f..23acc2649 100644
--- a/js/id/ui.js
+++ b/js/id/ui.js
@@ -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 '' + text + '' + '' + ' ' + (t('tooltip_keyhint')) + ' ' + ' ' + key + '
';
+ var s = '' + text + '';
+ if (key) {
+ s += '' +
+ ' ' + (t('tooltip_keyhint')) + ' ' +
+ ' ' + key + '
';
+ }
+ return s;
};
diff --git a/js/id/ui/account.js b/js/id/ui/account.js
index d7f32b0b7..a7360561c 100644
--- a/js/id/ui/account.js
+++ b/js/id/ui/account.js
@@ -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);
diff --git a/js/id/ui/background.js b/js/id/ui/background.js
index 9e88acef7..b9fc8eedf 100644
--- a/js/id/ui/background.js
+++ b/js/id/ui/background.js
@@ -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);
diff --git a/js/id/ui/feature_info.js b/js/id/ui/feature_info.js
new file mode 100644
index 000000000..81e429ec2
--- /dev/null
+++ b/js/id/ui/feature_info.js
@@ -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('
'));
+ });
+
+ 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);
+ });
+ };
+};
diff --git a/js/id/ui/help.js b/js/id/ui/help.js
index eef828b1d..820318925 100644
--- a/js/id/ui/help.js
+++ b/js/id/ui/help.js
@@ -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);
diff --git a/js/id/ui/map_data.js b/js/id/ui/map_data.js
new file mode 100644
index 000000000..a5cc8584a
--- /dev/null
+++ b/js/id/ui/map_data.js
@@ -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 += '' + t('map_data.autohidden') + '
';
+ }
+ 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;
+};
diff --git a/package.json b/package.json
index d0e5d9595..e5a92805b 100644
--- a/package.json
+++ b/package.json
@@ -28,7 +28,7 @@
"jshint": "2.3.0",
"mocha": "1",
"mocha-phantomjs": "3",
- "chai": "~1.4",
+ "chai": "~1.9.2",
"sinon": "~1.6",
"sinon-chai": "~2.3.1",
"happen": "0.1.2",
diff --git a/test/index.html b/test/index.html
index a3fdcba7e..f5cc7da2a 100644
--- a/test/index.html
+++ b/test/index.html
@@ -52,6 +52,7 @@
+
@@ -80,11 +81,13 @@
+
+
@@ -256,6 +259,7 @@
+
diff --git a/test/index_packaged.html b/test/index_packaged.html
index 4a4f3f135..9df77e3cc 100644
--- a/test/index_packaged.html
+++ b/test/index_packaged.html
@@ -67,6 +67,7 @@
+
diff --git a/test/spec/core/history.js b/test/spec/core/history.js
index 05660a3fa..f46907f21 100644
--- a/test/spec/core/history.js
+++ b/test/spec/core/history.js
@@ -240,8 +240,8 @@ describe("iD.History", function () {
history.perform(iD.actions.DeleteNode('n3')); // deletion
var json = JSON.parse(history.toJSON());
expect(json.version).to.eql(3);
- expect(json.entities).to.eql([node_1, node2.update({tags: {k: 'v'}})]);
- expect(json.baseEntities).to.eql([node2, node3]);
+ expect( _.isEqual(json.entities, [node_1, node2.update({tags: {k: 'v'}})]) ).to.be.ok;
+ expect( _.isEqual(json.baseEntities, [node2, node3]) ).to.be.ok;
});
});
diff --git a/test/spec/core/node.js b/test/spec/core/node.js
index 810da7b4e..dfaaf19d8 100644
--- a/test/spec/core/node.js
+++ b/test/spec/core/node.js
@@ -14,7 +14,7 @@ describe('iD.Node', function () {
describe("#extent", function() {
it("returns a point extent", function() {
- expect(iD.Node({loc: [5, 10]}).extent()).to.eql([[5, 10], [5, 10]]);
+ expect(iD.Node({loc: [5, 10]}).extent().equals([[5, 10], [5, 10]])).to.be.ok;
});
});
diff --git a/test/spec/core/relation.js b/test/spec/core/relation.js
index 451a53321..4a617421b 100644
--- a/test/spec/core/relation.js
+++ b/test/spec/core/relation.js
@@ -33,7 +33,7 @@ describe('iD.Relation', function () {
r = iD.Relation({members: [{id: a.id}, {id: b.id}]}),
graph = iD.Graph([a, b, r]);
- expect(r.extent(graph)).to.eql([[0, 0], [5, 10]])
+ expect(r.extent(graph).equals([[0, 0], [5, 10]])).to.be.ok;
});
it("returns the known extent of incomplete relations", function () {
@@ -42,13 +42,13 @@ describe('iD.Relation', function () {
r = iD.Relation({members: [{id: a.id}, {id: b.id}]}),
graph = iD.Graph([a, r]);
- expect(r.extent(graph)).to.eql([[0, 0], [0, 0]])
+ expect(r.extent(graph).equals([[0, 0], [0, 0]])).to.be.ok;
});
it("does not error on self-referencing relations", function () {
var r = iD.Relation();
r = r.addMember({id: r.id});
- expect(r.extent(iD.Graph([r]))).to.eql(iD.geo.Extent())
+ expect(r.extent(iD.Graph([r]))).to.eql(iD.geo.Extent());
});
});
diff --git a/test/spec/core/way.js b/test/spec/core/way.js
index a551db1ce..0ff1a56fc 100644
--- a/test/spec/core/way.js
+++ b/test/spec/core/way.js
@@ -69,7 +69,7 @@ describe('iD.Way', function() {
node2 = iD.Node({loc: [5, 10]}),
way = iD.Way({nodes: [node1.id, node2.id]}),
graph = iD.Graph([node1, node2, way]);
- expect(way.extent(graph)).to.eql([[0, 0], [5, 10]]);
+ expect(way.extent(graph).equals([[0, 0], [5, 10]])).to.be.ok;
});
});
diff --git a/test/spec/geo.js b/test/spec/geo.js
index b0919e8ff..e3f5b22e7 100644
--- a/test/spec/geo.js
+++ b/test/spec/geo.js
@@ -35,7 +35,7 @@ describe('iD.geo', function() {
var o = [0, 0],
a = [-2, 0],
b = [2, 0];
- expect(iD.geo.cross(o, a, b)).to.eql(0);
+ expect(iD.geo.cross(o, a, b)).to.equal(0);
});
});
diff --git a/test/spec/geo/extent.js b/test/spec/geo/extent.js
index c299f4279..fb880548e 100644
--- a/test/spec/geo/extent.js
+++ b/test/spec/geo/extent.js
@@ -1,31 +1,31 @@
describe("iD.geo.Extent", function () {
describe("constructor", function () {
it("defaults to infinitely empty extent", function () {
- expect(iD.geo.Extent()).to.eql([[Infinity, Infinity], [-Infinity, -Infinity]]);
+ expect(iD.geo.Extent().equals([[Infinity, Infinity], [-Infinity, -Infinity]])).to.be.ok;
});
it("constructs via a point", function () {
var p = [0, 0];
- expect(iD.geo.Extent(p)).to.eql([p, p]);
+ expect(iD.geo.Extent(p).equals([p, p])).to.be.ok;
});
it("constructs via two points", function () {
var min = [0, 0],
max = [5, 10];
- expect(iD.geo.Extent(min, max)).to.eql([min, max]);
+ expect(iD.geo.Extent(min, max).equals([min, max])).to.be.ok;
});
it("constructs via an extent", function () {
var min = [0, 0],
max = [5, 10];
- expect(iD.geo.Extent([min, max])).to.eql([min, max]);
+ expect(iD.geo.Extent([min, max]).equals([min, max])).to.be.ok;
});
it("constructs via an iD.geo.Extent", function () {
var min = [0, 0],
max = [5, 10],
extent = iD.geo.Extent(min, max);
- expect(iD.geo.Extent(extent)).to.equal(extent);
+ expect(iD.geo.Extent(extent).equals(extent)).to.be.ok;
});
it("has length 2", function () {
@@ -45,6 +45,16 @@ describe("iD.geo.Extent", function () {
});
});
+ describe("#equals", function () {
+ it("tests extent equality", function () {
+ var e1 = iD.geo.Extent([0, 0], [10, 10]),
+ e2 = iD.geo.Extent([0, 0], [10, 10]),
+ e3 = iD.geo.Extent([0, 0], [12, 12]);
+ expect(e1.equals(e2)).to.be.ok;
+ expect(e1.equals(e3)).to.be.not.ok;
+ });
+ });
+
describe("#center", function () {
it("returns the center point", function () {
expect(iD.geo.Extent([0, 0], [5, 10]).center()).to.eql([2.5, 5]);
@@ -73,17 +83,17 @@ describe("iD.geo.Extent", function () {
it("does not modify self", function () {
var extent = iD.geo.Extent([0, 0], [0, 0]);
extent.extend([1, 1]);
- expect(extent).to.eql([[0, 0], [0, 0]]);
+ expect(extent.equals([[0, 0], [0, 0]])).to.be.ok;
});
it("returns the minimal extent containing self and the given point", function () {
- expect(iD.geo.Extent().extend([0, 0])).to.eql([[0, 0], [0, 0]]);
- expect(iD.geo.Extent([0, 0], [0, 0]).extend([5, 10])).to.eql([[0, 0], [5, 10]]);
+ expect(iD.geo.Extent().extend([0, 0]).equals([[0, 0], [0, 0]])).to.be.ok;
+ expect(iD.geo.Extent([0, 0], [0, 0]).extend([5, 10]).equals([[0, 0], [5, 10]])).to.be.ok;
});
it("returns the minimal extent containing self and the given extent", function () {
- expect(iD.geo.Extent().extend([[0, 0], [5, 10]])).to.eql([[0, 0], [5, 10]]);
- expect(iD.geo.Extent([0, 0], [0, 0]).extend([[4, -1], [5, 10]])).to.eql([[0, -1], [5, 10]]);
+ expect(iD.geo.Extent().extend([[0, 0], [5, 10]]).equals([[0, 0], [5, 10]])).to.be.ok;
+ expect(iD.geo.Extent([0, 0], [0, 0]).extend([[4, -1], [5, 10]]).equals([[0, -1], [5, 10]])).to.be.ok;
});
});
@@ -91,11 +101,11 @@ describe("iD.geo.Extent", function () {
it("extends self to the minimal extent containing self and the given extent", function () {
var e = iD.geo.Extent();
e._extend([[0, 0], [5, 10]]);
- expect(e).to.eql([[0, 0], [5, 10]]);
+ expect(e.equals([[0, 0], [5, 10]])).to.be.ok;
e = iD.geo.Extent([0, 0], [0, 0]);
e._extend([[4, -1], [5, 10]]);
- expect(e).to.eql([[0, -1], [5, 10]]);
+ expect(e.equals([[0, -1], [5, 10]])).to.be.ok;
});
});
diff --git a/test/spec/renderer/features.js b/test/spec/renderer/features.js
new file mode 100644
index 000000000..c887d43c8
--- /dev/null
+++ b/test/spec/renderer/features.js
@@ -0,0 +1,505 @@
+describe('iD.Features', function() {
+ var dimensions = [1000, 1000],
+ features;
+
+ beforeEach(function() {
+ features = iD().features();
+ });
+
+ describe('#keys', function() {
+ it('returns feature keys', function() {
+ var keys = features.keys();
+ expect(keys).to.have.members([
+ 'points', 'major_roads', 'minor_roads', 'paths',
+ 'buildings', 'landuse', 'boundaries', 'water', 'rail',
+ 'power', 'past_future', 'others'
+ ]);
+ });
+ });
+
+ describe('#disable', function() {
+ it('disables features', function() {
+ features.disable('water');
+ expect(features.disabled()).to.include('water');
+ expect(features.enabled()).to.not.include('water');
+ });
+ });
+
+ describe('#enable', function() {
+ it('enables features', function() {
+ features.disable('water');
+ features.enable('water');
+ expect(features.disabled()).to.not.include('water');
+ expect(features.enabled()).to.include('water');
+ });
+ });
+
+ describe('#toggle', function() {
+ it('toggles features', function() {
+ features.toggle('water');
+ expect(features.disabled()).to.include('water');
+ expect(features.enabled()).to.not.include('water');
+
+ features.toggle('water');
+ expect(features.disabled()).to.not.include('water');
+ expect(features.enabled()).to.include('water');
+ });
+ });
+
+ describe('#gatherStats', function() {
+ it('counts features', function() {
+ var graph = iD.Graph([
+ iD.Node({id: 'point_bar', tags: {amenity: 'bar'}, version: 1}),
+ iD.Node({id: 'point_dock', tags: {waterway: 'dock'}, version: 1}),
+ iD.Node({id: 'point_rail_station', tags: {railway: 'station'}, version: 1}),
+ iD.Node({id: 'point_generator', tags: {power: 'generator'}, version: 1}),
+ iD.Node({id: 'point_old_rail_station', tags: {railway: 'station', disused: 'yes'}, version: 1}),
+ iD.Way({id: 'motorway', tags: {highway: 'motorway'}, version: 1}),
+ iD.Way({id: 'building_yes', tags: {area: 'yes', amenity: 'school', building: 'yes'}, version: 1}),
+ iD.Way({id: 'boundary', tags: {boundary: 'administrative'}, version: 1}),
+ iD.Way({id: 'fence', tags: {barrier: 'fence'}, version: 1})
+ ]),
+ all = _.values(graph.base().entities),
+ stats;
+
+ features.gatherStats(all, graph, dimensions);
+ stats = features.stats();
+
+ expect(stats.boundaries).to.eql(1);
+ expect(stats.buildings).to.eql(1);
+ expect(stats.landuse).to.eql(0);
+ expect(stats.major_roads).to.eql(1);
+ expect(stats.minor_roads).to.eql(0);
+ expect(stats.others).to.eql(1);
+ expect(stats.past_future).to.eql(1);
+ expect(stats.paths).to.eql(0);
+ expect(stats.points).to.eql(5);
+ expect(stats.power).to.eql(1);
+ expect(stats.rail).to.eql(2);
+ expect(stats.water).to.eql(1);
+ });
+ });
+
+ describe("matching", function() {
+ var graph = iD.Graph([
+ // Points
+ iD.Node({id: 'point_bar', tags: {amenity: 'bar'}, version: 1}),
+ iD.Node({id: 'point_dock', tags: {waterway: 'dock'}, version: 1}),
+ iD.Node({id: 'point_rail_station', tags: {railway: 'station'}, version: 1}),
+ iD.Node({id: 'point_generator', tags: {power: 'generator'}, version: 1}),
+ iD.Node({id: 'point_old_rail_station', tags: {railway: 'station', disused: 'yes'}, version: 1}),
+
+ // Major Roads
+ iD.Way({id: 'motorway', tags: {highway: 'motorway'}, version: 1}),
+ iD.Way({id: 'motorway_link', tags: {highway: 'motorway_link'}, version: 1}),
+ iD.Way({id: 'trunk', tags: {highway: 'trunk'}, version: 1}),
+ iD.Way({id: 'trunk_link', tags: {highway: 'trunk_link'}, version: 1}),
+ iD.Way({id: 'primary', tags: {highway: 'primary'}, version: 1}),
+ iD.Way({id: 'primary_link', tags: {highway: 'primary_link'}, version: 1}),
+ iD.Way({id: 'secondary', tags: {highway: 'secondary'}, version: 1}),
+ iD.Way({id: 'secondary_link', tags: {highway: 'secondary_link'}, version: 1}),
+ iD.Way({id: 'tertiary', tags: {highway: 'tertiary'}, version: 1}),
+ iD.Way({id: 'tertiary_link', tags: {highway: 'tertiary_link'}, version: 1}),
+ iD.Way({id: 'residential', tags: {highway: 'residential'}, version: 1}),
+
+ // Minor Roads
+ iD.Way({id: 'service', tags: {highway: 'service'}, version: 1}),
+ iD.Way({id: 'living_street', tags: {highway: 'living_street'}, version: 1}),
+ iD.Way({id: 'road', tags: {highway: 'road'}, version: 1}),
+ iD.Way({id: 'unclassified', tags: {highway: 'unclassified'}, version: 1}),
+ iD.Way({id: 'track', tags: {highway: 'track'}, version: 1}),
+
+ // Paths
+ iD.Way({id: 'path', tags: {highway: 'path'}, version: 1}),
+ iD.Way({id: 'footway', tags: {highway: 'footway'}, version: 1}),
+ iD.Way({id: 'cycleway', tags: {highway: 'cycleway'}, version: 1}),
+ iD.Way({id: 'bridleway', tags: {highway: 'bridleway'}, version: 1}),
+ iD.Way({id: 'steps', tags: {highway: 'steps'}, version: 1}),
+ iD.Way({id: 'pedestrian', tags: {highway: 'pedestrian'}, version: 1}),
+
+ // Buildings
+ iD.Way({id: 'building_yes', tags: {area: 'yes', amenity: 'school', building: 'yes'}, version: 1}),
+ iD.Way({id: 'building_no', tags: {area: 'yes', amenity: 'school', building: 'no'}, version: 1}),
+ iD.Way({id: 'building_part', tags: { 'building:part': 'yes'}, version: 1}),
+ iD.Way({id: 'shelter', tags: {area: 'yes', amenity: 'shelter'}, version: 1}),
+ iD.Way({id: 'garage1', tags: {area: 'yes', amenity: 'parking', parking: 'multi-storey'}, version: 1}),
+ iD.Way({id: 'garage2', tags: {area: 'yes', amenity: 'parking', parking: 'sheds'}, version: 1}),
+ iD.Way({id: 'garage3', tags: {area: 'yes', amenity: 'parking', parking: 'carports'}, version: 1}),
+ iD.Way({id: 'garage4', tags: {area: 'yes', amenity: 'parking', parking: 'garage_boxes'}, version: 1}),
+
+ // Landuse
+ iD.Way({id: 'forest', tags: {area: 'yes', landuse: 'forest'}, version: 1}),
+ iD.Way({id: 'scrub', tags: {area: 'yes', natural: 'scrub'}, version: 1}),
+ iD.Way({id: 'industrial', tags: {area: 'yes', landuse: 'industrial'}, version: 1}),
+ iD.Way({id: 'parkinglot', tags: {area: 'yes', amenity: 'parking', parking: 'surface'}, version: 1}),
+
+ // Boundaries
+ iD.Way({id: 'boundary', tags: {boundary: 'administrative'}, version: 1}),
+
+ // Water
+ iD.Way({id: 'water', tags: {area: 'yes', natural: 'water'}, version: 1}),
+ iD.Way({id: 'coastline', tags: {natural: 'coastline'}, version: 1}),
+ iD.Way({id: 'bay', tags: {area: 'yes', natural: 'bay'}, version: 1}),
+ iD.Way({id: 'pond', tags: {area: 'yes', landuse: 'pond'}, version: 1}),
+ iD.Way({id: 'basin', tags: {area: 'yes', landuse: 'basin'}, version: 1}),
+ iD.Way({id: 'reservoir', tags: {area: 'yes', landuse: 'reservoir'}, version: 1}),
+ iD.Way({id: 'salt_pond', tags: {area: 'yes', landuse: 'salt_pond'}, version: 1}),
+ iD.Way({id: 'river', tags: {waterway: 'river'}, version: 1}),
+
+ // Rail
+ iD.Way({id: 'railway', tags: {railway: 'rail'}, version: 1}),
+ iD.Way({id: 'rail_landuse', tags: {area: 'yes', landuse: 'railway'}, version: 1}),
+ iD.Way({id: 'rail_disused', tags: {railway: 'disused'}, version: 1}),
+ iD.Way({id: 'rail_streetcar', tags: {railway: 'tram', highway: 'residential'}, version: 1}),
+ iD.Way({id: 'rail_trail', tags: {railway: 'disused', highway: 'cycleway'}, version: 1}),
+
+ // Power
+ iD.Way({id: 'power_line', tags: {power: 'line'}, version: 1}),
+
+ // Past/Future
+ iD.Way({id: 'motorway_construction', tags: {highway: 'construction', construction: 'motorway'}, version: 1}),
+ iD.Way({id: 'cycleway_proposed', tags: {highway: 'proposed', proposed: 'cycleway'}, version: 1}),
+ iD.Way({id: 'landuse_construction', tags: {area: 'yes', landuse: 'construction'}, version: 1}),
+
+ // 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), graph), 'doMatch: ' + id).to.be.true;
+ });
+ }
+
+ function dontMatch(ids) {
+ _.each(ids, function(id) {
+ 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',
+ 'point_generator', 'point_old_rail_station'
+ ]);
+
+ dontMatch([
+ 'motorway', 'service', 'path', 'building_yes',
+ 'forest', 'boundary', 'water', 'railway', 'power_line',
+ 'motorway_construction', 'fence'
+ ]);
+ });
+
+
+ it("matches major roads", function () {
+ features.disable('major_roads');
+ features.gatherStats(all, graph, dimensions);
+
+ doMatch([
+ 'motorway', 'motorway_link', 'trunk', 'trunk_link',
+ 'primary', 'primary_link', 'secondary', 'secondary_link',
+ 'tertiary', 'tertiary_link', 'residential'
+ ]);
+
+ dontMatch([
+ 'point_bar', 'service', 'path', 'building_yes',
+ 'forest', 'boundary', 'water', 'railway', 'power_line',
+ 'motorway_construction', 'fence'
+ ]);
+ });
+
+
+ it("matches minor roads", function () {
+ features.disable('minor_roads');
+ features.gatherStats(all, graph, dimensions);
+
+ doMatch([
+ 'service', 'living_street', 'road', 'unclassified', 'track'
+ ]);
+
+ dontMatch([
+ 'point_bar', 'motorway', 'path', 'building_yes',
+ 'forest', 'boundary', 'water', 'railway', 'power_line',
+ 'motorway_construction', 'fence'
+ ]);
+ });
+
+
+ it("matches paths", function () {
+ features.disable('paths');
+ features.gatherStats(all, graph, dimensions);
+
+ doMatch([
+ 'path', 'footway', 'cycleway', 'bridleway',
+ 'steps', 'pedestrian'
+ ]);
+
+ dontMatch([
+ 'point_bar', 'motorway', 'service', 'building_yes',
+ 'forest', 'boundary', 'water', 'railway', 'power_line',
+ 'motorway_construction', 'fence'
+ ]);
+ });
+
+
+ it("matches buildings", function () {
+ features.disable('buildings');
+ features.gatherStats(all, graph, dimensions);
+
+ doMatch([
+ 'building_yes', 'building_part', 'shelter',
+ 'garage1', 'garage2', 'garage3', 'garage4'
+ ]);
+
+ dontMatch([
+ 'building_no', 'point_bar', 'motorway', 'service', 'path',
+ 'forest', 'boundary', 'water', 'railway', 'power_line',
+ 'motorway_construction', 'fence'
+ ]);
+ });
+
+
+ it("matches landuse", function () {
+ features.disable('landuse');
+ features.gatherStats(all, graph, dimensions);
+
+ doMatch([
+ 'forest', 'scrub', 'industrial', 'parkinglot', 'building_no',
+ 'rail_landuse', 'landuse_construction'
+ ]);
+
+ dontMatch([
+ 'point_bar', 'motorway', 'service', 'path', 'building_yes',
+ 'boundary', 'water', 'railway', 'power_line',
+ 'motorway_construction', 'fence'
+ ]);
+ });
+
+
+ it("matches boundaries", function () {
+ features.disable('boundaries');
+ features.gatherStats(all, graph, dimensions);
+
+ doMatch([
+ 'boundary'
+ ]);
+
+ dontMatch([
+ 'point_bar', 'motorway', 'service', 'path', 'building_yes',
+ 'forest', 'water', 'railway', 'power_line',
+ 'motorway_construction', 'fence'
+ ]);
+ });
+
+
+ it("matches water", function () {
+ features.disable('water');
+ features.gatherStats(all, graph, dimensions);
+
+ doMatch([
+ 'point_dock', 'water', 'coastline', 'bay', 'pond',
+ 'basin', 'reservoir', 'salt_pond', 'river'
+ ]);
+
+ dontMatch([
+ 'point_bar', 'motorway', 'service', 'path', 'building_yes',
+ 'forest', 'boundary', 'railway', 'power_line',
+ 'motorway_construction', 'fence'
+ ]);
+ });
+
+
+ it("matches rail", function () {
+ features.disable('rail');
+ features.gatherStats(all, graph, dimensions);
+
+ doMatch([
+ 'point_rail_station', 'point_old_rail_station',
+ 'railway', 'rail_landuse', 'rail_disused'
+ ]);
+
+ dontMatch([
+ 'rail_streetcar', 'rail_trail', // because rail also used as highway
+ 'point_bar', 'motorway', 'service', 'path', 'building_yes',
+ 'forest', 'boundary', 'water', 'power_line',
+ 'motorway_construction', 'fence'
+ ]);
+ });
+
+
+ it("matches power", function () {
+ features.disable('power');
+ features.gatherStats(all, graph, dimensions);
+
+ doMatch([
+ 'point_generator', 'power_line'
+ ]);
+
+ dontMatch([
+ 'point_bar', 'motorway', 'service', 'path', 'building_yes',
+ 'forest', 'boundary', 'water', 'railway',
+ 'motorway_construction', 'fence'
+ ]);
+ });
+
+
+ it("matches past/future", function () {
+ features.disable('past_future');
+ features.gatherStats(all, graph, dimensions);
+
+ doMatch([
+ 'point_old_rail_station', 'rail_disused',
+ 'motorway_construction', 'cycleway_proposed', 'landuse_construction'
+ ]);
+
+ dontMatch([
+ 'rail_trail', // because rail also used as highway
+ 'point_bar', 'motorway', 'service', 'path', 'building_yes',
+ 'forest', 'boundary', 'water', 'railway', 'power_line', 'fence'
+ ]);
+ });
+
+
+ it("matches others", function () {
+ features.disable('others');
+ features.gatherStats(all, graph, dimensions);
+
+ doMatch([
+ 'fence', 'pipeline'
+ ]);
+
+ dontMatch([
+ 'point_bar', 'motorway', 'service', 'path', 'building_yes',
+ 'forest', 'boundary', 'water', 'railway', 'power_line',
+ 'motorway_construction',
+ ]);
+ });
+ });
+
+
+ describe('hiding', function() {
+ it('hides child vertices on a hidden way', 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]),
+ all = _.values(graph.base().entities);
+
+ features.disable('paths');
+ features.gatherStats(all, graph, dimensions);
+
+ expect(features.isHiddenChild(a, graph)).to.be.true;
+ expect(features.isHidden(a, graph)).to.be.true;
+ });
+
+ it('hides child ways on a hidden multipolygon relation', function() {
+ 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(inner, graph)).to.be.true;
+ expect(features.isHidden(inner, graph)).to.be.true;
+ });
+
+ it('hides only versioned entities', function() {
+ var a = iD.Node({id: 'a', version: 1}),
+ b = iD.Node({id: 'b'}),
+ graph = iD.Graph([a, b]),
+ all = _.values(graph.base().entities);
+
+ features.disable('points');
+ features.gatherStats(all, graph, dimensions);
+
+ expect(features.isHidden(a, graph)).to.be.true;
+ expect(features.isHidden(b, graph)).to.be.false;
+ });
+
+ it('auto-hides features', function() {
+ var graph = iD.Graph([]),
+ maxPoints = 200,
+ all, hidden, autoHidden, i, msg;
+
+ for(i = 0; i < maxPoints; i++) {
+ graph.rebase([iD.Node({version: 1})], [graph]);
+ }
+
+ all = _.values(graph.base().entities);
+ features.gatherStats(all, graph, dimensions);
+ hidden = features.hidden();
+ autoHidden = features.autoHidden();
+ msg = i + ' points';
+
+ expect(hidden, msg).to.not.include('points');
+ expect(autoHidden, msg).to.not.include('points');
+
+ graph.rebase([iD.Node({version: 1})], [graph]);
+
+ all = _.values(graph.base().entities);
+ features.gatherStats(all, graph, dimensions);
+ hidden = features.hidden();
+ autoHidden = features.autoHidden();
+ msg = ++i + ' points';
+
+ expect(hidden, msg).to.include('points');
+ expect(autoHidden, msg).to.include('points');
+ });
+
+ it('doubles auto-hide threshold when doubling viewport size', function() {
+ var graph = iD.Graph([]),
+ maxPoints = 400,
+ dimensions = [2000, 1000],
+ all, hidden, autoHidden, i, msg;
+
+ for(i = 0; i < maxPoints; i++) {
+ graph.rebase([iD.Node({version: 1})], [graph]);
+ }
+
+ all = _.values(graph.base().entities);
+ features.gatherStats(all, graph, dimensions);
+ hidden = features.hidden();
+ autoHidden = features.autoHidden();
+ msg = i + ' points';
+
+ expect(hidden, msg).to.not.include('points');
+ expect(autoHidden, msg).to.not.include('points');
+
+ graph.rebase([iD.Node({version: 1})], [graph]);
+
+ all = _.values(graph.base().entities);
+ features.gatherStats(all, graph, dimensions);
+ hidden = features.hidden();
+ autoHidden = features.autoHidden();
+ msg = ++i + ' points';
+
+ expect(hidden, msg).to.include('points');
+ expect(autoHidden, msg).to.include('points');
+ });
+ });
+
+});