diff --git a/combobox.html b/combobox.html index c739f4898..92949ecfa 100644 --- a/combobox.html +++ b/combobox.html @@ -90,7 +90,6 @@ - diff --git a/css/map.css b/css/map.css index 749aeb7b3..7b35f6bc5 100644 --- a/css/map.css +++ b/css/map.css @@ -181,95 +181,72 @@ path.shadow.selected { } path.area.stroke, -path.multipolygon { +path.line.member-type-multipolygon.stroke { stroke-width:2; - stroke:#fff; } -path.area.fill, -path.multipolygon { - fill:#fff; - fill-opacity:0.3; -} - -path.multipolygon { - fill-rule: evenodd; -} - -path.area.fill.member-type-multipolygon { - fill: none; -} - -path.area.stroke.selected { +path.area.stroke.selected, +path.line.member-type-multipolygon.stroke.selected { stroke-width:4 !important; } -path.area.stroke.tag-natural, -path.multipolygon.tag-natural { +path.area.stroke { + stroke:#fff; +} +path.area.fill { + fill:#fff; + fill-opacity:0.3; + fill-rule: evenodd; +} + +path.stroke.tag-natural { stroke: #b6e199; stroke-width:1; } -path.area.fill.tag-natural, -path.multipolygon.tag-natural { +path.fill.tag-natural { fill: #b6e199; } -path.area.stroke.tag-natural-water, -path.multipolygon.tag-natural-water { +path.stroke.tag-natural-water { stroke: #77d3de; } -path.area.fill.tag-natural-water, -path.multipolygon.tag-natural-water { +path.fill.tag-natural-water { fill: #77d3de; } -path.area.stroke.tag-building, -path.multipolygon.tag-building { +path.stroke.tag-building { stroke: #e06e5f; stroke-width: 1; } -path.area.fill.tag-building, -path.multipolygon.tag-building { +path.fill.tag-building { fill: #e06e5f; } -path.area.stroke.tag-landuse, -path.area.stroke.tag-natural-wood, -path.area.stroke.tag-natural-tree, -path.area.stroke.tag-natural-grassland, -path.area.stroke.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 { +path.stroke.tag-landuse, +path.stroke.tag-natural-wood, +path.stroke.tag-natural-tree, +path.stroke.tag-natural-grassland, +path.stroke.tag-leisure-park { stroke: #8cd05f; stroke-width: 1; } -path.area.fill.tag-landuse, -path.area.fill.tag-natural-wood, -path.area.fill.tag-natural-tree, -path.area.fill.tag-natural-grassland, -path.area.fill.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 { +path.fill.tag-landuse, +path.fill.tag-natural-wood, +path.fill.tag-natural-tree, +path.fill.tag-natural-grassland, +path.fill.tag-leisure-park { fill: #8cd05f; fill-opacity: 0.2; } -path.area.stroke.tag-amenity-parking, -path.multipolygon.tag-amenity-parking { +path.stroke.tag-amenity-parking { stroke: #aaa; stroke-width: 1; } -path.area.fill.tag-amenity-parking, -path.multipolygon.tag-amenity-parking { +path.fill.tag-amenity-parking { fill: #aaa; } -path.multipolygon.tag-boundary { +path.fill.tag-boundary { fill: none; } @@ -526,7 +503,7 @@ path.casing.tag-railway-subway { /* waterways */ -path.area.fill.tag-waterway { +path.fill.tag-waterway { fill: #77d3de; } @@ -686,9 +663,7 @@ text.point { } .mode-select .area, -.mode-browse .area, -.mode-select .multipolygon, -.mode-browse .multipolygon { +.mode-browse .area { cursor: url(../img/cursor-select-area.png), pointer; } @@ -701,7 +676,6 @@ text.point { .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 ee00705ac..0e200fb90 100644 --- a/index.html +++ b/index.html @@ -49,7 +49,6 @@ - @@ -73,6 +72,7 @@ + @@ -99,7 +99,6 @@ - @@ -156,6 +155,8 @@ .call(id.ui()) }); + + + + + diff --git a/js/id/behavior/drag.js b/js/id/behavior/drag.js index d88e8a44a..cd6f607bf 100644 --- a/js/id/behavior/drag.js +++ b/js/id/behavior/drag.js @@ -14,7 +14,7 @@ * Delegation is supported via the `delegate` function. */ -iD.behavior.drag = function () { +iD.behavior.drag = function() { function d3_eventCancel() { d3.event.stopPropagation(); d3.event.preventDefault(); @@ -50,21 +50,21 @@ iD.behavior.drag = function () { moved = 0; var w = d3.select(window) - .on(touchId != null ? "touchmove.drag-" + touchId : "mousemove.drag", dragmove) - .on(touchId != null ? "touchend.drag-" + touchId : "mouseup.drag", dragend, true); + .on(touchId !== null ? "touchmove.drag-" + touchId : "mousemove.drag", dragmove) + .on(touchId !== null ? "touchend.drag-" + touchId : "mouseup.drag", dragend, true); if (origin) { offset = origin.apply(target, arguments); - offset = [ offset[0] - origin_[0], offset[1] - origin_[1] ]; + offset = [offset[0] - origin_[0], offset[1] - origin_[1]]; } else { - offset = [ 0, 0 ]; + offset = [0, 0]; } - if (touchId == null) d3_eventCancel(); + if (touchId === null) d3_eventCancel(); function point() { var p = target.parentNode; - return touchId != null ? d3.touches(p).filter(function (p) { + return touchId !== null ? d3.touches(p).filter(function(p) { return p.identifier === touchId; })[0] : d3.mouse(p); } @@ -103,8 +103,8 @@ iD.behavior.drag = function () { if (d3.event.target === eventTarget) w.on("click.drag", click, true); } - w.on(touchId != null ? "touchmove.drag-" + touchId : "mousemove.drag", null) - .on(touchId != null ? "touchend.drag-" + touchId : "mouseup.drag", null); + w.on(touchId !== null ? "touchmove.drag-" + touchId : "mousemove.drag", null) + .on(touchId !== null ? "touchend.drag-" + touchId : "mouseup.drag", null); } function click() { diff --git a/js/id/behavior/drag_midpoint.js b/js/id/behavior/drag_midpoint.js deleted file mode 100644 index 4e39be2c6..000000000 --- a/js/id/behavior/drag_midpoint.js +++ /dev/null @@ -1,29 +0,0 @@ -iD.behavior.DragMidpoint = function(context) { - var behavior = iD.behavior.drag() - .delegate(".midpoint") - .origin(function(d) { - return context.projection(d.loc); - }) - .on('start', function(d) { - var node = iD.Node(); - - context.perform(iD.actions.AddMidpoint(d, node)); - - var vertex = context.surface().selectAll('.vertex') - .filter(function(data) { return data.id === node.id; }); - - behavior.target(vertex.node(), vertex.datum()); - }) - .on('move', function(d) { - d3.event.sourceEvent.stopPropagation(); - context.replace( - iD.actions.MoveNode(d.id, context.projection.invert(d3.event.point))); - }) - .on('end', function() { - context.replace( - iD.actions.Noop(), - t('operations.add.annotation.vertex')); - }); - - return behavior; -}; diff --git a/js/id/behavior/drag_node.js b/js/id/behavior/drag_node.js index 92318756c..acba7919d 100644 --- a/js/id/behavior/drag_node.js +++ b/js/id/behavior/drag_node.js @@ -1,5 +1,6 @@ iD.behavior.DragNode = function(context) { - var nudgeInterval; + var nudgeInterval, + wasMidpoint; function edge(point, size) { var pad = [30, 100, 30, 100]; @@ -35,6 +36,23 @@ iD.behavior.DragNode = function(context) { } function start(entity) { + + wasMidpoint = entity.type === 'midpoint'; + if (wasMidpoint) { + var midpoint = entity; + entity = iD.Node(); + context.perform(iD.actions.AddMidpoint(midpoint, entity)); + + var vertex = context.surface() + .selectAll('.vertex') + .filter(function(d) { return d.id === entity.id; }); + behavior.target(vertex.node(), entity); + + } else { + context.perform( + iD.actions.Noop()); + } + var activeIDs = _.pluck(context.graph().parentWays(entity), 'id'); activeIDs.push(entity.id); @@ -43,9 +61,6 @@ iD.behavior.DragNode = function(context) { .selectAll('.node, .way') .filter(function (d) { return activeIDs.indexOf(d.id) >= 0; }) .classed('active', true); - - context.perform( - iD.actions.Noop()); } function datum() { @@ -66,7 +81,7 @@ iD.behavior.DragNode = function(context) { var loc = context.map().mouseCoordinates(); var d = datum(); - if (d.type === 'node') { + if (d.type === 'node' && d.id !== entity.id) { loc = d.loc; } else if (d.type === 'way') { loc = iD.geo.chooseIndex(d, d3.mouse(context.surface().node()), context).loc; @@ -96,6 +111,11 @@ iD.behavior.DragNode = function(context) { iD.actions.Connect([entity.id, d.id]), connectAnnotation(d)); + } else if (wasMidpoint) { + context.replace( + iD.actions.Noop(), + t('operations.add.annotation.vertex')); + } else { context.replace( iD.actions.Noop(), @@ -103,10 +123,12 @@ iD.behavior.DragNode = function(context) { } } - return iD.behavior.drag() - .delegate("g.node") + var behavior = iD.behavior.drag() + .delegate("g.node, g.midpoint") .origin(origin) .on('start', start) .on('move', move) .on('end', end); + + return behavior; }; diff --git a/js/id/behavior/draw.js b/js/id/behavior/draw.js index 656eb7988..b0da771a4 100644 --- a/js/id/behavior/draw.js +++ b/js/id/behavior/draw.js @@ -1,23 +1,39 @@ iD.behavior.Draw = function(context) { - var event = d3.dispatch('move', 'click', 'clickWay', 'clickNode', 'undo', 'cancel', 'finish'), + var event = d3.dispatch('move', 'click', 'clickWay', + 'clickNode', 'undo', 'cancel', 'finish'), keybinding = d3.keybinding('draw'), - hover = iD.behavior.Hover(); + hover = iD.behavior.Hover(), + closeTolerance = 4, + tolerance = 12; function datum() { - if (d3.event.altKey) { - return {}; - } else { - return d3.event.target.__data__ || {}; - } + if (d3.event.altKey) return {}; + else return d3.event.target.__data__ || {}; } function mousedown() { - var selection = d3.select(this); - selection.on('mousemove.draw', null); - d3.select(window) - .on('mouseup.draw', function() { - selection.on('mousemove.draw', mousemove); + function point() { + var p = target.node().parentNode; + return touchId !== null ? d3.touches(p).filter(function(p) { + return p.identifier === touchId; + })[0] : d3.mouse(p); + } + + var target = d3.select(this), + touchId = d3.event.touches ? d3.event.changedTouches[0].identifier : null, + time = +new Date(), + pos = point(); + + target.on('mousemove.draw', null); + + d3.select(window).on('click.draw', function() { + target.on('mousemove.draw', mousemove); + if (iD.geo.dist(pos, point()) < closeTolerance || + (iD.geo.dist(pos, point()) < tolerance && + (+new Date() - time) < 500)) { + click(); + } }); } @@ -77,8 +93,7 @@ iD.behavior.Draw = function(context) { selection .on('mousedown.draw', mousedown) - .on('mousemove.draw', mousemove) - .on('click.draw', click); + .on('mousemove.draw', mousemove); d3.select(document) .call(keybinding) @@ -93,10 +108,9 @@ iD.behavior.Draw = function(context) { selection .on('mousedown.draw', null) - .on('mousemove.draw', null) - .on('click.draw', null); + .on('mousemove.draw', null); - d3.select(window).on('mouseup.draw', null); + d3.select(window).on('click.draw', null); d3.select(document) .call(keybinding.off) diff --git a/js/id/behavior/draw_way.js b/js/id/behavior/draw_way.js index 6b648db0e..8c4c4a5cc 100644 --- a/js/id/behavior/draw_way.js +++ b/js/id/behavior/draw_way.js @@ -28,7 +28,13 @@ iD.behavior.DrawWay = function(context, wayId, index, mode, baseGraph) { function move(datum) { var loc = context.map().mouseCoordinates(); - if (datum.type === 'node') { + if (datum.id === end.id || datum.id === segment.id) { + context.surface().selectAll('.way, .node') + .filter(function (d) { + return d.id === end.id || d.id === segment.id; + }) + .classed('active', true); + } else if (datum.type === 'node') { loc = datum.loc; } else if (datum.type === 'way') { loc = iD.geo.chooseIndex(datum, d3.mouse(context.surface().node()), context).loc; diff --git a/js/id/behavior/hash.js b/js/id/behavior/hash.js index c8762fb13..ced648e45 100644 --- a/js/id/behavior/hash.js +++ b/js/id/behavior/hash.js @@ -29,7 +29,7 @@ iD.behavior.Hash = function(context) { var move = _.throttle(function() { var s1 = formatter(context.map()); if (s0 !== s1) location.replace(s0 = s1); // don't recenter the map! - }, 100); + }, 500); function hashchange() { if (location.hash === s0) return; // ignore spurious hashchange events diff --git a/js/id/behavior/select.js b/js/id/behavior/select.js index b5276a4d0..3d10db144 100644 --- a/js/id/behavior/select.js +++ b/js/id/behavior/select.js @@ -1,23 +1,66 @@ iD.behavior.Select = function(context) { - function click() { - var datum = d3.select(d3.event.target).datum(); - if (datum instanceof iD.Entity) { - if (d3.event.shiftKey) { - context.enter(iD.modes.Select(context, context.selection().concat([datum.id]))); - } else { - context.enter(iD.modes.Select(context, [datum.id])); - } - } else if (!d3.event.shiftKey) { - context.enter(iD.modes.Browse(context)); - } - } var behavior = function(selection) { - selection.on('click.select', click); + + var timeout = null, + // the position of the first mousedown + pos = null; + + function click(event) { + d3.event = event; + var datum = d3.select(d3.event.target).datum(); + if (datum instanceof iD.Entity) { + if (d3.event.shiftKey) { + context.enter(iD.modes.Select(context, context.selection().concat([datum.id]))); + } else { + context.enter(iD.modes.Select(context, [datum.id])); + } + } else if (!d3.event.shiftKey) { + context.enter(iD.modes.Browse(context)); + } + } + + function mousedown() { + pos = d3.mouse(context.surface().node()); + selection + .on('mousemove.select', mousemove) + .on('touchmove.select', mousemove); + + // we've seen a mousedown within 400ms of this one, so ignore + // both because they will be a double click + if (timeout !== null) { + window.clearTimeout(timeout); + selection.on('mousemove.select', null); + timeout = null; + } else { + // queue the click handler to fire in 400ms if no other clicks + // are detected + timeout = window.setTimeout((function(event) { + return function() { + click(event); + timeout = null; + selection.on('mousemove.select', null); + }; + // save the event for the click handler + })(d3.event), 400); + } + } + + // allow mousemoves to cancel the click + function mousemove() { + if (iD.geo.dist(d3.mouse(context.surface().node()), pos) > 4) { + window.clearTimeout(timeout); + timeout = null; + } + } + + selection + .on('mousedown.select', mousedown) + .on('touchstart.select', mousedown); }; behavior.off = function(selection) { - selection.on('click.select', null); + selection.on('mousedown.select', null); }; return behavior; diff --git a/js/id/graph/graph.js b/js/id/graph/graph.js index 2692aeba6..d9118045a 100644 --- a/js/id/graph/graph.js +++ b/js/id/graph/graph.js @@ -227,10 +227,53 @@ iD.Graph.prototype = { var items = []; for (var i in this.entities) { var entity = this.entities[i]; - if (entity && entity.intersects(extent, this)) { + if (entity && this.hasAllChildren(entity) && entity.intersects(extent, this)) { items.push(entity); } } return items; + }, + + hasAllChildren: function(entity) { + // we're only checking changed entities, since we assume fetched data + // must have all children present + if (this.entities.hasOwnProperty(entity.id)) { + if (entity.type === 'way') { + for (i = 0; i < entity.nodes.length; i++) { + if (!this.entities[entity.nodes[i]]) return false; + } + } else if (entity.type === 'relation') { + for (i = 0; i < entity.members.length; i++) { + if (!this.entities[entity.members[i].id]) return false; + } + } + } + return true; + }, + + // Obliterates any existing entities + load: function(entities) { + + var base = this.base(), + i, entity, prefix; + this.entities = Object.create(base.entities); + + for (i in entities) { + entity = entities[i]; + prefix = i[0]; + + if (prefix == 'n') { + this.entities[i] = new iD.Node(entity); + + } else if (prefix == 'w') { + this.entities[i] = new iD.Way(entity); + + } else if (prefix == 'r') { + this.entities[i] = new iD.Relation(entity); + } + this._updateCalculated(base.entities[i], this.entities[i]); + } + return this; } + }; diff --git a/js/id/graph/history.js b/js/id/graph/history.js index a0930203c..ac36d564f 100644 --- a/js/id/graph/history.js +++ b/js/id/graph/history.js @@ -1,7 +1,8 @@ -iD.History = function() { +iD.History = function(context) { var stack, index, imagery_used = 'Bing', - dispatch = d3.dispatch('change', 'undone', 'redone'); + dispatch = d3.dispatch('change', 'undone', 'redone'), + lock = false; function perform(actions) { actions = Array.prototype.slice.call(actions); @@ -26,6 +27,10 @@ iD.History = function() { return difference; } + function getKey(n) { + return 'iD_' + window.location.origin + '_' + n; + } + var history = { graph: function() { return stack[index].graph; @@ -149,7 +154,65 @@ iD.History = function() { stack = [{graph: iD.Graph()}]; index = 0; dispatch.change(); + }, + + save: function() { + if (!lock) return; + context.storage(getKey('lock'), null); + + if (!stack.length) { + context.storage(getKey('history'), null); + context.storage(getKey('nextIDs'), null); + context.storage(getKey('index'), null); + return; + } + + var json = JSON.stringify(stack.map(function(i) { + return _.extend(i, { + graph: i.graph.entities + }); + })); + + context.storage(getKey('history'), json); + context.storage(getKey('nextIDs'), JSON.stringify(iD.Entity.id.next)); + context.storage(getKey('index'), index); + }, + + lock: function() { + if (context.storage(getKey('lock'))) return false; + context.storage(getKey('lock'), true); + lock = true; + return lock; + }, + + restorableChanges: function() { + if (!this.lock()) return false; + return !!context.storage(getKey('history')); + }, + + load: function() { + if (!lock) return; + + var json = context.storage(getKey('history')), + nextIDs = context.storage(getKey('nextIDs')), + index_ = context.storage(getKey('index')); + + if (!json) return; + if (nextIDs) iD.Entity.id.next = JSON.parse(nextIDs); + if (index_ !== null) index = parseInt(index_, 10); + + context.storage(getKey('history', null)); + context.storage(getKey('nextIDs', null)); + context.storage(getKey('index', null)); + + stack = JSON.parse(json).map(function(d, i) { + d.graph = iD.Graph(stack[0].graph).load(d.graph); + return d; + }); + dispatch.change(); + } + }; history.reset(); diff --git a/js/id/graph/relation.js b/js/id/graph/relation.js index 1ed9e8ff1..f8540db1f 100644 --- a/js/id/graph/relation.js +++ b/js/id/graph/relation.js @@ -26,7 +26,7 @@ _.extend(iD.Relation.prototype, { }, geometry: function() { - return 'relation'; + return this.isMultipolygon() ? 'area' : 'relation'; }, // Return the first member with the given role. A copy of the member object @@ -83,6 +83,31 @@ _.extend(iD.Relation.prototype, { return r; }, + asGeoJSON: function(resolver) { + if (this.isMultipolygon()) { + return { + type: 'Feature', + properties: this.tags, + geometry: { + type: 'MultiPolygon', + coordinates: this.multipolygon(resolver) + } + }; + } else { + return { + type: 'FeatureCollection', + properties: this.tags, + features: this.members.map(function(member) { + return _.extend({role: member.role}, resolver.entity(member.id).asGeoJSON(resolver)); + }) + }; + } + }, + + isMultipolygon: function() { + return this.tags.type === 'multipolygon'; + }, + isRestriction: function() { return !!(this.tags.type && this.tags.type.match(/^restriction:?/)); }, @@ -145,22 +170,20 @@ _.extend(iD.Relation.prototype, { } } - return joined; + return joined.map(function (nodes) { return _.pluck(nodes, 'loc'); }); } function findOuter(inner) { var o, outer; - inner = _.pluck(inner, 'loc'); - for (o = 0; o < outers.length; o++) { - outer = _.pluck(outers[o], 'loc'); + outer = outers[o]; if (iD.geo.polygonContainsPolygon(outer, inner)) return o; } for (o = 0; o < outers.length; o++) { - outer = _.pluck(outers[o], 'loc'); + outer = outers[o]; if (iD.geo.polygonIntersectsPolygon(outer, inner)) return o; } diff --git a/js/id/graph/way.js b/js/id/graph/way.js index aae89a6ce..1830e61a4 100644 --- a/js/id/graph/way.js +++ b/js/id/graph/way.js @@ -49,6 +49,7 @@ _.extend(iD.Way.prototype, { isArea: function() { return this.tags.area === 'yes' || (this.isClosed() && + !_.isEmpty(this.tags) && this.tags.area !== 'no' && !this.tags.highway && !this.tags.barrier); @@ -103,13 +104,24 @@ _.extend(iD.Way.prototype, { }, asGeoJSON: function(resolver) { - return { - type: 'Feature', - properties: this.tags, - geometry: { - type: 'LineString', - coordinates: _.pluck(resolver.childNodes(this), 'loc') - } - }; + if (this.isArea()) { + return { + type: 'Feature', + properties: this.tags, + geometry: { + type: 'Polygon', + coordinates: [_.pluck(resolver.childNodes(this), 'loc')] + } + }; + } else { + return { + type: 'Feature', + properties: this.tags, + geometry: { + type: 'LineString', + coordinates: _.pluck(resolver.childNodes(this), 'loc') + } + }; + } } }); diff --git a/js/id/id.js b/js/id/id.js index 20dad70d8..e83c442c4 100644 --- a/js/id/id.js +++ b/js/id/id.js @@ -1,18 +1,20 @@ window.iD = function () { var context = {}, - history = iD.History(), - storage = localStorage || {}, + storage = localStorage || {}; + + context.storage = function(k, v) { + if (arguments.length === 1) return storage[k]; + else if (v === null) delete storage[k]; + else storage[k] = v; + }; + + var history = iD.History(context), dispatch = d3.dispatch('enter', 'exit'), mode, container, ui = iD.ui(context), map = iD.Map(context); - context.storage = function(k, v) { - if (arguments.length === 1) return storage[k]; - else storage[k] = v; - }; - // the connection requires .storage() to be available on calling. var connection = iD.Connection(context); diff --git a/js/id/modes/browse.js b/js/id/modes/browse.js index 33add1262..97f0d6935 100644 --- a/js/id/modes/browse.js +++ b/js/id/modes/browse.js @@ -10,8 +10,7 @@ iD.modes.Browse = function(context) { var behaviors = [ iD.behavior.Hover(), iD.behavior.Select(context), - iD.behavior.DragNode(context), - iD.behavior.DragMidpoint(context)]; + iD.behavior.DragNode(context)]; mode.enter = function() { behaviors.forEach(function(behavior) { diff --git a/js/id/modes/move_way.js b/js/id/modes/move_way.js index 986eef5bd..e21dded78 100644 --- a/js/id/modes/move_way.js +++ b/js/id/modes/move_way.js @@ -6,7 +6,8 @@ iD.modes.MoveWay = function(context, wayId) { var keybinding = d3.keybinding('move-way'); mode.enter = function() { - var origin = point(), + var origin = context.map().mouseCoordinates(), + nudgeInterval, annotation = t('operations.move.annotation.' + context.geometry(wayId)); // If intiated via keyboard @@ -16,17 +17,44 @@ iD.modes.MoveWay = function(context, wayId) { iD.actions.Noop(), annotation); + function edge(point, size) { + var pad = [30, 100, 30, 100]; + if (point[0] > size[0] - pad[0]) return [-10, 0]; + else if (point[0] < pad[2]) return [10, 0]; + else if (point[1] > size[1] - pad[1]) return [0, -10]; + else if (point[1] < pad[3]) return [0, 10]; + return null; + } + + function startNudge(nudge) { + if (nudgeInterval) window.clearInterval(nudgeInterval); + nudgeInterval = window.setInterval(function() { + context.map().pan(nudge).redraw(); + }, 50); + } + + function stopNudge() { + if (nudgeInterval) window.clearInterval(nudgeInterval); + nudgeInterval = null; + } + function point() { - return d3.mouse(context.surface().node()); + return d3.mouse(context.map().surface.node()); } function move() { - var p = point(), - delta = origin ? - [p[0] - origin[0], p[1] - origin[1]] : - [0, 0]; + var p = point(); - origin = p; + var delta = origin ? + [p[0] - context.projection(origin)[0], + p[1] - context.projection(origin)[1]] : + [0, 0]; + + var nudge = edge(p, context.map().size()); + if (nudge) startNudge(nudge); + else stopNudge(); + + origin = context.map().mouseCoordinates(); context.replace( iD.actions.MoveWay(wayId, delta, context.projection), diff --git a/js/id/modes/select.js b/js/id/modes/select.js index 99fef11ab..0bcfa8c0b 100644 --- a/js/id/modes/select.js +++ b/js/id/modes/select.js @@ -9,8 +9,7 @@ iD.modes.Select = function(context, selection, initial) { behaviors = [ iD.behavior.Hover(), iD.behavior.Select(context), - iD.behavior.DragNode(context), - iD.behavior.DragMidpoint(context)], + iD.behavior.DragNode(context)], radialMenu; function changeTags(d, tags) { @@ -113,9 +112,29 @@ iD.modes.Select = function(context, selection, initial) { d3.mouse(context.surface().node()), context), node = iD.Node({ loc: choice.loc }); - context.perform( - iD.actions.AddEntity(node), - iD.actions.AddVertex(datum.id, node.id, choice.index), + var prev = datum.nodes[choice.index - 1], + next = datum.nodes[choice.index], + prevParents = context.graph().parentWays({ id: prev }), + ways = []; + + + for (var i = 0; i < prevParents.length; i++) { + var p = prevParents[i]; + for (var k = 0; k < p.nodes.length; k++) { + if (p.nodes[k] === prev) { + if (p.nodes[k-1] === next) { + ways.push({ id: p.id, index: k}); + break; + } else if (p.nodes[k+1] === next) { + ways.push({ id: p.id, index: k+1}); + break; + } + } + } + } + + context.perform(iD.actions.AddEntity(node), + iD.actions.AddMidpoint({ ways: ways, loc: node.loc }, node), t('operations.add.annotation.vertex')); d3.event.preventDefault(); @@ -123,13 +142,22 @@ iD.modes.Select = function(context, selection, initial) { } } + function selected(entity) { + if (!entity) return false; + if (selection.indexOf(entity.id) >= 0) return true; + return d3.select(this).classed('stroke') && + _.any(context.graph().parentRelations(entity), function(parent) { + return selection.indexOf(parent.id) >= 0; + }); + } + d3.select(document) .call(keybinding); context.surface() .on('dblclick.select', dblclick) .selectAll("*") - .filter(function(d) { return d && selection.indexOf(d.id) >= 0; }) + .filter(selected) .classed('selected', true); radialMenu = iD.ui.RadialMenu(operations); diff --git a/js/id/renderer/background.js b/js/id/renderer/background.js index cae44ca09..1f0834ed1 100644 --- a/js/id/renderer/background.js +++ b/js/id/renderer/background.js @@ -130,8 +130,6 @@ iD.Background = function() { .on('load', load); image.style(transformProp, imageTransform); - - if (Object.keys(cache).length > 100) cache = {}; } background.offset = function(_) { diff --git a/js/id/renderer/map.js b/js/id/renderer/map.js index 19354d7f3..5074abd92 100644 --- a/js/id/renderer/map.js +++ b/js/id/renderer/map.js @@ -19,7 +19,6 @@ iD.Map = function(context) { 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(), @@ -87,7 +86,6 @@ iD.Map = function(context) { .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, dimensions, !difference); } diff --git a/js/id/svg.js b/js/id/svg.js index aadb6b830..3b662b2e7 100644 --- a/js/id/svg.js +++ b/js/id/svg.js @@ -27,5 +27,17 @@ iD.svg = { return projection(n.loc); }).join('L')); }; + }, + + MultipolygonMemberTags: function (graph) { + return function (entity) { + var tags = entity.tags; + graph.parentRelations(entity).forEach(function (relation) { + if (relation.isMultipolygon()) { + tags = _.extend({}, relation.tags, tags); + } + }); + return tags; + } } }; diff --git a/js/id/svg/areas.js b/js/id/svg/areas.js index 991b5644c..cd9525d4d 100644 --- a/js/id/svg/areas.js +++ b/js/id/svg/areas.js @@ -1,38 +1,39 @@ iD.svg.Areas = function(projection) { return function drawAreas(surface, graph, entities, filter) { - var areas = []; + var path = d3.geo.path().projection(projection), + areas = []; for (var i = 0; i < entities.length; i++) { var entity = entities[i]; if (entity.geometry(graph) === 'area') { - var points = graph.childNodes(entity).map(function(n) { - return projection(n.loc); - }); - areas.push({ entity: entity, - area: entity.isDegenerate() ? 0 : Math.abs(d3.geom.polygon(points).area()) + area: Math.abs(path.area(entity.asGeoJSON(graph))) }); } } areas.sort(function(a, b) { return b.area - a.area; }); - var lineString = iD.svg.LineString(projection, graph); + function drawPaths(group, areas, filter, klass) { + var tagClasses = iD.svg.TagClasses(); + + if (klass === 'stroke') { + tagClasses.tags(iD.svg.MultipolygonMemberTags(graph)); + } - function drawPaths(group, areas, filter, classes) { var paths = group.selectAll('path.area') .filter(filter) .data(areas, iD.Entity.key); paths.enter() .append('path') - .attr('class', classes); + .attr('class', function (d) { return d.type + ' area ' + klass; }); paths .order() - .attr('d', lineString) - .call(iD.svg.TagClasses()) + .attr('d', function (entity) { return path(entity.asGeoJSON(graph)); }) + .call(tagClasses) .call(iD.svg.MemberClasses(graph)); paths.exit() @@ -43,9 +44,14 @@ iD.svg.Areas = function(projection) { areas = _.pluck(areas, 'entity'); + var strokes = areas.filter(function (area) { + return area.type === 'way'; + }); + var fill = surface.select('.layer-fill'), - stroke = surface.select('.layer-stroke'), - fills = drawPaths(fill, areas, filter, 'way area fill'), - strokes = drawPaths(stroke, areas, filter, 'way area stroke'); + stroke = surface.select('.layer-stroke'); + + drawPaths(fill, areas, filter, 'fill'); + drawPaths(stroke, strokes, filter, 'stroke'); }; }; diff --git a/js/id/svg/labels.js b/js/id/svg/labels.js index 9b2cbd1e9..31d2f03d5 100644 --- a/js/id/svg/labels.js +++ b/js/id/svg/labels.js @@ -335,9 +335,8 @@ iD.svg.Labels = function(projection) { } function getAreaLabel(entity, width, height) { - var nodes = _.pluck(graph.childNodes(entity), 'loc') - .map(iD.svg.RoundProjection(projection)), - centroid = d3.geom.polygon(nodes).centroid(), + var path = d3.geo.path().projection(projection), + centroid = path.centroid(entity.asGeoJSON(graph)), extent = entity.extent(graph), entitywidth = projection(extent[1])[0] - projection(extent[0])[0]; diff --git a/js/id/svg/lines.js b/js/id/svg/lines.js index 624237d93..2fd7b9152 100644 --- a/js/id/svg/lines.js +++ b/js/id/svg/lines.js @@ -34,19 +34,25 @@ iD.svg.Lines = function(projection) { } return function drawLines(surface, graph, entities, filter) { - function drawPaths(group, lines, filter, classes, lineString) { + function drawPaths(group, lines, filter, klass, lineString) { + var tagClasses = iD.svg.TagClasses(); + + if (klass === 'stroke') { + tagClasses.tags(iD.svg.MultipolygonMemberTags(graph)); + } + var paths = group.selectAll('path.line') .filter(filter) .data(lines, iD.Entity.key); paths.enter() .append('path') - .attr('class', classes); + .attr('class', 'way line ' + klass); paths .order() .attr('d', lineString) - .call(iD.svg.TagClasses()) + .call(tagClasses) .call(iD.svg.MemberClasses(graph)); paths.exit() @@ -56,13 +62,16 @@ iD.svg.Lines = function(projection) { } if (!alength) { - var arrow = surface.append('text').text(arrowtext); + var container = surface.append('g') + .attr('class', 'oneway'), + arrow = container.append('text') + .attr('class', 'textpath') + .text(arrowtext); alength = arrow.node().getComputedTextLength(); - arrow.remove(); + container.remove(); } - var lines = [], - lineStrings = {}; + var lines = []; for (var i = 0; i < entities.length; i++) { var entity = entities[i]; @@ -80,9 +89,9 @@ iD.svg.Lines = function(projection) { stroke = surface.select('.layer-stroke'), defs = surface.select('defs'), text = surface.select('.layer-text'), - shadows = drawPaths(shadow, lines, filter, 'way line shadow', lineString), - casings = drawPaths(casing, lines, filter, 'way line casing', lineString), - strokes = drawPaths(stroke, lines, filter, 'way line stroke', lineString); + shadows = drawPaths(shadow, lines, filter, 'shadow', lineString), + casings = drawPaths(casing, lines, filter, 'casing', lineString), + strokes = drawPaths(stroke, lines, filter, 'stroke', lineString); // Determine the lengths of oneway paths var lengths = {}, diff --git a/js/id/svg/multipolygons.js b/js/id/svg/multipolygons.js deleted file mode 100644 index b4f050ad5..000000000 --- a/js/id/svg/multipolygons.js +++ /dev/null @@ -1,55 +0,0 @@ -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(graph) === '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 471bc4b4d..a7fdb10fe 100644 --- a/js/id/svg/points.js +++ b/js/id/svg/points.js @@ -79,7 +79,7 @@ iD.svg.Points.imageIndex = [ }, { tags: { man_made: 'lighthouse' }, - icon: 'lighthouselevel_crossing' + icon: 'lighthouse' }, { tags: { natural: 'peak' }, diff --git a/js/id/svg/tag_classes.js b/js/id/svg/tag_classes.js index 5ce585add..dad388ffe 100644 --- a/js/id/svg/tag_classes.js +++ b/js/id/svg/tag_classes.js @@ -3,10 +3,11 @@ iD.svg.TagClasses = function() { 'highway', 'railway', 'waterway', 'power', 'motorway', 'amenity', 'natural', 'landuse', 'building', 'oneway', 'bridge', 'boundary', 'leisure', 'construction' - ]), tagClassRe = /^tag-/; + ]), tagClassRe = /^tag-/, + tags = function(entity) { return entity.tags; }; - return function tagClassesSelection(selection) { - selection.each(function tagClassesEach(d, i) { + var tagClasses = function(selection) { + selection.each(function tagClassesEach(entity) { var classes, value = this.className; if (value.baseVal !== undefined) value = value.baseVal; @@ -15,11 +16,10 @@ iD.svg.TagClasses = function() { return name.length && !tagClassRe.test(name); }).join(' '); - var tags = d.tags; - for (var k in tags) { + var t = tags(entity); + for (var k in t) { if (!keys[k]) continue; - classes += ' tag-' + k + ' ' + - 'tag-' + k + '-' + tags[k]; + classes += ' tag-' + k + ' ' + 'tag-' + k + '-' + t[k]; } classes = classes.trim(); @@ -29,4 +29,12 @@ iD.svg.TagClasses = function() { } }); }; + + tagClasses.tags = function(_) { + if (!arguments.length) return tags; + tags = _; + return tagClasses; + }; + + return tagClasses; }; diff --git a/js/id/ui.js b/js/id/ui.js index 747bccb70..768d95813 100644 --- a/js/id/ui.js +++ b/js/id/ui.js @@ -195,6 +195,7 @@ iD.ui = function(context) { history.on('change.editor', function() { window.onbeforeunload = history.hasChanges() ? function() { + history.save(); return 'You have unsaved changes.'; } : null; @@ -253,8 +254,13 @@ iD.ui = function(context) { context.enter(iD.modes.Browse(context)); if (!context.storage('sawSplash')) { - iD.ui.splash(); + iD.ui.splash(context.container()); context.storage('sawSplash', true); } + + if (history.restorableChanges()) { + iD.ui.restore(context.container(), history); + } + }; }; diff --git a/js/id/ui/layerswitcher.js b/js/id/ui/layerswitcher.js index b73ede9d3..2007b45fa 100644 --- a/js/id/ui/layerswitcher.js +++ b/js/id/ui/layerswitcher.js @@ -108,7 +108,13 @@ iD.ui.layerswitcher = function(context) { d = configured; } context.background().source(d); - context.history().imagery_used(d.data.sourcetag || d.data.name); + if (d.data.name === 'Custom (customized)') { + context.history() + .imagery_used('Custom (' + d.data.template + ')'); + } else { + context.history() + .imagery_used(d.data.sourcetag || d.data.name); + } context.redraw(); selectLayer(d); } diff --git a/js/id/ui/restore.js b/js/id/ui/restore.js new file mode 100644 index 000000000..3df275d89 --- /dev/null +++ b/js/id/ui/restore.js @@ -0,0 +1,34 @@ +iD.ui.restore = function(selection, history) { + var modal = iD.ui.modal(selection); + + modal.select('.modal') + .attr('class', 'modal-splash modal'); + + var introModal = modal.select('.content') + .append('div') + .attr('class', 'modal-section fillL') + .text('You have unsaved changes from a previous editing session. Do you wish to restore these changes?'); + + buttons = introModal + .append('div') + .attr('class', 'buttons cf') + .append('div') + .attr('class', 'button-wrap joined col4'); + + buttons.append('button') + .attr('class', 'save action button col6') + .text('Restore') + .on('click', function() { + history.load(); + modal.remove(); + }); + + buttons.append('button') + .attr('class', 'cancel button col6') + .text('Reset') + .on('click', function() { + modal.remove(); + }); + + return modal; +}; diff --git a/js/id/ui/splash.js b/js/id/ui/splash.js index a4d2a915e..bfcbf3e05 100644 --- a/js/id/ui/splash.js +++ b/js/id/ui/splash.js @@ -1,5 +1,5 @@ -iD.ui.splash = function() { - var modal = iD.ui.modal(); +iD.ui.splash = function(selection) { + var modal = iD.ui.modal(selection); modal.select('.modal') .attr('class', 'modal-splash modal'); diff --git a/test/index.html b/test/index.html index b722274b2..a66758ddb 100644 --- a/test/index.html +++ b/test/index.html @@ -52,7 +52,6 @@ - @@ -96,7 +95,6 @@ - @@ -142,7 +140,7 @@ iD.debug = true; mocha.setup({ ui: 'bdd', - globals: ['__onresize.tail-size'] + globals: ['__onresize.tail-size', '__onmousemove.zoom', '__onmouseup.zoom', '__onclick.draw'] }); var expect = chai.expect; @@ -187,7 +185,6 @@ - diff --git a/test/index_packaged.html b/test/index_packaged.html index d21d48ff2..8dd73d666 100644 --- a/test/index_packaged.html +++ b/test/index_packaged.html @@ -66,7 +66,6 @@ - diff --git a/test/rendering.html b/test/rendering.html index 196122b2c..ff624305a 100644 --- a/test/rendering.html +++ b/test/rendering.html @@ -21,7 +21,6 @@ - diff --git a/test/spec/behavior/select.js b/test/spec/behavior/select.js index 42c568cca..e794cda8e 100644 --- a/test/spec/behavior/select.js +++ b/test/spec/behavior/select.js @@ -29,21 +29,31 @@ describe("iD.behavior.Select", function() { container.remove(); }); - specify("click on entity selects the entity", function() { - happen.click(context.surface().select('.' + a.id).node()); - expect(context.selection()).to.eql([a.id]); + specify("click on entity selects the entity", function(done) { + happen.mousedown(context.surface().select('.' + a.id).node()); + window.setTimeout(function() { + expect(context.selection()).to.eql([a.id]); + done(); + }, 600); }); - specify("click on empty space clears the selection", function() { + specify("click on empty space clears the selection", function(done) { context.enter(iD.modes.Select(context, [a.id])); happen.click(context.surface().node()); - expect(context.selection()).to.eql([]); + happen.mousedown(context.surface().node()); + window.setTimeout(function() { + expect(context.selection()).to.eql([]); + done(); + }, 600); }); - specify("shift-click on entity adds the entity to the selection", function() { + specify("shift-click on entity adds the entity to the selection", function(done) { context.enter(iD.modes.Select(context, [a.id])); - happen.click(context.surface().select('.' + b.id).node(), {shiftKey: true}); - expect(context.selection()).to.eql([a.id, b.id]); + happen.mousedown(context.surface().select('.' + b.id).node(), {shiftKey: true}); + window.setTimeout(function() { + expect(context.selection()).to.eql([a.id, b.id]); + done(); + }, 600); }); specify("shift-click on empty space leaves the selection unchanged", function() { diff --git a/test/spec/graph/relation.js b/test/spec/graph/relation.js index 6e00dcacb..b07f9bee7 100644 --- a/test/spec/graph/relation.js +++ b/test/spec/graph/relation.js @@ -146,107 +146,135 @@ describe('iD.Relation', function () { }); }); + describe("#asGeoJSON", function (){ + it('converts a multipolygon to a GeoJSON MultiPolygon feature', function() { + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), + w = iD.Way({nodes: [a.id, b.id, c.id, a.id]}), + r = iD.Relation({tags: {type: 'multipolygon'}, members: [{id: w.id, type: 'way'}]}), + g = iD.Graph([a, b, c, w, r]), + json = r.asGeoJSON(g); + + expect(json.type).to.equal('Feature'); + expect(json.properties).to.eql({type: 'multipolygon'}); + expect(json.geometry.type).to.equal('MultiPolygon'); + expect(json.geometry.coordinates).to.eql([[[[1, 1], [2, 2], [3, 3], [1, 1]]]]); + }); + + it('converts a relation to a GeoJSON FeatureCollection', function() { + var a = iD.Node({loc: [1, 1]}), + r = iD.Relation({tags: {type: 'type'}, members: [{id: a.id, role: 'role'}]}), + g = iD.Graph([a, r]), + json = r.asGeoJSON(g); + + expect(json.type).to.equal('FeatureCollection'); + expect(json.properties).to.eql({type: 'type'}); + expect(json.features).to.eql([_.extend({role: 'role'}, a.asGeoJSON(g))]); + }); + }); + describe("#multipolygon", function () { specify("single polygon consisting of a single way", function () { - var a = iD.Node(), - b = iD.Node(), - c = iD.Node(), + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), w = iD.Way({nodes: [a.id, b.id, c.id, a.id]}), r = iD.Relation({members: [{id: w.id, type: 'way'}]}), g = iD.Graph([a, b, c, w, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c, a]]]); + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, a.loc]]]); }); specify("single polygon consisting of multiple ways", function () { - var a = iD.Node(), - b = iD.Node(), - c = iD.Node(), - d = iD.Node(), + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), + d = iD.Node({loc: [4, 4]}), w1 = iD.Way({nodes: [a.id, b.id, c.id]}), w2 = iD.Way({nodes: [c.id, d.id, a.id]}), r = iD.Relation({members: [{id: w2.id, type: 'way'}, {id: w1.id, type: 'way'}]}), g = iD.Graph([a, b, c, d, w1, w2, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c, d, a]]]); // TODO: not the only valid ordering + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, d.loc, a.loc]]]); // TODO: not the only valid ordering }); specify("single polygon consisting of multiple ways, one needing reversal", function () { - var a = iD.Node(), - b = iD.Node(), - c = iD.Node(), - d = iD.Node(), + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), + d = iD.Node({loc: [4, 4]}), w1 = iD.Way({nodes: [a.id, b.id, c.id]}), w2 = iD.Way({nodes: [a.id, d.id, c.id]}), r = iD.Relation({members: [{id: w2.id, type: 'way'}, {id: w1.id, type: 'way'}]}), g = iD.Graph([a, b, c, d, w1, w2, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c, d, a]]]); // TODO: not the only valid ordering + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, d.loc, a.loc]]]); // TODO: not the only valid ordering }); specify("multiple polygons consisting of single ways", function () { - var a = iD.Node(), - b = iD.Node(), - c = iD.Node(), - d = iD.Node(), - e = iD.Node(), - f = iD.Node(), + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), + d = iD.Node({loc: [4, 4]}), + e = iD.Node({loc: [5, 5]}), + f = iD.Node({loc: [6, 6]}), w1 = iD.Way({nodes: [a.id, b.id, c.id, a.id]}), w2 = iD.Way({nodes: [d.id, e.id, f.id, d.id]}), r = iD.Relation({members: [{id: w2.id, type: 'way'}, {id: w1.id, type: 'way'}]}), g = iD.Graph([a, b, c, d, e, f, w1, w2, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c, a]], [[d, e, f, d]]]); + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, a.loc]], [[d.loc, e.loc, f.loc, d.loc]]]); }); specify("invalid geometry: unclosed ring consisting of a single way", function () { - var a = iD.Node(), - b = iD.Node(), - c = iD.Node(), + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), w = iD.Way({nodes: [a.id, b.id, c.id]}), r = iD.Relation({members: [{id: w.id, type: 'way'}]}), g = iD.Graph([a, b, c, w, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c]]]); + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc]]]); }); specify("invalid geometry: unclosed ring consisting of multiple ways", function () { - var a = iD.Node(), - b = iD.Node(), - c = iD.Node(), - d = iD.Node(), + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), + d = iD.Node({loc: [4, 4]}), w1 = iD.Way({nodes: [a.id, b.id, c.id]}), w2 = iD.Way({nodes: [c.id, d.id]}), r = iD.Relation({members: [{id: w2.id, type: 'way'}, {id: w1.id, type: 'way'}]}), g = iD.Graph([a, b, c, d, w1, w2, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c, d]]]); + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, d.loc]]]); }); specify("invalid geometry: unclosed ring consisting of multiple ways, alternate order", function () { - var a = iD.Node(), - b = iD.Node(), - c = iD.Node(), - d = iD.Node(), + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), + d = iD.Node({loc: [4, 4]}), w1 = iD.Way({nodes: [c.id, d.id]}), w2 = iD.Way({nodes: [a.id, b.id, c.id]}), r = iD.Relation({members: [{id: w2.id, type: 'way'}, {id: w1.id, type: 'way'}]}), g = iD.Graph([a, b, c, d, w1, w2, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c, d]]]); + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, d.loc]]]); }); specify("invalid geometry: unclosed ring consisting of multiple ways, one needing reversal", function () { - var a = iD.Node(), - b = iD.Node(), - c = iD.Node(), - d = iD.Node(), + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), + d = iD.Node({loc: [4, 4]}), w1 = iD.Way({nodes: [a.id, b.id, c.id]}), w2 = iD.Way({nodes: [d.id, c.id]}), r = iD.Relation({members: [{id: w2.id, type: 'way'}, {id: w1.id, type: 'way'}]}), g = iD.Graph([a, b, c, d, w1, w2, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c, d]]]); + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, d.loc]]]); }); specify("invalid geometry: unclosed ring consisting of multiple ways, one needing reversal, alternate order", function () { @@ -259,7 +287,7 @@ describe('iD.Relation', function () { r = iD.Relation({members: [{id: w2.id, type: 'way'}, {id: w1.id, type: 'way'}]}), g = iD.Graph([a, b, c, d, w1, w2, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c, d]]]); + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, d.loc]]]); }); specify("single polygon with single single-way inner", function () { @@ -274,7 +302,7 @@ describe('iD.Relation', function () { r = iD.Relation({members: [{id: outer.id, type: 'way'}, {id: inner.id, role: 'inner', type: 'way'}]}), g = iD.Graph([a, b, c, d, e, f, outer, inner, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c, a], [d, e, f, d]]]); + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, a.loc], [d.loc, e.loc, f.loc, d.loc]]]); }); specify("single polygon with single multi-way inner", function () { @@ -293,7 +321,7 @@ describe('iD.Relation', function () { {id: inner1.id, role: 'inner', type: 'way'}]}), graph = iD.Graph([a, b, c, d, e, f, outer, inner1, inner2, r]); - expect(r.multipolygon(graph)).to.eql([[[a, b, c, a], [d, e, f, d]]]); + expect(r.multipolygon(graph)).to.eql([[[a.loc, b.loc, c.loc, a.loc], [d.loc, e.loc, f.loc, d.loc]]]); }); specify("single polygon with multiple single-way inners", function () { @@ -315,7 +343,7 @@ describe('iD.Relation', function () { {id: inner1.id, role: 'inner', type: 'way'}]}), graph = iD.Graph([a, b, c, d, e, f, g, h, i, outer, inner1, inner2, r]); - expect(r.multipolygon(graph)).to.eql([[[a, b, c, a], [d, e, f, d], [g, h, i, g]]]); + expect(r.multipolygon(graph)).to.eql([[[a.loc, b.loc, c.loc, a.loc], [d.loc, e.loc, f.loc, d.loc], [g.loc, h.loc, i.loc, g.loc]]]); }); specify("multiple polygons with single single-way inner", function () { @@ -337,30 +365,30 @@ describe('iD.Relation', function () { {id: inner.id, role: 'inner', type: 'way'}]}), graph = iD.Graph([a, b, c, d, e, f, g, h, i, outer1, outer2, inner, r]); - expect(r.multipolygon(graph)).to.eql([[[a, b, c, a], [d, e, f, d]], [[g, h, i, g]]]); + expect(r.multipolygon(graph)).to.eql([[[a.loc, b.loc, c.loc, a.loc], [d.loc, e.loc, f.loc, d.loc]], [[g.loc, h.loc, i.loc, g.loc]]]); }); specify("invalid geometry: unmatched inner", function () { - var a = iD.Node(), - b = iD.Node(), - c = iD.Node(), + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), w = iD.Way({nodes: [a.id, b.id, c.id, a.id]}), r = iD.Relation({members: [{id: w.id, role: 'inner', type: 'way'}]}), g = iD.Graph([a, b, c, w, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c, a]]]); + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, a.loc]]]); }); specify("incomplete relation", function () { - var a = iD.Node(), - b = iD.Node(), - c = iD.Node(), + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), 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]]]); + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc]]]); }); }); }); diff --git a/test/spec/graph/way.js b/test/spec/graph/way.js index 6b6b1543f..05864d5c3 100644 --- a/test/spec/graph/way.js +++ b/test/spec/graph/way.js @@ -95,8 +95,12 @@ describe('iD.Way', function() { expect(iD.Way({tags: { area: 'yes' }}).isArea()).to.equal(true); }); - it('returns true if the way is closed and has no tags', function() { - expect(iD.Way({nodes: ['n1', 'n1']}).isArea()).to.equal(true); + it('returns false if the way is closed and has no tags', function() { + expect(iD.Way({nodes: ['n1', 'n1']}).isArea()).to.equal(false); + }); + + it('returns true if the way is closed and has tags', function() { + expect(iD.Way({nodes: ['n1', 'n1'], tags: {a: 'b'}}).isArea()).to.equal(true); }); it('returns false if the way is closed and has tag area=no', function() { @@ -207,7 +211,7 @@ describe('iD.Way', function() { }); describe("#asGeoJSON", function () { - it("converts to a GeoJSON LineString features", function () { + it("converts a line to a GeoJSON LineString features", function () { var a = iD.Node({loc: [1, 2]}), b = iD.Node({loc: [3, 4]}), w = iD.Way({tags: {highway: 'residential'}, nodes: [a.id, b.id]}), @@ -219,5 +223,19 @@ describe('iD.Way', function() { expect(json.geometry.type).to.equal('LineString'); expect(json.geometry.coordinates).to.eql([[1, 2], [3, 4]]); }); + + it("converts an area to a GeoJSON Polygon features", function () { + var a = iD.Node({loc: [1, 2]}), + b = iD.Node({loc: [3, 4]}), + c = iD.Node({loc: [5, 6]}), + w = iD.Way({tags: {area: 'yes'}, nodes: [a.id, b.id, c.id, a.id]}), + graph = iD.Graph([a, b, c, w]), + json = w.asGeoJSON(graph); + + expect(json.type).to.equal('Feature'); + expect(json.properties).to.eql({area: 'yes'}); + expect(json.geometry.type).to.equal('Polygon'); + expect(json.geometry.coordinates).to.eql([[[1, 2], [3, 4], [5, 6], [1, 2]]]); + }); }); }); diff --git a/test/spec/modes/add_point.js b/test/spec/modes/add_point.js index 38a614198..5807a3c0b 100644 --- a/test/spec/modes/add_point.js +++ b/test/spec/modes/add_point.js @@ -1,7 +1,7 @@ -describe("iD.modes.AddPoint", function () { +describe("iD.modes.AddPoint", function() { var context; - beforeEach(function () { + beforeEach(function() { var container = d3.select(document.createElement('div')); context = iD() @@ -15,20 +15,22 @@ describe("iD.modes.AddPoint", function () { }); describe("clicking the map", function () { - it("adds a node", function () { - happen.click(context.surface().node(), {}); + it("adds a node", function() { + happen.mousedown(context.surface().node(), {}); + happen.click(window, {}); expect(context.changes().created).to.have.length(1); }); - it("selects the node", function () { - happen.click(context.surface().node(), {}); + it("selects the node", function() { + happen.mousedown(context.surface().node(), {}); + happen.click(window, {}); expect(context.mode().id).to.equal('select'); expect(context.mode().selection()).to.eql([context.changes().created[0].id]); }); }); - describe("pressing ⎋", function () { - it("exits to browse mode", function () { + describe("pressing ⎋", function() { + it("exits to browse mode", function() { happen.keydown(document, {keyCode: 27}); expect(context.mode().id).to.equal('browse'); }); diff --git a/test/spec/svg/areas.js b/test/spec/svg/areas.js index 807a799f4..bbf8674ae 100644 --- a/test/spec/svg/areas.js +++ b/test/spec/svg/areas.js @@ -69,4 +69,46 @@ describe("iD.svg.Areas", function () { expect(surface.select('.area:nth-child(1)')).to.be.classed('tag-landuse-park'); expect(surface.select('.area:nth-child(2)')).to.be.classed('tag-building-yes'); }); + + it("renders fills for multipolygon areas", function () { + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), + w = iD.Way({nodes: [a.id, b.id, c.id, a.id]}), + r = iD.Relation({tags: {type: 'multipolygon'}, members: [{id: w.id, type: 'way'}]}), + graph = iD.Graph([a, b, c, w, r]), + areas = [w, r]; + + surface.call(iD.svg.Areas(projection), graph, areas, filter); + + expect(surface.select('.fill')).to.be.classed('relation'); + }); + + it("renders no strokes for multipolygon areas", function () { + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), + w = iD.Way({nodes: [a.id, b.id, c.id, a.id]}), + r = iD.Relation({tags: {type: 'multipolygon'}, members: [{id: w.id, type: 'way'}]}), + graph = iD.Graph([a, b, c, w, r]), + areas = [w, r]; + + surface.call(iD.svg.Areas(projection), graph, areas, filter); + + expect(surface.selectAll('.stroke')[0].length).to.equal(0); + }); + + it("adds stroke classes for the tags of the parent relation of multipolygon members", function() { + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), + w = iD.Way({tags: {area: 'yes'}, nodes: [a.id, b.id, c.id, a.id]}), + r = iD.Relation({members: [{id: w.id}], tags: {type: 'multipolygon', natural: 'wood'}}), + graph = iD.Graph([a, b, c, w, r]); + + surface.call(iD.svg.Areas(projection), graph, [w], filter); + + expect(surface.select('.stroke')).to.be.classed('tag-natural-wood'); + expect(surface.select('.fill')).not.to.be.classed('tag-natural-wood'); + }); }); diff --git a/test/spec/svg/lines.js b/test/spec/svg/lines.js index 61c3229a9..989022632 100644 --- a/test/spec/svg/lines.js +++ b/test/spec/svg/lines.js @@ -39,6 +39,16 @@ describe("iD.svg.Lines", function () { expect(surface.select('.line')).to.be.classed('member-type-route'); }); + it("adds stroke classes for the tags of the parent relation of multipolygon members", function() { + var line = iD.Way(), + relation = iD.Relation({members: [{id: line.id}], tags: {type: 'multipolygon', natural: 'wood'}}), + graph = iD.Graph([line, relation]); + + surface.call(iD.svg.Lines(projection), graph, [line], filter); + + expect(surface.select('.stroke')).to.be.classed('tag-natural-wood'); + }); + it("preserves non-line paths", function () { var line = iD.Way(), graph = iD.Graph([line]); diff --git a/test/spec/svg/multipolygons.js b/test/spec/svg/multipolygons.js deleted file mode 100644 index 67f44ebae..000000000 --- a/test/spec/svg/multipolygons.js +++ /dev/null @@ -1,43 +0,0 @@ -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/tag_classes.js b/test/spec/svg/tag_classes.js index dd899e08e..048378815 100644 --- a/test/spec/svg/tag_classes.js +++ b/test/spec/svg/tag_classes.js @@ -19,6 +19,13 @@ describe("iD.svg.TagClasses", function () { expect(selection.attr('class')).to.equal('tag-highway tag-highway-primary'); }); + it('adds tags based on the result of the `tags` accessor', function() { + selection + .datum(iD.Entity()) + .call(iD.svg.TagClasses().tags(d3.functor({highway: 'primary'}))); + expect(selection.attr('class')).to.equal('tag-highway tag-highway-primary'); + }); + it('removes classes for tags that are no longer present', function() { selection .attr('class', 'tag-highway tag-highway-primary')