diff --git a/README.md b/README.md index 770629f39..711cb5418 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Build Status](https://secure.travis-ci.org/systemed/iD.png)](https://travis-ci.org/systemed/iD) -[![](https://raw.github.com/systemed/iD/master/screenshot.jpg)](http://geowiki.com/iD/) +[![](http://ideditor.com/img/editor.png)](http://geowiki.com/iD/) [Try the online demo of the most recent code.](http://geowiki.com/iD/) and [open issues for bugs and ideas!](https://github.com/systemed/iD/issues) diff --git a/css/app.css b/css/app.css index 47bf15f45..49e2a8e65 100644 --- a/css/app.css +++ b/css/app.css @@ -256,6 +256,9 @@ button { height:40px; cursor:url(../img/cursor-pointer.png) 6 1, auto; border-radius:4px; + -webkit-transition: background 100ms; + -moz-transition: background 100ms; + transition: background 100ms; } button:hover { @@ -1023,6 +1026,14 @@ div.typeahead a:first-child { text-align: center; } +/* Success +------------------------------------------------------- */ +a.success-action { + display:inline-block; + padding:10px; + margin:10px; +} + /* Notices ------------------------------------------------------- */ @@ -1085,17 +1096,17 @@ div.typeahead a:first-child { } .tooltip-inner { - text-align: left; - width: 200px; - font-size: 11px; - font-weight: bold; - line-height: 20px; - padding: 5px 10px; - color: #333; - background-color: white; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; + text-align: left; + width: 200px; + font-size: 11px; + font-weight: bold; + line-height: 20px; + padding: 5px 10px; + color: #333; + background-color: white; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; } .tooltip-arrow { @@ -1142,6 +1153,17 @@ div.typeahead a:first-child { left: 30px; } +.tooltip .keyhint { + float: right; + background: #eee; + font-size: 10px; + padding: 0 4px; + background:#aaa; + color:#fff; + border-radius: 2px; + margin-left: 4px; +} + .tail { pointer-events:none; position: absolute; diff --git a/icons/unknown.png b/icons/unknown.png index a47dd947d..404602aa4 100644 Binary files a/icons/unknown.png and b/icons/unknown.png differ diff --git a/js/id/behavior/drag.js b/js/id/behavior/drag.js index 29f033c1c..d88e8a44a 100644 --- a/js/id/behavior/drag.js +++ b/js/id/behavior/drag.js @@ -23,7 +23,9 @@ iD.behavior.drag = function () { var event = d3.dispatch("start", "move", "end"), origin = null, selector = '', - filter = null; + filter = null, + keybinding = d3.keybinding('drag'), + event_, target; event.of = function(thiz, argumentz) { return function(e1) { @@ -39,9 +41,9 @@ iD.behavior.drag = function () { }; function mousedown() { - var target = this, - event_ = event.of(target, arguments), - eventTarget = d3.event.target, + target = this, + event_ = event.of(target, arguments); + var eventTarget = d3.event.target, touchId = d3.event.touches ? d3.event.changedTouches[0].identifier : null, offset, origin_ = point(), @@ -135,6 +137,9 @@ iD.behavior.drag = function () { drag.off = function(selection) { selection.on("mousedown.drag" + selector, null) .on("touchstart.drag" + selector, null); + keybinding + .on('⌘+Z', null) + .on('⌃+Z', null); }; drag.delegate = function(_) { @@ -155,5 +160,25 @@ iD.behavior.drag = function () { return drag; }; + drag.cancel = function() { + d3.select(window) + .on("mousemove.drag", null) + .on("mouseup.drag", null); + return drag; + }; + + drag.target = function() { + if (!arguments.length) return target; + target = arguments[0]; + event_ = event.of(target, Array.prototype.slice.call(arguments, 1)); + return drag; + }; + + keybinding + .on('⌘+Z', drag.cancel) + .on('⌃+Z', drag.cancel); + + d3.select(document).call(keybinding); + return d3.rebind(drag, event, "on"); }; diff --git a/js/id/behavior/drag_midpoint.js b/js/id/behavior/drag_midpoint.js index 5445003b8..9f73a7115 100644 --- a/js/id/behavior/drag_midpoint.js +++ b/js/id/behavior/drag_midpoint.js @@ -1,8 +1,7 @@ iD.behavior.DragMidpoint = function(mode) { var history = mode.history, - projection = mode.map.projection; - - return iD.behavior.drag() + projection = mode.map.projection, + behavior = iD.behavior.drag() .delegate(".midpoint") .origin(function(d) { return projection(d.loc); @@ -21,15 +20,20 @@ iD.behavior.DragMidpoint = function(mode) { } } history.perform.apply(history, args); + var node = d3.selectAll('.node.vertex') + .filter(function(data) { return data.id === d.node.id; }); + behavior.target(node.node(), node.datum()); + }) .on('move', function(d) { d3.event.sourceEvent.stopPropagation(); history.replace( - iD.actions.MoveNode(d.node.id, projection.invert(d3.event.point))); + iD.actions.MoveNode(d.id, projection.invert(d3.event.point))); }) .on('end', function() { history.replace( iD.actions.Noop(), 'added a node to a way'); }); + return behavior; }; diff --git a/js/id/behavior/drag_node.js b/js/id/behavior/drag_node.js index e104c795f..084c81895 100644 --- a/js/id/behavior/drag_node.js +++ b/js/id/behavior/drag_node.js @@ -14,7 +14,8 @@ iD.behavior.DragNode = function(mode) { .on('move', function(entity) { d3.event.sourceEvent.stopPropagation(); history.replace( - iD.actions.MoveNode(entity.id, projection.invert(d3.event.point))); + iD.actions.MoveNode(entity.id, projection.invert(d3.event.point)), + 'moved a node'); }) .on('end', function() { history.replace( diff --git a/js/id/behavior/drag_way.js b/js/id/behavior/drag_way.js index 47045f5ac..6de8ca252 100644 --- a/js/id/behavior/drag_way.js +++ b/js/id/behavior/drag_way.js @@ -17,7 +17,8 @@ iD.behavior.DragWay = function(mode) { .on('move', function(entity) { d3.event.sourceEvent.stopPropagation(); history.replace( - iD.actions.MoveWay(entity.id, d3.event.delta, projection)); + iD.actions.MoveWay(entity.id, d3.event.delta, projection), + 'moved a way'); }) .on('end', function() { history.replace( diff --git a/js/id/behavior/draw_way.js b/js/id/behavior/draw_way.js index a9f1f5c91..0e5bcf4fd 100644 --- a/js/id/behavior/draw_way.js +++ b/js/id/behavior/draw_way.js @@ -1,9 +1,10 @@ -iD.behavior.DrawWay = function(wayId, headId, tailId, index, mode) { +iD.behavior.DrawWay = function(wayId, headId, tailId, index, mode, baseGraph) { var map = mode.map, history = mode.history, controller = mode.controller, event = d3.dispatch('add', 'addHead', 'addTail', 'addNode', 'addWay'), way = mode.history.graph().entity(wayId), + finished = false, hover, draw; var node = iD.Node({loc: map.mouseCoordinates()}), @@ -56,6 +57,9 @@ iD.behavior.DrawWay = function(wayId, headId, tailId, index, mode) { }; drawWay.off = function(surface) { + if (!finished) + history.pop(); + map.fastEnable(true) .minzoom(0) .tail(false); @@ -86,6 +90,7 @@ iD.behavior.DrawWay = function(wayId, headId, tailId, index, mode) { ReplaceTemporaryNode(node), annotation); + finished = true; controller.enter(mode); }; @@ -99,6 +104,7 @@ iD.behavior.DrawWay = function(wayId, headId, tailId, index, mode) { ReplaceTemporaryNode(newNode), annotation); + finished = true; controller.enter(mode); }; @@ -111,6 +117,7 @@ iD.behavior.DrawWay = function(wayId, headId, tailId, index, mode) { ReplaceTemporaryNode(newNode), annotation); + finished = true; controller.enter(mode); }; @@ -118,6 +125,7 @@ iD.behavior.DrawWay = function(wayId, headId, tailId, index, mode) { // nodes to be valid, it's selected. Otherwise, return to browse mode. drawWay.finish = function() { history.pop(); + finished = true; var way = history.graph().entity(wayId); if (way) { @@ -129,7 +137,11 @@ iD.behavior.DrawWay = function(wayId, headId, tailId, index, mode) { // Cancel the draw operation and return to browse, deleting everything drawn. drawWay.cancel = function() { - history.perform(iD.actions.DeleteWay(wayId), 'cancelled drawing'); + history.perform( + d3.functor(baseGraph), + 'cancelled drawing'); + + finished = true; controller.enter(iD.modes.Browse()); }; diff --git a/js/id/connection.js b/js/id/connection.js index 53c96bd96..0460b09ab 100644 --- a/js/id/connection.js +++ b/js/id/connection.js @@ -10,6 +10,10 @@ iD.Connection = function() { loadedTiles = {}, oauth = iD.OAuth().url(url); + function changesetUrl(changesetId) { + return url + '/browse/changeset/' + changesetId; + } + function bboxUrl(b) { return url + '/api/0.6/map?bbox=' + [b[0][0],b[1][1],b[1][0],b[0][1]]; } @@ -325,6 +329,7 @@ iD.Connection = function() { }; connection.bboxFromAPI = bboxFromAPI; + connection.changesetUrl = changesetUrl; connection.loadFromURL = loadFromURL; connection.loadTiles = _.debounce(loadTiles, 100); connection.userDetails = userDetails; diff --git a/js/id/id.js b/js/id/id.js index 770cbde43..ab6dd37f2 100644 --- a/js/id/id.js +++ b/js/id/id.js @@ -21,6 +21,10 @@ window.iD = function(container) { return; } + function hintprefix(x, y) { + return '' + x + ' ' + y; + } + var m = container.append('div') .attr('id', 'map') .call(map); @@ -40,8 +44,10 @@ window.iD = function(container) { .enter().append('button') .attr('tabindex', -1) .attr('class', function (mode) { return mode.title + ' add-button col3'; }) - .attr('data-original-title', function (mode) { return mode.description; }) - .call(bootstrap.tooltip().placement('bottom')) + .attr('data-original-title', function (mode) { + return hintprefix(mode.key, mode.description); + }) + .call(bootstrap.tooltip().placement('bottom').html(true)) .on('click.editor', function (mode) { controller.enter(mode); }); function disableTooHigh() { @@ -82,7 +88,7 @@ window.iD = function(container) { var undo_buttons = limiter.append('div') .attr('class', 'button-wrap joined col1'), - undo_tooltip = bootstrap.tooltip().placement('bottom'); + undo_tooltip = bootstrap.tooltip().placement('bottom').html(true); undo_buttons.append('button') .attr({ id: 'undo', 'class': 'col6' }) @@ -201,12 +207,12 @@ window.iD = function(container) { limiter.select('#undo') .property('disabled', !undo) - .attr('data-original-title', undo) + .attr('data-original-title', hintprefix('⌘Z', undo)) .call(refreshTooltip); limiter.select('#redo') .property('disabled', !redo) - .attr('data-original-title', redo) + .attr('data-original-title', hintprefix('⌘⇧Z', redo)) .call(refreshTooltip); }); @@ -215,16 +221,16 @@ window.iD = function(container) { }); var keybinding = d3.keybinding('main') - .on('M', function() { if (map.editable()) controller.enter(iD.modes.Browse()); }) - .on('P', function() { if (map.editable()) controller.enter(iD.modes.AddPoint()); }) - .on('L', function() { if (map.editable()) controller.enter(iD.modes.AddLine()); }) - .on('A', function() { if (map.editable()) controller.enter(iD.modes.AddArea()); }) .on('⌘+Z', function() { history.undo(); }) .on('⌃+Z', function() { history.undo(); }) .on('⌘+⇧+Z', function() { history.redo(); }) .on('⌃+⇧+Z', function() { history.redo(); }) .on('⌫', function() { d3.event.preventDefault(); }); + [iD.modes.Browse(), iD.modes.AddPoint(), iD.modes.AddLine(), iD.modes.AddArea()].forEach(function(m) { + keybinding.on(m.key, function() { if (map.editable()) controller.enter(m); }); + }); + d3.select(document) .call(keybinding); diff --git a/js/id/modes/add_area.js b/js/id/modes/add_area.js index e00031723..d1a3efb08 100644 --- a/js/id/modes/add_area.js +++ b/js/id/modes/add_area.js @@ -3,7 +3,8 @@ iD.modes.AddArea = function() { id: 'add-area', button: 'area', title: 'Area', - description: 'Add parks, buildings, lakes, or other areas to the map.' + description: 'Add parks, buildings, lakes, or other areas to the map.', + key: 'a' }; var behavior, @@ -15,18 +16,20 @@ iD.modes.AddArea = function() { controller = mode.controller; function startFromNode(node) { - var way = iD.Way({tags: defaultTags}); + var graph = history.graph(), + way = iD.Way({tags: defaultTags}); history.perform( iD.actions.AddWay(way), iD.actions.AddWayNode(way.id, node.id), iD.actions.AddWayNode(way.id, node.id)); - controller.enter(iD.modes.DrawArea(way.id)); + controller.enter(iD.modes.DrawArea(way.id, graph)); } function startFromWay(other, loc, index) { - var node = iD.Node({loc: loc}), + var graph = history.graph(), + node = iD.Node({loc: loc}), way = iD.Way({tags: defaultTags}); history.perform( @@ -36,11 +39,12 @@ iD.modes.AddArea = function() { iD.actions.AddWayNode(way.id, node.id), iD.actions.AddWayNode(other.id, node.id, index)); - controller.enter(iD.modes.DrawArea(way.id)); + controller.enter(iD.modes.DrawArea(way.id, graph)); } function start(loc) { - var node = iD.Node({loc: loc}), + var graph = history.graph(), + node = iD.Node({loc: loc}), way = iD.Way({tags: defaultTags}); history.perform( @@ -49,7 +53,7 @@ iD.modes.AddArea = function() { iD.actions.AddWayNode(way.id, node.id), iD.actions.AddWayNode(way.id, node.id)); - controller.enter(iD.modes.DrawArea(way.id)); + controller.enter(iD.modes.DrawArea(way.id, graph)); } behavior = iD.behavior.AddWay(mode) diff --git a/js/id/modes/add_line.js b/js/id/modes/add_line.js index ad446a0f0..bf419d2bb 100644 --- a/js/id/modes/add_line.js +++ b/js/id/modes/add_line.js @@ -3,7 +3,8 @@ iD.modes.AddLine = function() { id: 'add-line', button: 'line', title: 'Line', - description: 'Lines can be highways, streets, pedestrian paths, or even canals.' + description: 'Lines can be highways, streets, pedestrian paths, or even canals.', + key: 'l' }; var behavior, @@ -20,10 +21,10 @@ iD.modes.AddLine = function() { isLine = parent && parent.geometry(graph) === 'line'; if (isLine && parent.first() === node.id) { - controller.enter(iD.modes.DrawLine(parent.id, 'backward')); + controller.enter(iD.modes.DrawLine(parent.id, 'backward', graph)); } else if (isLine && parent.last() === node.id) { - controller.enter(iD.modes.DrawLine(parent.id, 'forward')); + controller.enter(iD.modes.DrawLine(parent.id, 'forward', graph)); } else { var way = iD.Way({tags: defaultTags}); @@ -32,12 +33,13 @@ iD.modes.AddLine = function() { iD.actions.AddWay(way), iD.actions.AddWayNode(way.id, node.id)); - controller.enter(iD.modes.DrawLine(way.id, 'forward')); + controller.enter(iD.modes.DrawLine(way.id, 'forward', graph)); } } function startFromWay(other, loc, index) { - var node = iD.Node({loc: loc}), + var graph = history.graph(), + node = iD.Node({loc: loc}), way = iD.Way({tags: defaultTags}); history.perform( @@ -46,11 +48,12 @@ iD.modes.AddLine = function() { iD.actions.AddWayNode(way.id, node.id), iD.actions.AddWayNode(other.id, node.id, index)); - controller.enter(iD.modes.DrawLine(way.id, 'forward')); + controller.enter(iD.modes.DrawLine(way.id, 'forward', graph)); } function start(loc) { - var node = iD.Node({loc: loc}), + var graph = history.graph(), + node = iD.Node({loc: loc}), way = iD.Way({tags: defaultTags}); history.perform( @@ -58,7 +61,7 @@ iD.modes.AddLine = function() { iD.actions.AddWay(way), iD.actions.AddWayNode(way.id, node.id)); - controller.enter(iD.modes.DrawLine(way.id, 'forward')); + controller.enter(iD.modes.DrawLine(way.id, 'forward', graph)); } behavior = iD.behavior.AddWay(mode) diff --git a/js/id/modes/add_point.js b/js/id/modes/add_point.js index 89f7aa536..1a51c314d 100644 --- a/js/id/modes/add_point.js +++ b/js/id/modes/add_point.js @@ -2,7 +2,8 @@ iD.modes.AddPoint = function() { var mode = { id: 'add-point', title: 'Point', - description: 'Restaurants, monuments, and postal boxes are points.' + description: 'Restaurants, monuments, and postal boxes are points.', + key: 'p' }; var behavior; diff --git a/js/id/modes/browse.js b/js/id/modes/browse.js index 4b2ea448f..9782bac27 100644 --- a/js/id/modes/browse.js +++ b/js/id/modes/browse.js @@ -2,8 +2,9 @@ iD.modes.Browse = function() { var mode = { button: 'browse', id: 'browse', - title: 'Move', - description: 'Pan and zoom the map' + title: 'Browse', + description: 'Pan and zoom the map', + key: 'b' }; var behaviors; diff --git a/js/id/modes/draw_area.js b/js/id/modes/draw_area.js index 9e2bd6a32..721452bae 100644 --- a/js/id/modes/draw_area.js +++ b/js/id/modes/draw_area.js @@ -1,4 +1,4 @@ -iD.modes.DrawArea = function(wayId) { +iD.modes.DrawArea = function(wayId, baseGraph) { var mode = { button: 'area', id: 'draw-area' @@ -29,7 +29,7 @@ iD.modes.DrawArea = function(wayId) { behavior.add(loc, annotation); } - behavior = iD.behavior.DrawWay(wayId, headId, tailId, index, mode) + behavior = iD.behavior.DrawWay(wayId, headId, tailId, index, mode, baseGraph) .on('addHead', addHeadTail) .on('addTail', addHeadTail) .on('addNode', addNode) diff --git a/js/id/modes/draw_line.js b/js/id/modes/draw_line.js index 7980b0549..289d91c14 100644 --- a/js/id/modes/draw_line.js +++ b/js/id/modes/draw_line.js @@ -1,4 +1,4 @@ -iD.modes.DrawLine = function(wayId, direction) { +iD.modes.DrawLine = function(wayId, direction, baseGraph) { var mode = { button: 'line', id: 'draw-line' @@ -38,7 +38,7 @@ iD.modes.DrawLine = function(wayId, direction) { behavior.add(loc, annotation); } - behavior = iD.behavior.DrawWay(wayId, headId, tailId, index, mode) + behavior = iD.behavior.DrawWay(wayId, headId, tailId, index, mode, baseGraph) .on('addHead', addHead) .on('addTail', addTail) .on('addNode', addNode) diff --git a/js/id/renderer/map.js b/js/id/renderer/map.js index bc4ca8e03..60ad3de39 100644 --- a/js/id/renderer/map.js +++ b/js/id/renderer/map.js @@ -165,24 +165,33 @@ iD.Map = function() { } function resetTransform() { - if (!surface.style(transformProp)) return; + if (!surface.style(transformProp)) return false; surface.style(transformProp, ''); tilegroup.style(transformProp, ''); + return true; } function redraw(difference) { - resetTransform(); + // If we are in the middle of a zoom/pan, we can't do differenced redraws. + // It would result in artifacts where differenced entities are redrawn with + // one transform and unchanged entities with another. + if (resetTransform()) + difference = undefined; + surface.attr('data-zoom', ~~map.zoom()); tilegroup.call(background); + if (map.editable()) { connection.loadTiles(projection, dimensions); drawVector(difference); } else { editOff(); } + transformStart = [ projection.scale(), projection.translate().slice()]; + return map; } diff --git a/js/id/ui/modal.js b/js/id/ui/modal.js index 3ea341f42..b91c2772a 100644 --- a/js/id/ui/modal.js +++ b/js/id/ui/modal.js @@ -1,6 +1,13 @@ iD.ui.modal = function(blocking) { + var animate = d3.select('div.modal').empty(); + var keybinding = d3.keybinding('modal') + .on('⌫', close) + .on('⎋', close); + + d3.select(document).call(keybinding); + d3.select('div.modal').transition() .style('opacity', 0).remove(); @@ -30,5 +37,10 @@ iD.ui.modal = function(blocking) { shaded.style('opacity', 1); } + function close() { + shaded.remove(); + keybinding.off(); + } + return shaded; }; diff --git a/js/id/ui/save.js b/js/id/ui/save.js index 21af956d9..50f9891f7 100644 --- a/js/id/ui/save.js +++ b/js/id/ui/save.js @@ -37,7 +37,7 @@ iD.ui.save = function() { id: changeset_id, comment: e.comment }) - .call(iD.ui.success() + .call(iD.ui.success(connection) .on('cancel', function() { modal.remove(); })); diff --git a/js/id/ui/success.js b/js/id/ui/success.js index d135f7d80..8cc6e328b 100644 --- a/js/id/ui/success.js +++ b/js/id/ui/success.js @@ -1,4 +1,4 @@ -iD.ui.success = function() { +iD.ui.success = function(connection) { var event = d3.dispatch('cancel', 'save'); function success(selection) { @@ -9,22 +9,31 @@ iD.ui.success = function() { var section = body.append('div').attr('class','modal-section fillD'); header.append('h2').text('You Just Edited OpenStreetMap!'); - header.append('p').text('You just improved the world\'s best free map'); var m = ''; if (changeset.comment) { m = '"' + changeset.comment.substring(0, 20) + '" '; } - var message = 'Edited OpenStreetMap! ' + m + - 'http://osm.org/browse/changeset/' + changeset.id; + var message = (m || 'Edited OSM!') + + connection.changesetUrl(changeset.id); - section.append('a') + header.append('a') + .attr('href', function(d) { + return connection.changesetUrl(changeset.id); + }) + .attr('target', '_blank') + .attr('class', 'success-action') + .text('View on OSM'); + + header.append('a') + .attr('target', '_blank') .attr('href', function(d) { return 'https://twitter.com/intent/tweet?source=webclient&text=' + encodeURIComponent(message); }) - .text('Tweet: ' + message); + .attr('class', 'success-action') + .text('Tweet'); var buttonwrap = section.append('div') .attr('class', 'buttons cf');