diff --git a/Makefile b/Makefile
index 2819f2884..788eef43e 100644
--- a/Makefile
+++ b/Makefile
@@ -17,6 +17,7 @@ all: \
js/lib/d3.keybinding.js \
js/lib/d3.one.js \
js/lib/d3.size.js \
+ js/lib/d3.tail.js \
js/lib/d3.trigger.js \
js/lib/d3.typeahead.js \
js/lib/jxon.js \
@@ -29,6 +30,8 @@ all: \
js/id/oauth.js \
js/id/services/*.js \
js/id/util.js \
+ js/id/geo.js \
+ js/id/geo/*.js \
js/id/actions.js \
js/id/actions/*.js \
js/id/behavior.js \
diff --git a/css/app.css b/css/app.css
index 4aba98318..932ca3b52 100644
--- a/css/app.css
+++ b/css/app.css
@@ -1044,7 +1044,17 @@ div.typeahead a:first-child {
}
.Browse .tooltip .tooltip-arrow {
- left: 30px;
- }
-
+ left: 30px;
+}
+.tail {
+ pointer-events:none;
+ position: absolute;
+ background: rgba(255, 255, 255, 0.7);
+ max-width: 250px;
+ margin-top: -15px;
+ padding: 5px;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+}
diff --git a/css/map.css b/css/map.css
index 9d09b1b63..5d529ea55 100644
--- a/css/map.css
+++ b/css/map.css
@@ -142,29 +142,41 @@ path.stroke.tag-railway-subway {
stroke-dasharray: 8,8;
}
-path.area {
+path.area,
+path.multipolygon {
stroke-width:2;
stroke:#fff;
fill:#fff;
fill-opacity:0.3;
}
+path.multipolygon {
+ fill-rule: evenodd;
+}
+
+path.area.member-type-multipolygon {
+ fill: none;
+}
+
path.area.selected {
stroke-width:4 !important;
}
-path.area.tag-natural {
+path.area.tag-natural,
+path.multipolygon.tag-natural {
stroke: #ADD6A5;
fill: #ADD6A5;
stroke-width:1;
}
-path.area.tag-natural-water {
+path.area.tag-natural-water,
+path.multipolygon.tag-natural-water {
stroke: #6382FF;
fill: #ADBEFF;
}
-path.area.tag-building {
+path.area.tag-building,
+path.multipolygon.tag-building {
stroke: #9E176A;
stroke-width: 1;
fill: #ff6ec7;
@@ -173,11 +185,24 @@ path.area.tag-building {
path.area.tag-landuse,
path.area.tag-natural-wood,
path.area.tag-natural-tree,
-path.area.tag-natural-grassland {
+path.area.tag-natural-grassland,
+path.area.tag-leisure-park,
+path.multipolygon.tag-landuse,
+path.multipolygon.tag-natural-wood,
+path.multipolygon.tag-natural-tree,
+path.multipolygon.tag-natural-grassland,
+path.multipolygon.tag-leisure-park {
stroke: #006B34;
stroke-width: 1;
fill: #189E59;
- fill-opacity:0.2;
+ fill-opacity: 0.2;
+}
+
+path.area.tag-amenity-parking,
+path.multipolygon.tag-amenity-parking {
+ stroke: #beb267;
+ stroke-width: 1;
+ fill: #edecc0;
}
/* highways */
@@ -321,7 +346,9 @@ text.textpath-label, text.text-label {
}
.mode-select .area,
-.mode-browse .area {
+.mode-browse .area,
+.mode-select .multipolygon,
+.mode-browse .multipolygon {
cursor: url(../img/cursor-select-area.png), pointer;
}
@@ -334,6 +361,7 @@ text.textpath-label, text.text-label {
.vertex:active,
.line:active,
.area:active,
+.multipolygon:active,
.midpoint:active,
.mode-select .selected {
cursor: url(../img/cursor-select-acting.png), pointer;
diff --git a/index.html b/index.html
index 9938836eb..bbefb2c04 100644
--- a/index.html
+++ b/index.html
@@ -22,6 +22,7 @@
+
@@ -32,6 +33,9 @@
+
+
+
@@ -40,7 +44,9 @@
+
+
@@ -109,6 +115,7 @@
+
diff --git a/js/id/connection.js b/js/id/connection.js
index 71f11e23d..003750ff2 100644
--- a/js/id/connection.js
+++ b/js/id/connection.js
@@ -27,8 +27,6 @@ iD.Connection = function() {
return callback(null, parse(dom));
}
return d3.xml(url).get().on('load', done);
- inflight.push(d3.xml(url).get()
- .on('load', done));
}
function getNodes(obj) {
diff --git a/js/id/geo.js b/js/id/geo.js
new file mode 100644
index 000000000..06b63ae14
--- /dev/null
+++ b/js/id/geo.js
@@ -0,0 +1 @@
+iD.geo = {};
diff --git a/js/id/geo/extent.js b/js/id/geo/extent.js
new file mode 100644
index 000000000..80bf631c6
--- /dev/null
+++ b/js/id/geo/extent.js
@@ -0,0 +1,37 @@
+iD.geo.Extent = function (min, max) {
+ if (!(this instanceof iD.geo.Extent)) return new iD.geo.Extent(min, max);
+ if (min instanceof iD.geo.Extent) {
+ return min;
+ } else if (min && min.length === 2 && min[0].length === 2 && min[1].length === 2) {
+ this[0] = min[0];
+ this[1] = min[1];
+ } else {
+ this[0] = min || [ Infinity, Infinity];
+ this[1] = max || min || [-Infinity, -Infinity];
+ }
+};
+
+iD.geo.Extent.prototype = [[], []];
+
+_.extend(iD.geo.Extent.prototype, {
+ extend: function (obj) {
+ obj = iD.geo.Extent(obj);
+ return iD.geo.Extent([Math.min(obj[0][0], this[0][0]),
+ Math.min(obj[0][1], this[0][1])],
+ [Math.max(obj[1][0], this[1][0]),
+ Math.max(obj[1][1], this[1][1])]);
+ },
+
+ center: function () {
+ return [(this[0][0] + this[1][0]) / 2,
+ (this[0][1] + this[1][1]) / 2];
+ },
+
+ intersects: function (obj) {
+ obj = iD.geo.Extent(obj);
+ return obj[0][0] <= this[1][0] &&
+ obj[0][1] <= this[1][1] &&
+ obj[1][0] >= this[0][0] &&
+ obj[1][1] >= this[0][1];
+ }
+});
diff --git a/js/id/graph/entity.js b/js/id/graph/entity.js
index afc152589..8c8f9a6fb 100644
--- a/js/id/graph/entity.js
+++ b/js/id/graph/entity.js
@@ -74,11 +74,7 @@ iD.Entity.prototype = {
},
intersects: function(extent, resolver) {
- var _extent = this.extent(resolver);
- return _extent[0][0] > extent[0][0] &&
- _extent[1][0] < extent[1][0] &&
- _extent[0][1] < extent[0][1] &&
- _extent[1][1] > extent[1][1];
+ return this.extent(resolver).intersects(extent);
},
hasInterestingTags: function() {
diff --git a/js/id/graph/node.js b/js/id/graph/node.js
index 9500991fa..fd3a1cd69 100644
--- a/js/id/graph/node.js
+++ b/js/id/graph/node.js
@@ -2,7 +2,7 @@ iD.Node = iD.Entity.extend({
type: "node",
extent: function() {
- return [this.loc, this.loc];
+ return iD.geo.Extent(this.loc);
},
geometry: function() {
diff --git a/js/id/graph/relation.js b/js/id/graph/relation.js
index 4788ad0d1..7d8a9aa89 100644
--- a/js/id/graph/relation.js
+++ b/js/id/graph/relation.js
@@ -2,8 +2,16 @@ iD.Relation = iD.Entity.extend({
type: "relation",
members: [],
- extent: function() {
- return [[NaN, NaN], [NaN, NaN]];
+ extent: function(resolver) {
+ return resolver.transient(this, 'extent', function() {
+ return this.members.reduce(function (extent, member) {
+ if (member = resolver.entity(member.id)) {
+ return extent.extend(member.extent(resolver))
+ } else {
+ return extent;
+ }
+ }, iD.geo.Extent());
+ });
},
geometry: function() {
@@ -22,7 +30,7 @@ iD.Relation = iD.Entity.extend({
//
multipolygon: function(resolver) {
var members = this.members
- .filter(function (m) { return m.type === 'way'; })
+ .filter(function (m) { return m.type === 'way' && resolver.entity(m.id); })
.map(function (m) { return { role: m.role || 'outer', id: m.id, nodes: resolver.fetch(m.id).nodes }; });
function join(ways) {
diff --git a/js/id/graph/validate.js b/js/id/graph/validate.js
new file mode 100644
index 000000000..addb774ca
--- /dev/null
+++ b/js/id/graph/validate.js
@@ -0,0 +1,47 @@
+iD.validate = function(changes) {
+ var warnings = [], change;
+
+ // https://github.com/openstreetmap/josm/blob/mirror/src/org/
+ // openstreetmap/josm/data/validation/tests/UnclosedWays.java#L80
+ function tagSuggestsArea(change) {
+ if (_.isEmpty(change.tags)) return false;
+ var tags = change.tags;
+ var presence = ['landuse', 'amenities', 'tourism', 'shop'];
+ for (var i = 0; i < presence.length; i++) {
+ if (tags[presence[i]] !== undefined) {
+ return presence[i] + '=' + tags[presence[i]];
+ }
+ }
+ if (tags.building && tags.building === 'yes') return 'building=yes';
+ }
+
+ if (changes.created.length) {
+ for (var i = 0; i < changes.created.length; i++) {
+ change = changes.created[i];
+
+ if (change.geometry() === 'point' && _.isEmpty(change.tags)) {
+ warnings.push({
+ message: 'Untagged point which is not part of a line or area',
+ entity: change
+ });
+ }
+
+ if (change.geometry() === 'line' && _.isEmpty(change.tags)) {
+ warnings.push({ message: 'Untagged line', entity: change });
+ }
+
+ if (change.geometry() === 'area' && _.isEmpty(change.tags)) {
+ warnings.push({ message: 'Untagged area', entity: change });
+ }
+
+ if (change.geometry() === 'line' && tagSuggestsArea(change)) {
+ warnings.push({
+ message: 'The tag ' + tagSuggestsArea(change) + ' suggests line should be area, but it is not and area',
+ entity: change
+ });
+ }
+ }
+ }
+
+ return warnings.length ? [warnings] : [];
+};
diff --git a/js/id/graph/way.js b/js/id/graph/way.js
index 5c3e1210b..305f12051 100644
--- a/js/id/graph/way.js
+++ b/js/id/graph/way.js
@@ -4,14 +4,11 @@ iD.Way = iD.Entity.extend({
extent: function(resolver) {
return resolver.transient(this, 'extent', function() {
- var extent = [[-Infinity, Infinity], [Infinity, -Infinity]];
+ var extent = iD.geo.Extent();
for (var i = 0, l = this.nodes.length; i < l; i++) {
var node = this.nodes[i];
if (node.loc === undefined) node = resolver.entity(node);
- if (node.loc[0] > extent[0][0]) extent[0][0] = node.loc[0];
- if (node.loc[0] < extent[1][0]) extent[1][0] = node.loc[0];
- if (node.loc[1] < extent[0][1]) extent[0][1] = node.loc[1];
- if (node.loc[1] > extent[1][1]) extent[1][1] = node.loc[1];
+ extent = extent.extend(node.loc);
}
return extent;
});
diff --git a/js/id/id.js b/js/id/id.js
index 712656cd6..afcdfc3e4 100644
--- a/js/id/id.js
+++ b/js/id/id.js
@@ -96,7 +96,7 @@ window.iD = function(container) {
var save_button = bar.append('button')
.attr('class', 'save action wide')
- .call(iD.ui.save().map(map));
+ .call(iD.ui.save().map(map).controller(controller));
history.on('change.warn-unload', function() {
var changes = history.changes(),
@@ -156,8 +156,12 @@ window.iD = function(container) {
.attr('class','about-block fillD pad1');
contributors.append('span')
.attr('class', 'icon nearby icon-pre-text');
- contributors.append('pan')
+ contributors.append('span')
.text('Viewing contributions by ');
+ contributors.append('span')
+ .attr('class', 'contributor-list');
+ contributors.append('span')
+ .attr('class', 'contributor-count');
history.on('change.buttons', function() {
var undo = history.undoAnnotation(),
@@ -180,9 +184,9 @@ window.iD = function(container) {
.call(redo ? refreshTooltip : undo_tooltip.hide);
});
- window.onresize = function() {
+ d3.select(window).on('resize.map-size', function() {
map.size(m.size());
- };
+ });
map.keybinding()
.on('a', function(evt, mods) {
@@ -205,8 +209,7 @@ window.iD = function(container) {
var hash = iD.Hash().map(map);
if (!hash.hadHash) {
- map.zoom(20)
- .center([-77.02271,38.90085]);
+ map.centerZoom([-77.02271, 38.90085], 20);
}
d3.select('.user-container').call(iD.ui.userpanel(connection)
diff --git a/js/id/modes/add_area.js b/js/id/modes/add_area.js
index 6977f2e15..a9fcb830e 100644
--- a/js/id/modes/add_area.js
+++ b/js/id/modes/add_area.js
@@ -12,7 +12,7 @@ iD.modes.AddArea = function() {
controller = mode.controller;
map.dblclickEnable(false)
- .hint('Click on the map to start drawing an area, like a park, lake, or building.');
+ .tail('Click on the map to start drawing an area, like a park, lake, or building.');
map.surface.on('click.addarea', function() {
var datum = d3.select(d3.event.target).datum() || {},
@@ -47,7 +47,7 @@ iD.modes.AddArea = function() {
window.setTimeout(function() {
mode.map.dblclickEnable(true);
}, 1000);
- mode.map.hint(false);
+ mode.map.tail(false);
mode.map.surface.on('click.addarea', null);
mode.map.keybinding().on('⎋.addarea', null);
};
diff --git a/js/id/modes/add_line.js b/js/id/modes/add_line.js
index 60f376181..b83202097 100644
--- a/js/id/modes/add_line.js
+++ b/js/id/modes/add_line.js
@@ -13,7 +13,7 @@ iD.modes.AddLine = function() {
controller = mode.controller;
map.dblclickEnable(false)
- .hint('Click on the map to start drawing an road, path, or route.');
+ .tail('Click on the map to start drawing an road, path, or route.');
map.surface.on('click.addline', function() {
var datum = d3.select(d3.event.target).datum() || {},
@@ -67,7 +67,7 @@ iD.modes.AddLine = function() {
mode.exit = function() {
mode.map.dblclickEnable(true);
- mode.map.hint(false);
+ mode.map.tail(false);
mode.map.surface.on('click.addline', null);
mode.map.keybinding().on('⎋.addline', null);
};
diff --git a/js/id/modes/add_point.js b/js/id/modes/add_point.js
index 37767f3de..21d6343e0 100644
--- a/js/id/modes/add_point.js
+++ b/js/id/modes/add_point.js
@@ -10,7 +10,7 @@ iD.modes.AddPoint = function() {
history = mode.history,
controller = mode.controller;
- map.hint('Click on the map to add a point.');
+ map.tail('Click on the map to add a point.');
map.surface.on('click.addpoint', function() {
var node = iD.Node({loc: map.mouseCoordinates(), _poi: true});
@@ -28,7 +28,7 @@ iD.modes.AddPoint = function() {
};
mode.exit = function() {
- mode.map.hint(false);
+ mode.map.tail(false);
mode.map.surface.on('click.addpoint', null);
mode.map.keybinding().on('⎋.addpoint', null);
};
diff --git a/js/id/modes/draw_area.js b/js/id/modes/draw_area.js
index 88d61d323..cdb966351 100644
--- a/js/id/modes/draw_area.js
+++ b/js/id/modes/draw_area.js
@@ -18,8 +18,7 @@ iD.modes.DrawArea = function(wayId) {
map.dblclickEnable(false)
.fastEnable(false);
- map.hint('Click on the map to add points to your area. Finish the ' +
- 'area by clicking on your first point');
+ map.tail('Click to add points to your area. Click the first point to finish the area.');
history.perform(
iD.actions.AddNode(node),
@@ -116,7 +115,7 @@ iD.modes.DrawArea = function(wayId) {
surface.selectAll('.way, .node')
.classed('active', false);
- mode.map.hint(false);
+ mode.map.tail(false);
mode.map.fastEnable(true);
surface
diff --git a/js/id/modes/draw_line.js b/js/id/modes/draw_line.js
index a59e3a7e9..68130d7b8 100644
--- a/js/id/modes/draw_line.js
+++ b/js/id/modes/draw_line.js
@@ -19,7 +19,7 @@ iD.modes.DrawLine = function(wayId, direction) {
map.dblclickEnable(false)
.fastEnable(false)
- .hint('Click to add more points to the line. ' +
+ .tail('Click to add more points to the line. ' +
'Click on other lines to connect to them, and double-click to ' +
'end the line.');
@@ -152,7 +152,7 @@ iD.modes.DrawLine = function(wayId, direction) {
surface.selectAll('.way, .node')
.classed('active', false);
- mode.map.hint(false);
+ mode.map.tail(false);
mode.map.fastEnable(true);
mode.map.minzoom(0);
diff --git a/js/id/modes/select.js b/js/id/modes/select.js
index d091fdc57..62b309e29 100644
--- a/js/id/modes/select.js
+++ b/js/id/modes/select.js
@@ -57,14 +57,13 @@ iD.modes.Select = function (entity) {
// of the inspector
var inspector_size = d3.select('.inspector-wrap').size(),
map_size = mode.map.size(),
- entity_extent = entity.extent(mode.history.graph()),
- left_edge = map_size[0] - inspector_size[0],
- left = mode.map.projection(entity_extent[1])[0],
- right = mode.map.projection(entity_extent[0])[0];
+ offset = 50,
+ shift_left = d3.event.x - map_size[0] + inspector_size[0] + offset,
+ center = (map_size[0] / 2) + shift_left + offset;
- if (left > left_edge &&
- right > left_edge) mode.map.centerEase(
- mode.map.projection.invert([(window.innerWidth), d3.event.y]));
+ if (shift_left > 0 && inspector_size[1] > d3.event.y) {
+ mode.map.centerEase(mode.map.projection.invert([center, map_size[1]/2]));
+ }
inspector
.on('changeTags', changeTags)
diff --git a/js/id/renderer/hash.js b/js/id/renderer/hash.js
index 05691ef59..bb454811b 100644
--- a/js/id/renderer/hash.js
+++ b/js/id/renderer/hash.js
@@ -1,5 +1,5 @@
iD.Hash = function() {
- var hash = {},
+ var hash = { hadHash: false },
s0, // cached location.hash
lat = 90 - 1e-8, // allowable latitude range
map;
@@ -10,8 +10,9 @@ iD.Hash = function() {
if (args.length < 3 || args.some(isNaN)) {
return true; // replace bogus hash
} else {
- map.zoom(args[0])
- .center([args[2], Math.min(lat, Math.max(-lat, args[1]))]);
+ map.centerZoom([args[2],
+ Math.min(lat, Math.max(-lat, args[1]))],
+ args[0]);
}
};
@@ -27,12 +28,13 @@ iD.Hash = function() {
var move = _.throttle(function() {
var s1 = formatter(map);
if (s0 !== s1) location.replace(s0 = s1); // don't recenter the map!
- }, 1000);
+ }, 100);
function hashchange() {
if (location.hash === s0) return; // ignore spurious hashchange events
- if (parser(map, (s0 = location.hash).substring(2)))
+ if (parser(map, (s0 = location.hash).substring(2))) {
move(); // replace bogus hash
+ }
}
hash.map = function(x) {
diff --git a/js/id/renderer/map.js b/js/id/renderer/map.js
index 99ad5a438..1ffb5cbb9 100644
--- a/js/id/renderer/map.js
+++ b/js/id/renderer/map.js
@@ -5,6 +5,7 @@ iD.Map = function() {
translateStart,
keybinding = d3.keybinding(),
projection = d3.geo.mercator().scale(1024),
+ roundedProjection = iD.svg.RoundProjection(projection),
zoom = d3.behavior.zoom()
.translate(projection.translate())
.scale(projection.scale())
@@ -16,12 +17,14 @@ iD.Map = function() {
background = iD.Background()
.projection(projection),
transformProp = iD.util.prefixCSSProperty('Transform'),
- points = iD.svg.Points(),
- vertices = iD.svg.Vertices(),
- lines = iD.svg.Lines(),
- areas = iD.svg.Areas(),
- midpoints = iD.svg.Midpoints(),
- labels = iD.svg.Labels(),
+ points = iD.svg.Points(roundedProjection),
+ vertices = iD.svg.Vertices(roundedProjection),
+ lines = iD.svg.Lines(roundedProjection),
+ areas = iD.svg.Areas(roundedProjection),
+ multipolygons = iD.svg.Multipolygons(roundedProjection),
+ midpoints = iD.svg.Midpoints(roundedProjection),
+ labels = iD.svg.Labels(roundedProjection),
+ tail = d3.tail(),
surface, tilegroup;
function map(selection) {
@@ -45,9 +48,13 @@ iD.Map = function() {
})
.call(iD.svg.Surface());
+
map.size(selection.size());
map.surface = surface;
+ supersurface
+ .call(tail);
+
d3.select(document).call(keybinding);
}
@@ -63,26 +70,32 @@ iD.Map = function() {
all = graph.intersects(extent);
filter = d3.functor(true);
} else {
- var only = {},
- filterOnly = {};
- for (var j = 0; j < difference.length; j++) {
- var id = difference[j],
- entity = graph.fetch(id);
- // Even if the entity is false (deleted), it needs to be
- // removed from the surface
- only[id] = entity;
- if (entity && entity.intersects(extent, graph)) {
- if (only[id].type === 'node') {
- var parents = graph.parentWays(only[id]);
- for (var k = 0; k < parents.length; k++) {
- // Don't re-fetch parents
- if (only[parents[k].id] === undefined) {
- only[parents[k].id] = graph.fetch(parents[k].id);
- }
- }
+ var only = {};
+
+ function addParents(parents) {
+ for (var i = 0; i < parents.length; i++) {
+ var parent = parents[i];
+ if (only[parent.id] === undefined) {
+ only[parent.id] = graph.fetch(parent.id);
+ addParents(graph.parentRelations(parent));
}
}
}
+
+ for (var j = 0; j < difference.length; j++) {
+ var id = difference[j],
+ entity = graph.fetch(id);
+
+ // Even if the entity is false (deleted), it needs to be
+ // removed from the surface
+ only[id] = entity;
+
+ if (entity && entity.intersects(extent, graph)) {
+ addParents(graph.parentWays(only[id]));
+ addParents(graph.parentRelations(only[id]));
+ }
+ }
+
all = _.compact(_.values(only));
filter = function(d) { return d.midpoint ? d.way in only : d.id in only; };
}
@@ -93,11 +106,12 @@ iD.Map = function() {
}
surface
- .call(points, graph, all, filter, projection)
- .call(vertices, graph, all, filter, projection)
- .call(lines, graph, all, filter, projection)
- .call(areas, graph, all, filter, projection)
- .call(midpoints, graph, all, filter, projection)
+ .call(points, graph, all, filter)
+ .call(vertices, graph, all, filter)
+ .call(lines, graph, all, filter)
+ .call(areas, graph, all, filter)
+ .call(multipolygons, graph, all, filter)
+ .call(midpoints, graph, all, filter)
.call(labels, graph, all, filter, projection);
}
@@ -149,7 +163,7 @@ iD.Map = function() {
redraw();
}
- function redraw(difference) {
+ var redraw = _.throttle(function(difference) {
dispatch.move(map);
surface.attr('data-zoom', ~~map.zoom());
tilegroup.call(background);
@@ -160,7 +174,7 @@ iD.Map = function() {
editOff();
}
return map;
- }
+ }, 10);
function pointLocation(p) {
var translate = projection.translate(),
@@ -195,10 +209,7 @@ iD.Map = function() {
return map;
};
- map.zoom = function(z) {
- if (!arguments.length) {
- return Math.max(Math.log(projection.scale()) / Math.LN2 - 8, 0);
- }
+ function setZoom(z) {
var scale = 256 * Math.pow(2, z),
center = pxCenter(),
l = pointLocation(center);
@@ -211,8 +222,17 @@ iD.Map = function() {
t[1] += center[1] - l[1];
projection.translate(t);
zoom.translate(projection.translate());
- return redraw();
- };
+ }
+
+ function setCenter(loc) {
+ var t = projection.translate(),
+ c = pxCenter(),
+ ll = projection(loc);
+ projection.translate([
+ t[0] - ll[0] + c[0],
+ t[1] - ll[1] + c[1]]);
+ zoom.translate(projection.translate());
+ }
map.size = function(_) {
if (!arguments.length) return dimensions;
@@ -232,17 +252,25 @@ iD.Map = function() {
if (!arguments.length) {
return projection.invert(pxCenter());
} else {
- var t = projection.translate(),
- c = pxCenter(),
- ll = projection(loc);
- projection.translate([
- t[0] - ll[0] + c[0],
- t[1] - ll[1] + c[1]]);
- zoom.translate(projection.translate());
+ setCenter(loc);
return redraw();
}
};
+ map.zoom = function(z) {
+ if (!arguments.length) {
+ return Math.max(Math.log(projection.scale()) / Math.LN2 - 8, 0);
+ }
+ setZoom(z);
+ return redraw();
+ };
+
+ map.centerZoom = function(loc, z) {
+ setCenter(loc);
+ setZoom(z);
+ return redraw();
+ };
+
map.centerEase = function(loc) {
var from = map.center().slice(), t = 0;
d3.timer(function() {
@@ -251,26 +279,23 @@ iD.Map = function() {
}, 20);
};
- map.extent = function(tl, br) {
+ map.extent = function(_) {
if (!arguments.length) {
- return [projection.invert([0, 0]), projection.invert(dimensions)];
+ return iD.geo.Extent(projection.invert([0, dimensions[1]]),
+ projection.invert([dimensions[0], 0]));
} else {
-
- var TL = projection(tl),
- BR = projection(br);
+ var extent = iD.geo.Extent(_),
+ tl = projection([extent[0][0], extent[1][1]]),
+ br = projection([extent[1][0], extent[0][1]]);
// Calculate maximum zoom that fits extent
- var hFactor = (BR[0] - TL[0]) / dimensions[0],
- vFactor = (BR[1] - TL[1]) / dimensions[1],
+ var hFactor = (br[0] - tl[0]) / dimensions[0],
+ vFactor = (br[1] - tl[1]) / dimensions[1],
hZoomDiff = Math.log(Math.abs(hFactor)) / Math.LN2,
vZoomDiff = Math.log(Math.abs(vFactor)) / Math.LN2,
newZoom = map.zoom() - Math.max(hZoomDiff, vZoomDiff);
- // Calculate center of projected extent
- var midPoint = [(TL[0] + BR[0]) / 2, (TL[1] + BR[1]) / 2],
- midLoc = projection.invert(midPoint);
-
- map.zoom(newZoom).center(midLoc);
+ map.centerZoom(extent.center(), newZoom);
}
};
@@ -286,6 +311,11 @@ iD.Map = function() {
return map;
};
+ map.tail = function (_) {
+ tail.text(_);
+ return map;
+ };
+
map.hint = function (_) {
if (_ === false) {
d3.select('div.inspector-wrap')
diff --git a/js/id/svg.js b/js/id/svg.js
index 5860126e9..e2b1dc2ad 100644
--- a/js/id/svg.js
+++ b/js/id/svg.js
@@ -6,9 +6,24 @@ iD.svg = {
},
PointTransform: function (projection) {
- projection = iD.svg.RoundProjection(projection);
return function (entity) {
return 'translate(' + projection(entity.loc) + ')';
};
+ },
+
+ LineString: function (projection) {
+ var cache = {};
+ return function (entity) {
+ if (cache[entity.id] !== undefined) {
+ return cache[entity.id];
+ }
+
+ if (entity.nodes.length === 0) {
+ return (cache[entity.id] = null);
+ }
+
+ return (cache[entity.id] =
+ 'M' + entity.nodes.map(function (n) { return projection(n.loc); }).join('L'));
+ }
}
};
diff --git a/js/id/svg/areas.js b/js/id/svg/areas.js
index ff24afecc..5a3c31288 100644
--- a/js/id/svg/areas.js
+++ b/js/id/svg/areas.js
@@ -1,4 +1,4 @@
-iD.svg.Areas = function() {
+iD.svg.Areas = function(projection) {
var area_stack = {
building: 0,
@@ -26,7 +26,7 @@ iD.svg.Areas = function() {
return as - bs;
}
- return function drawAreas(surface, graph, entities, filter, projection) {
+ return function drawAreas(surface, graph, entities, filter) {
var areas = [];
for (var i = 0; i < entities.length; i++) {
@@ -38,20 +38,10 @@ iD.svg.Areas = function() {
areas.sort(areastack);
- var lineStrings = {};
-
- function lineString(entity) {
- if (lineStrings[entity.id] !== undefined) {
- return lineStrings[entity.id];
- }
- var nodes = _.pluck(entity.nodes, 'loc');
- if (nodes.length === 0) return (lineStrings[entity.id] = '');
- else return (lineStrings[entity.id] =
- 'M' + nodes.map(iD.svg.RoundProjection(projection)).join('L'));
- }
+ var lineString = iD.svg.LineString(projection);
function drawPaths(group, areas, filter, classes) {
- var paths = group.selectAll('path')
+ var paths = group.selectAll('path.area')
.filter(filter)
.data(areas, iD.Entity.key);
@@ -62,7 +52,8 @@ iD.svg.Areas = function() {
paths
.order()
.attr('d', lineString)
- .call(iD.svg.TagClasses());
+ .call(iD.svg.TagClasses())
+ .call(iD.svg.MemberClasses(graph));
paths.exit()
.remove();
diff --git a/js/id/svg/lines.js b/js/id/svg/lines.js
index 48eef2a26..a0e951591 100644
--- a/js/id/svg/lines.js
+++ b/js/id/svg/lines.js
@@ -1,4 +1,4 @@
-iD.svg.Lines = function() {
+iD.svg.Lines = function(projection) {
var arrowtext = '►\u3000\u3000',
alength;
@@ -33,30 +33,28 @@ iD.svg.Lines = function() {
return as - bs;
}
- function drawPaths(group, lines, filter, classes, lineString, prefix) {
- var paths = group.selectAll('path')
- .filter(filter)
- .data(lines, iD.Entity.key);
+ return function drawLines(surface, graph, entities, filter) {
+ function drawPaths(group, lines, filter, classes, lineString, prefix) {
+ var paths = group.selectAll('path')
+ .filter(filter)
+ .data(lines, iD.Entity.key);
- paths.enter()
- .append('path')
- .attr('id', function(d) {
- return prefix + d.id;
- })
- .attr('class', classes);
+ paths.enter()
+ .append('path')
+ .attr('id', function(d) { return prefix + d.id;})
+ .attr('class', classes);
- paths
- .order()
- .attr('d', lineString)
- .call(iD.svg.TagClasses());
+ paths
+ .order()
+ .attr('d', lineString)
+ .call(iD.svg.TagClasses())
+ .call(iD.svg.MemberClasses(graph));
- paths.exit()
- .remove();
+ paths.exit()
+ .remove();
- return paths;
- }
-
- return function drawLines(surface, graph, entities, filter, projection) {
+ return paths;
+ }
if (!alength) {
var arrow = surface.append('text').text(arrowtext);
@@ -76,15 +74,7 @@ iD.svg.Lines = function() {
lines.sort(waystack);
- function lineString(entity) {
- if (lineStrings[entity.id] !== undefined) {
- return lineStrings[entity.id];
- }
- var nodes = _.pluck(entity.nodes, 'loc');
- if (nodes.length === 0) return (lineStrings[entity.id] = '');
- else return (lineStrings[entity.id] =
- 'M' + nodes.map(iD.svg.RoundProjection(projection)).join('L'));
- }
+ var lineString = iD.svg.LineString(projection);
var casing = surface.select('.layer-casing'),
stroke = surface.select('.layer-stroke'),
diff --git a/js/id/svg/member_classes.js b/js/id/svg/member_classes.js
new file mode 100644
index 000000000..b675288c3
--- /dev/null
+++ b/js/id/svg/member_classes.js
@@ -0,0 +1,32 @@
+iD.svg.MemberClasses = function(graph) {
+ var tagClassRe = /^member-?/;
+
+ return function memberClassesSelection(selection) {
+ selection.each(function memberClassesEach(d, i) {
+ var classes, value = this.className;
+
+ if (value.baseVal !== undefined) value = value.baseVal;
+
+ classes = value.trim().split(/\s+/).filter(function(name) {
+ return name.length && !tagClassRe.test(name);
+ }).join(' ');
+
+ var relations = graph.parentRelations(d);
+
+ if (relations.length) {
+ classes += ' member';
+ }
+
+ relations.forEach(function (relation) {
+ classes += ' member-type-' + relation.tags.type;
+ classes += ' member-role-' + _.find(relation.members, function (member) { return member.id == d.id; }).role;
+ });
+
+ classes = classes.trim();
+
+ if (classes !== value) {
+ d3.select(this).attr('class', classes);
+ }
+ });
+ };
+};
diff --git a/js/id/svg/midpoints.js b/js/id/svg/midpoints.js
index 9c060ea3b..321e75c8e 100644
--- a/js/id/svg/midpoints.js
+++ b/js/id/svg/midpoints.js
@@ -1,5 +1,5 @@
-iD.svg.Midpoints = function() {
- return function drawMidpoints(surface, graph, entities, filter, projection) {
+iD.svg.Midpoints = function(projection) {
+ return function drawMidpoints(surface, graph, entities, filter) {
var midpoints = [];
for (var i = 0; i < entities.length; i++) {
diff --git a/js/id/svg/multipolygons.js b/js/id/svg/multipolygons.js
new file mode 100644
index 000000000..6330cf685
--- /dev/null
+++ b/js/id/svg/multipolygons.js
@@ -0,0 +1,55 @@
+iD.svg.Multipolygons = function(projection) {
+ return function(surface, graph, entities, filter) {
+ var multipolygons = [];
+
+ for (var i = 0; i < entities.length; i++) {
+ var entity = entities[i];
+ if (entity.geometry() === 'relation' && entity.tags.type === 'multipolygon') {
+ multipolygons.push(entity);
+ }
+ }
+
+ var lineStrings = {};
+
+ function lineString(entity) {
+ if (lineStrings[entity.id] !== undefined) {
+ return lineStrings[entity.id];
+ }
+
+ var multipolygon = entity.multipolygon(graph);
+ if (entity.members.length == 0 || !multipolygon) {
+ return (lineStrings[entity.id] = null);
+ }
+
+ multipolygon = _.flatten(multipolygon, true);
+ return (lineStrings[entity.id] =
+ multipolygon.map(function (ring) {
+ return 'M' + ring.map(function (node) { return projection(node.loc); }).join('L');
+ }).join(""));
+ }
+
+ function drawPaths(group, multipolygons, filter, classes) {
+ var paths = group.selectAll('path.multipolygon')
+ .filter(filter)
+ .data(multipolygons, iD.Entity.key);
+
+ paths.enter()
+ .append('path')
+ .attr('class', classes);
+
+ paths
+ .order()
+ .attr('d', lineString)
+ .call(iD.svg.TagClasses())
+ .call(iD.svg.MemberClasses(graph));
+
+ paths.exit()
+ .remove();
+
+ return paths;
+ }
+
+ var fill = surface.select('.layer-fill'),
+ paths = drawPaths(fill, multipolygons, filter, 'relation multipolygon');
+ };
+};
diff --git a/js/id/svg/points.js b/js/id/svg/points.js
index 7a2600814..5862a5cb9 100644
--- a/js/id/svg/points.js
+++ b/js/id/svg/points.js
@@ -1,4 +1,4 @@
-iD.svg.Points = function() {
+iD.svg.Points = function(projection) {
function imageHref(d) {
// TODO: optimize
for (var k in d.tags) {
@@ -10,7 +10,7 @@ iD.svg.Points = function() {
return 'icons/unknown.png';
}
- return function drawPoints(surface, graph, entities, filter, projection) {
+ return function drawPoints(surface, graph, entities, filter) {
var points = [];
for (var i = 0; i < entities.length; i++) {
@@ -45,7 +45,8 @@ iD.svg.Points = function() {
.attr('transform', 'translate(-8, -8)');
groups.attr('transform', iD.svg.PointTransform(projection))
- .call(iD.svg.TagClasses());
+ .call(iD.svg.TagClasses())
+ .call(iD.svg.MemberClasses(graph));
// Selecting the following implicitly
// sets the data (point entity) on the element
diff --git a/js/id/svg/tag_classes.js b/js/id/svg/tag_classes.js
index 43562fd5c..a7660e1f5 100644
--- a/js/id/svg/tag_classes.js
+++ b/js/id/svg/tag_classes.js
@@ -1,7 +1,8 @@
iD.svg.TagClasses = function() {
var keys = iD.util.trueObj([
'highway', 'railway', 'motorway', 'amenity', 'natural',
- 'landuse', 'building', 'oneway', 'bridge'
+ 'landuse', 'building', 'oneway', 'bridge', 'boundary',
+ 'leisure'
]), tagClassRe = /^tag-/;
return function tagClassesSelection(selection) {
diff --git a/js/id/svg/vertices.js b/js/id/svg/vertices.js
index 700ef5538..ce81dface 100644
--- a/js/id/svg/vertices.js
+++ b/js/id/svg/vertices.js
@@ -1,5 +1,5 @@
-iD.svg.Vertices = function() {
- return function drawVertices(surface, graph, entities, filter, projection) {
+iD.svg.Vertices = function(projection) {
+ return function drawVertices(surface, graph, entities, filter) {
var vertices = [];
for (var i = 0; i < entities.length; i++) {
@@ -31,6 +31,7 @@ iD.svg.Vertices = function() {
groups.attr('transform', iD.svg.PointTransform(projection))
.call(iD.svg.TagClasses())
+ .call(iD.svg.MemberClasses(graph))
.classed('shared', function(entity) { return graph.parentWays(entity).length > 1; });
// Selecting the following implicitly
diff --git a/js/id/ui/commit.js b/js/id/ui/commit.js
index 547f4505f..06d072275 100644
--- a/js/id/ui/commit.js
+++ b/js/id/ui/commit.js
@@ -1,5 +1,5 @@
iD.ui.commit = function() {
- var event = d3.dispatch('cancel', 'save');
+ var event = d3.dispatch('cancel', 'save', 'fix');
function zipSame(d) {
var c = [], n = -1;
@@ -58,12 +58,12 @@ iD.ui.commit = function() {
header.append('p').text('The changes you upload will be visible on all maps that use OpenStreetMap data.');
- var commit = body.append('div').attr('class','modal-section');
- commit.append('textarea')
- .attr('class', 'changeset-comment')
- .attr('placeholder', 'Brief Description of your contributions');
+ var comment_section = body.append('div').attr('class','modal-section');
+ comment_section.append('textarea')
+ .attr('class', 'changeset-comment')
+ .attr('placeholder', 'Brief Description of your contributions');
- var buttonwrap = commit.append('div')
+ var buttonwrap = comment_section.append('div')
.attr('class', 'buttons');
var savebutton = buttonwrap.append('button')
@@ -84,12 +84,39 @@ iD.ui.commit = function() {
cancelbutton.append('span').attr('class','icon close icon-pre-text');
cancelbutton.append('span').attr('class','label').text('Cancel');
+ var warnings = body.selectAll('div.warning-section')
+ .data(iD.validate(changes))
+ .enter()
+ .append('div').attr('class', 'modal-section warning-section');
+
+ warnings.append('h3')
+ .text('Warnings');
+
+ var warning_li = warnings.append('ul')
+ .attr('class', 'changeset-list')
+ .selectAll('li')
+ .data(function(d) { return d; })
+ .enter()
+ .append('li');
+
+ warning_li.append('button')
+ .attr('class', 'minor')
+ .on('click', event.fix)
+ .append('span')
+ .attr('class', 'icon inspect');
+
+ warning_li.append('strong').text(function(d) {
+ return d.message;
+ });
+
var section = body.selectAll('div.commit-section')
.data(['modified', 'deleted', 'created'].filter(changesLength))
.enter()
.append('div').attr('class', 'commit-section modal-section fillL2');
- section.append('h3').text(String)
+ section.append('h3').text(function(d) {
+ return d.charAt(0).toUpperCase() + d.slice(1);
+ })
.append('small')
.attr('class', 'count')
.text(changesLength);
diff --git a/js/id/ui/contributors.js b/js/id/ui/contributors.js
index 72dedf1d1..c0036a677 100644
--- a/js/id/ui/contributors.js
+++ b/js/id/ui/contributors.js
@@ -3,22 +3,48 @@ iD.ui.contributors = function(map) {
function contributors(selection) {
var users = {},
+ limit = 3,
entities = map.history().graph().intersects(map.extent());
+
for (var i in entities) {
- if (entities[i].user) {
- users[entities[i].user] = true;
- if (Object.keys(users).length > 10) break;
- }
+ if (entities[i].user) users[entities[i].user] = true;
}
- var u = Object.keys(users);
- var l = selection.selectAll('a.user-link').data(u);
+
+ var u = Object.keys(users),
+ subset = u.slice(0, limit);
+
+ var l = selection
+ .select('.contributor-list')
+ .selectAll('a.user-link')
+ .data(subset);
+
+
l.enter().append('a')
.attr('class', 'user-link')
.attr('href', function(d) { return map.connection().userUrl(d); })
.attr('target', '_blank')
.text(String);
+
l.exit().remove();
+ selection
+ .select('.contributor-count')
+ .html('');
+
+ if (u.length > limit) {
+ selection
+ .select('.contributor-count')
+ .append('a')
+ .attr('target', '_blank')
+ .attr('href', function() {
+ var ext = map.extent();
+ return 'http://www.openstreetmap.org/browse/changesets?bbox=' + [
+ ext[0][0], ext[0][1],
+ ext[1][0], ext[1][1]];
+ })
+ .text(' and ' + (u.length - limit) + ' others');
+ }
+
if (!u.length) {
selection.transition().style('opacity', 0);
} else if (selection.style('opacity') === '0') {
diff --git a/js/id/ui/geocoder.js b/js/id/ui/geocoder.js
index 87eaa7d74..4bd8c0629 100644
--- a/js/id/ui/geocoder.js
+++ b/js/id/ui/geocoder.js
@@ -16,7 +16,7 @@ iD.ui.geocoder = function() {
.text('No location found for "' + resp.query[0] + '"');
}
var bounds = resp.results[0][0].bounds;
- map.extent([bounds[0], bounds[3]], [bounds[2], bounds[1]]);
+ map.extent(iD.geo.Extent([bounds[0], bounds[1]], [bounds[2], bounds[3]]));
});
}
diff --git a/js/id/ui/inspector.js b/js/id/ui/inspector.js
index 3af39c1b7..600538e4c 100644
--- a/js/id/ui/inspector.js
+++ b/js/id/ui/inspector.js
@@ -114,12 +114,14 @@ iD.ui.inspector = function() {
inputs.append('input')
.property('type', 'text')
.attr('class', 'key')
+ .attr('maxlength', 255)
.property('value', function(d) { return d.key; })
.on('change', function(d) { d.key = this.value; });
inputs.append('input')
.property('type', 'text')
.attr('class', 'value')
+ .attr('maxlength', 255)
.property('value', function(d) { return d.value; })
.on('change', function(d) { d.value = this.value; })
.on('keydown.push-more', pushMore);
@@ -271,7 +273,7 @@ iD.ui.inspector = function() {
inspector.tags = function (tags) {
if (!arguments.length) {
- var tags = {};
+ tags = {};
tagList.selectAll('li').each(function() {
var row = d3.select(this),
key = row.selectAll('.key').property('value'),
diff --git a/js/id/ui/save.js b/js/id/ui/save.js
index 27d6ef1e5..6bf8419c3 100644
--- a/js/id/ui/save.js
+++ b/js/id/ui/save.js
@@ -1,6 +1,6 @@
iD.ui.save = function() {
- var map;
+ var map, controller;
function save(selection) {
@@ -59,6 +59,12 @@ iD.ui.save = function() {
.on('cancel', function() {
modal.remove();
})
+ .on('fix', function(d) {
+ map.extent(d.entity.extent(map.history().graph()));
+ if (map.zoom() > 19) map.zoom(19);
+ controller.enter(iD.modes.Select(d.entity));
+ modal.remove();
+ })
.on('save', commit));
});
} else {
@@ -91,5 +97,11 @@ iD.ui.save = function() {
return save;
};
+ save.controller = function(_) {
+ if (!arguments.length) return controller;
+ controller = _;
+ return save;
+ };
+
return save;
};
diff --git a/js/lib/d3.tail.js b/js/lib/d3.tail.js
new file mode 100644
index 000000000..8046e4a96
--- /dev/null
+++ b/js/lib/d3.tail.js
@@ -0,0 +1,70 @@
+d3.tail = function() {
+ var text = false,
+ container,
+ xmargin = 20,
+ tooltip_size = [0, 0],
+ selection_size = [0, 0],
+ transformProp = iD.util.prefixCSSProperty('Transform');
+
+ var tail = function(selection) {
+
+ d3.select(window).on('resize.tail-size', function() {
+ selection_size = selection.size();
+ });
+
+ function setup() {
+
+ container = d3.select(document.body)
+ .append('div').attr('class', 'tail');
+
+ selection
+ .on('mousemove.tail', mousemove)
+ .on('mouseover.tail', mouseover)
+ .on('mouseout.tail', mouseout);
+
+ container
+ .on('mousemove.tail', mousemove);
+
+ selection_size = selection.size();
+
+ }
+
+ function mousemove() {
+ if (text === false) return;
+ var xoffset = ((d3.event.x + tooltip_size[0] + xmargin) > selection_size[0]) ?
+ -tooltip_size[0] - xmargin : xoffset = xmargin;
+ container.style(transformProp, 'translate(' +
+ (~~d3.event.x + xoffset) + 'px,' +
+ ~~d3.event.y + 'px)');
+ }
+
+ function mouseout() {
+ if (d3.event.relatedTarget !== container.node() &&
+ text !== false) container.style('display', 'none');
+ }
+
+ function mouseover() {
+ if (d3.event.relatedTarget !== container.node() &&
+ text !== false) container.style('display', 'block');
+ }
+
+ if (!container) setup();
+
+ };
+
+ tail.text = function(_) {
+ if (_ === false) {
+ text = _;
+ container.style('display', 'none');
+ return tail;
+ } else if (container.style('display') == 'none') {
+ container.style('display', 'block');
+ }
+ text = _;
+ container.text(text);
+ tooltip_size = container.size();
+ return tail;
+ };
+
+ return tail;
+};
diff --git a/test/index.html b/test/index.html
index f8d570f36..0777751d5 100644
--- a/test/index.html
+++ b/test/index.html
@@ -25,6 +25,7 @@
+
@@ -33,6 +34,9 @@
+
+
+
@@ -41,7 +45,9 @@
+
+
@@ -107,7 +113,10 @@
@@ -134,6 +143,8 @@
+
+
@@ -147,7 +158,11 @@
+
+
+
+
diff --git a/test/index_packaged.html b/test/index_packaged.html
index 92c61df79..630ed1088 100644
--- a/test/index_packaged.html
+++ b/test/index_packaged.html
@@ -20,7 +20,10 @@
@@ -47,6 +50,8 @@
+
+
@@ -60,7 +65,11 @@
+
+
+
+
diff --git a/test/spec/geo/extent.js b/test/spec/geo/extent.js
new file mode 100644
index 000000000..54f328efb
--- /dev/null
+++ b/test/spec/geo/extent.js
@@ -0,0 +1,100 @@
+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]]);
+ });
+
+ it("constructs via a point", function () {
+ var p = [0, 0];
+ expect(iD.geo.Extent(p)).to.eql([p, p]);
+ });
+
+ it("constructs via two points", function () {
+ var min = [0, 0],
+ max = [5, 10];
+ expect(iD.geo.Extent(min, max)).to.eql([min, max]);
+ });
+
+ it("constructs via an extent", function () {
+ var min = [0, 0],
+ max = [5, 10];
+ expect(iD.geo.Extent([min, max])).to.eql([min, max]);
+ });
+
+ 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);
+ });
+
+ it("has length 2", function () {
+ expect(iD.geo.Extent().length).to.equal(2);
+ });
+
+ it("has min element", function () {
+ var min = [0, 0],
+ max = [5, 10];
+ expect(iD.geo.Extent(min, max)[0]).to.equal(min);
+ });
+
+ it("has max element", function () {
+ var min = [0, 0],
+ max = [5, 10];
+ expect(iD.geo.Extent(min, max)[1]).to.equal(max);
+ });
+ });
+
+ describe("#center", function () {
+ it("returns the center point", function () {
+ expect(iD.geo.Extent([0, 0], [5, 10]).center()).to.eql([2.5, 5]);
+ });
+ });
+
+ describe("#extend", 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]]);
+ });
+
+ 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]]);
+ });
+
+ 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]]);
+ });
+ });
+
+ describe('#intersects', function () {
+ it("returns true for a point inside self", function () {
+ expect(iD.geo.Extent([0, 0], [5, 5]).intersects([2, 2])).to.be.true;
+ });
+
+ it("returns true for a point on the boundary of self", function () {
+ expect(iD.geo.Extent([0, 0], [5, 5]).intersects([0, 0])).to.be.true;
+ });
+
+ it("returns false for a point outside self", function () {
+ expect(iD.geo.Extent([0, 0], [5, 5]).intersects([6, 6])).to.be.false;
+ });
+
+ it("returns true for an extent contained by self", function () {
+ expect(iD.geo.Extent([0, 0], [5, 5]).intersects([[1, 1], [2, 2]])).to.be.true;
+ expect(iD.geo.Extent([1, 1], [2, 2]).intersects([[0, 0], [5, 5]])).to.be.true;
+ });
+
+ it("returns true for an extent intersected by self", function () {
+ expect(iD.geo.Extent([0, 0], [5, 5]).intersects([[1, 1], [6, 6]])).to.be.true;
+ expect(iD.geo.Extent([1, 1], [6, 6]).intersects([[0, 0], [5, 5]])).to.be.true;
+ });
+
+ it("returns false for an extent not intersected by self", function () {
+ expect(iD.geo.Extent([0, 0], [5, 5]).intersects([[6, 6], [7, 7]])).to.be.false;
+ expect(iD.geo.Extent([[6, 6], [7, 7]]).intersects([[0, 0], [5, 5]])).to.be.false;
+ });
+ });
+});
diff --git a/test/spec/graph/node.js b/test/spec/graph/node.js
index 929946b04..8045270b3 100644
--- a/test/spec/graph/node.js
+++ b/test/spec/graph/node.js
@@ -29,11 +29,11 @@ describe('iD.Node', function () {
describe("#intersects", function () {
it("returns true for a node within the given extent", function () {
- expect(iD.Node({loc: [0, 0]}).intersects([[-180, 90], [180, -90]])).to.equal(true);
+ expect(iD.Node({loc: [0, 0]}).intersects([[-5, -5], [5, 5]])).to.equal(true);
});
it("returns false for a node outside the given extend", function () {
- expect(iD.Node({loc: [0, 0]}).intersects([[100, 90], [180, -90]])).to.equal(false);
+ expect(iD.Node({loc: [6, 6]}).intersects([[-5, -5], [5, 5]])).to.equal(false);
});
});
diff --git a/test/spec/graph/relation.js b/test/spec/graph/relation.js
index 26a1a2022..8880fc98c 100644
--- a/test/spec/graph/relation.js
+++ b/test/spec/graph/relation.js
@@ -36,7 +36,23 @@ describe('iD.Relation', function () {
});
describe("#extent", function () {
- it("returns the minimal extent containing the extents of all members");
+ it("returns the minimal extent containing the extents of all members", function () {
+ var a = iD.Node({loc: [0, 0]}),
+ b = iD.Node({loc: [5, 10]}),
+ 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]])
+ });
+
+ it("returns the known extent of incomplete relations", function () {
+ var a = iD.Node({loc: [0, 0]}),
+ b = iD.Node({loc: [5, 10]}),
+ 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]])
+ });
});
describe("#multipolygon", function () {
@@ -232,5 +248,17 @@ describe('iD.Relation', function () {
expect(r.multipolygon(graph)).to.eql([[[a, b, c, a], [d, e, f, d]], [[g, h, i, g]]]);
});
+
+ specify("incomplete relation", function () {
+ var a = iD.Node(),
+ b = iD.Node(),
+ c = iD.Node(),
+ w1 = iD.Way({nodes: [a.id, b.id, c.id]}),
+ w2 = iD.Way(),
+ r = iD.Relation({members: [{id: w2.id, type: 'way'}, {id: w1.id, type: 'way'}]}),
+ g = iD.Graph([a, b, c, w1, r]);
+
+ expect(r.multipolygon(g)).to.eql([[[a, b, c]]]);
+ });
});
});
diff --git a/test/spec/graph/way.js b/test/spec/graph/way.js
index 3aa8045fb..19a4514db 100644
--- a/test/spec/graph/way.js
+++ b/test/spec/graph/way.js
@@ -41,7 +41,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([[5, 0], [0, 10]]);
+ expect(way.extent(graph)).to.eql([[0, 0], [5, 10]]);
});
});
@@ -50,14 +50,14 @@ describe('iD.Way', function() {
var node = iD.Node({loc: [0, 0]}),
way = iD.Way({nodes: [node.id]}),
graph = iD.Graph([node, way]);
- expect(way.intersects([[-180, 90], [180, -90]], graph)).to.equal(true);
+ expect(way.intersects([[-5, -5], [5, 5]], graph)).to.equal(true);
});
it("returns false for way with no nodes within the given extent", function () {
- var node = iD.Node({loc: [0, 0]}),
+ var node = iD.Node({loc: [6, 6]}),
way = iD.Way({nodes: [node.id]}),
graph = iD.Graph([node, way]);
- expect(way.intersects([[100, 90], [180, -90]], graph)).to.equal(false);
+ expect(way.intersects([[-5, -5], [5, 5]], graph)).to.equal(false);
});
});
diff --git a/test/spec/renderer/hash.js b/test/spec/renderer/hash.js
index 64945683d..44d3c64a2 100644
--- a/test/spec/renderer/hash.js
+++ b/test/spec/renderer/hash.js
@@ -7,7 +7,8 @@ describe("hash", function () {
on: function () { return map; },
off: function () { return map; },
zoom: function () { return arguments.length ? map : 0; },
- center: function () { return arguments.length ? map : [0, 0] }
+ center: function () { return arguments.length ? map : [0, 0] },
+ centerZoom: function () { return arguments.length ? map : [0, 0] }
};
});
@@ -28,18 +29,11 @@ describe("hash", function () {
expect(hash.hadHash).to.be.true;
});
- it("zooms map to requested level", function () {
+ it("centerZooms map to requested level", function () {
location.hash = "?map=20.00/38.87952/-77.02405";
- sinon.spy(map, 'zoom');
+ sinon.spy(map, 'centerZoom');
hash.map(map);
- expect(map.zoom).to.have.been.calledWith(20.0);
- });
-
- it("centers map at requested coordinates", function () {
- location.hash = "?map=20.00/38.87952/-77.02405";
- sinon.spy(map, 'center');
- hash.map(map);
- expect(map.center).to.have.been.calledWith([-77.02405, 38.87952]);
+ expect(map.centerZoom).to.have.been.calledWith([-77.02405,38.87952], 20.0);
});
it("binds the map's move event", function () {
@@ -66,23 +60,13 @@ describe("hash", function () {
d3.select(window).one("hashchange", fn);
}
- it("zooms map to requested level", function (done) {
+ it("centerZooms map at requested coordinates", function (done) {
onhashchange(function () {
- expect(map.zoom).to.have.been.calledWith(20.0);
+ expect(map.centerZoom).to.have.been.calledWith([-77.02405,38.87952], 20.0);
done();
});
- sinon.spy(map, 'zoom');
- location.hash = "#?map=20.00/38.87952/-77.02405";
- });
-
- it("centers map at requested coordinates", function (done) {
- onhashchange(function () {
- expect(map.center).to.have.been.calledWith([-77.02405, 38.87952]);
- done();
- });
-
- sinon.spy(map, 'center');
+ sinon.spy(map, 'centerZoom');
location.hash = "#?map=20.00/38.87952/-77.02405";
});
});
diff --git a/test/spec/renderer/map.js b/test/spec/renderer/map.js
index 4cec560e9..611a2d1ff 100644
--- a/test/spec/renderer/map.js
+++ b/test/spec/renderer/map.js
@@ -54,16 +54,17 @@ describe('Map', function() {
describe('#extent', function() {
it('gets and sets extent', function() {
- expect(map.size([100, 100])).to.equal(map);
- expect(map.center([0, 0])).to.equal(map);
+ map.size([100, 100])
+ .center([0, 0]);
+
expect(map.extent()[0][0]).to.be.closeTo(-17.5, 0.5);
expect(map.extent()[1][0]).to.be.closeTo(17.5, 0.5);
- expect(map.extent([10, 1], [30, 1]));
+ expect(map.extent([[10, 1], [30, 1]]));
expect(map.extent()[0][0]).to.be.closeTo(10, 0.1);
expect(map.extent()[1][0]).to.be.closeTo(30, 0.1);
- expect(map.extent([-1, -20], [1, -40]));
- expect(map.extent()[0][1]).to.be.closeTo(-20, 0.1);
- expect(map.extent()[1][1]).to.be.closeTo(-40, 0.1);
+ expect(map.extent([[-1, -40], [1, -20]]));
+ expect(map.extent()[0][1]).to.be.closeTo(-40, 1);
+ expect(map.extent()[1][1]).to.be.closeTo(-20, 1);
});
});
diff --git a/test/spec/svg.js b/test/spec/svg.js
new file mode 100644
index 000000000..265562071
--- /dev/null
+++ b/test/spec/svg.js
@@ -0,0 +1,17 @@
+describe("iD.svg.LineString", function () {
+ it("returns an SVG path description for the entity's nodes", function () {
+ var a = iD.Node({loc: [0, 0]}),
+ b = iD.Node({loc: [2, 3]}),
+ way = iD.Way({nodes: [a, b]}),
+ projection = Object;
+
+ expect(iD.svg.LineString(projection)(way)).to.equal("M0,0L2,3");
+ });
+
+ it("returns null for an entity with no nodes", function () {
+ var way = iD.Way(),
+ projection = Object;
+
+ expect(iD.svg.LineString(projection)(way)).to.be.null;
+ });
+});
diff --git a/test/spec/svg/areas.js b/test/spec/svg/areas.js
index 54a7c1d35..4f984540a 100644
--- a/test/spec/svg/areas.js
+++ b/test/spec/svg/areas.js
@@ -1,6 +1,6 @@
describe("iD.svg.Areas", function () {
var surface,
- projection = d3.geo.mercator(),
+ projection = Object,
filter = d3.functor(true);
beforeEach(function () {
@@ -8,13 +8,48 @@ describe("iD.svg.Areas", function () {
.call(iD.svg.Surface());
});
+ it("adds way and area classes", function () {
+ var area = iD.Way({tags: {area: 'yes'}}),
+ graph = iD.Graph([area]);
+
+ surface.call(iD.svg.Areas(projection), graph, [area], filter);
+
+ expect(surface.select('path')).to.be.classed('way');
+ expect(surface.select('path')).to.be.classed('area');
+ });
+
it("adds tag classes", function () {
var area = iD.Way({tags: {area: 'yes', building: 'yes'}}),
graph = iD.Graph([area]);
- surface.call(iD.svg.Areas(), graph, [area], filter, projection);
+ surface.call(iD.svg.Areas(projection), graph, [area], filter);
expect(surface.select('.area')).to.be.classed('tag-building');
expect(surface.select('.area')).to.be.classed('tag-building-yes');
});
+
+ it("adds member classes", function () {
+ var area = iD.Way({tags: {area: 'yes'}}),
+ relation = iD.Relation({members: [{id: area.id, role: 'outer'}], tags: {type: 'multipolygon'}}),
+ graph = iD.Graph([area, relation]);
+
+ surface.call(iD.svg.Areas(projection), graph, [area], filter);
+
+ expect(surface.select('.area')).to.be.classed('member');
+ expect(surface.select('.area')).to.be.classed('member-role-outer');
+ expect(surface.select('.area')).to.be.classed('member-type-multipolygon');
+ });
+
+ it("preserves non-area paths", function () {
+ var area = iD.Way({tags: {area: 'yes'}}),
+ graph = iD.Graph([area]);
+
+ surface.select('.layer-fill')
+ .append('path')
+ .attr('class', 'other');
+
+ surface.call(iD.svg.Areas(projection), graph, [area], filter);
+
+ expect(surface.selectAll('.other')[0].length).to.equal(1);
+ });
});
diff --git a/test/spec/svg/lines.js b/test/spec/svg/lines.js
new file mode 100644
index 000000000..61c3229a9
--- /dev/null
+++ b/test/spec/svg/lines.js
@@ -0,0 +1,54 @@
+describe("iD.svg.Lines", function () {
+ var surface,
+ projection = Object,
+ filter = d3.functor(true);
+
+ beforeEach(function () {
+ surface = d3.select(document.createElementNS('http://www.w3.org/2000/svg', 'svg'))
+ .call(iD.svg.Surface());
+ });
+
+ it("adds way and area classes", function () {
+ var line = iD.Way(),
+ graph = iD.Graph([line]);
+
+ surface.call(iD.svg.Lines(projection), graph, [line], filter);
+
+ expect(surface.select('path')).to.be.classed('way');
+ expect(surface.select('path')).to.be.classed('line');
+ });
+
+ it("adds tag classes", function () {
+ var line = iD.Way({tags: {highway: 'residential'}}),
+ graph = iD.Graph([line]);
+
+ surface.call(iD.svg.Lines(projection), graph, [line], filter);
+
+ expect(surface.select('.line')).to.be.classed('tag-highway');
+ expect(surface.select('.line')).to.be.classed('tag-highway-residential');
+ });
+
+ it("adds member classes", function () {
+ var line = iD.Way(),
+ relation = iD.Relation({members: [{id: line.id}], tags: {type: 'route'}}),
+ graph = iD.Graph([line, relation]);
+
+ surface.call(iD.svg.Lines(projection), graph, [line], filter);
+
+ expect(surface.select('.line')).to.be.classed('member');
+ expect(surface.select('.line')).to.be.classed('member-type-route');
+ });
+
+ it("preserves non-line paths", function () {
+ var line = iD.Way(),
+ graph = iD.Graph([line]);
+
+ surface.select('.layer-fill')
+ .append('path')
+ .attr('class', 'other');
+
+ surface.call(iD.svg.Lines(projection), graph, [line], filter);
+
+ expect(surface.selectAll('.other')[0].length).to.equal(1);
+ });
+});
diff --git a/test/spec/svg/member_classes.js b/test/spec/svg/member_classes.js
new file mode 100644
index 000000000..651ba2342
--- /dev/null
+++ b/test/spec/svg/member_classes.js
@@ -0,0 +1,54 @@
+describe("iD.svg.MemberClasses", function () {
+ var selection;
+
+ beforeEach(function () {
+ selection = d3.select(document.createElementNS('http://www.w3.org/2000/svg', 'g'));
+ });
+
+ it("adds no classes to elements that aren't a member of any relations", function() {
+ var node = iD.Node(),
+ graph = iD.Graph([node]);
+
+ selection
+ .datum(node)
+ .call(iD.svg.MemberClasses(graph));
+
+ expect(selection.attr('class')).to.equal(null);
+ });
+
+ it("adds tags for member, role, and type", function() {
+ var node = iD.Node(),
+ relation = iD.Relation({members: [{id: node.id, role: 'r'}], tags: {type: 't'}}),
+ graph = iD.Graph([node, relation]);
+
+ selection
+ .datum(node)
+ .call(iD.svg.MemberClasses(graph));
+
+ expect(selection.attr('class')).to.equal('member member-type-t member-role-r');
+ });
+
+ it('removes classes for tags that are no longer present', function() {
+ var node = iD.Entity(),
+ graph = iD.Graph([node]);
+
+ selection
+ .attr('class', 'member member-type-t member-role-r')
+ .datum(node)
+ .call(iD.svg.MemberClasses(graph));
+
+ expect(selection.attr('class')).to.equal('');
+ });
+
+ it("preserves existing non-'member-'-prefixed classes", function() {
+ var node = iD.Entity(),
+ graph = iD.Graph([node]);
+
+ selection
+ .attr('class', 'selected')
+ .datum(node)
+ .call(iD.svg.MemberClasses(graph));
+
+ expect(selection.attr('class')).to.equal('selected');
+ });
+});
diff --git a/test/spec/svg/multipolygons.js b/test/spec/svg/multipolygons.js
new file mode 100644
index 000000000..67f44ebae
--- /dev/null
+++ b/test/spec/svg/multipolygons.js
@@ -0,0 +1,43 @@
+describe("iD.svg.Multipolygons", function () {
+ var surface,
+ projection = Object,
+ filter = d3.functor(true);
+
+ beforeEach(function () {
+ surface = d3.select(document.createElementNS('http://www.w3.org/2000/svg', 'svg'))
+ .call(iD.svg.Surface());
+ });
+
+ it("adds relation and multipolygon classes", function () {
+ var relation = iD.Relation({tags: {type: 'multipolygon'}}),
+ graph = iD.Graph([relation]);
+
+ surface.call(iD.svg.Multipolygons(projection), graph, [relation], filter);
+
+ expect(surface.select('path')).to.be.classed('relation');
+ expect(surface.select('path')).to.be.classed('multipolygon');
+ });
+
+ it("adds tag classes", function () {
+ var relation = iD.Relation({tags: {type: 'multipolygon', boundary: "administrative"}}),
+ graph = iD.Graph([relation]);
+
+ surface.call(iD.svg.Multipolygons(projection), graph, [relation], filter);
+
+ expect(surface.select('.relation')).to.be.classed('tag-boundary');
+ expect(surface.select('.relation')).to.be.classed('tag-boundary-administrative');
+ });
+
+ it("preserves non-multipolygon paths", function () {
+ var relation = iD.Relation({tags: {type: 'multipolygon'}}),
+ graph = iD.Graph([relation]);
+
+ surface.select('.layer-fill')
+ .append('path')
+ .attr('class', 'other');
+
+ surface.call(iD.svg.Multipolygons(projection), graph, [relation], filter);
+
+ expect(surface.selectAll('.other')[0].length).to.equal(1);
+ });
+});
diff --git a/test/spec/svg/points.js b/test/spec/svg/points.js
index 664f88a80..6e991962c 100644
--- a/test/spec/svg/points.js
+++ b/test/spec/svg/points.js
@@ -1,6 +1,6 @@
describe("iD.svg.Points", function () {
var surface,
- projection = d3.geo.mercator(),
+ projection = Object,
filter = d3.functor(true);
beforeEach(function () {
@@ -12,7 +12,7 @@ describe("iD.svg.Points", function () {
var node = iD.Node({tags: {amenity: "cafe"}, loc: [0, 0], _poi: true}),
graph = iD.Graph([node]);
- surface.call(iD.svg.Points(), graph, [node], filter, projection);
+ surface.call(iD.svg.Points(projection), graph, [node], filter);
expect(surface.select('.point')).to.be.classed('tag-amenity');
expect(surface.select('.point')).to.be.classed('tag-amenity-cafe');
diff --git a/test/spec/svg/vertices.js b/test/spec/svg/vertices.js
index 760734053..8ce5e3a80 100644
--- a/test/spec/svg/vertices.js
+++ b/test/spec/svg/vertices.js
@@ -1,6 +1,6 @@
describe("iD.svg.Vertices", function () {
var surface,
- projection = d3.geo.mercator(),
+ projection = Object,
filter = d3.functor(true);
beforeEach(function () {
@@ -12,7 +12,7 @@ describe("iD.svg.Vertices", function () {
var node = iD.Node({tags: {highway: "traffic_signals"}, loc: [0, 0]}),
graph = iD.Graph([node]);
- surface.call(iD.svg.Vertices(), graph, [node], filter, projection);
+ surface.call(iD.svg.Vertices(projection), graph, [node], filter);
expect(surface.select('.vertex')).to.be.classed('tag-highway');
expect(surface.select('.vertex')).to.be.classed('tag-highway-traffic_signals');
@@ -24,7 +24,7 @@ describe("iD.svg.Vertices", function () {
way2 = iD.Way({nodes: [node.id]}),
graph = iD.Graph([node, way1, way2]);
- surface.call(iD.svg.Vertices(), graph, [node], filter, projection);
+ surface.call(iD.svg.Vertices(projection), graph, [node], filter);
expect(surface.select('.vertex')).to.be.classed('shared');
});
diff --git a/test/spec/util.js b/test/spec/util.js
index 3ac1cdda2..2cfa8dc16 100644
--- a/test/spec/util.js
+++ b/test/spec/util.js
@@ -92,5 +92,25 @@ describe('Util', function() {
expect(iD.util.geo.polygonContainsPolygon(outer, inner)).to.be.false;
});
});
+
+ describe('#polygonIntersectsPolygon', function() {
+ it('says a polygon in a polygon intersects it', function() {
+ var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]];
+ var inner = [[1, 1], [1, 2], [2, 2], [2, 1], [1, 1]];
+ expect(iD.util.geo.polygonIntersectsPolygon(outer, inner)).to.be.true;
+ });
+
+ it('says a polygon that partially intersects does', function() {
+ var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]];
+ var inner = [[-1, -1], [1, 2], [2, 2], [2, 1], [1, 1]];
+ expect(iD.util.geo.polygonIntersectsPolygon(outer, inner)).to.be.true;
+ });
+
+ it('says totally disjoint polygons do not intersect', function() {
+ var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]];
+ var inner = [[-1, -1], [-1, -2], [-2, -2], [-2, -1], [-1, -1]];
+ expect(iD.util.geo.polygonIntersectsPolygon(outer, inner)).to.be.false;
+ });
+ });
});
});