Merge branch 'master' into labels

Conflicts:
	js/id/renderer/map.js
	js/id/svg/lines.js
This commit is contained in:
Ansis Brammanis
2013-01-18 17:19:12 -05:00
52 changed files with 974 additions and 226 deletions

View File

@@ -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 \

View File

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

View File

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

View File

@@ -22,6 +22,7 @@
<script src='js/lib/d3.size.js'></script>
<script src='js/lib/d3.trigger.js'></script>
<script src='js/lib/d3.keybinding.js'></script>
<script src='js/lib/d3.tail.js'></script>
<script src='js/lib/d3-compat.js'></script>
<script src='js/lib/queue.js'></script>
<script src='js/lib/bootstrap-tooltip.js'></script>
@@ -32,6 +33,9 @@
<script src='js/id/oauth.js'></script>
<script src='js/id/services/taginfo.js'></script>
<script src="js/id/geo.js"></script>
<script src="js/id/geo/extent.js"></script>
<script src='js/id/renderer/background.js'></script>
<script src='js/id/renderer/background_source.js'></script>
<script src='js/id/renderer/map.js'></script>
@@ -40,7 +44,9 @@
<script src="js/id/svg.js"></script>
<script src="js/id/svg/areas.js"></script>
<script src="js/id/svg/lines.js"></script>
<script src="js/id/svg/member_classes.js"></script>
<script src="js/id/svg/midpoints.js"></script>
<script src="js/id/svg/multipolygons.js"></script>
<script src="js/id/svg/points.js"></script>
<script src="js/id/svg/surface.js"></script>
<script src="js/id/svg/tag_classes.js"></script>
@@ -109,6 +115,7 @@
<script src='js/id/graph/node.js'></script>
<script src='js/id/graph/relation.js'></script>
<script src='js/id/graph/way.js'></script>
<script src='js/id/graph/validate.js'></script>
<script src='js/id/connection.js'></script>
</head>

View File

@@ -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) {

1
js/id/geo.js Normal file
View File

@@ -0,0 +1 @@
iD.geo = {};

37
js/id/geo/extent.js Normal file
View File

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

View File

@@ -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() {

View File

@@ -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() {

View File

@@ -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) {

47
js/id/graph/validate.js Normal file
View File

@@ -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] : [];
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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++) {

View File

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

View File

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

View File

@@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

70
js/lib/d3.tail.js Normal file
View File

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

View File

@@ -25,6 +25,7 @@
<script src='../js/lib/d3.trigger.js'></script>
<script src='../js/lib/d3.typeahead.js'></script>
<script src='../js/lib/d3.one.js'></script>
<script src='../js/lib/d3.tail.js'></script>
<script src='../js/lib/ohauth.js'></script>
<script src='../js/lib/jxon.js'></script>
@@ -33,6 +34,9 @@
<script src='../js/id/oauth.js'></script>
<script src='../js/id/services/taginfo.js'></script>
<script src="../js/id/geo.js"></script>
<script src="../js/id/geo/extent.js"></script>
<script src='../js/id/renderer/background.js'></script>
<script src='../js/id/renderer/background_source.js'></script>
<script src='../js/id/renderer/map.js'></script>
@@ -41,7 +45,9 @@
<script src="../js/id/svg.js"></script>
<script src="../js/id/svg/areas.js"></script>
<script src="../js/id/svg/lines.js"></script>
<script src="../js/id/svg/member_classes.js"></script>
<script src="../js/id/svg/midpoints.js"></script>
<script src="../js/id/svg/multipolygons.js"></script>
<script src="../js/id/svg/points.js"></script>
<script src="../js/id/svg/surface.js"></script>
<script src="../js/id/svg/tag_classes.js"></script>
@@ -107,7 +113,10 @@
<script>
iD.debug = true;
mocha.setup('bdd');
mocha.setup({
ui: 'bdd',
globals: ['__onresize.tail-size']
});
var expect = chai.expect;
</script>
@@ -134,6 +143,8 @@
<script src="spec/format/geojson.js"></script>
<script src="spec/format/xml.js"></script>
<script src="spec/geo/extent.js"></script>
<script src="spec/graph/graph.js"></script>
<script src="spec/graph/entity.js"></script>
<script src="spec/graph/node.js"></script>
@@ -147,7 +158,11 @@
<script src="spec/renderer/hash.js"></script>
<script src="spec/renderer/map.js"></script>
<script src="spec/svg.js"></script>
<script src="spec/svg/areas.js"></script>
<script src="spec/svg/lines.js"></script>
<script src="spec/svg/member_classes.js"></script>
<script src="spec/svg/multipolygons.js"></script>
<script src="spec/svg/points.js"></script>
<script src="spec/svg/vertices.js"></script>
<script src="spec/svg/tag_classes.js"></script>

View File

@@ -20,7 +20,10 @@
<script>
iD.debug = true;
mocha.setup('bdd');
mocha.setup({
ui: 'bdd',
globals: ['__onresize.tail-size']
});
var expect = chai.expect;
</script>
@@ -47,6 +50,8 @@
<script src="spec/format/geojson.js"></script>
<script src="spec/format/xml.js"></script>
<script src="spec/geo/extent.js"></script>
<script src="spec/graph/graph.js"></script>
<script src="spec/graph/entity.js"></script>
<script src="spec/graph/node.js"></script>
@@ -60,7 +65,11 @@
<script src="spec/renderer/hash.js"></script>
<script src="spec/renderer/map.js"></script>
<script src="spec/svg.js"></script>
<script src="spec/svg/areas.js"></script>
<script src="spec/svg/lines.js"></script>
<script src="spec/svg/member_classes.js"></script>
<script src="spec/svg/multipolygons.js"></script>
<script src="spec/svg/points.js"></script>
<script src="spec/svg/vertices.js"></script>
<script src="spec/svg/tag_classes.js"></script>

100
test/spec/geo/extent.js Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

17
test/spec/svg.js Normal file
View File

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

View File

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

54
test/spec/svg/lines.js Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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