diff --git a/Makefile b/Makefile index 1db4f2efb..3b0f2d1b7 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,6 @@ all: \ js/id/modes/*.js \ js/id/operations.js \ js/id/operations/*.js \ - js/id/controller.js \ js/id/graph/*.js \ js/id/renderer/*.js \ js/id/svg.js \ diff --git a/combobox.html b/combobox.html new file mode 100644 index 000000000..c739f4898 --- /dev/null +++ b/combobox.html @@ -0,0 +1,170 @@ + + + + + iD + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + diff --git a/css/app.css b/css/app.css index 718c0c39a..7a0751d02 100644 --- a/css/app.css +++ b/css/app.css @@ -476,7 +476,7 @@ button[disabled] .icon.nearby { background-position: -340px -40px;} .icon-operation-circularize { background-position: -20px -140px;} .icon-operation-straighten { background-position: -40px -140px;} .icon-operation-split { background-position: -60px -140px;} -.icon-operation-unjoin { background-position: -80px -140px;} +.icon-operation-disconnect { background-position: -80px -140px;} .icon-operation-reverse { background-position: -100px -140px;} .icon-operation-move { background-position: -120px -140px;} .icon-operation-merge { background-position: -140px -140px;} @@ -1398,3 +1398,46 @@ a.success-action { .icon.icon-pre-text { margin-right: 0px;} .save .label, .apply .label, .cancel .label { display: block;} } + + + + + +div.combobox { + width:155px; + z-index: 9999; + display: none; + box-shadow: 0 5px 10px 0 rgba(0,0,0,.2); + margin-top: -1px; + background: white; + max-height: 180px; + overflow: auto; + border: 1px solid #ccc; +} + +div.combobox a { + height: 25px; + line-height: 25px; + cursor: pointer; + display: block; + border-top:1px solid #ccc; + background-color: #fff; + padding:1px 4px; + white-space: nowrap; +} + +div.combobox a:hover, +div.combobox a.selected { + background: #e1e8ff; + color: #154dff; +} + +div.combobox a:first-child { + border-top: 0; +} + +div.combobox-carat { + cursor: pointer; + padding:0 5px; + vertical-align:middle; +} diff --git a/css/map.css b/css/map.css index 8ad55c790..ab2a352c9 100644 --- a/css/map.css +++ b/css/map.css @@ -150,11 +150,6 @@ path.stroke { stroke-width: 2; } -path.stroke, -path.casing { - shape-rendering: optimizeSpeed; -} - path.shadow { pointer-events: stroke; stroke-width: 10; @@ -725,6 +720,7 @@ text.point { } /* Ensure drawing doesn't interact with area fills. */ +.mode-add-point .area, .mode-draw-line .area, .mode-draw-area .area, .mode-add-line .area, diff --git a/index.html b/index.html index c8ddd7562..f9b627c16 100644 --- a/index.html +++ b/index.html @@ -76,15 +76,18 @@ + + + + - - - + + @@ -110,11 +113,13 @@ + + - + @@ -122,9 +127,8 @@ - - + @@ -134,12 +138,15 @@ locale.current = 'en'; d3.json('keys.json', function(err, keys) { var id = iD(); - id.connection().keys(keys) - .url('http://api06.dev.openstreetmap.org'); - d3.select("#iD").call(id); - }); - + id.connection() + .keys(keys) + .url('http://api06.dev.openstreetmap.org'); + + d3.select("#iD") + .call(id.ui()) + }); + + diff --git a/index_packaged.html b/index_packaged.html index c48a21873..a961a5202 100644 --- a/index_packaged.html +++ b/index_packaged.html @@ -16,8 +16,16 @@
diff --git a/js/id/actions/circularize.js b/js/id/actions/circularize.js index edbd0d8b3..532dcdb45 100644 --- a/js/id/actions/circularize.js +++ b/js/id/actions/circularize.js @@ -1,12 +1,11 @@ -iD.actions.Circularize = function(wayId, map) { +iD.actions.Circularize = function(wayId, projection) { var action = function(graph) { var way = graph.entity(wayId), - nodes = graph.childNodes(way), - tags = {}, key, role; + nodes = _.uniq(graph.childNodes(way)); var points = nodes.map(function(n) { - return map.projection(n.loc); + return projection(n.loc); }), centroid = d3.geom.polygon(points).centroid(), radius = d3.median(points, function(p) { @@ -15,14 +14,12 @@ iD.actions.Circularize = function(wayId, map) { circular_nodes = []; for (var i = 0; i < 12; i++) { - circular_nodes.push(iD.Node({ loc: map.projection.invert([ + circular_nodes.push(iD.Node({ loc: projection.invert([ centroid[0] + Math.cos((i / 12) * Math.PI * 2) * radius, centroid[1] + Math.sin((i / 12) * Math.PI * 2) * radius]) })); } - circular_nodes.push(circular_nodes[0]); - for (i = 0; i < nodes.length; i++) { if (graph.parentWays(nodes[i]).length > 1) { var closest, closest_dist = Infinity, dist; @@ -34,10 +31,6 @@ iD.actions.Circularize = function(wayId, map) { } } circular_nodes.splice(closest, 1, nodes[i]); - if (closest === 0) circular_nodes.splice(circular_nodes.length - 1, 1, nodes[i]); - else if (closest === circular_nodes.length - 1) circular_nodes.splice(0, 1, nodes[i]); - } else { - graph = graph.remove(nodes[i]); } } @@ -45,9 +38,18 @@ iD.actions.Circularize = function(wayId, map) { graph = graph.replace(circular_nodes[i]); } - return graph.replace(way.update({ - nodes: _.pluck(circular_nodes, 'id') - })); + var ids = _.pluck(circular_nodes, 'id'), + difference = _.difference(_.uniq(way.nodes), ids); + + ids.push(ids[0]); + + graph = graph.replace(way.update({nodes: ids})); + + for (i = 0; i < difference.length; i++) { + graph = iD.actions.DeleteNode(difference[i])(graph); + } + + return graph; }; action.enabled = function(graph) { diff --git a/js/id/actions/delete_multiple.js b/js/id/actions/delete_multiple.js new file mode 100644 index 000000000..839c1d780 --- /dev/null +++ b/js/id/actions/delete_multiple.js @@ -0,0 +1,15 @@ +iD.actions.DeleteMultiple = function(ids) { + return function(graph) { + var actions = { + way: iD.actions.DeleteWay, + node: iD.actions.DeleteNode, + relation: iD.actions.DeleteRelation + }; + + ids.forEach(function (id) { + graph = actions[graph.entity(id).type](id)(graph); + }); + + return graph; + }; +}; diff --git a/js/id/actions/delete_relation.js b/js/id/actions/delete_relation.js new file mode 100644 index 000000000..48c62f1e1 --- /dev/null +++ b/js/id/actions/delete_relation.js @@ -0,0 +1,13 @@ +// https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/DeleteRelationAction.as +iD.actions.DeleteRelation = function(relationId) { + return function(graph) { + var relation = graph.entity(relationId); + + graph.parentRelations(relation) + .forEach(function(parent) { + graph = graph.replace(parent.removeMember(relationId)); + }); + + return graph.remove(relation); + }; +}; diff --git a/js/id/actions/unjoin_node.js b/js/id/actions/disconnect.js similarity index 92% rename from js/id/actions/unjoin_node.js rename to js/id/actions/disconnect.js index 39e05a9cf..8645c436b 100644 --- a/js/id/actions/unjoin_node.js +++ b/js/id/actions/disconnect.js @@ -1,4 +1,4 @@ -// Unjoin the ways at the given node. +// Disconect the ways at the given node. // // For testing convenience, accepts an ID to assign to the (first) new node. // Normally, this will be undefined and the way will automatically @@ -8,7 +8,7 @@ // https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/UnjoinNodeAction.as // https://github.com/openstreetmap/josm/blob/mirror/src/org/openstreetmap/josm/actions/UnGlueAction.java // -iD.actions.UnjoinNode = function(nodeId, newNodeId) { +iD.actions.Disconnect = function(nodeId, newNodeId) { var action = function(graph) { if (!action.enabled(graph)) return graph; diff --git a/js/id/actions/join.js b/js/id/actions/join.js new file mode 100644 index 000000000..fd6862a9b --- /dev/null +++ b/js/id/actions/join.js @@ -0,0 +1,65 @@ +// Join ways at the end node they share. +// +// This is the inverse of `iD.actions.Split`. +// +// Reference: +// https://github.com/systemed/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/MergeWaysAction.as +// https://github.com/openstreetmap/josm/blob/mirror/src/org/openstreetmap/josm/actions/CombineWayAction.java +// +iD.actions.Join = function(idA, idB) { + var action = function(graph) { + var a = graph.entity(idA), + b = graph.entity(idB), + nodes, tags; + + if (a.first() === b.first()) { + // a <-- b ==> c + // Expected result: + // a <-- b <-- c + nodes = b.nodes.slice().reverse().concat(a.nodes.slice(1)); + + } else if (a.first() === b.last()) { + // a <-- b <== c + // Expected result: + // a <-- b <-- c + nodes = b.nodes.concat(a.nodes.slice(1)); + + } else if (a.last() === b.first()) { + // a --> b ==> c + // Expected result: + // a --> b --> c + nodes = a.nodes.concat(b.nodes.slice(1)); + + } else if (a.last() === b.last()) { + // a --> b <== c + // Expected result: + // a --> b --> c + nodes = a.nodes.concat(b.nodes.slice().reverse().slice(1)); + } + + graph.parentRelations(b) + .forEach(function (parent) { + var memberA = parent.memberById(idA), + memberB = parent.memberById(idB); + if (!memberA) { + graph = graph.replace(parent.addMember({id: idA, role: memberB.role})); + } + }); + + graph = graph.replace(a.mergeTags(b.tags).update({nodes: nodes})); + graph = iD.actions.DeleteWay(idB)(graph); + + return graph; + }; + + action.enabled = function(graph) { + var a = graph.entity(idA), + b = graph.entity(idB); + return a.first() === b.first() || + a.first() === b.last() || + a.last() === b.first() || + a.last() === b.last(); + }; + + return action; +}; diff --git a/js/id/actions/reverse_way.js b/js/id/actions/reverse.js similarity index 98% rename from js/id/actions/reverse_way.js rename to js/id/actions/reverse.js index 160017637..5c8315467 100644 --- a/js/id/actions/reverse_way.js +++ b/js/id/actions/reverse.js @@ -27,7 +27,7 @@ http://wiki.openstreetmap.org/wiki/Route#Members http://josm.openstreetmap.de/browser/josm/trunk/src/org/openstreetmap/josm/corrector/ReverseWayTagCorrector.java */ -iD.actions.ReverseWay = function(wayId) { +iD.actions.Reverse = function(wayId) { var replacements = [ [/:right$/, ':left'], [/:left$/, ':right'], [/:forward$/, ':backward'], [/:backward$/, ':forward'] diff --git a/js/id/actions/split_way.js b/js/id/actions/split.js similarity index 96% rename from js/id/actions/split_way.js rename to js/id/actions/split.js index 6fbd08595..19a71c619 100644 --- a/js/id/actions/split_way.js +++ b/js/id/actions/split.js @@ -1,5 +1,7 @@ // Split a way at the given node. // +// This is the inverse of `iD.actions.Join`. +// // For testing convenience, accepts an ID to assign to the new way. // Normally, this will be undefined and the way will automatically // be assigned a new ID. @@ -7,7 +9,7 @@ // Reference: // https://github.com/systemed/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/SplitWayAction.as // -iD.actions.SplitWay = function(nodeId, newWayId) { +iD.actions.Split = function(nodeId, newWayId) { function candidateWays(graph) { var node = graph.entity(nodeId), parents = graph.parentWays(node); diff --git a/js/id/behavior/add_way.js b/js/id/behavior/add_way.js index 89ba7f1cd..207c88566 100644 --- a/js/id/behavior/add_way.js +++ b/js/id/behavior/add_way.js @@ -1,8 +1,6 @@ -iD.behavior.AddWay = function(mode) { - var map = mode.map, - controller = mode.controller, - event = d3.dispatch('start', 'startFromWay', 'startFromNode', 'startFromMidpoint'), - draw = iD.behavior.Draw(map); +iD.behavior.AddWay = function(context) { + var event = d3.dispatch('start', 'startFromWay', 'startFromNode', 'startFromMidpoint'), + draw = iD.behavior.Draw(context); var addWay = function(surface) { draw.on('click', event.start) @@ -12,7 +10,8 @@ iD.behavior.AddWay = function(mode) { .on('cancel', addWay.cancel) .on('finish', addWay.cancel); - map.fastEnable(false) + context.map() + .fastEnable(false) .minzoom(16) .dblclickEnable(false); @@ -20,19 +19,20 @@ iD.behavior.AddWay = function(mode) { }; addWay.off = function(surface) { - map.fastEnable(true) + context.map() + .fastEnable(true) .minzoom(0) .tail(false); window.setTimeout(function() { - map.dblclickEnable(true); + context.map().dblclickEnable(true); }, 1000); surface.call(draw.off); }; addWay.cancel = function() { - controller.enter(iD.modes.Browse()); + context.enter(iD.modes.Browse(context)); }; return d3.rebind(addWay, event, 'on'); diff --git a/js/id/behavior/drag_midpoint.js b/js/id/behavior/drag_midpoint.js index 6041946cd..42099963c 100644 --- a/js/id/behavior/drag_midpoint.js +++ b/js/id/behavior/drag_midpoint.js @@ -1,16 +1,13 @@ -iD.behavior.DragMidpoint = function(mode) { - var history = mode.history, - projection = mode.map.projection; - +iD.behavior.DragMidpoint = function(context) { var behavior = iD.behavior.drag() .delegate(".midpoint") .origin(function(d) { - return projection(d.loc); + return context.projection(d.loc); }) .on('start', function(d) { var node = iD.Node(); - history.perform(iD.actions.AddMidpoint(d, node)); + context.perform(iD.actions.AddMidpoint(d, node)); var vertex = d3.selectAll('.vertex') .filter(function(data) { return data.id === node.id; }); @@ -19,11 +16,11 @@ iD.behavior.DragMidpoint = function(mode) { }) .on('move', function(d) { d3.event.sourceEvent.stopPropagation(); - history.replace( - iD.actions.MoveNode(d.id, projection.invert(d3.event.point))); + context.replace( + iD.actions.MoveNode(d.id, context.projection.invert(d3.event.point))); }) .on('end', function() { - history.replace( + context.replace( iD.actions.Noop(), t('operations.add.annotation.vertex')); }); diff --git a/js/id/behavior/drag_node.js b/js/id/behavior/drag_node.js index b9846617d..b62162e94 100644 --- a/js/id/behavior/drag_node.js +++ b/js/id/behavior/drag_node.js @@ -1,8 +1,6 @@ -iD.behavior.DragNode = function(mode) { - var history = mode.history, - size = mode.map.size(), - nudgeInterval, - projection = mode.map.projection; +iD.behavior.DragNode = function(context) { + var size = context.map().size(), + nudgeInterval; function edge(point) { var pad = [30, 100, 30, 100]; @@ -16,7 +14,7 @@ iD.behavior.DragNode = function(mode) { function startNudge(nudge) { if (nudgeInterval) window.clearInterval(nudgeInterval); nudgeInterval = window.setInterval(function() { - mode.map.pan(nudge).redraw(); + context.map().pan(nudge).redraw(); }, 50); } @@ -26,16 +24,16 @@ iD.behavior.DragNode = function(mode) { } function annotation(entity) { - return t('operations.move.annotation.' + entity.geometry(mode.history.graph())); + return t('operations.move.annotation.' + entity.geometry(context.graph())); } return iD.behavior.drag() .delegate(".node") .origin(function(entity) { - return projection(entity.loc); + return context.projection(entity.loc); }) .on('start', function() { - history.perform( + context.perform( iD.actions.Noop()); }) .on('move', function(entity) { @@ -45,13 +43,13 @@ iD.behavior.DragNode = function(mode) { if (nudge) startNudge(nudge); else stopNudge(); - history.replace( - iD.actions.MoveNode(entity.id, projection.invert(d3.event.point)), + context.replace( + iD.actions.MoveNode(entity.id, context.projection.invert(d3.event.point)), annotation(entity)); }) .on('end', function(entity) { stopNudge(); - history.replace( + context.replace( iD.actions.Noop(), annotation(entity)); }); diff --git a/js/id/behavior/draw.js b/js/id/behavior/draw.js index bce4470b6..625fc04a4 100644 --- a/js/id/behavior/draw.js +++ b/js/id/behavior/draw.js @@ -1,7 +1,8 @@ -iD.behavior.Draw = function(map) { +iD.behavior.Draw = function(context) { var event = d3.dispatch('move', 'click', 'clickWay', 'clickNode', 'clickMidpoint', 'undo', 'cancel', 'finish'), keybinding = d3.keybinding('draw'), - down, surface, hover; + hover = iD.behavior.Hover(), + down; function datum() { if (d3.event.altKey) { @@ -28,7 +29,7 @@ iD.behavior.Draw = function(map) { function click() { var d = datum(); if (d.type === 'way') { - var choice = iD.geo.chooseIndex(d, d3.mouse(map.surface.node()), map); + var choice = iD.geo.chooseIndex(d, d3.mouse(context.surface().node()), context); event.clickWay(d, choice.loc, choice.index); } else if (d.type === 'node') { @@ -38,19 +39,19 @@ iD.behavior.Draw = function(map) { event.clickMidpoint(d); } else { - event.click(map.mouseCoordinates()); + event.click(context.map().mouseCoordinates()); } } function keydown() { if (d3.event.keyCode === d3.keybinding.modifierCodes.alt) { - surface.call(hover.off); + context.uninstall(hover); } } function keyup() { if (d3.event.keyCode === d3.keybinding.modifierCodes.alt) { - surface.call(hover); + context.install(hover); } } @@ -70,8 +71,7 @@ iD.behavior.Draw = function(map) { } function draw(selection) { - surface = selection; - hover = iD.behavior.Hover(); + context.install(hover); keybinding .on('⌫', backspace) @@ -83,8 +83,7 @@ iD.behavior.Draw = function(map) { .on('mousedown.draw', mousedown) .on('mouseup.draw', mouseup) .on('mousemove.draw', mousemove) - .on('click.draw', click) - .call(hover); + .on('click.draw', click); d3.select(document) .call(keybinding) @@ -95,12 +94,13 @@ iD.behavior.Draw = function(map) { } draw.off = function(selection) { + context.uninstall(hover); + selection .on('mousedown.draw', null) .on('mouseup.draw', null) .on('mousemove.draw', null) - .on('click.draw', null) - .call(hover.off); + .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 1cc62b755..3089bd6ee 100644 --- a/js/id/behavior/draw_way.js +++ b/js/id/behavior/draw_way.js @@ -1,35 +1,35 @@ -iD.behavior.DrawWay = function(wayId, index, mode, baseGraph) { - var map = mode.map, - history = mode.history, - controller = mode.controller, - way = history.graph().entity(wayId), +iD.behavior.DrawWay = function(context, wayId, index, mode, baseGraph) { + var way = context.entity(wayId), finished = false, annotation = t((way.isDegenerate() ? 'operations.start.annotation.' : - 'operations.continue.annotation.') + way.geometry(history.graph())), - draw = iD.behavior.Draw(map); + 'operations.continue.annotation.') + context.geometry(wayId)), + draw = iD.behavior.Draw(context); - var node = iD.Node({loc: map.mouseCoordinates()}), + var node = iD.Node({loc: context.map().mouseCoordinates()}), nodeId = node.id; - history[way.isDegenerate() ? 'replace' : 'perform']( + context[way.isDegenerate() ? 'replace' : 'perform']( iD.actions.AddEntity(node), iD.actions.AddVertex(wayId, node.id, index)); function move(datum) { - var loc = map.mouseCoordinates(); + var loc = context.map().mouseCoordinates(); - if (datum.type === 'node' || datum.type === 'midpoint') { + if (datum.type === 'node') { loc = datum.loc; - } else if (datum.type === 'way') { - loc = iD.geo.chooseIndex(datum, d3.mouse(map.surface.node()), map).loc; + } else if (datum.type === 'midpoint' || datum.type === 'way') { + var way = datum.type === 'way' ? + datum : + context.entity(datum.ways[0].id); + loc = iD.geo.chooseIndex(way, d3.mouse(context.surface().node()), context).loc; } - history.replace(iD.actions.MoveNode(nodeId, loc)); + context.replace(iD.actions.MoveNode(nodeId, loc)); } function undone() { - controller.enter(iD.modes.Browse()); + context.enter(iD.modes.Browse(context)); } var drawWay = function(surface) { @@ -38,11 +38,12 @@ iD.behavior.DrawWay = function(wayId, index, mode, baseGraph) { .on('clickWay', drawWay.addWay) .on('clickNode', drawWay.addNode) .on('clickMidpoint', drawWay.addMidpoint) - .on('undo', history.undo) + .on('undo', context.undo) .on('cancel', drawWay.cancel) .on('finish', drawWay.finish); - map.fastEnable(false) + context.map() + .fastEnable(false) .minzoom(16) .dblclickEnable(false); @@ -51,26 +52,29 @@ iD.behavior.DrawWay = function(wayId, index, mode, baseGraph) { .filter(function (d) { return d.id === wayId || d.id === nodeId; }) .classed('active', true); - history.on('undone.draw', undone); + context.history() + .on('undone.draw', undone); }; drawWay.off = function(surface) { if (!finished) - history.pop(); + context.pop(); - map.fastEnable(true) + context.map() + .fastEnable(true) .minzoom(0) .tail(false); window.setTimeout(function() { - map.dblclickEnable(true); + context.map().dblclickEnable(true); }, 1000); surface.call(draw.off) .selectAll('.way, .node') .classed('active', false); - history.on('undone.draw', null); + context.history() + .on('undone.draw', null); }; function ReplaceTemporaryNode(newNode) { @@ -85,74 +89,74 @@ iD.behavior.DrawWay = function(wayId, index, mode, baseGraph) { drawWay.add = function(loc) { var newNode = iD.Node({loc: loc}); - history.replace( + context.replace( iD.actions.AddEntity(newNode), ReplaceTemporaryNode(newNode), annotation); finished = true; - controller.enter(mode); + context.enter(mode); }; // Connect the way to an existing way. drawWay.addWay = function(way, loc, wayIndex) { var newNode = iD.Node({loc: loc}); - history.perform( + context.perform( iD.actions.AddEntity(newNode), iD.actions.AddVertex(way.id, newNode.id, wayIndex), ReplaceTemporaryNode(newNode), annotation); finished = true; - controller.enter(mode); + context.enter(mode); }; // Connect the way to an existing node and continue drawing. drawWay.addNode = function(node) { - history.perform( + context.perform( ReplaceTemporaryNode(node), annotation); finished = true; - controller.enter(mode); + context.enter(mode); }; // Add a midpoint, connect the way to it, and continue drawing. drawWay.addMidpoint = function(midpoint) { var node = iD.Node(); - history.perform( + context.perform( iD.actions.AddMidpoint(midpoint, node), ReplaceTemporaryNode(node), annotation); finished = true; - controller.enter(mode); + context.enter(mode); }; // Finish the draw operation, removing the temporary node. If the way has enough // nodes to be valid, it's selected. Otherwise, return to browse mode. drawWay.finish = function() { - history.pop(); + context.pop(); finished = true; - var way = history.graph().entity(wayId); + var way = context.entity(wayId); if (way) { - controller.enter(iD.modes.Select([way.id], true)); + context.enter(iD.modes.Select(context, [way.id], true)); } else { - controller.enter(iD.modes.Browse()); + context.enter(iD.modes.Browse(context)); } }; // Cancel the draw operation and return to browse, deleting everything drawn. drawWay.cancel = function() { - history.perform( + context.perform( d3.functor(baseGraph), t('operations.cancel_draw.annotation')); finished = true; - controller.enter(iD.modes.Browse()); + context.enter(iD.modes.Browse(context)); }; return d3.rebind(drawWay, event, 'on'); diff --git a/js/id/behavior/hash.js b/js/id/behavior/hash.js index 65e748e28..c8762fb13 100644 --- a/js/id/behavior/hash.js +++ b/js/id/behavior/hash.js @@ -1,4 +1,4 @@ -iD.behavior.Hash = function(controller, map) { +iD.behavior.Hash = function(context) { var s0 = null, // cached location.hash lat = 90 - 1e-8; // allowable latitude range @@ -27,13 +27,13 @@ iD.behavior.Hash = function(controller, map) { }; var move = _.throttle(function() { - var s1 = formatter(map); + var s1 = formatter(context.map()); if (s0 !== s1) location.replace(s0 = s1); // don't recenter the map! }, 100); function hashchange() { if (location.hash === s0) return; // ignore spurious hashchange events - if (parser(map, (s0 = location.hash).substring(1))) { + if (parser(context.map(), (s0 = location.hash).substring(1))) { move(); // replace bogus hash } } @@ -42,24 +42,24 @@ iD.behavior.Hash = function(controller, map) { // do so before any features are loaded. thus wait for the feature to // be loaded and then select function willselect(id) { - map.on('drawn.hash', function() { - var entity = map.history().graph().entity(id); - if (entity === undefined) return; - else selectoff(); - controller.enter(iD.modes.Select([entity.id])); - map.on('drawn.hash', null); + context.map().on('drawn.hash', function() { + if (!context.entity(id)) return; + selectoff(); + context.enter(iD.modes.Select(context, [id])); }); - controller.on('enter.hash', function() { - if (controller.mode.id !== 'browse') selectoff(); + + context.on('enter.hash', function() { + if (context.mode().id !== 'browse') selectoff(); }); } function selectoff() { - map.on('drawn.hash', null); + context.map().on('drawn.hash', null); } function hash() { - map.on('move.hash', move); + context.map() + .on('move.hash', move); d3.select(window) .on('hashchange.hash', hashchange); @@ -75,7 +75,8 @@ iD.behavior.Hash = function(controller, map) { } hash.off = function() { - map.on('move.hash', null); + context.map() + .on('move.hash', null); d3.select(window) .on('hashchange.hash', null); diff --git a/js/id/behavior/select.js b/js/id/behavior/select.js index cee2cc940..b5276a4d0 100644 --- a/js/id/behavior/select.js +++ b/js/id/behavior/select.js @@ -1,12 +1,14 @@ -iD.behavior.Select = function(mode) { - var controller = mode.controller; - +iD.behavior.Select = function(context) { function click() { var datum = d3.select(d3.event.target).datum(); if (datum instanceof iD.Entity) { - controller.enter(iD.modes.Select([datum.id])); - } else { - controller.enter(iD.modes.Browse()); + 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)); } } diff --git a/js/id/connection.js b/js/id/connection.js index d2afa6281..794232682 100644 --- a/js/id/connection.js +++ b/js/id/connection.js @@ -1,14 +1,13 @@ -iD.Connection = function() { +iD.Connection = function(context) { var event = d3.dispatch('auth', 'load'), url = 'http://www.openstreetmap.org', connection = {}, user = {}, - version, keys, inflight = {}, loadedTiles = {}, - oauth = iD.OAuth().url(url); + oauth = iD.OAuth(context).url(url); function changesetUrl(changesetId) { return url + '/browse/changeset/' + changesetId; @@ -186,7 +185,7 @@ iD.Connection = function() { content: JXON.stringify(connection.changesetJXON({ imagery_used: imagery_used.join(';'), comment: comment, - created_by: 'iD ' + (version || '') + created_by: 'iD ' + iD.version })) }, function (err, changeset_id) { if (err) return callback(err); @@ -322,12 +321,6 @@ iD.Connection = function() { return oauth.authenticate(done); }; - connection.version = function(_) { - if (!arguments.length) return version; - version = _; - return connection; - }; - connection.bboxFromAPI = bboxFromAPI; connection.changesetUrl = changesetUrl; connection.loadFromURL = loadFromURL; diff --git a/js/id/controller.js b/js/id/controller.js deleted file mode 100644 index 6c4e0fd1e..000000000 --- a/js/id/controller.js +++ /dev/null @@ -1,23 +0,0 @@ -// A controller holds a single action at a time and calls `.enter` and `.exit` -// to bind and unbind actions. -iD.Controller = function(map, history) { - var event = d3.dispatch('enter', 'exit'); - var controller = { mode: null }; - - controller.enter = function(mode) { - mode.controller = controller; - mode.history = history; - mode.map = map; - - if (controller.mode) { - controller.mode.exit(); - event.exit(controller.mode); - } - - mode.enter(); - controller.mode = mode; - event.enter(mode); - }; - - return d3.rebind(controller, event, 'on'); -}; diff --git a/js/id/geo.js b/js/id/geo.js index fcce74b9a..dcc11717f 100644 --- a/js/id/geo.js +++ b/js/id/geo.js @@ -14,11 +14,11 @@ iD.geo.dist = function(a, b) { Math.pow(a[1] - b[1], 2)); }; -iD.geo.chooseIndex = function(way, point, map) { +iD.geo.chooseIndex = function(way, point, context) { var dist = iD.geo.dist, - graph = map.history().graph(), + graph = context.graph(), nodes = graph.childNodes(way), - projNodes = nodes.map(function(n) { return map.projection(n.loc); }); + projNodes = nodes.map(function(n) { return context.projection(n.loc); }); for (var i = 0, changes = []; i < projNodes.length - 1; i++) { changes[i] = diff --git a/js/id/graph/difference.js b/js/id/graph/difference.js new file mode 100644 index 000000000..e2159271a --- /dev/null +++ b/js/id/graph/difference.js @@ -0,0 +1,121 @@ +/* + iD.Difference represents the difference between two graphs. + It knows how to calculate the set of entities that were + created, modified, or deleted, and also contains the logic + for recursively extending a difference to the complete set + of entities that will require a redraw, taking into account + child and parent relationships. + */ +iD.Difference = function (base, head) { + var changes = {}, length = 0; + + _.each(head.entities, function(h, id) { + var b = base.entities[id]; + if (h !== b) { + changes[id] = {base: b, head: h}; + length++; + } + }); + + _.each(base.entities, function(b, id) { + var h = head.entities[id]; + if (!changes[id] && h !== b) { + changes[id] = {base: b, head: h}; + length++; + } + }); + + var difference = {}; + + difference.length = function () { + return length; + }; + + difference.changes = function() { + return changes; + }; + + difference.extantIDs = function() { + var result = []; + _.each(changes, function(change, id) { + if (change.head) result.push(id); + }); + return result; + }; + + difference.modified = function() { + var result = []; + _.each(changes, function(change) { + if (change.base && change.head) result.push(change.head); + }); + return result; + }; + + difference.created = function() { + var result = []; + _.each(changes, function(change) { + if (!change.base && change.head) result.push(change.head); + }); + return result; + }; + + difference.deleted = function() { + var result = []; + _.each(changes, function(change) { + if (change.base && !change.head) result.push(change.base); + }); + return result; + }; + + difference.complete = function(extent) { + var result = {}, id, change; + + function addParents(parents) { + for (var i = 0; i < parents.length; i++) { + var parent = parents[i]; + + if (parent.id in result) + continue; + + result[parent.id] = parent; + addParents(head.parentRelations(parent)); + } + } + + for (id in changes) { + change = changes[id]; + + var h = change.head, + b = change.base, + entity = h || b; + + if (extent && !entity.intersects(extent, h ? head : base)) + continue; + + result[id] = h; + + if (entity.type === 'way') { + var nh = h ? h.nodes : [], + nb = b ? b.nodes : [], + diff; + + diff = _.difference(nh, nb); + for (var i = 0; i < diff.length; i++) { + result[diff[i]] = head.entity(diff[i]); + } + + diff = _.difference(nb, nh); + for (var i = 0; i < diff.length; i++) { + result[diff[i]] = head.entity(diff[i]); + } + } + + addParents(head.parentWays(entity)); + addParents(head.parentRelations(entity)); + } + + return result; + }; + + return difference; +}; diff --git a/js/id/graph/entity.js b/js/id/graph/entity.js index f91bb89d9..bce9675ee 100644 --- a/js/id/graph/entity.js +++ b/js/id/graph/entity.js @@ -43,7 +43,6 @@ iD.Entity.prototype = { if (!this.id && this.type) { this.id = iD.Entity.id(this.type); - this._updated = true; } if (iD.debug) { @@ -63,15 +62,21 @@ iD.Entity.prototype = { }, update: function(attrs) { - return iD.Entity(this, attrs, {_updated: true}); + return iD.Entity(this, attrs); }, - created: function() { - return this._updated && this.osmId().charAt(0) === '-'; - }, - - modified: function() { - return this._updated && this.osmId().charAt(0) !== '-'; + mergeTags: function(tags) { + var merged = _.clone(this.tags); + for (var k in tags) { + var t1 = merged[k], + t2 = tags[k]; + if (t1 && t1 !== t2) { + merged[k] = t1 + "; " + t2; + } else { + merged[k] = t2; + } + } + return this.update({tags: merged}); }, intersects: function(extent, resolver) { diff --git a/js/id/graph/graph.js b/js/id/graph/graph.js index a19bfe094..2692aeba6 100644 --- a/js/id/graph/graph.js +++ b/js/id/graph/graph.js @@ -232,65 +232,5 @@ iD.Graph.prototype = { } } return items; - }, - - difference: function (graph) { - - function diff(a, b) { - var result = [], - keys = Object.keys(a.entities), - entity, oldentity, id, i; - - for (i = 0; i < keys.length; i++) { - id = keys[i]; - entity = a.entities[id]; - oldentity = b.entities[id]; - if (entity !== oldentity) { - - // maybe adding affected children better belongs in renderer/map.js? - if (entity && entity.type === 'way' && - oldentity && oldentity.type === 'way') { - result = result - .concat(_.difference(entity.nodes, oldentity.nodes)) - .concat(_.difference(oldentity.nodes, entity.nodes)); - - } else if (entity && entity.type === 'way') { - result = result.concat(entity.nodes); - - } else if (oldentity && oldentity.type === 'way') { - result = result.concat(oldentity.nodes); - } - - result.push(id); - } - } - return result; - } - - return _.unique(diff(this, graph).concat(diff(graph, this)).sort()); - }, - - modified: function() { - var result = [], base = this.base().entities; - _.each(this.entities, function(entity, id) { - if (entity && base[id]) result.push(id); - }); - return result; - }, - - created: function() { - var result = [], base = this.base().entities; - _.each(this.entities, function(entity, id) { - if (entity && !base[id]) result.push(id); - }); - return result; - }, - - deleted: function() { - var result = [], base = this.base().entities; - _.each(this.entities, function(entity, id) { - if (!entity && base[id]) result.push(id); - }); - return result; } }; diff --git a/js/id/graph/history.js b/js/id/graph/history.js index 42e1b1604..3cc1ace01 100644 --- a/js/id/graph/history.js +++ b/js/id/graph/history.js @@ -21,7 +21,9 @@ iD.History = function() { } function change(previous) { - dispatch.change(history.graph().difference(previous)); + var difference = iD.Difference(previous, history.graph()); + dispatch.change(difference); + return difference; } var history = { @@ -33,6 +35,8 @@ iD.History = function() { for (var i = 0; i < stack.length; i++) { stack[i].graph.rebase(entities); } + + dispatch.change(); }, perform: function () { @@ -42,7 +46,7 @@ iD.History = function() { stack.push(perform(arguments)); index++; - change(previous); + return change(previous); }, replace: function () { @@ -51,7 +55,7 @@ iD.History = function() { // assert(index == stack.length - 1) stack[index] = perform(arguments); - change(previous); + return change(previous); }, pop: function () { @@ -60,7 +64,7 @@ iD.History = function() { if (index > 0) { index--; stack.pop(); - change(previous); + return change(previous); } }, @@ -80,7 +84,7 @@ iD.History = function() { } dispatch.undone(); - change(previous); + return change(previous); }, redo: function () { @@ -92,7 +96,7 @@ iD.History = function() { } dispatch.redone(); - change(previous); + return change(previous); }, undoAnnotation: function () { @@ -111,31 +115,27 @@ iD.History = function() { } }, - changes: function () { - var initial = stack[0].graph, - current = stack[index].graph; + difference: function () { + var base = stack[0].graph, + head = stack[index].graph; + return iD.Difference(base, head); + }, + changes: function () { + var difference = history.difference(); return { - modified: current.modified().map(function (id) { - return current.entity(id); - }), - created: current.created().map(function (id) { - return current.entity(id); - }), - deleted: current.deleted().map(function (id) { - return initial.entity(id); - }) - }; + modified: difference.modified(), + created: difference.created(), + deleted: difference.deleted() + } }, hasChanges: function() { - return !!this.numChanges(); + return this.difference().length() > 0; }, numChanges: function() { - return d3.sum(d3.values(this.changes()).map(function(c) { - return c.length; - })); + return this.difference().length(); }, imagery_used: function(source) { diff --git a/js/id/id.js b/js/id/id.js index 3e8aabcc3..56d3e481a 100644 --- a/js/id/id.js +++ b/js/id/id.js @@ -1,280 +1,103 @@ -window.iD = function(container) { - // the reported, displayed version of iD. - var version = '0.0.0-alpha1'; - - var connection = iD.Connection() - .version(version), +window.iD = function () { + var context = {}, history = iD.History(), - map = iD.Map() - .connection(connection) - .history(history), - controller = iD.Controller(map, history); + storage = localStorage || {}, + dispatch = d3.dispatch('enter', 'exit'), + mode, + container, + ui = iD.ui(context), + map = iD.Map(context); - map.background.source(iD.BackgroundSource.Bing); - - function editor(container) { - if (!iD.supported()) { - container.html('This editor is supported in Firefox, Chrome, Safari, Opera, ' + - 'and Internet Explorer 9 and above. Please upgrade your browser ' + - 'or use Potlatch 2 to edit the map.') - .style('text-align:center;font-style:italic;'); - return; - } - - function hintprefix(x, y) { - return '' + y + '' + '
' + x + '
'; - } - - var m = container.append('div') - .attr('id', 'map') - .call(map); - - var bar = container.append('div') - .attr('id', 'bar') - .attr('class','pad1 fillD'); - - var limiter = bar.append('div') - .attr('class', 'limiter'); - - var buttons_joined = limiter.append('div') - .attr('class', 'button-wrap joined col4'); - - var buttons = buttons_joined.selectAll('button.add-button') - .data([iD.modes.Browse(), iD.modes.AddPoint(), iD.modes.AddLine(), iD.modes.AddArea()]) - .enter().append('button') - .attr('tabindex', -1) - .attr('class', function (mode) { return mode.title + ' add-button col3'; }) - .call(bootstrap.tooltip().placement('bottom').html(true)) - .attr('data-original-title', function (mode) { - return hintprefix(mode.key, mode.description); - }) - .on('click.editor', function (mode) { controller.enter(mode); }); - - function disableTooHigh() { - if (map.editable()) { - notice.message(false); - buttons.attr('disabled', null); - } else { - buttons.attr('disabled', 'disabled'); - notice.message(true); - controller.enter(iD.modes.Browse()); - } - } - - var notice = iD.ui.notice(limiter) - .message(false) - .on('zoom', function() { map.zoom(16); }); - - map.on('move.editor', _.debounce(function() { - disableTooHigh(); - contributors.call(iD.ui.contributors(map)); - }, 500)); - - buttons.append('span') - .attr('class', function(d) { - return d.id + ' icon icon-pre-text'; - }); - - buttons.append('span').attr('class', 'label').text(function (mode) { return mode.title; }); - - controller.on('enter.editor', function (entered) { - buttons.classed('active', function (mode) { return entered.button === mode.button; }); - container.classed("mode-" + entered.id, true); - }); - - controller.on('exit.editor', function (exited) { - container.classed("mode-" + exited.id, false); - }); - - var undo_buttons = limiter.append('div') - .attr('class', 'button-wrap joined col1'), - undo_tooltip = bootstrap.tooltip().placement('bottom').html(true); - - undo_buttons.append('button') - .attr({ id: 'undo', 'class': 'col6' }) - .property('disabled', true) - .html("") - .on('click.editor', history.undo) - .call(undo_tooltip); - - undo_buttons.append('button') - .attr({ id: 'redo', 'class': 'col6' }) - .property('disabled', true) - .html("") - .on('click.editor', history.redo) - .call(undo_tooltip); - - var save_button = limiter.append('div').attr('class','button-wrap col1').append('button') - .attr('class', 'save col12') - .call(iD.ui.save().map(map).controller(controller)); - - var zoom = container.append('div') - .attr('class', 'zoombuttons map-control') - .selectAll('button') - .data([['zoom-in', '+', map.zoomIn, 'Zoom In'], ['zoom-out', '-', map.zoomOut, 'Zoom Out']]) - .enter() - .append('button') - .attr('tabindex', -1) - .attr('class', function(d) { return d[0]; }) - .attr('title', function(d) { return d[3]; }) - .on('click.editor', function(d) { return d[2](); }) - .append('span') - .attr('class', function(d) { - return d[0] + ' icon'; - }); - - if (navigator.geolocation) { - container.append('div') - .call(iD.ui.geolocate(map)); - } - - var gc = container.append('div').attr('class', 'geocode-control map-control') - .call(iD.ui.geocoder().map(map)); - - container.append('div').attr('class', 'map-control layerswitcher-control') - .call(iD.ui.layerswitcher(map)); - - container.append('div') - .style('display', 'none') - .attr('class', 'inspector-wrap fr col5'); - - var about = container.append('div') - .attr('class','col12 about-block fillD pad1'); - - about.append('div') - .attr('class', 'user-container') - .append('div') - .attr('class', 'hello'); - - var aboutList = about.append('ul') - .attr('id','about') - .attr('class','link-list'); - - var linkList = aboutList.append('ul') - .attr('id','about') - .attr('class','pad1 fillD about-block link-list'); - linkList.append('li').append('a').attr('target', '_blank') - .attr('href', 'http://github.com/systemed/iD').text(version); - linkList.append('li').append('a').attr('target', '_blank') - .attr('href', 'http://github.com/systemed/iD/issues').text('report a bug'); - - var imagery = linkList.append('li').attr('id', 'attribution'); - imagery.append('span').text('imagery'); - imagery.append('a').attr('target', '_blank') - .attr('href', 'http://opengeodata.org/microsoft-imagery-details').text(' provided by bing'); - - linkList.append('li').attr('class', 'source-switch').append('a').attr('href', '#') - .text('dev') - .on('click.editor', function() { - d3.event.preventDefault(); - if (d3.select(this).classed('live')) { - map.flush().connection() - .url('http://api06.dev.openstreetmap.org'); - d3.select(this).text('dev').classed('live', false); - } else { - map.flush().connection() - .url('http://www.openstreetmap.org'); - d3.select(this).text('live').classed('live', true); - } - }); - - var contributors = linkList.append('li') - .attr('id', 'user-list'); - contributors.append('span') - .attr('class', 'icon nearby icon-pre-text'); - contributors.append('span') - .text('Viewing contributions by '); - contributors.append('span') - .attr('class', 'contributor-list'); - contributors.append('span') - .attr('class', 'contributor-count'); - - history.on('change.editor', function() { - window.onbeforeunload = history.hasChanges() ? function() { - return 'You have unsaved changes.'; - } : null; - - var undo = history.undoAnnotation(), - redo = history.redoAnnotation(); - - function refreshTooltip(selection) { - if (selection.property('disabled')) { - selection.call(undo_tooltip.hide); - } else if (selection.property('tooltipVisible')) { - selection.call(undo_tooltip.show); - } - } - - limiter.select('#undo') - .property('disabled', !undo) - .attr('data-original-title', hintprefix('⌘ + Z', undo)) - .call(refreshTooltip); - - limiter.select('#redo') - .property('disabled', !redo) - .attr('data-original-title', hintprefix('⌘ + ⇧ + Z', redo)) - .call(refreshTooltip); - }); - - d3.select(window).on('resize.editor', function() { - map.size(m.size()); - }); - - var keybinding = d3.keybinding('main') - .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); - - var hash = iD.behavior.Hash(controller, map); - - hash(); - - if (!hash.hadHash) { - map.centerZoom([-77.02271, 38.90085], 20); - } - - d3.select('.user-container').call(iD.ui.userpanel(connection) - .on('logout.editor', connection.logout) - .on('login.editor', connection.authenticate)); - - controller.enter(iD.modes.Browse()); - - if (!localStorage.sawSplash) { - iD.ui.splash(); - localStorage.sawSplash = true; - } - } - - editor.connection = function(_) { - if (!arguments.length) return connection; - connection = _; - return editor; + context.storage = function(k, v) { + if (arguments.length === 1) return storage[k]; + else storage[k] = v; }; - editor.map = function() { - return map; + // the connection requires .storage() to be available on calling. + var connection = iD.Connection(context); + + connection.on('load.context', function (err, result) { + history.merge(result); + }); + + /* Straight accessors. Avoid using these if you can. */ + context.ui = function() { return ui; }; + context.connection = function() { return connection; }; + context.history = function() { return history; }; + context.map = function() { return map; }; + + /* History */ + context.graph = history.graph; + context.perform = history.perform; + context.replace = history.replace; + context.pop = history.pop; + context.undo = history.undo; + context.redo = history.undo; + context.changes = history.changes; + + /* Graph */ + context.entity = function(id) { + return history.graph().entity(id); }; - editor.controller = function() { - return controller; + context.geometry = function(id) { + return context.entity(id).geometry(history.graph()); }; - if (arguments.length) { - d3.select(container).call(editor); - } + /* Modes */ + context.enter = function(newMode) { + if (mode) { + mode.exit(); + dispatch.exit(mode); + } - return editor; + mode = newMode; + mode.enter(); + dispatch.enter(mode); + }; + + context.mode = function() { + return mode; + }; + + context.selection = function() { + if (mode.id === 'select') { + return mode.selection(); + } else { + return []; + } + }; + + /* Behaviors */ + context.install = function(behavior) { + context.surface().call(behavior); + }; + + context.uninstall = function(behavior) { + context.surface().call(behavior.off); + }; + + /* Map */ + context.background = function() { return map.background; }; + context.surface = function() { return map.surface; }; + context.projection = map.projection; + context.tail = map.tail; + context.redraw = map.redraw; + + context.container = function(_) { + if (!arguments.length) return container; + container = _; + return context; + }; + + context.background() + .source(iD.BackgroundSource.Bing); + + return d3.rebind(context, dispatch, 'on'); }; +iD.version = '0.0.0-alpha1'; + iD.supported = function() { if (navigator.appName !== 'Microsoft Internet Explorer') { return true; diff --git a/js/id/modes/add_area.js b/js/id/modes/add_area.js index bda4e0daa..0a9496bb6 100644 --- a/js/id/modes/add_area.js +++ b/js/id/modes/add_area.js @@ -1,4 +1,4 @@ -iD.modes.AddArea = function() { +iD.modes.AddArea = function(context) { var mode = { id: 'add-area', button: 'area', @@ -7,81 +7,75 @@ iD.modes.AddArea = function() { key: t('modes.add_area.key') }; - var behavior, - defaultTags = {area: 'yes'}; - - mode.enter = function() { - var map = mode.map, - history = mode.history, - controller = mode.controller; - - function start(loc) { - var graph = history.graph(), - node = iD.Node({loc: loc}), - way = iD.Way({tags: defaultTags}); - - history.perform( - iD.actions.AddEntity(node), - iD.actions.AddEntity(way), - iD.actions.AddVertex(way.id, node.id), - iD.actions.AddVertex(way.id, node.id)); - - controller.enter(iD.modes.DrawArea(way.id, graph)); - } - - function startFromWay(other, loc, index) { - var graph = history.graph(), - node = iD.Node({loc: loc}), - way = iD.Way({tags: defaultTags}); - - history.perform( - iD.actions.AddEntity(node), - iD.actions.AddEntity(way), - iD.actions.AddVertex(way.id, node.id), - iD.actions.AddVertex(way.id, node.id), - iD.actions.AddVertex(other.id, node.id, index)); - - controller.enter(iD.modes.DrawArea(way.id, graph)); - } - - function startFromNode(node) { - var graph = history.graph(), - way = iD.Way({tags: defaultTags}); - - history.perform( - iD.actions.AddEntity(way), - iD.actions.AddVertex(way.id, node.id), - iD.actions.AddVertex(way.id, node.id)); - - controller.enter(iD.modes.DrawArea(way.id, graph)); - } - - function startFromMidpoint(midpoint) { - var graph = history.graph(), - node = iD.Node(), - way = iD.Way({tags: defaultTags}); - - history.perform( - iD.actions.AddMidpoint(midpoint, node), - iD.actions.AddEntity(way), - iD.actions.AddVertex(way.id, node.id), - iD.actions.AddVertex(way.id, node.id)); - - controller.enter(iD.modes.DrawArea(way.id, graph)); - } - - behavior = iD.behavior.AddWay(mode) + var behavior = iD.behavior.AddWay(context) .on('start', start) .on('startFromWay', startFromWay) .on('startFromNode', startFromNode) - .on('startFromMidpoint', startFromMidpoint); + .on('startFromMidpoint', startFromMidpoint), + defaultTags = {area: 'yes'}; - mode.map.surface.call(behavior); - mode.map.tail(t('modes.add_area.tail')); + function start(loc) { + var graph = context.graph(), + node = iD.Node({loc: loc}), + way = iD.Way({tags: defaultTags}); + + context.perform( + iD.actions.AddEntity(node), + iD.actions.AddEntity(way), + iD.actions.AddVertex(way.id, node.id), + iD.actions.AddVertex(way.id, node.id)); + + context.enter(iD.modes.DrawArea(context, way.id, graph)); + } + + function startFromWay(other, loc, index) { + var graph = context.graph(), + node = iD.Node({loc: loc}), + way = iD.Way({tags: defaultTags}); + + context.perform( + iD.actions.AddEntity(node), + iD.actions.AddEntity(way), + iD.actions.AddVertex(way.id, node.id), + iD.actions.AddVertex(way.id, node.id), + iD.actions.AddVertex(other.id, node.id, index)); + + context.enter(iD.modes.DrawArea(context, way.id, graph)); + } + + function startFromNode(node) { + var graph = context.graph(), + way = iD.Way({tags: defaultTags}); + + context.perform( + iD.actions.AddEntity(way), + iD.actions.AddVertex(way.id, node.id), + iD.actions.AddVertex(way.id, node.id)); + + context.enter(iD.modes.DrawArea(context, way.id, graph)); + } + + function startFromMidpoint(midpoint) { + var graph = context.graph(), + node = iD.Node(), + way = iD.Way({tags: defaultTags}); + + context.perform( + iD.actions.AddMidpoint(midpoint, node), + iD.actions.AddEntity(way), + iD.actions.AddVertex(way.id, node.id), + iD.actions.AddVertex(way.id, node.id)); + + context.enter(iD.modes.DrawArea(context, way.id, graph)); + } + + mode.enter = function() { + context.install(behavior); + context.tail(t('modes.add_area.tail')); }; mode.exit = function() { - mode.map.surface.call(behavior.off); + context.uninstall(behavior); }; return mode; diff --git a/js/id/modes/add_line.js b/js/id/modes/add_line.js index 92e9a64d7..7845e6e6f 100644 --- a/js/id/modes/add_line.js +++ b/js/id/modes/add_line.js @@ -1,4 +1,4 @@ -iD.modes.AddLine = function() { +iD.modes.AddLine = function(context) { var mode = { id: 'add-line', button: 'line', @@ -7,88 +7,82 @@ iD.modes.AddLine = function() { key: t('modes.add_line.key') }; - var behavior, - defaultTags = {highway: 'residential'}; - - mode.enter = function() { - var map = mode.map, - history = mode.history, - controller = mode.controller; - - function start(loc) { - var graph = history.graph(), - node = iD.Node({loc: loc}), - way = iD.Way({tags: defaultTags}); - - history.perform( - iD.actions.AddEntity(node), - iD.actions.AddEntity(way), - iD.actions.AddVertex(way.id, node.id)); - - controller.enter(iD.modes.DrawLine(way.id, 'forward', graph)); - } - - function startFromWay(other, loc, index) { - var graph = history.graph(), - node = iD.Node({loc: loc}), - way = iD.Way({tags: defaultTags}); - - history.perform( - iD.actions.AddEntity(node), - iD.actions.AddEntity(way), - iD.actions.AddVertex(way.id, node.id), - iD.actions.AddVertex(other.id, node.id, index)); - - controller.enter(iD.modes.DrawLine(way.id, 'forward', graph)); - } - - function startFromNode(node) { - var graph = history.graph(), - parent = graph.parentWays(node)[0], - isLine = parent && parent.geometry(graph) === 'line'; - - if (isLine && parent.first() === node.id) { - controller.enter(iD.modes.DrawLine(parent.id, 'backward', graph)); - - } else if (isLine && parent.last() === node.id) { - controller.enter(iD.modes.DrawLine(parent.id, 'forward', graph)); - - } else { - var way = iD.Way({tags: defaultTags}); - - history.perform( - iD.actions.AddEntity(way), - iD.actions.AddVertex(way.id, node.id)); - - controller.enter(iD.modes.DrawLine(way.id, 'forward', graph)); - } - } - - function startFromMidpoint(midpoint) { - var graph = history.graph(), - node = iD.Node(), - way = iD.Way({tags: defaultTags}); - - history.perform( - iD.actions.AddMidpoint(midpoint, node), - iD.actions.AddEntity(way), - iD.actions.AddVertex(way.id, node.id)); - - controller.enter(iD.modes.DrawLine(way.id, 'forward', graph)); - } - - behavior = iD.behavior.AddWay(mode) + var behavior = iD.behavior.AddWay(context) .on('start', start) .on('startFromWay', startFromWay) .on('startFromNode', startFromNode) - .on('startFromMidpoint', startFromMidpoint); + .on('startFromMidpoint', startFromMidpoint), + defaultTags = {highway: 'residential'}; - mode.map.surface.call(behavior); - mode.map.tail(t('modes.add_line.tail')); + function start(loc) { + var graph = context.graph(), + node = iD.Node({loc: loc}), + way = iD.Way({tags: defaultTags}); + + context.perform( + iD.actions.AddEntity(node), + iD.actions.AddEntity(way), + iD.actions.AddVertex(way.id, node.id)); + + context.enter(iD.modes.DrawLine(context, way.id, 'forward', graph)); + } + + function startFromWay(other, loc, index) { + var graph = context.graph(), + node = iD.Node({loc: loc}), + way = iD.Way({tags: defaultTags}); + + context.perform( + iD.actions.AddEntity(node), + iD.actions.AddEntity(way), + iD.actions.AddVertex(way.id, node.id), + iD.actions.AddVertex(other.id, node.id, index)); + + context.enter(iD.modes.DrawLine(context, way.id, 'forward', graph)); + } + + function startFromNode(node) { + var graph = context.graph(), + parent = graph.parentWays(node)[0], + isLine = parent && parent.geometry(graph) === 'line'; + + if (isLine && parent.first() === node.id) { + context.enter(iD.modes.DrawLine(context, parent.id, 'backward', graph)); + + } else if (isLine && parent.last() === node.id) { + context.enter(iD.modes.DrawLine(context, parent.id, 'forward', graph)); + + } else { + var way = iD.Way({tags: defaultTags}); + + context.perform( + iD.actions.AddEntity(way), + iD.actions.AddVertex(way.id, node.id)); + + context.enter(iD.modes.DrawLine(context, way.id, 'forward', graph)); + } + } + + function startFromMidpoint(midpoint) { + var graph = context.graph(), + node = iD.Node(), + way = iD.Way({tags: defaultTags}); + + context.perform( + iD.actions.AddMidpoint(midpoint, node), + iD.actions.AddEntity(way), + iD.actions.AddVertex(way.id, node.id)); + + context.enter(iD.modes.DrawLine(context, way.id, 'forward', graph)); + } + + mode.enter = function() { + context.install(behavior); + context.tail(t('modes.add_line.tail')); }; mode.exit = function() { - mode.map.surface.call(behavior.off); + context.uninstall(behavior); }; return mode; diff --git a/js/id/modes/add_point.js b/js/id/modes/add_point.js index 6dd524bb5..5a24dad25 100644 --- a/js/id/modes/add_point.js +++ b/js/id/modes/add_point.js @@ -1,4 +1,4 @@ -iD.modes.AddPoint = function() { +iD.modes.AddPoint = function(context) { var mode = { id: 'add-point', title: t('modes.add_point.title'), @@ -6,53 +6,44 @@ iD.modes.AddPoint = function() { key: t('modes.add_point.key') }; - var behavior; + var behavior = iD.behavior.Draw(context) + .on('click', add) + .on('clickWay', addWay) + .on('clickNode', addNode) + .on('clickMidpoint', addNode) + .on('cancel', cancel) + .on('finish', cancel); + + function add(loc) { + var node = iD.Node({loc: loc}); + + context.perform( + iD.actions.AddEntity(node), + t('operations.add.annotation.point')); + + context.enter(iD.modes.Select(context, [node.id], true)); + } + + function addWay(way, loc, index) { + add(loc); + } + + function addNode(node) { + add(node.loc); + } + + function cancel() { + context.enter(iD.modes.Browse(context)); + } mode.enter = function() { - var map = mode.map, - history = mode.history, - controller = mode.controller; - - function add(loc) { - var node = iD.Node({loc: loc}); - - history.perform( - iD.actions.AddEntity(node), - t('operations.add.annotation.point')); - - controller.enter(iD.modes.Select([node.id], true)); - } - - function addWay(way, loc, index) { - add(loc); - } - - function addNode(node) { - add(node.loc); - } - - function cancel() { - controller.enter(iD.modes.Browse()); - } - - behavior = iD.behavior.Draw(map) - .on('click', add) - .on('clickWay', addWay) - .on('clickNode', addNode) - .on('clickMidpoint', addNode) - .on('cancel', cancel) - .on('finish', cancel); - - mode.map.surface.call(behavior); - mode.map.tail(t('modes.add_point.tail')); + context.install(behavior); + context.tail(t('modes.add_point.tail')); }; mode.exit = function() { - var map = mode.map, - surface = map.surface; - - map.tail(false); - behavior.off(surface); + context.uninstall(behavior); + context.tail(false); }; return mode; diff --git a/js/id/modes/browse.js b/js/id/modes/browse.js index 7652576ea..33add1262 100644 --- a/js/id/modes/browse.js +++ b/js/id/modes/browse.js @@ -1,4 +1,4 @@ -iD.modes.Browse = function() { +iD.modes.Browse = function(context) { var mode = { button: 'browse', id: 'browse', @@ -7,27 +7,21 @@ iD.modes.Browse = function() { key: t('modes.browse.key') }; - var behaviors; + var behaviors = [ + iD.behavior.Hover(), + iD.behavior.Select(context), + iD.behavior.DragNode(context), + iD.behavior.DragMidpoint(context)]; mode.enter = function() { - var surface = mode.map.surface; - - behaviors = [ - iD.behavior.Hover(), - iD.behavior.Select(mode), - iD.behavior.DragNode(mode), - iD.behavior.DragMidpoint(mode)]; - behaviors.forEach(function(behavior) { - behavior(surface); + context.install(behavior); }); }; mode.exit = function() { - var surface = mode.map.surface; - behaviors.forEach(function(behavior) { - behavior.off(surface); + context.uninstall(behavior); }); }; diff --git a/js/id/modes/draw_area.js b/js/id/modes/draw_area.js index 9801a092c..aee2f153c 100644 --- a/js/id/modes/draw_area.js +++ b/js/id/modes/draw_area.js @@ -1,4 +1,4 @@ -iD.modes.DrawArea = function(wayId, baseGraph) { +iD.modes.DrawArea = function(context, wayId, baseGraph) { var mode = { button: 'area', id: 'draw-area' @@ -7,11 +7,11 @@ iD.modes.DrawArea = function(wayId, baseGraph) { var behavior; mode.enter = function() { - var way = mode.history.graph().entity(wayId), + var way = context.entity(wayId), headId = way.nodes[way.nodes.length - 2], tailId = way.first(); - behavior = iD.behavior.DrawWay(wayId, -1, mode, baseGraph); + behavior = iD.behavior.DrawWay(context, wayId, -1, mode, baseGraph); var addNode = behavior.addNode; @@ -23,12 +23,12 @@ iD.modes.DrawArea = function(wayId, baseGraph) { } }; - mode.map.surface.call(behavior); - mode.map.tail(t('modes.draw_area.tail')); + context.install(behavior); + context.tail(t('modes.draw_area.tail')); }; mode.exit = function() { - mode.map.surface.call(behavior.off); + context.uninstall(behavior); }; return mode; diff --git a/js/id/modes/draw_line.js b/js/id/modes/draw_line.js index 5ae8c2703..9fd07c937 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, baseGraph) { +iD.modes.DrawLine = function(context, wayId, direction, baseGraph) { var mode = { button: 'line', id: 'draw-line' @@ -7,11 +7,11 @@ iD.modes.DrawLine = function(wayId, direction, baseGraph) { var behavior; mode.enter = function() { - var way = mode.history.graph().entity(wayId), + var way = context.entity(wayId), index = (direction === 'forward') ? undefined : 0, headId = (direction === 'forward') ? way.last() : way.first(); - behavior = iD.behavior.DrawWay(wayId, index, mode, baseGraph); + behavior = iD.behavior.DrawWay(context, wayId, index, mode, baseGraph); var addNode = behavior.addNode; @@ -23,12 +23,12 @@ iD.modes.DrawLine = function(wayId, direction, baseGraph) { } }; - mode.map.surface.call(behavior); - mode.map.tail(t('modes.draw_line.tail')); + context.install(behavior); + context.tail(t('modes.draw_line.tail')); }; mode.exit = function() { - mode.map.surface.call(behavior.off); + context.uninstall(behavior); }; return mode; diff --git a/js/id/modes/move_way.js b/js/id/modes/move_way.js index 5c262e259..986eef5bd 100644 --- a/js/id/modes/move_way.js +++ b/js/id/modes/move_way.js @@ -1,4 +1,4 @@ -iD.modes.MoveWay = function(wayId) { +iD.modes.MoveWay = function(context, wayId) { var mode = { id: 'move-way' }; @@ -6,55 +6,53 @@ iD.modes.MoveWay = function(wayId) { var keybinding = d3.keybinding('move-way'); mode.enter = function() { - var map = mode.map, - history = mode.history, - graph = history.graph(), - selection = map.surface, - controller = mode.controller, - projection = map.projection, - way = graph.entity(wayId), - origin = d3.mouse(selection.node()), - annotation = t('operations.move.annotation.' + way.geometry(graph)); + var origin = point(), + annotation = t('operations.move.annotation.' + context.geometry(wayId)); // If intiated via keyboard if (!origin[0] && !origin[1]) origin = null; - history.perform( + context.perform( iD.actions.Noop(), annotation); + function point() { + return d3.mouse(context.surface().node()); + } + function move() { - var p = d3.mouse(selection.node()), + var p = point(), delta = origin ? [p[0] - origin[0], p[1] - origin[1]] : [0, 0]; origin = p; - history.replace( - iD.actions.MoveWay(wayId, delta, projection), + context.replace( + iD.actions.MoveWay(wayId, delta, context.projection), annotation); } function finish() { d3.event.stopPropagation(); - controller.enter(iD.modes.Select([way.id], true)); + context.enter(iD.modes.Select(context, [wayId], true)); } function cancel() { - history.pop(); - controller.enter(iD.modes.Select([way.id], true)); + context.pop(); + context.enter(iD.modes.Select(context, [wayId], true)); } function undone() { - controller.enter(iD.modes.Browse()); + context.enter(iD.modes.Browse(context)); } - selection + context.surface() .on('mousemove.move-way', move) .on('click.move-way', finish); - history.on('undone.move-way', undone); + context.history() + .on('undone.move-way', undone); keybinding .on('⎋', cancel) @@ -65,15 +63,12 @@ iD.modes.MoveWay = function(wayId) { }; mode.exit = function() { - var map = mode.map, - history = mode.history, - selection = map.surface; - - selection + context.surface() .on('mousemove.move-way', null) .on('click.move-way', null); - history.on('undone.move-way', null); + context.history() + .on('undone.move-way', null); keybinding.off(); }; diff --git a/js/id/modes/select.js b/js/id/modes/select.js index 8d29450d6..02db6a57f 100644 --- a/js/id/modes/select.js +++ b/js/id/modes/select.js @@ -1,4 +1,4 @@ -iD.modes.Select = function(selection, initial) { +iD.modes.Select = function(context, selection, initial) { var mode = { id: 'select', button: 'browse' @@ -6,12 +6,16 @@ iD.modes.Select = function(selection, initial) { var inspector = iD.ui.inspector().initial(!!initial), keybinding = d3.keybinding('select'), - behaviors, + behaviors = [ + iD.behavior.Hover(), + iD.behavior.Select(context), + iD.behavior.DragNode(context), + iD.behavior.DragMidpoint(context)], radialMenu; function changeTags(d, tags) { if (!_.isEqual(singular().tags, tags)) { - mode.history.perform( + context.perform( iD.actions.ChangeTags(d.id, tags), t('operations.change_tags.annotation')); } @@ -19,7 +23,7 @@ iD.modes.Select = function(selection, initial) { function singular() { if (selection.length === 1) { - return mode.map.history().graph().entity(selection[0]); + return context.entity(selection[0]); } } @@ -28,24 +32,14 @@ iD.modes.Select = function(selection, initial) { }; mode.enter = function() { - var map = mode.map, - history = map.history(), - graph = history.graph(), - surface = map.surface, - entity = singular(); - - behaviors = [ - iD.behavior.Hover(), - iD.behavior.Select(mode), - iD.behavior.DragNode(mode), - iD.behavior.DragMidpoint(mode)]; + var entity = singular(); behaviors.forEach(function(behavior) { - behavior(surface); + context.install(behavior); }); var operations = d3.values(iD.operations) - .map(function (o) { return o(selection, mode); }) + .map(function (o) { return o(selection, context); }) .filter(function (o) { return o.available(); }); operations.forEach(function(operation) { @@ -62,9 +56,10 @@ iD.modes.Select = function(selection, initial) { }), true)); if (entity) { - inspector.graph(graph); + inspector.graph(context.graph()); - d3.select('.inspector-wrap') + context.container() + .select('.inspector-wrap') .style('display', 'block') .style('opacity', 1) .datum(entity) @@ -73,38 +68,39 @@ iD.modes.Select = function(selection, initial) { if (d3.event) { // Pan the map if the clicked feature intersects with the position // of the inspector - var inspector_size = d3.select('.inspector-wrap').size(), - map_size = mode.map.size(), + var inspector_size = context.container().select('.inspector-wrap').size(), + map_size = context.map().size(), offset = 50, shift_left = d3.event.x - map_size[0] + inspector_size[0] + offset, center = (map_size[0] / 2) + shift_left + offset; if (shift_left > 0 && inspector_size[1] > d3.event.y) { - mode.map.centerEase(mode.map.projection.invert([center, map_size[1]/2])); + context.map().centerEase(context.projection.invert([center, map_size[1]/2])); } } inspector .on('changeTags', changeTags) - .on('close', function() { mode.controller.enter(iD.modes.Browse()); }); - - history.on('change.select', function() { - // Exit mode if selected entity gets undone - var oldEntity = entity, - newEntity = history.graph().entity(selection[0]); - - if (!newEntity) { - mode.controller.enter(iD.modes.Browse()); - } else if (!_.isEqual(oldEntity.tags, newEntity.tags)) { - inspector.tags(newEntity.tags); - } - - surface.call(radialMenu.close); - }); + .on('close', function() { context.enter(iD.modes.Browse(context)); }); } - map.on('move.select', function() { - surface.call(radialMenu.close); + context.history().on('change.select', function() { + context.surface().call(radialMenu.close); + + if (_.any(selection, function (id) { return !context.entity(id); })) { + // Exit mode if selected entity gets undone + context.enter(iD.modes.Browse(context)); + + } else if (entity) { + var newEntity = context.entity(selection[0]); + if (!_.isEqual(entity.tags, newEntity.tags)) { + inspector.tags(newEntity.tags); + } + } + }); + + context.map().on('move.select', function() { + context.surface().call(radialMenu.close); }); function dblclick() { @@ -113,10 +109,10 @@ iD.modes.Select = function(selection, initial) { if (datum instanceof iD.Way && !target.classed('fill')) { var choice = iD.geo.chooseIndex(datum, - d3.mouse(mode.map.surface.node()), mode.map), + d3.mouse(context.surface().node()), context), node = iD.Node({ loc: choice.loc }); - history.perform( + context.perform( iD.actions.AddEntity(node), iD.actions.AddVertex(datum.id, node.id, choice.index), t('operations.add.annotation.vertex')); @@ -129,7 +125,8 @@ iD.modes.Select = function(selection, initial) { d3.select(document) .call(keybinding); - surface.on('dblclick.select', dblclick) + context.surface() + .on('dblclick.select', dblclick) .selectAll("*") .filter(function (d) { return d && selection.indexOf(d.id) >= 0; }) .classed('selected', true); @@ -137,25 +134,23 @@ iD.modes.Select = function(selection, initial) { radialMenu = iD.ui.RadialMenu(operations); if (d3.event && !initial) { - var loc = map.mouseCoordinates(); + var loc = context.map().mouseCoordinates(); if (entity && entity.type === 'node') { loc = entity.loc; } - surface.call(radialMenu, map.projection(loc)); + context.surface().call(radialMenu, context.projection(loc)); } }; mode.exit = function () { - var surface = mode.map.surface, - history = mode.history; - if (singular()) { changeTags(singular(), inspector.tags()); } - d3.select('.inspector-wrap') + context.container() + .select('.inspector-wrap') .style('display', 'none') .html(''); @@ -164,7 +159,7 @@ iD.modes.Select = function(selection, initial) { d3.selectAll('div.typeahead').remove(); behaviors.forEach(function(behavior) { - behavior.off(surface); + context.uninstall(behavior); }); var q = iD.util.stringQs(location.hash.substring(1)); @@ -172,13 +167,14 @@ iD.modes.Select = function(selection, initial) { keybinding.off(); - history.on('change.select', null); + context.history() + .on('change.select', null); - surface.on('dblclick.select', null) + context.surface() + .call(radialMenu.close) + .on('dblclick.select', null) .selectAll(".selected") .classed('selected', false); - - surface.call(radialMenu.close); }; return mode; diff --git a/js/id/oauth.js b/js/id/oauth.js index a09f2affc..d7cb095a8 100644 --- a/js/id/oauth.js +++ b/js/id/oauth.js @@ -1,4 +1,4 @@ -iD.OAuth = function() { +iD.OAuth = function(context) { var baseurl = 'http://www.openstreetmap.org', o = {}, keys, @@ -6,10 +6,6 @@ iD.OAuth = function() { function keyclean(x) { return x.replace(/\W/g, ''); } - if (token('oauth_token')) { - o.oauth_token = token('oauth_token'); - } - function timenonce(o) { o.oauth_timestamp = ohauth.timestamp(); o.oauth_nonce = ohauth.nonce(); @@ -17,11 +13,12 @@ iD.OAuth = function() { } // token getter/setter, namespaced to the current `apibase` value. - function token(k, x) { - if (arguments.length == 2) { - localStorage[keyclean(baseurl) + k] = x; - } - return localStorage[keyclean(baseurl) + k]; + function token() { + return context.storage.apply(context, arguments); + } + + if (token('oauth_token')) { + o.oauth_token = token('oauth_token'); } oauth.authenticated = function() { diff --git a/js/id/operations/circularize.js b/js/id/operations/circularize.js index 4b0f27bf9..2680da4ec 100644 --- a/js/id/operations/circularize.js +++ b/js/id/operations/circularize.js @@ -1,25 +1,19 @@ -iD.operations.Circularize = function(selection, mode) { +iD.operations.Circularize = function(selection, context) { var entityId = selection[0], - history = mode.map.history(), - action = iD.actions.Circularize(entityId, mode.map); + action = iD.actions.Circularize(entityId, context.projection); var operation = function() { - var graph = history.graph(), - entity = graph.entity(entityId), - annotation = t('operations.circularize.annotation.' + entity.geometry(graph)); - - history.perform(action, annotation); + var annotation = t('operations.circularize.annotation.' + context.geometry(entityId)); + context.perform(action, annotation); }; operation.available = function() { - var graph = history.graph(), - entity = graph.entity(entityId); - return selection.length === 1 && entity.type === 'way'; + return selection.length === 1 && + context.entity(entityId).type === 'way'; }; operation.enabled = function() { - var graph = history.graph(); - return action.enabled(graph); + return action.enabled(context.graph()); }; operation.id = "circularize"; diff --git a/js/id/operations/delete.js b/js/id/operations/delete.js index 12df22a09..2c9765b62 100644 --- a/js/id/operations/delete.js +++ b/js/id/operations/delete.js @@ -1,23 +1,20 @@ -iD.operations.Delete = function(selection, mode) { - var entityId = selection[0], - history = mode.map.history(); - +iD.operations.Delete = function(selection, context) { var operation = function() { - var graph = history.graph(), - entity = graph.entity(entityId), - action = {way: iD.actions.DeleteWay, node: iD.actions.DeleteNode}[entity.type], - annotation = t('operations.delete.annotation.' + entity.geometry(graph)); + var annotation; - history.perform( - action(entityId), + if (selection.length === 1) { + annotation = t('operations.delete.annotation.' + context.geometry(selection[0])); + } else { + annotation = t('operations.delete.annotation.multiple', {n: selection.length}); + } + + context.perform( + iD.actions.DeleteMultiple(selection), annotation); }; operation.available = function() { - var graph = history.graph(), - entity = graph.entity(entityId); - return selection.length === 1 && - (entity.type === 'way' || entity.type === 'node'); + return true; }; operation.enabled = function() { diff --git a/js/id/operations/disconnect.js b/js/id/operations/disconnect.js new file mode 100644 index 000000000..c8cc0d014 --- /dev/null +++ b/js/id/operations/disconnect.js @@ -0,0 +1,24 @@ +iD.operations.Disconnect = function(selection, context) { + var entityId = selection[0], + action = iD.actions.Disconnect(entityId); + + var operation = function() { + context.perform(action, t('operations.disconnect.annotation')); + }; + + operation.available = function() { + return selection.length === 1 && + context.geometry(entityId) === 'vertex'; + }; + + operation.enabled = function() { + return action.enabled(context.graph()); + }; + + operation.id = "disconnect"; + operation.key = t('operations.disconnect.key'); + operation.title = t('operations.disconnect.title'); + operation.description = t('operations.disconnect.description'); + + return operation; +}; diff --git a/js/id/operations/merge.js b/js/id/operations/merge.js new file mode 100644 index 000000000..d02222368 --- /dev/null +++ b/js/id/operations/merge.js @@ -0,0 +1,27 @@ +iD.operations.Merge = function(selection, context) { + var action = iD.actions.Join(selection[0], selection[1]); + + var operation = function() { + var annotation = t('operations.merge.annotation', {n: selection.length}), + difference = context.perform(action, annotation); + context.enter(iD.modes.Select(context, difference.extantIDs())); + }; + + operation.available = function() { + return selection.length === 2 && + _.all(selection, function (id) { + return context.geometry(id) === 'line'; + }); + }; + + operation.enabled = function() { + return action.enabled(context.graph()); + }; + + operation.id = "merge"; + operation.key = t('operations.merge.key'); + operation.title = t('operations.merge.title'); + operation.description = t('operations.merge.description'); + + return operation; +}; diff --git a/js/id/operations/move.js b/js/id/operations/move.js index 10405cc53..d9da39f64 100644 --- a/js/id/operations/move.js +++ b/js/id/operations/move.js @@ -1,15 +1,13 @@ -iD.operations.Move = function(selection, mode) { - var entityId = selection[0], - history = mode.map.history(); +iD.operations.Move = function(selection, context) { + var entityId = selection[0]; var operation = function() { - mode.controller.enter(iD.modes.MoveWay(entityId)); + context.enter(iD.modes.MoveWay(context, entityId)); }; operation.available = function() { - var graph = history.graph(); return selection.length === 1 && - graph.entity(entityId).type === 'way'; + context.entity(entityId).type === 'way'; }; operation.enabled = function() { diff --git a/js/id/operations/reverse.js b/js/id/operations/reverse.js index 78b8789dd..f750c134d 100644 --- a/js/id/operations/reverse.js +++ b/js/id/operations/reverse.js @@ -1,18 +1,15 @@ -iD.operations.Reverse = function(selection, mode) { - var entityId = selection[0], - history = mode.map.history(); +iD.operations.Reverse = function(selection, context) { + var entityId = selection[0]; var operation = function() { - history.perform( - iD.actions.ReverseWay(entityId), + context.perform( + iD.actions.Reverse(entityId), t('operations.reverse.annotation')); }; operation.available = function() { - var graph = history.graph(), - entity = graph.entity(entityId); return selection.length === 1 && - entity.geometry(graph) === 'line'; + context.geometry(entityId) === 'line'; }; operation.enabled = function() { diff --git a/js/id/operations/split.js b/js/id/operations/split.js index 4274fc41b..f49f4b9ed 100644 --- a/js/id/operations/split.js +++ b/js/id/operations/split.js @@ -1,22 +1,20 @@ -iD.operations.Split = function(selection, mode) { +iD.operations.Split = function(selection, context) { var entityId = selection[0], - history = mode.map.history(), - action = iD.actions.SplitWay(entityId); + action = iD.actions.Split(entityId); var operation = function() { - history.perform(action, t('operations.split.annotation')); + var annotation = t('operations.split.annotation'), + difference = context.perform(action, annotation); + context.enter(iD.modes.Select(context, difference.extantIDs())); }; operation.available = function() { - var graph = history.graph(), - entity = graph.entity(entityId); return selection.length === 1 && - entity.geometry(graph) === 'vertex'; + context.geometry(entityId) === 'vertex'; }; operation.enabled = function() { - var graph = history.graph(); - return action.enabled(graph); + return action.enabled(context.graph()); }; operation.id = "split"; diff --git a/js/id/operations/unjoin.js b/js/id/operations/unjoin.js deleted file mode 100644 index 2a40ec067..000000000 --- a/js/id/operations/unjoin.js +++ /dev/null @@ -1,28 +0,0 @@ -iD.operations.Unjoin = function(selection, mode) { - var entityId = selection[0], - history = mode.map.history(), - action = iD.actions.UnjoinNode(entityId); - - var operation = function() { - history.perform(action, 'Unjoined lines.'); - }; - - operation.available = function() { - var graph = history.graph(), - entity = graph.entity(entityId); - return selection.length === 1 && - entity.geometry(graph) === 'vertex'; - }; - - operation.enabled = function() { - var graph = history.graph(); - return action.enabled(graph); - }; - - operation.id = "unjoin"; - operation.key = t('operations.unjoin.key'); - operation.title = t('operations.unjoin.title'); - operation.description = t('operations.unjoin.description'); - - return operation; -}; diff --git a/js/id/renderer/background.js b/js/id/renderer/background.js index 54e3835ee..cae44ca09 100644 --- a/js/id/renderer/background.js +++ b/js/id/renderer/background.js @@ -83,7 +83,10 @@ iD.Background = function() { } }); - requests = uniqueBy(requests, 3); + requests = uniqueBy(requests, 3).filter(function(r) { + // don't re-request tiles which have failed in the past + return cache[r[3]] !== false; + }); function load(d) { cache[d[3]] = true; @@ -125,7 +128,7 @@ iD.Background = function() { .attr('src', function(d) { return d[3]; }) .on('error', error) .on('load', load); - + image.style(transformProp, imageTransform); if (Object.keys(cache).length > 100) cache = {}; diff --git a/js/id/renderer/map.js b/js/id/renderer/map.js index a2c4781ef..b2561f21f 100644 --- a/js/id/renderer/map.js +++ b/js/id/renderer/map.js @@ -1,6 +1,5 @@ -iD.Map = function() { - var connection, history, - dimensions = [], +iD.Map = function(context) { + var dimensions = [], dispatch = d3.dispatch('move', 'drawn'), projection = d3.geo.mercator().scale(1024), roundedProjection = iD.svg.RoundProjection(projection), @@ -27,6 +26,9 @@ iD.Map = function() { surface, tilegroup; function map(selection) { + context.history() + .on('change.map', redraw); + selection.call(zoom); tilegroup = selection.append('div') @@ -55,49 +57,23 @@ iD.Map = function() { function pxCenter() { return [dimensions[0] / 2, dimensions[1] / 2]; } function drawVector(difference) { - if (surface.style(transformProp) != 'none') return; var filter, all, extent = map.extent(), - graph = history.graph(); - - function addParents(parents) { - for (var i = 0; i < parents.length; i++) { - var parent = parents[i]; - if (only[parent.id] === undefined) { - only[parent.id] = parent; - addParents(graph.parentRelations(parent)); - } - } - } + graph = context.graph(); if (!difference) { all = graph.intersects(extent); filter = d3.functor(true); } else { - var only = {}; - - for (var j = 0; j < difference.length; j++) { - var id = difference[j], - entity = graph.entity(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)); + var complete = difference.complete(extent); + all = _.compact(_.values(complete)); filter = function(d) { if (d.type === 'midpoint') { for (var i = 0; i < d.ways.length; i++) { - if (d.ways[i].id in only) return true; + if (d.ways[i].id in complete) return true; } } else { - return d.id in only; + return d.id in complete; } }; } @@ -121,11 +97,6 @@ iD.Map = function() { surface.selectAll('.layer *').remove(); } - function connectionLoad(err, result) { - history.merge(result); - redraw(Object.keys(result)); - } - function zoomPan() { if (d3.event && d3.event.sourceEvent.type === 'dblclick') { if (!dblclickEnabled) { @@ -183,7 +154,7 @@ iD.Map = function() { tilegroup.call(background); if (map.editable()) { - connection.loadTiles(projection, dimensions); + context.connection().loadTiles(projection, dimensions); drawVector(difference); } else { editOff(); @@ -259,7 +230,6 @@ iD.Map = function() { t[0] - ll[0] + c[0], t[1] - ll[1] + c[1]]); zoom.translate(projection.translate()); - dispatch.move(map); return true; } @@ -348,15 +318,8 @@ iD.Map = function() { }; map.flush = function () { - connection.flush(); - history.reset(); - return map; - }; - - map.connection = function(_) { - if (!arguments.length) return connection; - connection = _; - connection.on('load.tile', connectionLoad); + context.connection().flush(); + context.history().reset(); return map; }; @@ -369,24 +332,6 @@ iD.Map = function() { return map; }; - map.hint = function (_) { - if (_ === false) { - d3.select('div.inspector-wrap') - .style('opacity', 0) - .style('display', 'none'); - } else { - d3.select('div.inspector-wrap') - .html('') - .style('display', 'block') - .transition() - .style('opacity', 1); - d3.select('div.inspector-wrap') - .append('div') - .attr('class','inspector-inner') - .text(_); - } - }; - map.editable = function() { return map.zoom() >= 16; }; @@ -397,13 +342,6 @@ iD.Map = function() { return map; }; - map.history = function (_) { - if (!arguments.length) return history; - history = _; - history.on('change.map', redraw); - return map; - }; - map.background = background; map.projection = projection; map.redraw = redraw; diff --git a/js/id/svg/surface.js b/js/id/svg/surface.js index 7561802e5..a11f8fbae 100644 --- a/js/id/svg/surface.js +++ b/js/id/svg/surface.js @@ -3,7 +3,7 @@ iD.svg.Surface = function() { selection.append('defs'); var layers = selection.selectAll('.layer') - .data(['shadow', 'fill', 'casing', 'stroke', 'text', 'hit', 'halo', 'label']); + .data(['fill', 'shadow', 'casing', 'stroke', 'text', 'hit', 'halo', 'label']); layers.enter().append('g') .attr('class', function(d) { return 'layer layer-' + d; }); diff --git a/js/id/ui.js b/js/id/ui.js index 8901175f4..87cc3745a 100644 --- a/js/id/ui.js +++ b/js/id/ui.js @@ -1 +1,257 @@ -iD.ui = {}; +iD.ui = function (context) { + return function(container) { + context.container(container); + + var connection = context.connection(), + history = context.history(), + map = context.map(); + + if (!iD.supported()) { + container.html('This editor is supported in Firefox, Chrome, Safari, Opera, ' + + 'and Internet Explorer 9 and above. Please upgrade your browser ' + + 'or use Potlatch 2 to edit the map.') + .style('text-align:center;font-style:italic;'); + return; + } + + function hintprefix(x, y) { + return '' + y + '' + '
' + x + '
'; + } + + var m = container.append('div') + .attr('id', 'map') + .call(map); + + var bar = container.append('div') + .attr('id', 'bar') + .attr('class','pad1 fillD'); + + var limiter = bar.append('div') + .attr('class', 'limiter'); + + var buttons_joined = limiter.append('div') + .attr('class', 'button-wrap joined col4'); + + var modes = [ + iD.modes.Browse(context), + iD.modes.AddPoint(context), + iD.modes.AddLine(context), + iD.modes.AddArea(context)]; + + var buttons = buttons_joined.selectAll('button.add-button') + .data(modes) + .enter().append('button') + .attr('tabindex', -1) + .attr('class', function (mode) { return mode.title + ' add-button col3'; }) + .call(bootstrap.tooltip().placement('bottom').html(true)) + .attr('data-original-title', function (mode) { + return hintprefix(mode.key, mode.description); + }) + .on('click.editor', function (mode) { context.enter(mode); }); + + function disableTooHigh() { + if (map.editable()) { + notice.message(false); + buttons.attr('disabled', null); + } else { + buttons.attr('disabled', 'disabled'); + notice.message(true); + context.enter(iD.modes.Browse(context)); + } + } + + var notice = iD.ui.notice(limiter) + .message(false) + .on('zoom', function() { map.zoom(16); }); + + map.on('move.editor', _.debounce(function() { + disableTooHigh(); + contributors.call(iD.ui.contributors(context)); + }, 500)); + + buttons.append('span') + .attr('class', function(d) { + return d.id + ' icon icon-pre-text'; + }); + + buttons.append('span').attr('class', 'label').text(function (mode) { return mode.title; }); + + context.on('enter.editor', function (entered) { + buttons.classed('active', function (mode) { return entered.button === mode.button; }); + container.classed("mode-" + entered.id, true); + }); + + context.on('exit.editor', function (exited) { + container.classed("mode-" + exited.id, false); + }); + + var undo_buttons = limiter.append('div') + .attr('class', 'button-wrap joined col1'), + undo_tooltip = bootstrap.tooltip().placement('bottom').html(true); + + undo_buttons.append('button') + .attr({ id: 'undo', 'class': 'col6' }) + .property('disabled', true) + .html("") + .on('click.editor', history.undo) + .call(undo_tooltip); + + undo_buttons.append('button') + .attr({ id: 'redo', 'class': 'col6' }) + .property('disabled', true) + .html("") + .on('click.editor', history.redo) + .call(undo_tooltip); + + limiter.append('div').attr('class','button-wrap col1').append('button') + .attr('class', 'save col12') + .call(iD.ui.save(context)); + + var zoom = container.append('div') + .attr('class', 'zoombuttons map-control') + .selectAll('button') + .data([['zoom-in', '+', map.zoomIn, 'Zoom In'], ['zoom-out', '-', map.zoomOut, 'Zoom Out']]) + .enter() + .append('button') + .attr('tabindex', -1) + .attr('class', function(d) { return d[0]; }) + .attr('title', function(d) { return d[3]; }) + .on('click.editor', function(d) { return d[2](); }) + .append('span') + .attr('class', function(d) { + return d[0] + ' icon'; + }); + + if (navigator.geolocation) { + container.append('div') + .call(iD.ui.geolocate(map)); + } + + container.append('div').attr('class', 'geocode-control map-control') + .call(iD.ui.geocoder().map(map)); + + container.append('div').attr('class', 'map-control layerswitcher-control') + .call(iD.ui.layerswitcher(context)); + + container.append('div') + .style('display', 'none') + .attr('class', 'inspector-wrap fr col5'); + + var about = container.append('div') + .attr('class','col12 about-block fillD pad1'); + + about.append('div') + .attr('class', 'user-container') + .append('div') + .attr('class', 'hello'); + + var aboutList = about.append('ul') + .attr('id','about') + .attr('class','link-list'); + + var linkList = aboutList.append('ul') + .attr('id','about') + .attr('class','pad1 fillD about-block link-list'); + linkList.append('li').append('a').attr('target', '_blank') + .attr('href', 'http://github.com/systemed/iD').text(iD.version); + linkList.append('li').append('a').attr('target', '_blank') + .attr('href', 'http://github.com/systemed/iD/issues').text('report a bug'); + + var imagery = linkList.append('li').attr('id', 'attribution'); + imagery.append('span').text('imagery'); + imagery.append('a').attr('target', '_blank') + .attr('href', 'http://opengeodata.org/microsoft-imagery-details').text(' provided by bing'); + + linkList.append('li').attr('class', 'source-switch').append('a').attr('href', '#') + .text('dev') + .on('click.editor', function() { + d3.event.preventDefault(); + if (d3.select(this).classed('live')) { + map.flush(); + context.connection() + .url('http://api06.dev.openstreetmap.org'); + d3.select(this).text('dev').classed('live', false); + } else { + map.flush(); + context.connection() + .url('http://www.openstreetmap.org'); + d3.select(this).text('live').classed('live', true); + } + }); + + var contributors = linkList.append('li') + .attr('id', 'user-list'); + contributors.append('span') + .attr('class', 'icon nearby icon-pre-text'); + contributors.append('span') + .text('Viewing contributions by '); + contributors.append('span') + .attr('class', 'contributor-list'); + contributors.append('span') + .attr('class', 'contributor-count'); + + history.on('change.editor', function() { + window.onbeforeunload = history.hasChanges() ? function() { + return 'You have unsaved changes.'; + } : null; + + var undo = history.undoAnnotation(), + redo = history.redoAnnotation(); + + function refreshTooltip(selection) { + if (selection.property('disabled')) { + selection.call(undo_tooltip.hide); + } else if (selection.property('tooltipVisible')) { + selection.call(undo_tooltip.show); + } + } + + limiter.select('#undo') + .property('disabled', !undo) + .attr('data-original-title', hintprefix('⌘ + Z', undo)) + .call(refreshTooltip); + + limiter.select('#redo') + .property('disabled', !redo) + .attr('data-original-title', hintprefix('⌘ + ⇧ + Z', redo)) + .call(refreshTooltip); + }); + + d3.select(window).on('resize.editor', function() { + map.size(m.size()); + }); + + var keybinding = d3.keybinding('main') + .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(); }); + + modes.forEach(function(m) { + keybinding.on(m.key, function() { if (map.editable()) context.enter(m); }); + }); + + d3.select(document) + .call(keybinding); + + var hash = iD.behavior.Hash(context); + + hash(); + + if (!hash.hadHash) { + map.centerZoom([-77.02271, 38.90085], 20); + } + + d3.select('.user-container').call(iD.ui.userpanel(connection) + .on('logout.editor', connection.logout) + .on('login.editor', connection.authenticate)); + + context.enter(iD.modes.Browse(context)); + + if (!context.storage('sawSplash')) { + iD.ui.splash(); + context.storage('sawSplash', true); + } + }; +}; diff --git a/js/id/ui/commit.js b/js/id/ui/commit.js index d883b1748..09bb99404 100644 --- a/js/id/ui/commit.js +++ b/js/id/ui/commit.js @@ -1,4 +1,4 @@ -iD.ui.commit = function(map) { +iD.ui.commit = function(context) { var event = d3.dispatch('cancel', 'save', 'fix'); function zipSame(d) { @@ -38,7 +38,7 @@ iD.ui.commit = function(map) { comment_section.append('textarea') .attr('class', 'changeset-comment') .attr('placeholder', 'Brief Description of your contributions') - .property('value', localStorage.comment || '') + .property('value', context.storage('comment') || '') .node().select(); var commit_info = @@ -89,7 +89,7 @@ iD.ui.commit = function(map) { cancelbutton.append('span').attr('class','label').text('Cancel'); var warnings = body.selectAll('div.warning-section') - .data(iD.validate(changes, map.history().graph())) + .data(iD.validate(changes, context.graph())) .enter() .append('div').attr('class', 'modal-section warning-section fillL'); diff --git a/js/id/ui/contributors.js b/js/id/ui/contributors.js index c64d8a2ac..e029bfd04 100644 --- a/js/id/ui/contributors.js +++ b/js/id/ui/contributors.js @@ -1,10 +1,10 @@ -iD.ui.contributors = function(map) { +iD.ui.contributors = function(context) { function contributors(selection) { var users = {}, limit = 3, - entities = map.history().graph().intersects(map.extent()); + entities = context.graph().intersects(context.map().extent()); for (var i in entities) { if (entities[i].user) users[entities[i].user] = true; @@ -21,7 +21,7 @@ iD.ui.contributors = function(map) { l.enter().append('a') .attr('class', 'user-link') - .attr('href', function(d) { return map.connection().userUrl(d); }) + .attr('href', function(d) { return context.connection().userUrl(d); }) .attr('target', '_blank') .text(String); @@ -37,7 +37,7 @@ iD.ui.contributors = function(map) { .append('a') .attr('target', '_blank') .attr('href', function() { - var ext = map.extent(); + var ext = context.map().extent(); return 'http://www.openstreetmap.org/browse/changesets?bbox=' + [ ext[0][0], ext[0][1], ext[1][0], ext[1][1]]; diff --git a/js/id/ui/geocoder.js b/js/id/ui/geocoder.js index ed37d2cb1..4aac10bf2 100644 --- a/js/id/ui/geocoder.js +++ b/js/id/ui/geocoder.js @@ -6,18 +6,20 @@ iD.ui.geocoder = function() { function keydown() { if (d3.event.keyCode !== 13) return; d3.event.preventDefault(); - d3.json('http://a.tiles.mapbox.com/v3/openstreetmap.map-hn253zqn/geocode/' + - encodeURIComponent(this.value) + '.json', function(err, resp) { + var searchVal = this.value; + d3.json('http://nominatim.openstreetmap.org/search/' + + encodeURIComponent(searchVal) + '?limit=10&format=json', function(err, resp) { if (err) return hide(); hide(); - if (!resp.results.length) { + if (!resp.length) { return iD.ui.flash() .select('.content') .append('h3') - .text('No location found for "' + resp.query[0] + '"'); + .text('No location found for "' + searchVal + '"'); } - var bounds = resp.results[0][0].bounds; - map.extent(iD.geo.Extent([bounds[0], bounds[1]], [bounds[2], bounds[3]])); + var bounds = resp[0].boundingbox; + map.extent(iD.geo.Extent([parseFloat(bounds[3]), parseFloat(bounds[0])], [parseFloat(bounds[2]), parseFloat(bounds[1])])); + if (map.zoom() > 19) map.zoom(19); }); } @@ -43,7 +45,7 @@ iD.ui.geocoder = function() { var button = selection.append('button') .attr('tabindex', -1) - .attr('title', 'Find A Location') + .attr('title', t('geocoder.find_location')) .html('') .on('click', toggle); @@ -51,7 +53,7 @@ iD.ui.geocoder = function() { gcForm.attr('class','content fillD map-overlay hide') .append('input') - .attr({ type: 'text', placeholder: 'find a place' }) + .attr({ type: 'text', placeholder: t('geocoder.find_a_place') }) .on('keydown', keydown); selection.call(clickoutside); diff --git a/js/id/ui/inspector.js b/js/id/ui/inspector.js index 9612cadd9..32e3fc857 100644 --- a/js/id/ui/inspector.js +++ b/js/id/ui/inspector.js @@ -35,7 +35,7 @@ iD.ui.inspector = function() { }); newTag.append('span').attr('class', 'icon icon-pre-text plus'); - newTag.append('span').attr('class','label').text('New tag') + newTag.append('span').attr('class','label').text(t('inspector.new_tag')); drawTags(entity.tags); @@ -63,7 +63,7 @@ iD.ui.inspector = function() { .attr('class', 'apply action') .on('click', apply); - inspectorButton.append('span').attr('class','label').text('Okay'); + inspectorButton.append('span').attr('class','label').text(t('okay')); var minorButtons = selection.append('div').attr('class','minor-buttons fl'); @@ -148,7 +148,7 @@ iD.ui.inspector = function() { iD.ui.flash() .select('.content') .append('h3') - .text(t('no_documentation_combination')); + .text(t('inspector.no_documentation_combination')); } }); } else if (d.key) { @@ -166,7 +166,7 @@ iD.ui.inspector = function() { iD.ui.flash() .select('.content') .append('h3') - .text(t('no_documentation_key')); + .text(t('inspector.no_documentation_key')); } }); } diff --git a/js/id/ui/layerswitcher.js b/js/id/ui/layerswitcher.js index 298c469e0..5c4835757 100644 --- a/js/id/ui/layerswitcher.js +++ b/js/id/ui/layerswitcher.js @@ -1,4 +1,4 @@ -iD.ui.layerswitcher = function(map) { +iD.ui.layerswitcher = function(context) { var event = d3.dispatch('cancel', 'save'), sources = [{ name: 'Bing', @@ -59,7 +59,7 @@ iD.ui.layerswitcher = function(map) { .append('div') .attr('class', 'opacity-options-wrapper'); - opa.append('h4').text(t('layers')); + opa.append('h4').text(t('layerswitcher.layers')); opa.append('ul') .attr('class', 'opacity-options') @@ -68,7 +68,7 @@ iD.ui.layerswitcher = function(map) { .enter() .append('li') .attr('data-original-title', function(d) { - return t('percent_opacity', { opacity: (d * 100) }); + return t('layerswitcher.percent_brightness', { opacity: (d * 100) }); }) .on('click.set-opacity', function(d) { d3.select('#tile-g') @@ -94,7 +94,7 @@ iD.ui.layerswitcher = function(map) { function selectLayer(d) { content.selectAll('a.layer') .classed('selected', function(d) { - return d.source === map.background.source(); + return d.source === context.background().source(); }); d3.select('#attribution a') .attr('href', d.link) @@ -126,9 +126,9 @@ iD.ui.layerswitcher = function(map) { d.source = configured; d.name = 'Custom (configured)'; } - map.background.source(d.source); - map.history().imagery_used(d.name); - map.redraw(); + context.background().source(d.source); + context.history().imagery_used(d.name); + context.redraw(); selectLayer(d); }) .insert('span') @@ -145,8 +145,8 @@ iD.ui.layerswitcher = function(map) { ['bottom', [0, 1]]]; function nudge(d) { - map.background.nudge(d[1]); - map.redraw(); + context.background().nudge(d[1]); + context.redraw(); } adjustments.append('a') @@ -181,12 +181,12 @@ iD.ui.layerswitcher = function(map) { .text('reset') .attr('class', 'reset') .on('click', function() { - map.background.offset([0, 0]); - map.redraw(); + context.background().offset([0, 0]); + context.redraw(); }); selection.call(clickoutside); - selectLayer(map.background.source()); + selectLayer(context.background().source()); } return d3.rebind(layerswitcher, event, 'on'); diff --git a/js/id/ui/save.js b/js/id/ui/save.js index b8085e21a..ba217adcf 100644 --- a/js/id/ui/save.js +++ b/js/id/ui/save.js @@ -1,11 +1,8 @@ -iD.ui.save = function() { - - var map, controller; - - function save(selection) { - - var history = map.history(), - connection = map.connection(), +iD.ui.save = function(context) { + return function (selection) { + var map = context.map(), + history = context.history(), + connection = context.connection(), tooltip = bootstrap.tooltip() .placement('bottom'); @@ -53,14 +50,14 @@ iD.ui.save = function() { modal.select('.content') .classed('commit-modal', true) .datum(changes) - .call(iD.ui.commit(map) + .call(iD.ui.commit(context) .on('cancel', function() { modal.remove(); }) .on('fix', function(d) { - map.extent(d.entity.extent(map.history().graph())); + map.extent(d.entity.extent(context.graph())); if (map.zoom() > 19) map.zoom(19); - controller.enter(iD.modes.Select([d.entity.id])); + context.enter(iD.modes.Select(context, [d.entity.id])); modal.remove(); }) .on('save', commit)); @@ -88,19 +85,5 @@ iD.ui.save = function() { selection.call(tooltip.hide); } }); - } - - save.map = function(_) { - if (!arguments.length) return map; - map = _; - return save; }; - - save.controller = function(_) { - if (!arguments.length) return controller; - controller = _; - return save; - }; - - return save; }; diff --git a/js/id/validate.js b/js/id/validate.js index aa7d8771b..6329b626f 100644 --- a/js/id/validate.js +++ b/js/id/validate.js @@ -15,31 +15,29 @@ iD.validate = function(changes, graph) { 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]; + for (var i = 0; i < changes.created.length; i++) { + change = changes.created[i]; - if (change.geometry(graph) === 'point' && _.isEmpty(change.tags)) { - warnings.push({ - message: t('validations.untagged_point'), - entity: change - }); - } + if (change.geometry(graph) === 'point' && _.isEmpty(change.tags)) { + warnings.push({ + message: t('validations.untagged_point'), + entity: change + }); + } - if (change.geometry(graph) === 'line' && _.isEmpty(change.tags)) { - warnings.push({ message: t('validations.untagged_line'), entity: change }); - } + if (change.geometry(graph) === 'line' && _.isEmpty(change.tags)) { + warnings.push({ message: t('validations.untagged_line'), entity: change }); + } - if (change.geometry(graph) === 'area' && _.isEmpty(change.tags)) { - warnings.push({ message: t('validations.untagged_area'), entity: change }); - } + if (change.geometry(graph) === 'area' && _.isEmpty(change.tags)) { + warnings.push({ message: t('validations.untagged_area'), entity: change }); + } - if (change.geometry(graph) === 'line' && tagSuggestsArea(change)) { - warnings.push({ - message: t('validations.tag_suggests_area', {tag: tagSuggestsArea(change)}), - entity: change - }); - } + if (change.geometry(graph) === 'line' && tagSuggestsArea(change)) { + warnings.push({ + message: t('validations.tag_suggests_area', {tag: tagSuggestsArea(change)}), + entity: change + }); } } diff --git a/js/lib/d3.combobox.js b/js/lib/d3.combobox.js new file mode 100644 index 000000000..165456563 --- /dev/null +++ b/js/lib/d3.combobox.js @@ -0,0 +1,147 @@ +d3.combobox = function() { + var event = d3.dispatch('accept'), + autohighlight = false, + autofilter = false, + input, + container, + data; + + var typeahead = function(selection) { + var hidden, idx = autohighlight ? 0 : -1; + + var rect = selection.select('input').node().getBoundingClientRect(); + + input = selection.select('input'); + + container = selection + .insert('div', ':first-child') + .attr('class', 'combobox') + .style({ + position: 'absolute', + display: 'none', + left: '0px', + width: rect.width + 'px', + top: rect.height + 'px' + }); + + carat = selection + .insert('div', ':first-child') + .attr('class', 'combobox-carat') + .text('+') + .style({ + position: 'absolute', + left: (rect.width - 20) + 'px', + top: '0px' + }) + .on('click', function() { + update(); + show(); + }); + + selection + .on('keyup.typeahead', key); + + hidden = false; + + function hide() { + idx = autohighlight ? 0 : -1; + hidden = true; + } + + function show() { + container.style('display', 'block'); + } + + function slowHide() { + if (autohighlight && container.select('a.selected').node()) { + select(container.select('a.selected').datum()); + event.accept(); + } + window.setTimeout(hide, 150); + } + + selection + .on('focus.typeahead', show) + .on('blur.typeahead', slowHide); + + function key() { + var len = container.selectAll('a').data().length; + if (d3.event.keyCode === 40) { + idx = Math.min(idx + 1, len - 1); + return highlight(); + } else if (d3.event.keyCode === 38) { + idx = Math.max(idx - 1, 0); + return highlight(); + } else if (d3.event.keyCode === 13) { + if (container.select('a.selected').node()) { + select(container.select('a.selected').datum()); + } + event.accept(); + hide(); + } else { + update(); + } + } + + function highlight() { + container + .selectAll('a') + .classed('selected', function(d, i) { return i == idx; }); + } + + function update() { + + function run(data) { + container.style('display', function() { + return data.length ? 'block' : 'none'; + }); + + var options = container + .selectAll('a') + .data(data, function(d) { return d.value; }); + + options.enter() + .append('a') + .text(function(d) { return d.value; }) + .attr('title', function(d) { return d.title; }) + .on('click', select); + + options.exit().remove(); + + options + .classed('selected', function(d, i) { return i == idx; }); + } + + if (typeof data === 'function') data(selection, run); + else run(data); + } + + function select(d) { + input + .property('value', d.value) + .trigger('change'); + container.style('display', 'none'); + } + + }; + + typeahead.data = function(_) { + if (!arguments.length) return data; + data = _; + return typeahead; + }; + + typeahead.autofilter = function(_) { + if (!arguments.length) return autofilter; + autofilter = _; + return typeahead; + }; + + typeahead.autohighlight = function(_) { + if (!arguments.length) return autohighlight; + autohighlight = _; + return typeahead; + }; + + return d3.rebind(typeahead, event, 'on'); +}; diff --git a/js/lib/d3.v3.js b/js/lib/d3.v3.js index 93b676f46..818bab9dc 100644 --- a/js/lib/d3.v3.js +++ b/js/lib/d3.v3.js @@ -12,12 +12,9 @@ }; } d3 = { - version: "3.0.0pre" + version: "3.0.5" }; - var π = Math.PI, ε = 1e-6, εε = .001, d3_radians = π / 180, d3_degrees = 180 / π; - function d3_zero() { - return 0; - } + var π = Math.PI, ε = 1e-6, d3_radians = π / 180, d3_degrees = 180 / π; function d3_target(d) { return d.target; } @@ -234,7 +231,7 @@ return s; }; d3.quantile = function(values, p) { - var H = (values.length - 1) * p + 1, h = Math.floor(H), v = values[h - 1], e = H - h; + var H = (values.length - 1) * p + 1, h = Math.floor(H), v = +values[h - 1], e = H - h; return e ? v + e * (values[h] - v) : v; }; d3.shuffle = function(array) { @@ -454,8 +451,13 @@ d3.rebind(xhr, dispatch, "on"); if (arguments.length === 2 && typeof mimeType === "function") callback = mimeType, mimeType = null; - return callback == null ? xhr : xhr.get(callback); + return callback == null ? xhr : xhr.get(d3_xhr_fixCallback(callback)); }; + function d3_xhr_fixCallback(callback) { + return callback.length === 1 ? function(error, request) { + callback(error == null ? request : null); + } : callback; + } d3.text = function() { return d3.xhr.apply(d3, arguments).response(d3_text); }; @@ -1687,13 +1689,13 @@ return value; } function bind(group, groupData) { - var i, n = group.length, m = groupData.length, n0 = Math.min(n, m), n1 = Math.max(n, m), updateNodes = [], enterNodes = [], exitNodes = [], node, nodeData; + var i, n = group.length, m = groupData.length, n0 = Math.min(n, m), updateNodes = new Array(m), enterNodes = new Array(m), exitNodes = new Array(n), node, nodeData; if (key) { - var nodeByKeyValue = new d3_Map(), keyValues = [], keyValue, j = groupData.length; + var nodeByKeyValue = new d3_Map(), dataByKeyValue = new d3_Map(), keyValues = [], keyValue; for (i = -1; ++i < n; ) { keyValue = key.call(node = group[i], node.__data__, i); if (nodeByKeyValue.has(keyValue)) { - exitNodes[j++] = node; + exitNodes[i] = node; } else { nodeByKeyValue.set(keyValue, node); } @@ -1701,14 +1703,13 @@ } for (i = -1; ++i < m; ) { keyValue = key.call(groupData, nodeData = groupData[i], i); - if (nodeByKeyValue.has(keyValue)) { - updateNodes[i] = node = nodeByKeyValue.get(keyValue); + if (node = nodeByKeyValue.get(keyValue)) { + updateNodes[i] = node; node.__data__ = nodeData; - enterNodes[i] = exitNodes[i] = null; - } else { + } else if (!dataByKeyValue.has(keyValue)) { enterNodes[i] = d3_selection_dataNode(nodeData); - updateNodes[i] = exitNodes[i] = null; } + dataByKeyValue.set(keyValue, nodeData); nodeByKeyValue.remove(keyValue); } for (i = -1; ++i < n; ) { @@ -1723,19 +1724,15 @@ if (node) { node.__data__ = nodeData; updateNodes[i] = node; - enterNodes[i] = exitNodes[i] = null; } else { enterNodes[i] = d3_selection_dataNode(nodeData); - updateNodes[i] = exitNodes[i] = null; } } for (;i < m; ++i) { enterNodes[i] = d3_selection_dataNode(groupData[i]); - updateNodes[i] = exitNodes[i] = null; } - for (;i < n1; ++i) { + for (;i < n; ++i) { exitNodes[i] = group[i]; - enterNodes[i] = updateNodes[i] = null; } } enterNodes.update = updateNodes; @@ -3769,7 +3766,7 @@ d3.behavior.zoom = function() { var translate = [ 0, 0 ], translate0, scale = 1, scale0, scaleExtent = d3_behavior_zoomInfinity, event = d3_eventDispatch(zoom, "zoom"), x0, x1, y0, y1, touchtime; function zoom() { - this.on("mousedown.zoom", mousedown).on("mousewheel.zoom", mousewheel).on("mousemove.zoom", mousemove).on("DOMMouseScroll.zoom", mousewheel).on("dblclick.zoom", dblclick).on("touchstart.zoom", touchstart).on("touchmove.zoom", touchmove).on("touchend.zoom", touchstart); + this.on("mousedown.zoom", mousedown).on("mousemove.zoom", mousemove).on(d3_behavior_zoomWheel + ".zoom", mousewheel).on("dblclick.zoom", dblclick).on("touchstart.zoom", touchstart).on("touchmove.zoom", touchmove).on("touchend.zoom", touchstart); } zoom.translate = function(x) { if (!arguments.length) return translate; @@ -3901,21 +3898,14 @@ } return d3.rebind(zoom, event, "on"); }; - var d3_behavior_zoomDiv, d3_behavior_zoomInfinity = [ 0, Infinity ]; - function d3_behavior_zoomDelta() { - if (!d3_behavior_zoomDiv) { - d3_behavior_zoomDiv = d3.select("body").append("div").style("visibility", "hidden").style("top", 0).style("height", 0).style("width", 0).style("overflow-y", "scroll").append("div").style("height", "2000px").node().parentNode; - } - var e = d3.event, delta; - try { - d3_behavior_zoomDiv.scrollTop = 1e3; - d3_behavior_zoomDiv.dispatchEvent(e); - delta = 1e3 - d3_behavior_zoomDiv.scrollTop; - } catch (error) { - delta = e.wheelDelta || -e.detail * 5; - } - return delta; - } + var d3_behavior_zoomInfinity = [ 0, Infinity ]; + var d3_behavior_zoomDelta, d3_behavior_zoomWheel = "onwheel" in document ? (d3_behavior_zoomDelta = function() { + return -d3.event.deltaY * (d3.event.deltaMode ? 120 : 1); + }, "wheel") : "onmousewheel" in document ? (d3_behavior_zoomDelta = function() { + return d3.event.wheelDelta; + }, "mousewheel") : (d3_behavior_zoomDelta = function() { + return -d3.event.detail; + }, "MozMousePixelScroll"); d3.layout = {}; d3.layout.bundle = function() { return function(links) { @@ -4625,10 +4615,8 @@ } d3.layout.hierarchy = function() { var sort = d3_layout_hierarchySort, children = d3_layout_hierarchyChildren, value = d3_layout_hierarchyValue; - function recurse(data, depth, nodes) { - var childs = children.call(hierarchy, data, depth), node = d3_layout_hierarchyInline ? data : { - data: data - }; + function recurse(node, depth, nodes) { + var childs = children.call(hierarchy, node, depth); node.depth = depth; nodes.push(node); if (childs && (n = childs.length)) { @@ -4642,7 +4630,7 @@ if (sort) c.sort(sort); if (value) node.value = v; } else if (value) { - node.value = +value.call(hierarchy, data, depth) || 0; + node.value = +value.call(hierarchy, node, depth) || 0; } return node; } @@ -4652,7 +4640,7 @@ var i = -1, n, j = depth + 1; while (++i < n) v += revalue(children[i], j); } else if (value) { - v = +value.call(hierarchy, d3_layout_hierarchyInline ? node : node.data, depth) || 0; + v = +value.call(hierarchy, node, depth) || 0; } if (value) node.value = v; return v; @@ -4685,11 +4673,8 @@ }; function d3_layout_hierarchyRebind(object, hierarchy) { d3.rebind(object, hierarchy, "sort", "children", "value"); + object.nodes = object; object.links = d3_layout_hierarchyLinks; - object.nodes = function(d) { - d3_layout_hierarchyInline = true; - return (object.nodes = object)(d); - }; return object; } function d3_layout_hierarchyChildren(d) { @@ -4711,7 +4696,6 @@ }); })); } - var d3_layout_hierarchyInline = false; d3.layout.pack = function() { var hierarchy = d3.layout.hierarchy().sort(d3_layout_packSort), padding = 0, size = [ 1, 1 ]; function pack(d, i) { @@ -5096,7 +5080,7 @@ function squarify(node) { var children = node.children; if (children && children.length) { - var rect = pad(node), row = [], remaining = children.slice(), child, best = Infinity, score, u = mode === "slice" ? rect.dx : mode === "dice" || mode === "slice-dice" && node.depth & 1 ? rect.dy : Math.min(rect.dx, rect.dy), n; + var rect = pad(node), row = [], remaining = children.slice(), child, best = Infinity, score, u = mode === "slice" ? rect.dx : mode === "dice" ? rect.dy : mode === "slice-dice" ? node.depth & 1 ? rect.dy : rect.dx : Math.min(rect.dx, rect.dy), n; scale(remaining, rect.dx * rect.dy / node.value); row.area = 0; while ((n = remaining.length) > 0) { @@ -5333,78 +5317,168 @@ d3.csv = d3_dsv(",", "text/csv"); d3.tsv = d3_dsv(" ", "text/tab-separated-values"); d3.geo = {}; - function d3_geo_type(types) { - for (var type in d3_geo_typeDefaults) { - if (!(type in types)) { - types[type] = d3_geo_typeDefaults[type]; + d3.geo.stream = function(object, listener) { + if (d3_geo_streamObjectType.hasOwnProperty(object.type)) { + d3_geo_streamObjectType[object.type](object, listener); + } else { + d3_geo_streamGeometry(object, listener); + } + }; + function d3_geo_streamGeometry(geometry, listener) { + if (d3_geo_streamGeometryType.hasOwnProperty(geometry.type)) { + d3_geo_streamGeometryType[geometry.type](geometry, listener); + } + } + var d3_geo_streamObjectType = { + Feature: function(feature, listener) { + d3_geo_streamGeometry(feature.geometry, listener); + }, + FeatureCollection: function(object, listener) { + var features = object.features, i = -1, n = features.length; + while (++i < n) d3_geo_streamGeometry(features[i].geometry, listener); + } + }; + var d3_geo_streamGeometryType = { + Sphere: function(object, listener) { + listener.sphere(); + }, + Point: function(object, listener) { + var coordinate = object.coordinates; + listener.point(coordinate[0], coordinate[1]); + }, + MultiPoint: function(object, listener) { + var coordinates = object.coordinates, i = -1, n = coordinates.length, coordinate; + while (++i < n) coordinate = coordinates[i], listener.point(coordinate[0], coordinate[1]); + }, + LineString: function(object, listener) { + d3_geo_streamLine(object.coordinates, listener, 0); + }, + MultiLineString: function(object, listener) { + var coordinates = object.coordinates, i = -1, n = coordinates.length; + while (++i < n) d3_geo_streamLine(coordinates[i], listener, 0); + }, + Polygon: function(object, listener) { + d3_geo_streamPolygon(object.coordinates, listener); + }, + MultiPolygon: function(object, listener) { + var coordinates = object.coordinates, i = -1, n = coordinates.length; + while (++i < n) d3_geo_streamPolygon(coordinates[i], listener); + }, + GeometryCollection: function(object, listener) { + var geometries = object.geometries, i = -1, n = geometries.length; + while (++i < n) d3_geo_streamGeometry(geometries[i], listener); + } + }; + function d3_geo_streamLine(coordinates, listener, closed) { + var i = -1, n = coordinates.length - closed, coordinate; + listener.lineStart(); + while (++i < n) coordinate = coordinates[i], listener.point(coordinate[0], coordinate[1]); + listener.lineEnd(); + } + function d3_geo_streamPolygon(coordinates, listener) { + var i = -1, n = coordinates.length; + listener.polygonStart(); + while (++i < n) d3_geo_streamLine(coordinates[i], listener, 1); + listener.polygonEnd(); + } + function d3_geo_spherical(cartesian) { + return [ Math.atan2(cartesian[1], cartesian[0]), Math.asin(Math.max(-1, Math.min(1, cartesian[2]))) ]; + } + function d3_geo_sphericalEqual(a, b) { + return Math.abs(a[0] - b[0]) < ε && Math.abs(a[1] - b[1]) < ε; + } + function d3_geo_cartesian(spherical) { + var λ = spherical[0], φ = spherical[1], cosφ = Math.cos(φ); + return [ cosφ * Math.cos(λ), cosφ * Math.sin(λ), Math.sin(φ) ]; + } + function d3_geo_cartesianDot(a, b) { + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; + } + function d3_geo_cartesianCross(a, b) { + return [ a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0] ]; + } + function d3_geo_cartesianAdd(a, b) { + a[0] += b[0]; + a[1] += b[1]; + a[2] += b[2]; + } + function d3_geo_cartesianScale(vector, k) { + return [ vector[0] * k, vector[1] * k, vector[2] * k ]; + } + function d3_geo_cartesianNormalize(d) { + var l = Math.sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]); + d[0] /= l; + d[1] /= l; + d[2] /= l; + } + function d3_geo_resample(project) { + var δ2 = .5, maxDepth = 16; + function resample(stream) { + var λ0, x0, y0, a0, b0, c0; + var resample = { + point: point, + lineStart: lineStart, + lineEnd: lineEnd, + polygonStart: function() { + stream.polygonStart(); + resample.lineStart = polygonLineStart; + }, + polygonEnd: function() { + stream.polygonEnd(); + resample.lineStart = lineStart; + } + }; + function point(x, y) { + x = project(x, y); + stream.point(x[0], x[1]); + } + function lineStart() { + x0 = NaN; + resample.point = linePoint; + stream.lineStart(); + } + function linePoint(λ, φ) { + var c = d3_geo_cartesian([ λ, φ ]), p = project(λ, φ); + resampleLineTo(x0, y0, λ0, a0, b0, c0, x0 = p[0], y0 = p[1], λ0 = λ, a0 = c[0], b0 = c[1], c0 = c[2], maxDepth, stream); + stream.point(x0, y0); + } + function lineEnd() { + resample.point = point; + stream.lineEnd(); + } + function polygonLineStart() { + var λ00, φ00, x00, y00, a00, b00, c00; + lineStart(); + resample.point = function(λ, φ) { + linePoint(λ00 = λ, φ00 = φ), x00 = x0, y00 = y0, a00 = a0, b00 = b0, c00 = c0; + resample.point = linePoint; + }; + resample.lineEnd = function() { + resampleLineTo(x0, y0, λ0, a0, b0, c0, x00, y00, λ00, a00, b00, c00, maxDepth, stream); + resample.lineEnd = lineEnd; + lineEnd(); + }; + } + return resample; + } + function resampleLineTo(x0, y0, λ0, a0, b0, c0, x1, y1, λ1, a1, b1, c1, depth, stream) { + var dx = x1 - x0, dy = y1 - y0, d2 = dx * dx + dy * dy; + if (d2 > 4 * δ2 && depth--) { + var a = a0 + a1, b = b0 + b1, c = c0 + c1, m = Math.sqrt(a * a + b * b + c * c), φ2 = Math.asin(c /= m), λ2 = Math.abs(Math.abs(c) - 1) < ε ? (λ0 + λ1) / 2 : Math.atan2(b, a), p = project(λ2, φ2), x2 = p[0], y2 = p[1], dx2 = x2 - x0, dy2 = y2 - y0, dz = dy * dx2 - dx * dy2; + if (dz * dz / d2 > δ2 || Math.abs((dx * dx2 + dy * dy2) / d2 - .5) > .3) { + resampleLineTo(x0, y0, λ0, a0, b0, c0, x2, y2, λ2, a /= m, b /= m, c, depth, stream); + stream.point(x2, y2); + resampleLineTo(x2, y2, λ2, a, b, c, x1, y1, λ1, a1, b1, c1, depth, stream); + } } } - return types; + resample.precision = function(_) { + if (!arguments.length) return Math.sqrt(δ2); + maxDepth = (δ2 = _ * _) > 0 && 16; + return resample; + }; + return resample; } - var d3_geo_typeDefaults = { - Feature: function(feature) { - this.geometry(feature.geometry); - }, - FeatureCollection: function(collection) { - var features = collection.features, i = -1, n = features.length; - while (++i < n) this.Feature(features[i]); - }, - GeometryCollection: function(collection) { - var geometries = collection.geometries, i = -1, n = geometries.length; - while (++i < n) this.geometry(geometries[i]); - }, - LineString: function(lineString) { - this.line(lineString.coordinates); - }, - MultiLineString: function(multiLineString) { - var coordinates = multiLineString.coordinates, i = -1, n = coordinates.length; - while (++i < n) this.line(coordinates[i]); - }, - MultiPoint: function(multiPoint) { - var coordinates = multiPoint.coordinates, i = -1, n = coordinates.length; - while (++i < n) this.point(coordinates[i]); - }, - MultiPolygon: function(multiPolygon) { - var coordinates = multiPolygon.coordinates, i = -1, n = coordinates.length; - while (++i < n) this.polygon(coordinates[i]); - }, - Point: function(point) { - this.point(point.coordinates); - }, - Polygon: function(polygon) { - this.polygon(polygon.coordinates); - }, - Sphere: d3_noop, - object: function(object) { - return d3_geo_typeObjects.hasOwnProperty(object.type) ? this[object.type](object) : this.geometry(object); - }, - geometry: function(geometry) { - return d3_geo_typeGeometries.hasOwnProperty(geometry.type) ? this[geometry.type](geometry) : null; - }, - point: d3_noop, - line: function(coordinates) { - var i = -1, n = coordinates.length; - while (++i < n) this.point(coordinates[i]); - }, - polygon: function(coordinates) { - var i = -1, n = coordinates.length; - while (++i < n) this.line(coordinates[i]); - } - }; - var d3_geo_typeGeometries = { - LineString: 1, - MultiLineString: 1, - MultiPoint: 1, - MultiPolygon: 1, - Point: 1, - Polygon: 1, - Sphere: 1 - }; - var d3_geo_typeObjects = { - Feature: 1, - FeatureCollection: 1, - GeometryCollection: 1 - }; d3.geo.albersUsa = function() { var lower48 = d3.geo.albers(); var alaska = d3.geo.albers().rotate([ 160, 0 ]).center([ 0, 60 ]).parallels([ 55, 65 ]); @@ -5417,15 +5491,6 @@ var lon = point[0], lat = point[1]; return lat > 50 ? alaska : lon < -140 ? hawaii : lat < 21 ? puertoRico : lower48; } - albersUsa.point = function(coordinates, context) { - return projection(coordinates).point(coordinates, context); - }; - albersUsa.line = function(coordinates, context) { - return projection(coordinates[0]).line(coordinates, context); - }; - albersUsa.polygon = function(coordinates, context) { - return projection(coordinates[0][0]).polygon(coordinates, context); - }; albersUsa.scale = function(x) { if (!arguments.length) return lower48.scale(); lower48.scale(x); @@ -5481,38 +5546,123 @@ return d3_geo_projection(d3_geo_azimuthalEquidistant); }).raw = d3_geo_azimuthalEquidistant; d3.geo.bounds = d3_geo_bounds(d3_identity); - function d3_geo_bounds(projection) { - var x0, y0, x1, y1, bounds = d3_geo_type({ - point: function(point) { - point = projection(point); - var x = point[0], y = point[1]; - if (x < x0) x0 = x; - if (x > x1) x1 = x; - if (y < y0) y0 = y; - if (y > y1) y1 = y; + function d3_geo_bounds(projectStream) { + var x0, y0, x1, y1; + var bound = { + point: boundPoint, + lineStart: d3_noop, + lineEnd: d3_noop, + polygonStart: function() { + bound.lineEnd = boundPolygonLineEnd; }, - polygon: function(coordinates) { - this.line(coordinates[0]); + polygonEnd: function() { + bound.point = boundPoint; } - }); + }; + function boundPoint(x, y) { + if (x < x0) x0 = x; + if (x > x1) x1 = x; + if (y < y0) y0 = y; + if (y > y1) y1 = y; + } + function boundPolygonLineEnd() { + bound.point = bound.lineEnd = d3_noop; + } return function(feature) { y1 = x1 = -(x0 = y0 = Infinity); - bounds.object(feature); + d3.geo.stream(feature, projectStream(bound)); return [ [ x0, y0 ], [ x1, y1 ] ]; }; } + d3.geo.centroid = function(object) { + d3_geo_centroidDimension = d3_geo_centroidW = d3_geo_centroidX = d3_geo_centroidY = d3_geo_centroidZ = 0; + d3.geo.stream(object, d3_geo_centroid); + var m; + if (d3_geo_centroidW && Math.abs(m = Math.sqrt(d3_geo_centroidX * d3_geo_centroidX + d3_geo_centroidY * d3_geo_centroidY + d3_geo_centroidZ * d3_geo_centroidZ)) > ε) { + return [ Math.atan2(d3_geo_centroidY, d3_geo_centroidX) * d3_degrees, Math.asin(Math.max(-1, Math.min(1, d3_geo_centroidZ / m))) * d3_degrees ]; + } + }; + var d3_geo_centroidDimension, d3_geo_centroidW, d3_geo_centroidX, d3_geo_centroidY, d3_geo_centroidZ; + var d3_geo_centroid = { + sphere: function() { + if (d3_geo_centroidDimension < 2) { + d3_geo_centroidDimension = 2; + d3_geo_centroidW = d3_geo_centroidX = d3_geo_centroidY = d3_geo_centroidZ = 0; + } + }, + point: d3_geo_centroidPoint, + lineStart: d3_geo_centroidLineStart, + lineEnd: d3_geo_centroidLineEnd, + polygonStart: function() { + if (d3_geo_centroidDimension < 2) { + d3_geo_centroidDimension = 2; + d3_geo_centroidW = d3_geo_centroidX = d3_geo_centroidY = d3_geo_centroidZ = 0; + } + d3_geo_centroid.lineStart = d3_geo_centroidRingStart; + }, + polygonEnd: function() { + d3_geo_centroid.lineStart = d3_geo_centroidLineStart; + } + }; + function d3_geo_centroidPoint(λ, φ) { + if (d3_geo_centroidDimension) return; + ++d3_geo_centroidW; + λ *= d3_radians; + var cosφ = Math.cos(φ *= d3_radians); + d3_geo_centroidX += (cosφ * Math.cos(λ) - d3_geo_centroidX) / d3_geo_centroidW; + d3_geo_centroidY += (cosφ * Math.sin(λ) - d3_geo_centroidY) / d3_geo_centroidW; + d3_geo_centroidZ += (Math.sin(φ) - d3_geo_centroidZ) / d3_geo_centroidW; + } + function d3_geo_centroidRingStart() { + var λ00, φ00; + d3_geo_centroidDimension = 1; + d3_geo_centroidLineStart(); + d3_geo_centroidDimension = 2; + var linePoint = d3_geo_centroid.point; + d3_geo_centroid.point = function(λ, φ) { + linePoint(λ00 = λ, φ00 = φ); + }; + d3_geo_centroid.lineEnd = function() { + d3_geo_centroid.point(λ00, φ00); + d3_geo_centroidLineEnd(); + d3_geo_centroid.lineEnd = d3_geo_centroidLineEnd; + }; + } + function d3_geo_centroidLineStart() { + var x0, y0, z0; + if (d3_geo_centroidDimension > 1) return; + if (d3_geo_centroidDimension < 1) { + d3_geo_centroidDimension = 1; + d3_geo_centroidW = d3_geo_centroidX = d3_geo_centroidY = d3_geo_centroidZ = 0; + } + d3_geo_centroid.point = function(λ, φ) { + λ *= d3_radians; + var cosφ = Math.cos(φ *= d3_radians); + x0 = cosφ * Math.cos(λ); + y0 = cosφ * Math.sin(λ); + z0 = Math.sin(φ); + d3_geo_centroid.point = nextPoint; + }; + function nextPoint(λ, φ) { + λ *= d3_radians; + var cosφ = Math.cos(φ *= d3_radians), x = cosφ * Math.cos(λ), y = cosφ * Math.sin(λ), z = Math.sin(φ), w = Math.atan2(Math.sqrt((w = y0 * z - z0 * y) * w + (w = z0 * x - x0 * z) * w + (w = x0 * y - y0 * x) * w), x0 * x + y0 * y + z0 * z); + d3_geo_centroidW += w; + d3_geo_centroidX += w * (x0 + (x0 = x)); + d3_geo_centroidY += w * (y0 + (y0 = y)); + d3_geo_centroidZ += w * (z0 + (z0 = z)); + } + } + function d3_geo_centroidLineEnd() { + d3_geo_centroid.point = d3_geo_centroidPoint; + } d3.geo.circle = function() { - var origin = [ 0, 0 ], angle, precision = 6, rotate, interpolate; + var origin = [ 0, 0 ], angle, precision = 6, interpolate; function circle() { - var o = typeof origin === "function" ? origin.apply(this, arguments) : origin; - rotate = d3_geo_rotation(-o[0] * d3_radians, -o[1] * d3_radians, 0); - var ring = []; + var center = typeof origin === "function" ? origin.apply(this, arguments) : origin, rotate = d3_geo_rotation(-center[0] * d3_radians, -center[1] * d3_radians, 0).invert, ring = []; interpolate(null, null, 1, { - lineTo: function(λ, φ) { - var point = rotate.invert(λ, φ); - point[0] *= d3_degrees; - point[1] *= d3_degrees; - ring.push(point); + point: function(x, y) { + ring.push(x = rotate(x, y)); + x[0] *= d3_degrees, x[1] *= d3_degrees; } }); return { @@ -5537,84 +5687,9 @@ }; return circle.angle(90); }; - function d3_geo_circleClip(degrees, rotate) { - var radians = degrees * d3_radians, cr = Math.cos(radians), interpolate = d3_geo_circleInterpolate(radians, 6 * d3_radians); - return { - point: function(coordinates, context) { - if (visible(coordinates = rotate(coordinates))) { - context.point(coordinates[0], coordinates[1]); - } - }, - line: function(coordinates, context) { - clipLine(coordinates, context); - }, - polygon: function(polygon, context) { - d3_geo_circleClipPolygon(polygon, context, clipLine, interpolate); - }, - sphere: function(context) { - d3_geo_projectionSphere(context, interpolate); - } - }; - function visible(point) { - return Math.cos(point[1]) * Math.cos(point[0]) > cr; - } - function clipLine(coordinates, context, ring) { - if (!(n = coordinates.length)) return [ ring && 0, false ]; - var point0 = rotate(coordinates[0]), point1, point2, v0 = visible(point0), v00 = ring && v0, v, n, clean = ring, area = 0, p, x0, x, y0, y; - if (clean) { - x0 = (p = d3_geo_stereographic(point0[0] + (v0 ? 0 : π), point0[1]))[0]; - y0 = p[1]; - } - if (v0) context.moveTo(point0[0], point0[1]); - for (var i = 1; i < n; i++) { - point1 = rotate(coordinates[i]); - v = visible(point1); - if (v !== v0) { - point2 = intersect(point0, point1); - if (d3_geo_circlePointsEqual(point0, point2) || d3_geo_circlePointsEqual(point1, point2)) { - point1[0] += ε; - point1[1] += ε; - v = visible(point1); - } - } - if (v !== v0) { - clean = false; - if (v0 = v) { - point2 = intersect(point1, point0); - context.moveTo(point2[0], point2[1]); - } else { - point2 = intersect(point0, point1); - context.lineTo(point2[0], point2[1]); - } - point0 = point2; - } - if (clean) { - p = d3_geo_stereographic(point1[0] + (v ? 0 : π), point1[1]); - x = p[0]; - y = p[1]; - area += y0 * x - x0 * y; - x0 = x; - y0 = y; - } - if (v && !d3_geo_circlePointsEqual(point0, point1)) context.lineTo(point1[0], point1[1]); - point0 = point1; - } - return [ clean && area * .5, v00 && v ]; - } - function intersect(a, b) { - var pa = d3_geo_circleCartesian(a, [ 0, 0, 0 ]), pb = d3_geo_circleCartesian(b, [ 0, 0, 0 ]); - var n1 = [ 1, 0, 0 ], n2 = d3_geo_circleCross(pa, pb), n2n2 = d3_geo_circleDot(n2, n2), n1n2 = n2[0], determinant = n2n2 - n1n2 * n1n2; - if (!determinant) return a; - var c1 = cr * n2n2 / determinant, c2 = -cr * n1n2 / determinant, n1xn2 = d3_geo_circleCross(n1, n2), A = d3_geo_circleScale(n1, c1), B = d3_geo_circleScale(n2, c2); - d3_geo_circleAdd(A, B); - var u = n1xn2, w = d3_geo_circleDot(A, u), uu = d3_geo_circleDot(u, u), t = Math.sqrt(w * w - uu * (d3_geo_circleDot(A, A) - 1)), q = d3_geo_circleScale(u, (-w - t) / uu); - d3_geo_circleAdd(q, A); - return d3_geo_circleSpherical(q); - } - } function d3_geo_circleInterpolate(radians, precision) { var cr = Math.cos(radians), sr = Math.sin(radians); - return function(from, to, direction, context) { + return function(from, to, direction, listener) { if (from != null) { from = d3_geo_circleAngle(cr, from); to = d3_geo_circleAngle(cr, to); @@ -5623,38 +5698,110 @@ from = radians + direction * 2 * π; to = radians; } + var point; for (var step = direction * precision, t = from; direction > 0 ? t > to : t < to; t -= step) { - var c = Math.cos(t), s = Math.sin(t), point = d3_geo_circleSpherical([ cr, -sr * c, -sr * s ]); - context.lineTo(point[0], point[1]); + listener.point((point = d3_geo_spherical([ cr, -sr * Math.cos(t), -sr * Math.sin(t) ]))[0], point[1]); } }; } - function d3_geo_circleClipPolygon(coordinates, context, clipLine, interpolate) { - var subject = [], clip = [], segments = [], buffer = d3_geo_circleBufferSegments(clipLine), draw = [], visibleArea = 0, invisibleArea = 0, invisible = false; - coordinates.forEach(function(ring) { - var x = buffer(ring, context), ringSegments = x[1], segment, n = ringSegments.length; - if (!n) { - invisible = true; - invisibleArea += x[0][0]; - return; + function d3_geo_circleAngle(cr, point) { + var a = d3_geo_cartesian(point); + a[0] -= cr; + d3_geo_cartesianNormalize(a); + var angle = Math.acos(Math.max(-1, Math.min(1, -a[1]))); + return ((-a[2] < 0 ? -angle : angle) + 2 * Math.PI - ε) % (2 * Math.PI); + } + function d3_geo_clip(pointVisible, clipLine, interpolate) { + return function(listener) { + var line = clipLine(listener); + var clip = { + point: point, + lineStart: lineStart, + lineEnd: lineEnd, + polygonStart: function() { + clip.point = pointRing; + clip.lineStart = ringStart; + clip.lineEnd = ringEnd; + invisible = false; + invisibleArea = visibleArea = 0; + segments = []; + listener.polygonStart(); + }, + polygonEnd: function() { + clip.point = point; + clip.lineStart = lineStart; + clip.lineEnd = lineEnd; + segments = d3.merge(segments); + if (segments.length) { + d3_geo_clipPolygon(segments, interpolate, listener); + } else if (visibleArea < -ε || invisible && invisibleArea < -ε) { + listener.lineStart(); + interpolate(null, null, 1, listener); + listener.lineEnd(); + } + listener.polygonEnd(); + segments = null; + }, + sphere: function() { + listener.polygonStart(); + listener.lineStart(); + interpolate(null, null, 1, listener); + listener.lineEnd(); + listener.polygonEnd(); + } + }; + function point(λ, φ) { + if (pointVisible(λ, φ)) listener.point(λ, φ); } - if (x[0][0] !== false) { - visibleArea += x[0][0]; - draw.push(segment = ringSegments[0]); - var point = segment[0], n = segment.length - 1, i = 0; - context.moveTo(point[0], point[1]); - while (++i < n) context.lineTo((point = segment[i])[0], point[1]); - context.closePath(); - return; + function pointLine(λ, φ) { + line.point(λ, φ); } - if (n > 1 && x[0][1]) ringSegments.push(ringSegments.pop().concat(ringSegments.shift())); - segments = segments.concat(ringSegments.filter(d3_geo_circleSegmentLength1)); - }); - if (!segments.length) { - if (visibleArea < 0 || invisible && invisibleArea < 0) { - d3_geo_projectionSphere(context, interpolate); + function lineStart() { + clip.point = pointLine; + line.lineStart(); } - } + function lineEnd() { + clip.point = point; + line.lineEnd(); + } + var segments, visibleArea, invisibleArea, invisible; + var buffer = d3_geo_clipBufferListener(), ringListener = clipLine(buffer), ring; + function pointRing(λ, φ) { + ringListener.point(λ, φ); + ring.push([ λ, φ ]); + } + function ringStart() { + ringListener.lineStart(); + ring = []; + } + function ringEnd() { + pointRing(ring[0][0], ring[0][1]); + ringListener.lineEnd(); + var clean = ringListener.clean(), ringSegments = buffer.buffer(), segment, n = ringSegments.length; + if (!n) { + invisible = true; + invisibleArea += d3_geo_clipAreaRing(ring, -1); + ring = null; + return; + } + ring = null; + if (clean & 1) { + segment = ringSegments[0]; + visibleArea += d3_geo_clipAreaRing(segment, 1); + var n = segment.length - 1, i = -1, point; + listener.lineStart(); + while (++i < n) listener.point((point = segment[i])[0], point[1]); + listener.lineEnd(); + return; + } + if (n > 1 && clean & 2) ringSegments.push(ringSegments.pop().concat(ringSegments.shift())); + segments.push(ringSegments.filter(d3_geo_clipSegmentLength1)); + } + return clip; + }; + } + function d3_geo_clipPolygon(segments, interpolate, listener) { + var subject = [], clip = []; segments.forEach(function(segment) { var n = segment.length; if (n <= 1) return; @@ -5696,112 +5843,224 @@ subject.push(a); clip.push(b); }); - clip.sort(d3_geo_circleClipSort); - d3_geo_circleLinkCircular(subject); - d3_geo_circleLinkCircular(clip); + clip.sort(d3_geo_clipSort); + d3_geo_clipLinkCircular(subject); + d3_geo_clipLinkCircular(clip); if (!subject.length) return; var start = subject[0], current, points, point; while (1) { current = start; while (current.visited) if ((current = current.next) === start) return; points = current.points; - context.moveTo((point = points.shift())[0], point[1]); + listener.lineStart(); do { current.visited = current.other.visited = true; if (current.entry) { if (current.subject) { - for (var i = 0; i < points.length; i++) context.lineTo((point = points[i])[0], point[1]); + for (var i = 0; i < points.length; i++) listener.point((point = points[i])[0], point[1]); } else { - interpolate(current.point, current.next.point, 1, context); + interpolate(current.point, current.next.point, 1, listener); } current = current.next; } else { if (current.subject) { points = current.prev.points; - for (var i = points.length; --i >= 0; ) context.lineTo((point = points[i])[0], point[1]); + for (var i = points.length; --i >= 0; ) listener.point((point = points[i])[0], point[1]); } else { - interpolate(current.point, current.prev.point, -1, context); + interpolate(current.point, current.prev.point, -1, listener); } current = current.prev; } current = current.other; points = current.points; } while (!current.visited); - context.closePath(); + listener.lineEnd(); } } - function d3_geo_circleLinkCircular(array) { - for (var i = 0, a = array[0], b, n = array.length; i < n; ) { - a.next = b = array[++i % n]; + function d3_geo_clipLinkCircular(array) { + if (!(n = array.length)) return; + var n, i = 0, a = array[0], b; + while (++i < n) { + a.next = b = array[i]; b.prev = a; a = b; } + a.next = b = array[0]; + b.prev = a; } - function d3_geo_circleClipSort(a, b) { + function d3_geo_clipSort(a, b) { return ((a = a.point)[0] < 0 ? a[1] - π / 2 - ε : π / 2 - a[1]) - ((b = b.point)[0] < 0 ? b[1] - π / 2 - ε : π / 2 - b[1]); } - function d3_geo_circleAngle(cr, point) { - var a = d3_geo_circleCartesian(point, [ cr, 0, 0 ]); - d3_geo_circleNormalize(a); - var angle = Math.acos(Math.max(-1, Math.min(1, -a[1]))); - return ((-a[2] < 0 ? -angle : angle) + 2 * Math.PI - ε) % (2 * Math.PI); - } - function d3_geo_circleCartesian(point, origin) { - var p0 = point[0], p1 = point[1], c1 = Math.cos(p1); - return [ c1 * Math.cos(p0) - origin[0], c1 * Math.sin(p0) - origin[1], Math.sin(p1) - origin[2] ]; - } - function d3_geo_circleSpherical(point) { - return [ Math.atan2(point[1], point[0]), Math.asin(Math.max(-1, Math.min(1, point[2]))) ]; - } - function d3_geo_circleDot(a, b) { - return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; - } - function d3_geo_circleCross(a, b) { - return [ a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0] ]; - } - function d3_geo_circleAdd(a, b) { - a[0] += b[0]; - a[1] += b[1]; - a[2] += b[2]; - } - function d3_geo_circleScale(vector, s) { - return [ vector[0] * s, vector[1] * s, vector[2] * s ]; - } - function d3_geo_circleNormalize(d) { - var l = Math.sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]); - d[0] /= l; - d[1] /= l; - d[2] /= l; - } - function d3_geo_circleBufferSegments(f) { - return function(coordinates) { - var segments = [], segment; - return [ f(coordinates, { - moveTo: function(x, y) { - segments.push(segment = [ [ x, y ] ]); - }, - lineTo: function(x, y) { - segment.push([ x, y ]); - } - }, true), segments ]; - }; - } - function d3_geo_circlePointsEqual(a, b) { - return Math.abs(a[0] - b[0]) < ε && Math.abs(a[1] - b[1]) < ε; - } - function d3_geo_circleSegmentLength1(segment) { + function d3_geo_clipSegmentLength1(segment) { return segment.length > 1; } + function d3_geo_clipBufferListener() { + var lines = [], line; + return { + lineStart: function() { + lines.push(line = []); + }, + point: function(λ, φ) { + line.push([ λ, φ ]); + }, + lineEnd: d3_noop, + buffer: function() { + var buffer = lines; + lines = []; + line = null; + return buffer; + } + }; + } + function d3_geo_clipAreaRing(ring, invisible) { + if (!(n = ring.length)) return 0; + var n, i = 0, area = 0, p = ring[0], λ = p[0], φ = p[1], cosφ = Math.cos(φ), x0 = Math.atan2(invisible * Math.sin(λ) * cosφ, Math.sin(φ)), y0 = 1 - invisible * Math.cos(λ) * cosφ, x1 = x0, x, y; + while (++i < n) { + p = ring[i]; + cosφ = Math.cos(φ = p[1]); + x = Math.atan2(invisible * Math.sin(λ = p[0]) * cosφ, Math.sin(φ)); + y = 1 - invisible * Math.cos(λ) * cosφ; + if (Math.abs(y0 - 2) < ε && Math.abs(y - 2) < ε) continue; + if (Math.abs(y) < ε || Math.abs(y0) < ε) {} else if (Math.abs(Math.abs(x - x0) - π) < ε) { + if (y + y0 > 2) area += 4 * (x - x0); + } else if (Math.abs(y0 - 2) < ε) area += 4 * (x - x1); else area += ((3 * π + x - x0) % (2 * π) - π) * (y0 + y); + x1 = x0, x0 = x, y0 = y; + } + return area; + } + var d3_geo_clipAntimeridian = d3_geo_clip(d3_true, d3_geo_clipAntimeridianLine, d3_geo_clipAntimeridianInterpolate); + function d3_geo_clipAntimeridianLine(listener) { + var λ0 = NaN, φ0 = NaN, sλ0 = NaN, clean; + return { + lineStart: function() { + listener.lineStart(); + clean = 1; + }, + point: function(λ1, φ1) { + var sλ1 = λ1 > 0 ? π : -π, dλ = Math.abs(λ1 - λ0); + if (Math.abs(dλ - π) < ε) { + listener.point(λ0, φ0 = (φ0 + φ1) / 2 > 0 ? π / 2 : -π / 2); + listener.point(sλ0, φ0); + listener.lineEnd(); + listener.lineStart(); + listener.point(sλ1, φ0); + listener.point(λ1, φ0); + clean = 0; + } else if (sλ0 !== sλ1 && dλ >= π) { + if (Math.abs(λ0 - sλ0) < ε) λ0 -= sλ0 * ε; + if (Math.abs(λ1 - sλ1) < ε) λ1 -= sλ1 * ε; + φ0 = d3_geo_clipAntimeridianIntersect(λ0, φ0, λ1, φ1); + listener.point(sλ0, φ0); + listener.lineEnd(); + listener.lineStart(); + listener.point(sλ1, φ0); + clean = 0; + } + listener.point(λ0 = λ1, φ0 = φ1); + sλ0 = sλ1; + }, + lineEnd: function() { + listener.lineEnd(); + λ0 = φ0 = NaN; + }, + clean: function() { + return 2 - clean; + } + }; + } + function d3_geo_clipAntimeridianIntersect(λ0, φ0, λ1, φ1) { + var cosφ0, cosφ1, sinλ0_λ1 = Math.sin(λ0 - λ1); + return Math.abs(sinλ0_λ1) > ε ? Math.atan((Math.sin(φ0) * (cosφ1 = Math.cos(φ1)) * Math.sin(λ1) - Math.sin(φ1) * (cosφ0 = Math.cos(φ0)) * Math.sin(λ0)) / (cosφ0 * cosφ1 * sinλ0_λ1)) : (φ0 + φ1) / 2; + } + function d3_geo_clipAntimeridianInterpolate(from, to, direction, listener) { + var φ; + if (from == null) { + φ = direction * π / 2; + listener.point(-π, φ); + listener.point(0, φ); + listener.point(π, φ); + listener.point(π, 0); + listener.point(π, -φ); + listener.point(0, -φ); + listener.point(-π, -φ); + listener.point(-π, 0); + listener.point(-π, φ); + } else if (Math.abs(from[0] - to[0]) > ε) { + var s = (from[0] < to[0] ? 1 : -1) * π; + φ = direction * s / 2; + listener.point(-s, φ); + listener.point(0, φ); + listener.point(s, φ); + } else { + listener.point(to[0], to[1]); + } + } + function d3_geo_clipCircle(degrees) { + var radians = degrees * d3_radians, cr = Math.cos(radians), interpolate = d3_geo_circleInterpolate(radians, 6 * d3_radians); + return d3_geo_clip(visible, clipLine, interpolate); + function visible(λ, φ) { + return Math.cos(λ) * Math.cos(φ) > cr; + } + function clipLine(listener) { + var point0, v0, v00, clean; + return { + lineStart: function() { + v00 = v0 = false; + clean = 1; + }, + point: function(λ, φ) { + var point1 = [ λ, φ ], point2, v = visible(λ, φ); + if (!point0 && (v00 = v0 = v)) listener.lineStart(); + if (v !== v0) { + point2 = intersect(point0, point1); + if (d3_geo_sphericalEqual(point0, point2) || d3_geo_sphericalEqual(point1, point2)) { + point1[0] += ε; + point1[1] += ε; + v = visible(point1[0], point1[1]); + } + } + if (v !== v0) { + clean = 0; + if (v0 = v) { + listener.lineStart(); + point2 = intersect(point1, point0); + listener.point(point2[0], point2[1]); + } else { + point2 = intersect(point0, point1); + listener.point(point2[0], point2[1]); + listener.lineEnd(); + } + point0 = point2; + } + if (v && (!point0 || !d3_geo_sphericalEqual(point0, point1))) listener.point(point1[0], point1[1]); + point0 = point1; + }, + lineEnd: function() { + if (v0) listener.lineEnd(); + point0 = null; + }, + clean: function() { + return clean | (v00 && v0) << 1; + } + }; + } + function intersect(a, b) { + var pa = d3_geo_cartesian(a, 0), pb = d3_geo_cartesian(b, 0); + var n1 = [ 1, 0, 0 ], n2 = d3_geo_cartesianCross(pa, pb), n2n2 = d3_geo_cartesianDot(n2, n2), n1n2 = n2[0], determinant = n2n2 - n1n2 * n1n2; + if (!determinant) return a; + var c1 = cr * n2n2 / determinant, c2 = -cr * n1n2 / determinant, n1xn2 = d3_geo_cartesianCross(n1, n2), A = d3_geo_cartesianScale(n1, c1), B = d3_geo_cartesianScale(n2, c2); + d3_geo_cartesianAdd(A, B); + var u = n1xn2, w = d3_geo_cartesianDot(A, u), uu = d3_geo_cartesianDot(u, u), t = Math.sqrt(w * w - uu * (d3_geo_cartesianDot(A, A) - 1)), q = d3_geo_cartesianScale(u, (-w - t) / uu); + d3_geo_cartesianAdd(q, A); + return d3_geo_spherical(q); + } + } function d3_geo_compose(a, b) { - if (a === d3_geo_equirectangular) return b; - if (b === d3_geo_equirectangular) return a; - function compose(λ, φ) { - var coordinates = a(λ, φ); - return b(coordinates[0], coordinates[1]); + function compose(x, y) { + return x = a(x, y), b(x[0], x[1]); } if (a.invert && b.invert) compose.invert = function(x, y) { - var coordinates = b.invert(x, y); - return a.invert(coordinates[0], coordinates[1]); + return x = b.invert(x, y), x && a.invert(x[0], x[1]); }; return compose; } @@ -5821,12 +6080,15 @@ var x1, x0, y1, y0, dx = 22.5, dy = dx, x, y, precision = 2.5; function graticule() { return { - type: "GeometryCollection", - geometries: graticule.lines() + type: "MultiLineString", + coordinates: lines() }; } + function lines() { + return d3.range(Math.ceil(x0 / dx) * dx, x1, dx).map(x).concat(d3.range(Math.ceil(y0 / dy) * dy, y1, dy).map(y)); + } graticule.lines = function() { - return d3.range(Math.ceil(x0 / dx) * dx, x1, dx).map(x).concat(d3.range(Math.ceil(y0 / dy) * dy, y1, dy).map(y)).map(function(coordinates) { + return lines().map(function(coordinates) { return { type: "LineString", coordinates: coordinates @@ -5877,11 +6139,23 @@ }); }; } + d3.geo.interpolate = function(source, target) { + return d3_geo_interpolate(source[0] * d3_radians, source[1] * d3_radians, target[0] * d3_radians, target[1] * d3_radians); + }; + function d3_geo_interpolate(x0, y0, x1, y1) { + var cy0 = Math.cos(y0), sy0 = Math.sin(y0), cy1 = Math.cos(y1), sy1 = Math.sin(y1), kx0 = cy0 * Math.cos(x0), ky0 = cy0 * Math.sin(x0), kx1 = cy1 * Math.cos(x1), ky1 = cy1 * Math.sin(x1), d = Math.acos(Math.max(-1, Math.min(1, sy0 * sy1 + cy0 * cy1 * Math.cos(x1 - x0)))), k = 1 / Math.sin(d); + function interpolate(t) { + var B = Math.sin(t *= d) * k, A = Math.sin(d - t) * k, x = A * kx0 + B * kx1, y = A * ky0 + B * ky1, z = A * sy0 + B * sy1; + return [ Math.atan2(y, x) / d3_radians, Math.atan2(z, Math.sqrt(x * x + y * y)) / d3_radians ]; + } + interpolate.distance = d; + return interpolate; + } d3.geo.greatArc = function() { - var source = d3_source, p0, target = d3_target, p1, precision = 6 * d3_radians, interpolate = d3_geo_greatArcInterpolator(); + var source = d3_source, source_, target = d3_target, target_, precision = 6 * d3_radians, interpolate; function greatArc() { - var d = greatArc.distance.apply(this, arguments), t = 0, dt = precision / d, coordinates = [ p0 ]; - while ((t += dt) < 1) coordinates.push(interpolate(t)); + var p0 = source_ || source.apply(this, arguments), p1 = target_ || target.apply(this, arguments), i = interpolate || d3.geo.interpolate(p0, p1), t = 0, dt = precision / i.distance, coordinates = [ p0 ]; + while ((t += dt) < 1) coordinates.push(i(t)); coordinates.push(p1); return { type: "LineString", @@ -5889,20 +6163,18 @@ }; } greatArc.distance = function() { - if (typeof source === "function") interpolate.source(p0 = source.apply(this, arguments)); - if (typeof target === "function") interpolate.target(p1 = target.apply(this, arguments)); - return interpolate.distance(); + return (interpolate || d3.geo.interpolate(source_ || source.apply(this, arguments), target_ || target.apply(this, arguments))).distance; }; greatArc.source = function(_) { if (!arguments.length) return source; - source = _; - if (typeof source !== "function") interpolate.source(p0 = source); + source = _, source_ = typeof _ === "function" ? null : _; + interpolate = source_ && target_ ? d3.geo.interpolate(source_, target_) : null; return greatArc; }; greatArc.target = function(_) { if (!arguments.length) return target; - target = _; - if (typeof target !== "function") interpolate.target(p1 = target); + target = _, target_ = typeof _ === "function" ? null : _; + interpolate = source_ && target_ ? d3.geo.interpolate(source_, target_) : null; return greatArc; }; greatArc.precision = function(_) { @@ -5912,36 +6184,6 @@ }; return greatArc; }; - function d3_geo_greatArcInterpolator() { - var x0, y0, cy0, sy0, kx0, ky0, x1, y1, cy1, sy1, kx1, ky1, d, k; - function interpolate(t) { - var B = Math.sin(t *= d) * k, A = Math.sin(d - t) * k, x = A * kx0 + B * kx1, y = A * ky0 + B * ky1, z = A * sy0 + B * sy1; - return [ Math.atan2(y, x) / d3_radians, Math.atan2(z, Math.sqrt(x * x + y * y)) / d3_radians ]; - } - interpolate.distance = function() { - if (d == null) k = 1 / Math.sin(d = Math.acos(Math.max(-1, Math.min(1, sy0 * sy1 + cy0 * cy1 * Math.cos(x1 - x0))))); - return d; - }; - interpolate.source = function(_) { - var cx0 = Math.cos(x0 = _[0] * d3_radians), sx0 = Math.sin(x0); - cy0 = Math.cos(y0 = _[1] * d3_radians); - sy0 = Math.sin(y0); - kx0 = cy0 * cx0; - ky0 = cy0 * sx0; - d = null; - return interpolate; - }; - interpolate.target = function(_) { - var cx1 = Math.cos(x1 = _[0] * d3_radians), sx1 = Math.sin(x1); - cy1 = Math.cos(y1 = _[1] * d3_radians); - sy1 = Math.sin(y1); - kx1 = cy1 * cx1; - ky1 = cy1 * sx1; - d = null; - return interpolate; - }; - return interpolate; - } function d3_geo_mercator(λ, φ) { return [ λ / (2 * π), Math.max(-.5, Math.min(+.5, Math.log(Math.tan(π / 4 + φ / 2)) / (2 * π))) ]; } @@ -5958,203 +6200,290 @@ return d3_geo_projection(d3_geo_orthographic); }).raw = d3_geo_orthographic; d3.geo.path = function() { - var pointRadius = 4.5, pointCircle = d3_geo_pathCircle(pointRadius), projection = d3.geo.albersUsa(), bounds, buffer = []; - var bufferContext = { - point: function(x, y) { - buffer.push("M", x, ",", y, pointCircle); - }, - moveTo: function(x, y) { - buffer.push("M", x, ",", y); - }, - lineTo: function(x, y) { - buffer.push("L", x, ",", y); - }, - closePath: function() { - buffer.push("Z"); - } - }; - var area, centroidWeight, x00, y00, x0, y0, cx, cy; - var areaContext = { - point: d3_noop, - moveTo: moveTo, - lineTo: function(x, y) { - area += y0 * x - x0 * y; - x0 = x; - y0 = y; - }, - closePath: closePath - }; - var lineCentroidContext = { - point: function(x, y) { - cx += x; - cy += y; - ++centroidWeight; - }, - moveTo: moveTo, - lineTo: function(x, y) { - var dx = x - x0, dy = y - y0, δ = Math.sqrt(dx * dx + dy * dy); - centroidWeight += δ; - cx += δ * (x0 + x) / 2; - cy += δ * (y0 + y) / 2; - x0 = x; - y0 = y; - }, - closePath: closePath - }; - var polygonCentroidContext = { - point: d3_noop, - moveTo: moveTo, - lineTo: function(x, y) { - var δ = y0 * x - x0 * y; - centroidWeight += δ * 3; - cx += δ * (x0 + x); - cy += δ * (y0 + y); - x0 = x; - y0 = y; - }, - closePath: closePath - }; - function moveTo(x, y) { - x00 = x0 = x; - y00 = y0 = y; - } - function closePath() { - this.lineTo(x00, y00); - } - var context = bufferContext; + var pointRadius = 4.5, projection, context, projectStream, contextStream; function path(object) { - var result = null; - if (object != result) { - if (typeof pointRadius === "function") pointCircle = d3_geo_pathCircle(pointRadius.apply(this, arguments)); - pathType.object(object); - if (buffer.length) result = buffer.join(""), buffer = []; - } - return result; - } - var pathType = d3_geo_type({ - line: function(coordinates) { - projection.line(coordinates, context); - }, - polygon: function(coordinates) { - projection.polygon(coordinates, context); - }, - point: function(coordinates) { - projection.point(coordinates, context); - }, - Sphere: function() { - projection.sphere(context); - } - }); - var areaType = d3_geo_type({ - Feature: function(feature) { - return areaType.geometry(feature.geometry); - }, - FeatureCollection: function(collection) { - return d3.sum(collection.features, areaType.Feature); - }, - GeometryCollection: function(collection) { - return d3.sum(collection.geometries, areaType.geometry); - }, - LineString: d3_zero, - MultiLineString: d3_zero, - MultiPoint: d3_zero, - MultiPolygon: function(multiPolygon) { - return d3.sum(multiPolygon.coordinates, polygonArea); - }, - Point: d3_zero, - Polygon: function(polygon) { - return polygonArea(polygon.coordinates); - }, - Sphere: sphereArea - }); - function polygonArea(coordinates) { - area = 0; - projection.polygon(coordinates, areaContext); - return Math.abs(area) / 2; - } - function sphereArea() { - area = 0; - projection.sphere(areaContext); - return Math.abs(area) / 2; + if (object) d3.geo.stream(object, projectStream(contextStream.pointRadius(typeof pointRadius === "function" ? +pointRadius.apply(this, arguments) : pointRadius))); + return contextStream.result(); } path.area = function(object) { - return areaType.object(object); - }; - var centroidType = d3_geo_type({ - Feature: function(feature) { - return centroidType.geometry(feature.geometry); - }, - LineString: weightedCentroid(function(lineString) { - projection.line(lineString.coordinates, lineCentroidContext); - }), - MultiLineString: weightedCentroid(function(multiLineString) { - var coordinates = multiLineString.coordinates, i = -1, n = coordinates.length; - while (++i < n) projection.line(coordinates[i], lineCentroidContext); - }), - MultiPoint: weightedCentroid(function(multiPoint) { - var coordinates = multiPoint.coordinates, i = -1, n = coordinates.length; - while (++i < n) projection.point(coordinates[i], lineCentroidContext); - }), - MultiPolygon: weightedCentroid(function(multiPolygon) { - var coordinates = multiPolygon.coordinates, i = -1, n = coordinates.length; - while (++i < n) projection.polygon(coordinates[i], polygonCentroidContext); - }), - Point: weightedCentroid(function(point) { - projection.point(point.coordinates, lineCentroidContext); - }), - Polygon: weightedCentroid(function(polygon) { - projection.polygon(polygon.coordinates, polygonCentroidContext); - }), - Sphere: weightedCentroid(function() { - projection.sphere(polygonCentroidContext); - }) - }); - function weightedCentroid(f) { - return function() { - centroidWeight = cx = cy = 0; - f.apply(this, arguments); - return centroidWeight ? [ cx / centroidWeight, cy / centroidWeight ] : null; - }; - } - path.bounds = function(object) { - return (bounds || (bounds = d3_geo_bounds(projection)))(object); + d3_geo_pathAreaSum = 0; + d3.geo.stream(object, projectStream(d3_geo_pathArea)); + return d3_geo_pathAreaSum; }; path.centroid = function(object) { - return centroidType.object(object); + d3_geo_centroidDimension = d3_geo_centroidX = d3_geo_centroidY = d3_geo_centroidZ = 0; + d3.geo.stream(object, projectStream(d3_geo_pathCentroid)); + return d3_geo_centroidZ ? [ d3_geo_centroidX / d3_geo_centroidZ, d3_geo_centroidY / d3_geo_centroidZ ] : undefined; + }; + path.bounds = function(object) { + return d3_geo_bounds(projectStream)(object); }; path.projection = function(_) { if (!arguments.length) return projection; - projection = _; - bounds = null; + projectStream = (projection = _) ? _.stream || d3_geo_pathProjectStream(_) : d3_identity; return path; }; path.context = function(_) { - if (!arguments.length) return context === bufferContext ? null : context; - context = _; - if (context == null) context = bufferContext; + if (!arguments.length) return context; + contextStream = (context = _) == null ? new d3_geo_pathBuffer() : new d3_geo_pathContext(_); return path; }; - path.pointRadius = function(x) { + path.pointRadius = function(_) { if (!arguments.length) return pointRadius; - if (typeof x === "function") pointRadius = x; else pointCircle = d3_geo_pathCircle(pointRadius = +x); + pointRadius = typeof _ === "function" ? _ : +_; return path; }; - return path; + return path.projection(d3.geo.albersUsa()).context(null); }; function d3_geo_pathCircle(radius) { return "m0," + radius + "a" + radius + "," + radius + " 0 1,1 0," + -2 * radius + "a" + radius + "," + radius + " 0 1,1 0," + +2 * radius + "z"; } - var d3_geo_pathIdentity = d3.geo.path().projection({ - polygon: function(polygon, context) { - polygon.forEach(function(ring) { - var n = ring.length, i = 0, point; - context.moveTo((point = ring[0])[0], point[1]); - while (++i < n) context.lineTo((point = ring[i])[0], point[1]); - context.closePath(); - }); + function d3_geo_pathProjectStream(project) { + var resample = d3_geo_resample(function(λ, φ) { + return project([ λ * d3_degrees, φ * d3_degrees ]); + }); + return function(stream) { + stream = resample(stream); + return { + point: function(λ, φ) { + stream.point(λ * d3_radians, φ * d3_radians); + }, + sphere: function() { + stream.sphere(); + }, + lineStart: function() { + stream.lineStart(); + }, + lineEnd: function() { + stream.lineEnd(); + }, + polygonStart: function() { + stream.polygonStart(); + }, + polygonEnd: function() { + stream.polygonEnd(); + } + }; + }; + } + function d3_geo_pathBuffer() { + var pointCircle = d3_geo_pathCircle(4.5), buffer = []; + var stream = { + point: point, + lineStart: function() { + stream.point = pointLineStart; + }, + lineEnd: lineEnd, + polygonStart: function() { + stream.lineEnd = lineEndPolygon; + }, + polygonEnd: function() { + stream.lineEnd = lineEnd; + stream.point = point; + }, + pointRadius: function(_) { + pointCircle = d3_geo_pathCircle(_); + return stream; + }, + result: function() { + if (buffer.length) { + var result = buffer.join(""); + buffer = []; + return result; + } + } + }; + function point(x, y) { + buffer.push("M", x, ",", y, pointCircle); } - }); - d3.geo.centroid = d3_geo_pathIdentity.centroid; + function pointLineStart(x, y) { + buffer.push("M", x, ",", y); + stream.point = pointLine; + } + function pointLine(x, y) { + buffer.push("L", x, ",", y); + } + function lineEnd() { + stream.point = point; + } + function lineEndPolygon() { + buffer.push("Z"); + } + return stream; + } + function d3_geo_pathContext(context) { + var pointRadius = 4.5; + var stream = { + point: point, + lineStart: function() { + stream.point = pointLineStart; + }, + lineEnd: lineEnd, + polygonStart: function() { + stream.lineEnd = lineEndPolygon; + }, + polygonEnd: function() { + stream.lineEnd = lineEnd; + stream.point = point; + }, + pointRadius: function(_) { + pointRadius = _; + return stream; + }, + result: d3_noop + }; + function point(x, y) { + context.moveTo(x, y); + context.arc(x, y, pointRadius, 0, 2 * π); + } + function pointLineStart(x, y) { + context.moveTo(x, y); + stream.point = pointLine; + } + function pointLine(x, y) { + context.lineTo(x, y); + } + function lineEnd() { + stream.point = point; + } + function lineEndPolygon() { + context.closePath(); + } + return stream; + } + var d3_geo_pathAreaSum, d3_geo_pathAreaPolygon, d3_geo_pathArea = { + point: d3_noop, + lineStart: d3_noop, + lineEnd: d3_noop, + polygonStart: function() { + d3_geo_pathAreaPolygon = 0; + d3_geo_pathArea.lineStart = d3_geo_pathAreaRingStart; + }, + polygonEnd: function() { + d3_geo_pathArea.lineStart = d3_geo_pathArea.lineEnd = d3_geo_pathArea.point = d3_noop; + d3_geo_pathAreaSum += Math.abs(d3_geo_pathAreaPolygon / 2); + } + }; + function d3_geo_pathAreaRingStart() { + var x00, y00, x0, y0; + d3_geo_pathArea.point = function(x, y) { + d3_geo_pathArea.point = nextPoint; + x00 = x0 = x, y00 = y0 = y; + }; + function nextPoint(x, y) { + d3_geo_pathAreaPolygon += y0 * x - x0 * y; + x0 = x, y0 = y; + } + d3_geo_pathArea.lineEnd = function() { + nextPoint(x00, y00); + }; + } + var d3_geo_pathCentroid = { + point: d3_geo_pathCentroidPoint, + lineStart: d3_geo_pathCentroidLineStart, + lineEnd: d3_geo_pathCentroidLineEnd, + polygonStart: function() { + d3_geo_pathCentroid.lineStart = d3_geo_pathCentroidRingStart; + }, + polygonEnd: function() { + d3_geo_pathCentroid.point = d3_geo_pathCentroidPoint; + d3_geo_pathCentroid.lineStart = d3_geo_pathCentroidLineStart; + d3_geo_pathCentroid.lineEnd = d3_geo_pathCentroidLineEnd; + } + }; + function d3_geo_pathCentroidPoint(x, y) { + if (d3_geo_centroidDimension) return; + d3_geo_centroidX += x; + d3_geo_centroidY += y; + ++d3_geo_centroidZ; + } + function d3_geo_pathCentroidLineStart() { + var x0, y0; + if (d3_geo_centroidDimension !== 1) { + if (d3_geo_centroidDimension < 1) { + d3_geo_centroidDimension = 1; + d3_geo_centroidX = d3_geo_centroidY = d3_geo_centroidZ = 0; + } else return; + } + d3_geo_pathCentroid.point = function(x, y) { + d3_geo_pathCentroid.point = nextPoint; + x0 = x, y0 = y; + }; + function nextPoint(x, y) { + var dx = x - x0, dy = y - y0, z = Math.sqrt(dx * dx + dy * dy); + d3_geo_centroidX += z * (x0 + x) / 2; + d3_geo_centroidY += z * (y0 + y) / 2; + d3_geo_centroidZ += z; + x0 = x, y0 = y; + } + } + function d3_geo_pathCentroidLineEnd() { + d3_geo_pathCentroid.point = d3_geo_pathCentroidPoint; + } + function d3_geo_pathCentroidRingStart() { + var x00, y00, x0, y0; + if (d3_geo_centroidDimension < 2) { + d3_geo_centroidDimension = 2; + d3_geo_centroidX = d3_geo_centroidY = d3_geo_centroidZ = 0; + } + d3_geo_pathCentroid.point = function(x, y) { + d3_geo_pathCentroid.point = nextPoint; + x00 = x0 = x, y00 = y0 = y; + }; + function nextPoint(x, y) { + var z = y0 * x - x0 * y; + d3_geo_centroidX += z * (x0 + x); + d3_geo_centroidY += z * (y0 + y); + d3_geo_centroidZ += z * 3; + x0 = x, y0 = y; + } + d3_geo_pathCentroid.lineEnd = function() { + nextPoint(x00, y00); + }; + } + d3.geo.area = function(object) { + d3_geo_areaSum = 0; + d3.geo.stream(object, d3_geo_area); + return d3_geo_areaSum; + }; + var d3_geo_areaSum, d3_geo_areaRing; + var d3_geo_area = { + sphere: function() { + d3_geo_areaSum += 4 * π; + }, + point: d3_noop, + lineStart: d3_noop, + lineEnd: d3_noop, + polygonStart: function() { + d3_geo_areaRing = 0; + d3_geo_area.lineStart = d3_geo_areaRingStart; + }, + polygonEnd: function() { + d3_geo_areaSum += d3_geo_areaRing < 0 ? 4 * π + d3_geo_areaRing : d3_geo_areaRing; + d3_geo_area.lineStart = d3_geo_area.lineEnd = d3_geo_area.point = d3_noop; + } + }; + function d3_geo_areaRingStart() { + var λ00, φ00, λ1, λ0, φ0, cosφ0, sinφ0; + d3_geo_area.point = function(λ, φ) { + d3_geo_area.point = nextPoint; + λ1 = λ0 = (λ00 = λ) * d3_radians, φ0 = (φ00 = φ) * d3_radians, cosφ0 = Math.cos(φ0), + sinφ0 = Math.sin(φ0); + }; + function nextPoint(λ, φ) { + λ *= d3_radians, φ *= d3_radians; + if (Math.abs(Math.abs(φ0) - π / 2) < ε && Math.abs(Math.abs(φ) - π / 2) < ε) return; + var cosφ = Math.cos(φ), sinφ = Math.sin(φ); + if (Math.abs(φ0 - π / 2) < ε) d3_geo_areaRing += (λ - λ1) * 2; else { + var dλ = λ - λ0, cosdλ = Math.cos(dλ), d = Math.atan2(Math.sqrt((d = cosφ * Math.sin(dλ)) * d + (d = cosφ0 * sinφ - sinφ0 * cosφ * cosdλ) * d), sinφ0 * sinφ + cosφ0 * cosφ * cosdλ), s = (d + π + φ0 + φ) / 4; + d3_geo_areaRing += (dλ < 0 && dλ > -π || dλ > π ? -4 : 4) * Math.atan(Math.sqrt(Math.abs(Math.tan(s) * Math.tan(s - d / 2) * Math.tan(s - π / 4 - φ0 / 2) * Math.tan(s - π / 4 - φ / 2)))); + } + λ1 = λ0, λ0 = λ, φ0 = φ, cosφ0 = cosφ, sinφ0 = sinφ; + } + d3_geo_area.lineEnd = function() { + nextPoint(λ00, φ00); + }; + } d3.geo.projection = d3_geo_projection; d3.geo.projectionMutator = d3_geo_projectionMutator; function d3_geo_projection(project) { @@ -6163,86 +6492,26 @@ })(); } function d3_geo_projectionMutator(projectAt) { - var project, rotate, projectRotate, k = 150, x = 480, y = 250, λ = 0, φ = 0, δλ = 0, δφ = 0, δγ = 0, δx = x, δy = y, δ2 = .5, clip = d3_geo_projectionCutAntemeridian(rotatePoint), clipAngle = null, context; - function projection(coordinates) { - coordinates = projectRotate(coordinates[0] * d3_radians, coordinates[1] * d3_radians); - return [ coordinates[0] * k + δx, δy - coordinates[1] * k ]; + var project, rotate, projectRotate, projectResample = d3_geo_resample(function(x, y) { + x = project(x, y); + return [ x[0] * k + δx, δy - x[1] * k ]; + }), k = 150, x = 480, y = 250, λ = 0, φ = 0, δλ = 0, δφ = 0, δγ = 0, δx, δy, clip = d3_geo_clipAntimeridian, clipAngle = null; + function projection(point) { + point = projectRotate(point[0] * d3_radians, point[1] * d3_radians); + return [ point[0] * k + δx, δy - point[1] * k ]; } - function invert(coordinates) { - coordinates = projectRotate.invert((coordinates[0] - δx) / k, (δy - coordinates[1]) / k); - return [ coordinates[0] * d3_degrees, coordinates[1] * d3_degrees ]; + function invert(point) { + point = projectRotate.invert((point[0] - δx) / k, (δy - point[1]) / k); + return point && [ point[0] * d3_degrees, point[1] * d3_degrees ]; } - projection.point = function(coordinates, c) { - context = c; - clip.point(coordinates, resample); - context = null; - }; - projection.line = function(coordinates, c) { - context = c; - clip.line(coordinates, resample); - context = null; - }; - projection.polygon = function(coordinates, c) { - context = c; - clip.polygon(coordinates, resample); - context = null; - }; - projection.sphere = function(c) { - context = c; - clip.sphere(resample); - context = null; + projection.stream = function(stream) { + return d3_geo_projectionRadiansRotate(rotate, clip(projectResample(stream))); }; projection.clipAngle = function(_) { if (!arguments.length) return clipAngle; - clip = _ == null ? (clipAngle = _, d3_geo_projectionCutAntemeridian(rotatePoint)) : d3_geo_circleClip(clipAngle = +_, rotatePoint); + clip = _ == null ? (clipAngle = _, d3_geo_clipAntimeridian) : d3_geo_clipCircle(clipAngle = +_); return projection; }; - var λ00, φ00, λ0, sinφ0, cosφ0, x0, y0, maxDepth = 16; - function point(λ, φ) { - var p = projectPoint(λ, φ); - context.point(p[0], p[1]); - } - function moveTo(λ, φ) { - var p = projectPoint(λ00 = λ0 = λ, φ00 = φ); - sinφ0 = Math.sin(φ); - cosφ0 = Math.cos(φ); - context.moveTo(x0 = p[0], y0 = p[1]); - } - function lineTo(λ, φ) { - var p = projectPoint(λ, φ); - resampleLineTo(x0, y0, λ0, sinφ0, cosφ0, x0 = p[0], y0 = p[1], λ0 = λ, sinφ0 = Math.sin(φ), cosφ0 = Math.cos(φ), maxDepth); - context.lineTo(x0, y0); - } - function resampleLineTo(x0, y0, λ0, sinφ0, cosφ0, x1, y1, λ1, sinφ1, cosφ1, depth) { - var dx = x1 - x0, dy = y1 - y0, distance2 = dx * dx + dy * dy; - if (distance2 > 4 * δ2 && depth--) { - var cosΩ = sinφ0 * sinφ1 + cosφ0 * cosφ1 * Math.cos(λ1 - λ0), k = 1 / (Math.SQRT2 * Math.sqrt(1 + cosΩ)), x = k * (cosφ0 * Math.cos(λ0) + cosφ1 * Math.cos(λ1)), y = k * (cosφ0 * Math.sin(λ0) + cosφ1 * Math.sin(λ1)), z = Math.max(-1, Math.min(1, k * (sinφ0 + sinφ1))), φ2 = Math.asin(z), zε = Math.abs(Math.abs(z) - 1), λ2 = zε < ε || zε < εε && (Math.abs(cosφ0) < εε || Math.abs(cosφ1) < εε) ? (λ0 + λ1) / 2 : Math.atan2(y, x), p = projectPoint(λ2, φ2), x2 = p[0], y2 = p[1], dx2 = x0 - x2, dy2 = y0 - y2, dz = dx * dy2 - dy * dx2; - if (dz * dz / distance2 > δ2) { - var cosφ2 = Math.cos(φ2); - resampleLineTo(x0, y0, λ0, sinφ0, cosφ0, x2, y2, λ2, z, cosφ2, depth); - context.lineTo(x2, y2); - resampleLineTo(x2, y2, λ2, z, cosφ2, x1, y1, λ1, sinφ1, cosφ1, depth); - } - } - } - function closePath() { - var p = projectPoint(λ00, φ00); - resampleLineTo(x0, y0, λ0, sinφ0, cosφ0, p[0], p[1], λ00, Math.sin(φ00), Math.cos(φ00), maxDepth); - context.closePath(); - } - var resample = { - point: point, - moveTo: moveTo, - lineTo: lineTo, - closePath: closePath - }; - function rotatePoint(coordinates) { - return rotate(coordinates[0] * d3_radians, coordinates[1] * d3_radians); - } - function projectPoint(λ, φ) { - var point = project(λ, φ); - return [ point[0] * k + δx, δy - point[1] * k ]; - } projection.scale = function(_) { if (!arguments.length) return k; k = +_; @@ -6267,11 +6536,7 @@ δγ = _.length > 2 ? _[2] % 360 * d3_radians : 0; return reset(); }; - projection.precision = function(_) { - if (!arguments.length) return Math.sqrt(δ2); - maxDepth = (δ2 = _ * _) > 0 && 16; - return projection; - }; + d3.rebind(projection, projectResample, "precision"); function reset() { projectRotate = d3_geo_compose(rotate = d3_geo_rotation(δλ, δφ, δγ), project); var center = project(λ, φ); @@ -6285,104 +6550,35 @@ return reset(); }; } - function d3_geo_projectionIntersectAntemeridian(λ0, φ0, λ1, φ1) { - var cosφ0, cosφ1, sinλ0_λ1 = Math.sin(λ0 - λ1); - return Math.abs(sinλ0_λ1) > ε ? Math.atan((Math.sin(φ0) * (cosφ1 = Math.cos(φ1)) * Math.sin(λ1) - Math.sin(φ1) * (cosφ0 = Math.cos(φ0)) * Math.sin(λ0)) / (cosφ0 * cosφ1 * sinλ0_λ1)) : (φ0 + φ1) / 2; - } - function d3_geo_projectionCutAntemeridian(rotatePoint) { - var clip = { - point: function(coordinates, context) { - var point = rotatePoint(coordinates); - context.point(point[0], point[1]); + function d3_geo_projectionRadiansRotate(rotate, stream) { + return { + point: function(x, y) { + y = rotate(x * d3_radians, y * d3_radians), x = y[0]; + stream.point(x > π ? x - 2 * π : x < -π ? x + 2 * π : x, y[1]); }, - line: function(coordinates, context, ring) { - if (!(n = coordinates.length)) return [ ring && 0, false ]; - var point = rotatePoint(coordinates[0]), λ0 = point[0], φ0 = point[1], λ1, φ1, sλ0 = λ0 > 0 ? π : -π, sλ1, dλ, i = 0, n, clean = ring, area = 0, x0 = (point = d3_geo_stereographic(λ0, φ0))[0], x, y0 = point[1], y; - context.moveTo(λ0, φ0); - while (++i < n) { - point = rotatePoint(coordinates[i]); - λ1 = point[0]; - φ1 = point[1]; - sλ1 = λ1 > 0 ? π : -π; - dλ = Math.abs(λ1 - λ0); - if (Math.abs(dλ - π) < ε) { - context.lineTo(λ0, φ0 = (φ0 + φ1) / 2 > 0 ? π / 2 : -π / 2); - context.lineTo(sλ0, φ0); - context.moveTo(sλ1, φ0); - context.lineTo(λ1, φ0); - clean = false; - } else if (sλ0 !== sλ1 && dλ >= π) { - if (Math.abs(λ0 - sλ0) < ε) λ0 -= sλ0 * ε; - if (Math.abs(λ1 - sλ1) < ε) λ1 -= sλ1 * ε; - φ0 = d3_geo_projectionIntersectAntemeridian(λ0, φ0, λ1, φ1); - context.lineTo(sλ0, φ0); - context.moveTo(sλ1, φ0); - clean = false; - } - if (clean) { - x = (point = d3_geo_stereographic(λ1, φ1))[0]; - y = point[1]; - area += y0 * x - x0 * y; - x0 = x; - y0 = y; - } - context.lineTo(λ0 = λ1, φ0 = φ1); - sλ0 = sλ1; - } - return [ clean && area, true ]; + sphere: function() { + stream.sphere(); }, - polygon: function(polygon, context) { - d3_geo_circleClipPolygon(polygon, context, clip.line, d3_geo_antemeridianInterpolate); + lineStart: function() { + stream.lineStart(); }, - sphere: function(context) { - d3_geo_projectionSphere(context, d3_geo_antemeridianInterpolate); + lineEnd: function() { + stream.lineEnd(); + }, + polygonStart: function() { + stream.polygonStart(); + }, + polygonEnd: function() { + stream.polygonEnd(); } }; - return clip; - } - function d3_geo_antemeridianInterpolate(from, to, direction, context) { - var φ; - if (from == null) { - φ = direction * π / 2; - context.lineTo(-π, φ); - context.lineTo(0, φ); - context.lineTo(π, φ); - context.lineTo(π, 0); - context.lineTo(π, -φ); - context.lineTo(0, -φ); - context.lineTo(-π, -φ); - context.lineTo(-π, 0); - } else if (Math.abs(from[0] - to[0]) > ε) { - var s = (from[0] < to[0] ? 1 : -1) * π; - φ = direction * s / 2; - context.lineTo(-s, φ); - context.lineTo(0, φ); - context.lineTo(s, φ); - } else { - context.lineTo(to[0], to[1]); - } - } - function d3_geo_projectionSphere(context, interpolate) { - var moved = false; - interpolate(null, null, 1, { - lineTo: function(x, y) { - (moved ? context.lineTo : (moved = true, context.moveTo))(x, y); - } - }); - context.closePath(); } function d3_geo_rotation(δλ, δφ, δγ) { - return δλ ? δφ || δγ ? d3_geo_compose(d3_geo_rotationλ(δλ), d3_geo_rotationφγ(δφ, δγ)) : d3_geo_rotationλ(δλ) : δφ || δγ ? d3_geo_rotationφγ(δφ, δγ) : d3_geo_identityRotation; + return δλ ? δφ || δγ ? d3_geo_compose(d3_geo_rotationλ(δλ), d3_geo_rotationφγ(δφ, δγ)) : d3_geo_rotationλ(δλ) : δφ || δγ ? d3_geo_rotationφγ(δφ, δγ) : d3_geo_equirectangular; } - function d3_geo_identityRotation(λ, φ) { - return [ λ > π ? λ - 2 * π : λ < -π ? λ + 2 * π : λ, φ ]; - } - d3_geo_identityRotation.invert = function(x, y) { - return [ x, y ]; - }; function d3_geo_forwardRotationλ(δλ) { return function(λ, φ) { - return [ (λ += δλ) > π ? λ - 2 * π : λ < -π ? λ + 2 * π : λ, φ ]; + return λ += δλ, [ λ > π ? λ - 2 * π : λ < -π ? λ + 2 * π : λ, φ ]; }; } function d3_geo_rotationλ(δλ) { diff --git a/locale/en.js b/locale/en.js index d7fc3628f..866c7eca3 100644 --- a/locale/en.js +++ b/locale/en.js @@ -73,9 +73,22 @@ locale.en = { point: "Deleted a point.", vertex: "Deleted a node from a way.", line: "Deleted a line.", - area: "Deleted an area." + area: "Deleted an area.", + multiple: "Deleted {n} objects." } }, + disconnect: { + title: "Disconnect", + description: "Disconnect these ways from each other.", + key: "D", + annotation: "Disconnected ways." + }, + merge: { + title: "Merge", + description: "Merge these lines.", + key: "C", + annotation: "Merged {n} lines." + }, move: { title: "Move", description: "Move this to a different location.", @@ -98,12 +111,6 @@ locale.en = { description: "Split this into two ways at this point.", key: "X", annotation: "Split a way." - }, - unjoin: { - title: "Unjoin", - description: "Disconnect these ways from each other.", - key: "⇧-J", - annotation: "Unjoined ways." } }, @@ -129,8 +136,11 @@ locale.en = { "layer_settings": "Layer Settings", - "no_documentation_combination": "This is no documentation available for this tag combination", - "no_documentation_key": "This is no documentation available for this key", + inspector: { + no_documentation_combination: "This is no documentation available for this tag combination", + no_documentation_key: "This is no documentation available for this key", + new_tag: "New Tag" + }, "view_on_osm": "View on OSM", @@ -138,13 +148,16 @@ locale.en = { "edit_tags": "Edit tags", - "find_location": "Find A Location", - "find_placeholder": "find a place", + geocoder: { + "find_location": "Find A Location", + "find_a_place": "find a place" + }, "description": "Description", "logout": "logout", - - "layers": "Layers", - "percent_opacity": "{opacity}% opacity" + layerswitcher: { + layers: "Layers", + percent_brightness: "{opacity}% brightness" + } }; diff --git a/test/index.html b/test/index.html index b85eb1b56..b6d925923 100644 --- a/test/index.html +++ b/test/index.html @@ -73,14 +73,17 @@ + + + + - - - + + @@ -106,11 +109,13 @@ + + - + @@ -119,7 +124,6 @@ - @@ -141,17 +145,17 @@ + + + + - - - - - - + + @@ -161,8 +165,7 @@ - - + @@ -189,6 +192,12 @@ + + + + + + diff --git a/test/index_packaged.html b/test/index_packaged.html index 60979e236..ef91e2823 100644 --- a/test/index_packaged.html +++ b/test/index_packaged.html @@ -35,17 +35,17 @@ + + + + - - - - - - + + @@ -55,8 +55,7 @@ - - + @@ -83,6 +82,12 @@ + + + + + + diff --git a/test/spec/actions/delete_multiple.js b/test/spec/actions/delete_multiple.js new file mode 100644 index 000000000..3a70adc38 --- /dev/null +++ b/test/spec/actions/delete_multiple.js @@ -0,0 +1,12 @@ +describe("iD.actions.DeleteMultiple", function () { + it("deletes multiple entities of heterogeneous types", function () { + var n = iD.Node(), + w = iD.Way(), + r = iD.Relation(), + action = iD.actions.DeleteMultiple([n.id, w.id, r.id]), + graph = action(iD.Graph([n, w, r])); + expect(graph.entity(n.id)).to.be.undefined; + expect(graph.entity(w.id)).to.be.undefined; + expect(graph.entity(r.id)).to.be.undefined; + }); +}); diff --git a/test/spec/actions/delete_relation.js b/test/spec/actions/delete_relation.js new file mode 100644 index 000000000..c96909f3f --- /dev/null +++ b/test/spec/actions/delete_relation.js @@ -0,0 +1,17 @@ +describe("iD.actions.DeleteRelation", function () { + it("removes the relation from the graph", function () { + var relation = iD.Relation(), + action = iD.actions.DeleteRelation(relation.id), + graph = action(iD.Graph([relation])); + expect(graph.entity(relation.id)).to.be.undefined; + }); + + it("removes the relation from parent relations", function () { + var a = iD.Relation(), + b = iD.Relation(), + parent = iD.Relation({members: [{ id: a.id }, { id: b.id }]}), + action = iD.actions.DeleteRelation(a.id), + graph = action(iD.Graph([a, b, parent])); + expect(graph.entity(parent.id).members).to.eql([{ id: b.id }]); + }); +}); diff --git a/test/spec/actions/unjoin_node.js b/test/spec/actions/disconnect.js similarity index 89% rename from test/spec/actions/unjoin_node.js rename to test/spec/actions/disconnect.js index 5901e97da..4ce298feb 100644 --- a/test/spec/actions/unjoin_node.js +++ b/test/spec/actions/disconnect.js @@ -1,9 +1,9 @@ -describe("iD.actions.UnjoinNode", function () { +describe("iD.actions.Disconnect", function () { describe("#enabled", function () { it("returns false for a node shared by less than two ways", function () { var graph = iD.Graph({'a': iD.Node()}); - expect(iD.actions.UnjoinNode('a').enabled(graph)).to.equal(false); + expect(iD.actions.Disconnect('a').enabled(graph)).to.equal(false); }); it("returns true for a node shared by two or more ways", function () { @@ -19,7 +19,7 @@ describe("iD.actions.UnjoinNode", function () { '|': iD.Way({id: '|', nodes: ['d', 'b']}) }); - expect(iD.actions.UnjoinNode('b').enabled(graph)).to.equal(true); + expect(iD.actions.Disconnect('b').enabled(graph)).to.equal(true); }); }); @@ -46,7 +46,7 @@ describe("iD.actions.UnjoinNode", function () { '|': iD.Way({id: '|', nodes: ['d', 'b']}) }); - graph = iD.actions.UnjoinNode('b', 'e')(graph); + graph = iD.actions.Disconnect('b', 'e')(graph); expect(graph.entity('-').nodes).to.eql(['a', 'b', 'c']); expect(graph.entity('|').nodes).to.eql(['d', 'e']); @@ -64,7 +64,7 @@ describe("iD.actions.UnjoinNode", function () { '|': iD.Way({id: '|', nodes: ['d', 'b']}) }); - graph = iD.actions.UnjoinNode('b', 'e')(graph); + graph = iD.actions.Disconnect('b', 'e')(graph); // Immutable loc => should be shared by identity. expect(graph.entity('b').loc).to.equal(loc); diff --git a/test/spec/actions/join.js b/test/spec/actions/join.js new file mode 100644 index 000000000..1cddb94bf --- /dev/null +++ b/test/spec/actions/join.js @@ -0,0 +1,172 @@ +describe("iD.actions.Join", function () { + describe("#enabled", function () { + it("returns true for ways that share an end/start node", function () { + // a --> b ==> c + var graph = iD.Graph({ + 'a': iD.Node({id: 'a'}), + 'b': iD.Node({id: 'b'}), + 'c': iD.Node({id: 'c'}), + '-': iD.Way({id: '-', nodes: ['a', 'b']}), + '=': iD.Way({id: '=', nodes: ['b', 'c']}) + }); + + expect(iD.actions.Join('-', '=').enabled(graph)).to.be.true; + }); + + it("returns true for ways that share a start/end node", function () { + // a <-- b <== c + var graph = iD.Graph({ + 'a': iD.Node({id: 'a'}), + 'b': iD.Node({id: 'b'}), + 'c': iD.Node({id: 'c'}), + '-': iD.Way({id: '-', nodes: ['b', 'a']}), + '=': iD.Way({id: '=', nodes: ['c', 'b']}) + }); + + expect(iD.actions.Join('-', '=').enabled(graph)).to.be.true; + }); + + it("returns true for ways that share a start/start node", function () { + // a <-- b ==> c + var graph = iD.Graph({ + 'a': iD.Node({id: 'a'}), + 'b': iD.Node({id: 'b'}), + 'c': iD.Node({id: 'c'}), + '-': iD.Way({id: '-', nodes: ['b', 'a']}), + '=': iD.Way({id: '=', nodes: ['b', 'c']}) + }); + + expect(iD.actions.Join('-', '=').enabled(graph)).to.be.true; + }); + + it("returns true for ways that share an end/end node", function () { + // a --> b <== c + var graph = iD.Graph({ + 'a': iD.Node({id: 'a'}), + 'b': iD.Node({id: 'b'}), + 'c': iD.Node({id: 'c'}), + '-': iD.Way({id: '-', nodes: ['a', 'b']}), + '=': iD.Way({id: '=', nodes: ['c', 'b']}) + }); + + expect(iD.actions.Join('-', '=').enabled(graph)).to.be.true; + }); + + it("returns false for ways that don't share the necessary nodes", function () { + // a -- b -- c + // | + // d + var graph = iD.Graph({ + 'a': iD.Node({id: 'a'}), + 'b': iD.Node({id: 'b'}), + 'c': iD.Node({id: 'c'}), + 'd': iD.Node({id: 'd'}), + '-': iD.Way({id: '-', nodes: ['a', 'b', 'c']}), + '=': iD.Way({id: '=', nodes: ['b', 'd']}) + }); + + expect(iD.actions.Join('-', '=').enabled(graph)).to.be.false; + }); + }); + + it("joins a --> b ==> c", function () { + // Expected result: + // a --> b --> c + var graph = iD.Graph({ + 'a': iD.Node({id: 'a'}), + 'b': iD.Node({id: 'b'}), + 'c': iD.Node({id: 'c'}), + '-': iD.Way({id: '-', nodes: ['a', 'b']}), + '=': iD.Way({id: '=', nodes: ['b', 'c']}) + }); + + graph = iD.actions.Join('-', '=')(graph); + + expect(graph.entity('-').nodes).to.eql(['a', 'b', 'c']); + expect(graph.entity('=')).to.be.undefined; + }); + + it("joins a <-- b <== c", function () { + // Expected result: + // a <-- b <-- c + var graph = iD.Graph({ + 'a': iD.Node({id: 'a'}), + 'b': iD.Node({id: 'b'}), + 'c': iD.Node({id: 'c'}), + '-': iD.Way({id: '-', nodes: ['b', 'a']}), + '=': iD.Way({id: '=', nodes: ['c', 'b']}) + }); + + graph = iD.actions.Join('-', '=')(graph); + + expect(graph.entity('-').nodes).to.eql(['c', 'b', 'a']); + expect(graph.entity('=')).to.be.undefined; + }); + + it("joins a <-- b ==> c", function () { + // Expected result: + // a <-- b <-- c + // tags on === reversed + var graph = iD.Graph({ + 'a': iD.Node({id: 'a'}), + 'b': iD.Node({id: 'b'}), + 'c': iD.Node({id: 'c'}), + '-': iD.Way({id: '-', nodes: ['b', 'a']}), + '=': iD.Way({id: '=', nodes: ['b', 'c']}) + }); + + graph = iD.actions.Join('-', '=')(graph); + + expect(graph.entity('-').nodes).to.eql(['c', 'b', 'a']); + expect(graph.entity('=')).to.be.undefined; + }); + + it("joins a --> b <== c", function () { + // Expected result: + // a --> b --> c + // tags on === reversed + var graph = iD.Graph({ + 'a': iD.Node({id: 'a'}), + 'b': iD.Node({id: 'b'}), + 'c': iD.Node({id: 'c'}), + '-': iD.Way({id: '-', nodes: ['a', 'b']}), + '=': iD.Way({id: '=', nodes: ['c', 'b']}) + }); + + graph = iD.actions.Join('-', '=')(graph); + + expect(graph.entity('-').nodes).to.eql(['a', 'b', 'c']); + expect(graph.entity('=')).to.be.undefined; + }); + + it("merges tags", function () { + var graph = iD.Graph({ + 'a': iD.Node({id: 'a'}), + 'b': iD.Node({id: 'b'}), + 'c': iD.Node({id: 'c'}), + '-': iD.Way({id: '-', nodes: ['a', 'b'], tags: {a: 'a', b: '-', c: 'c'}}), + '=': iD.Way({id: '=', nodes: ['b', 'c'], tags: {a: 'a', b: '=', d: 'd'}}) + }); + + graph = iD.actions.Join('-', '=')(graph); + + expect(graph.entity('-').tags).to.eql({a: 'a', b: '-; =', c: 'c', d: 'd'}); + }); + + it("merges relations", function () { + var graph = iD.Graph({ + 'a': iD.Node({id: 'a'}), + 'b': iD.Node({id: 'b'}), + 'c': iD.Node({id: 'c'}), + '-': iD.Way({id: '-', nodes: ['a', 'b']}), + '=': iD.Way({id: '=', nodes: ['b', 'c']}), + 'r1': iD.Relation({id: 'r1', members: [{id: '=', role: 'r1'}]}), + 'r2': iD.Relation({id: 'r2', members: [{id: '=', role: 'r1'}, {id: '-', role: 'r2'}]}) + }); + + graph = iD.actions.Join('-', '=')(graph); + + expect(graph.entity('r1').members).to.eql([{id: '-', role: 'r1'}]); + expect(graph.entity('r2').members).to.eql([{id: '-', role: 'r2'}]); + }); +}); diff --git a/test/spec/actions/reverse_way.js b/test/spec/actions/reverse.js similarity index 76% rename from test/spec/actions/reverse_way.js rename to test/spec/actions/reverse.js index ca61cb778..40d0f5500 100644 --- a/test/spec/actions/reverse_way.js +++ b/test/spec/actions/reverse.js @@ -1,9 +1,9 @@ -describe("iD.actions.ReverseWay", function () { +describe("iD.actions.Reverse", function () { it("reverses the order of nodes in the way", function () { var node1 = iD.Node(), node2 = iD.Node(), way = iD.Way({nodes: [node1.id, node2.id]}), - graph = iD.actions.ReverseWay(way.id)(iD.Graph([node1, node2, way])); + graph = iD.actions.Reverse(way.id)(iD.Graph([node1, node2, way])); expect(graph.entity(way.id).nodes).to.eql([node2.id, node1.id]); }); @@ -11,7 +11,7 @@ describe("iD.actions.ReverseWay", function () { var way = iD.Way({tags: {'highway': 'residential'}}), graph = iD.Graph([way]); - graph = iD.actions.ReverseWay(way.id)(graph); + graph = iD.actions.Reverse(way.id)(graph); expect(graph.entity(way.id).tags).to.eql({'highway': 'residential'}); }); @@ -19,7 +19,7 @@ describe("iD.actions.ReverseWay", function () { var way = iD.Way({tags: {'oneway': 'yes'}}), graph = iD.Graph([way]); - graph = iD.actions.ReverseWay(way.id)(graph); + graph = iD.actions.Reverse(way.id)(graph); expect(graph.entity(way.id).tags).to.eql({'oneway': 'yes'}); }); @@ -27,10 +27,10 @@ describe("iD.actions.ReverseWay", function () { var way = iD.Way({tags: {'cycleway:right': 'lane'}}), graph = iD.Graph([way]); - graph = iD.actions.ReverseWay(way.id)(graph); + graph = iD.actions.Reverse(way.id)(graph); expect(graph.entity(way.id).tags).to.eql({'cycleway:left': 'lane'}); - graph = iD.actions.ReverseWay(way.id)(graph); + graph = iD.actions.Reverse(way.id)(graph); expect(graph.entity(way.id).tags).to.eql({'cycleway:right': 'lane'}); }); @@ -38,10 +38,10 @@ describe("iD.actions.ReverseWay", function () { var way = iD.Way({tags: {'maxspeed:forward': '25'}}), graph = iD.Graph([way]); - graph = iD.actions.ReverseWay(way.id)(graph); + graph = iD.actions.Reverse(way.id)(graph); expect(graph.entity(way.id).tags).to.eql({'maxspeed:backward': '25'}); - graph = iD.actions.ReverseWay(way.id)(graph); + graph = iD.actions.Reverse(way.id)(graph); expect(graph.entity(way.id).tags).to.eql({'maxspeed:forward': '25'}); }); @@ -49,10 +49,10 @@ describe("iD.actions.ReverseWay", function () { var way = iD.Way({tags: {'incline': 'up'}}), graph = iD.Graph([way]); - graph = iD.actions.ReverseWay(way.id)(graph); + graph = iD.actions.Reverse(way.id)(graph); expect(graph.entity(way.id).tags).to.eql({'incline': 'down'}); - graph = iD.actions.ReverseWay(way.id)(graph); + graph = iD.actions.Reverse(way.id)(graph); expect(graph.entity(way.id).tags).to.eql({'incline': 'up'}); }); @@ -60,10 +60,10 @@ describe("iD.actions.ReverseWay", function () { var way = iD.Way({tags: {'incline': 'up'}}), graph = iD.Graph([way]); - graph = iD.actions.ReverseWay(way.id)(graph); + graph = iD.actions.Reverse(way.id)(graph); expect(graph.entity(way.id).tags).to.eql({'incline': 'down'}); - graph = iD.actions.ReverseWay(way.id)(graph); + graph = iD.actions.Reverse(way.id)(graph); expect(graph.entity(way.id).tags).to.eql({'incline': 'up'}); }); @@ -71,16 +71,16 @@ describe("iD.actions.ReverseWay", function () { var way = iD.Way({tags: {'incline': '5%'}}), graph = iD.Graph([way]); - graph = iD.actions.ReverseWay(way.id)(graph); + graph = iD.actions.Reverse(way.id)(graph); expect(graph.entity(way.id).tags).to.eql({'incline': '-5%'}); - graph = iD.actions.ReverseWay(way.id)(graph); + graph = iD.actions.Reverse(way.id)(graph); expect(graph.entity(way.id).tags).to.eql({'incline': '5%'}); way = iD.Way({tags: {'incline': '.8°'}}); graph = iD.Graph([way]); - graph = iD.actions.ReverseWay(way.id)(graph); + graph = iD.actions.Reverse(way.id)(graph); expect(graph.entity(way.id).tags).to.eql({'incline': '-.8°'}); }); @@ -88,10 +88,10 @@ describe("iD.actions.ReverseWay", function () { var way = iD.Way({tags: {'sidewalk': 'right'}}), graph = iD.Graph([way]); - graph = iD.actions.ReverseWay(way.id)(graph); + graph = iD.actions.Reverse(way.id)(graph); expect(graph.entity(way.id).tags).to.eql({'sidewalk': 'left'}); - graph = iD.actions.ReverseWay(way.id)(graph); + graph = iD.actions.Reverse(way.id)(graph); expect(graph.entity(way.id).tags).to.eql({'sidewalk': 'right'}); }); @@ -99,7 +99,7 @@ describe("iD.actions.ReverseWay", function () { var way = iD.Way({tags: {'maxspeed:forward': '25', 'maxspeed:backward': '30'}}), graph = iD.Graph([way]); - graph = iD.actions.ReverseWay(way.id)(graph); + graph = iD.actions.Reverse(way.id)(graph); expect(graph.entity(way.id).tags).to.eql({'maxspeed:backward': '25', 'maxspeed:forward': '30'}); }); @@ -108,10 +108,10 @@ describe("iD.actions.ReverseWay", function () { relation = iD.Relation({members: [{type: 'way', id: way.id, role: 'forward'}]}), graph = iD.Graph([way, relation]); - graph = iD.actions.ReverseWay(way.id)(graph); + graph = iD.actions.Reverse(way.id)(graph); expect(graph.entity(relation.id).members[0].role).to.eql('backward'); - graph = iD.actions.ReverseWay(way.id)(graph); + graph = iD.actions.Reverse(way.id)(graph); expect(graph.entity(relation.id).members[0].role).to.eql('forward'); }); }); diff --git a/test/spec/actions/split_way.js b/test/spec/actions/split.js similarity index 92% rename from test/spec/actions/split_way.js rename to test/spec/actions/split.js index 2958521a9..5ee68e67e 100644 --- a/test/spec/actions/split_way.js +++ b/test/spec/actions/split.js @@ -1,4 +1,4 @@ -describe("iD.actions.SplitWay", function () { +describe("iD.actions.Split", function () { describe("#enabled", function () { it("returns true for a non-end node of a single way", function () { var graph = iD.Graph({ @@ -8,7 +8,7 @@ describe("iD.actions.SplitWay", function () { '-': iD.Way({id: '-', nodes: ['a', 'b', 'c']}) }); - expect(iD.actions.SplitWay('b').enabled(graph)).to.be.true; + expect(iD.actions.Split('b').enabled(graph)).to.be.true; }); it("returns false for the first node of a single way", function () { @@ -18,7 +18,7 @@ describe("iD.actions.SplitWay", function () { '-': iD.Way({id: '-', nodes: ['a', 'b']}) }); - expect(iD.actions.SplitWay('a').enabled(graph)).to.be.false; + expect(iD.actions.Split('a').enabled(graph)).to.be.false; }); it("returns false for the last node of a single way", function () { @@ -28,7 +28,7 @@ describe("iD.actions.SplitWay", function () { '-': iD.Way({id: '-', nodes: ['a', 'b']}) }); - expect(iD.actions.SplitWay('b').enabled(graph)).to.be.false; + expect(iD.actions.Split('b').enabled(graph)).to.be.false; }); }); @@ -48,7 +48,7 @@ describe("iD.actions.SplitWay", function () { '-': iD.Way({id: '-', nodes: ['a', 'b', 'c']}) }); - graph = iD.actions.SplitWay('b', '=')(graph); + graph = iD.actions.Split('b', '=')(graph); expect(graph.entity('-').nodes).to.eql(['a', 'b']); expect(graph.entity('=').nodes).to.eql(['b', 'c']); @@ -63,7 +63,7 @@ describe("iD.actions.SplitWay", function () { '-': iD.Way({id: '-', nodes: ['a', 'b', 'c'], tags: tags}) }); - graph = iD.actions.SplitWay('b', '=')(graph); + graph = iD.actions.Split('b', '=')(graph); // Immutable tags => should be shared by identity. expect(graph.entity('-').tags).to.equal(tags); @@ -92,7 +92,7 @@ describe("iD.actions.SplitWay", function () { '|': iD.Way({id: '|', nodes: ['d', 'b']}) }); - graph = iD.actions.SplitWay('b', '=')(graph); + graph = iD.actions.Split('b', '=')(graph); expect(graph.entity('-').nodes).to.eql(['a', 'b']); expect(graph.entity('=').nodes).to.eql(['b', 'c']); @@ -118,7 +118,7 @@ describe("iD.actions.SplitWay", function () { 'r': iD.Relation({id: 'r', members: [{id: '-', type: 'way'}]}) }); - graph = iD.actions.SplitWay('b', '=')(graph); + graph = iD.actions.Split('b', '=')(graph); expect(_.pluck(graph.entity('r').members, 'id')).to.eql(['-', '=']); }); @@ -144,7 +144,7 @@ describe("iD.actions.SplitWay", function () { 'r': iD.Relation({id: 'r', members: [{id: '-', type: 'way'}, {id: '~', type: 'way'}]}) }); - graph = iD.actions.SplitWay('b', '=')(graph); + graph = iD.actions.Split('b', '=')(graph); expect(_.pluck(graph.entity('r').members, 'id')).to.eql(['-', '=', '~']); }); @@ -170,7 +170,7 @@ describe("iD.actions.SplitWay", function () { 'r': iD.Relation({id: 'r', members: [{id: '~', type: 'way'}, {id: '-', type: 'way'}]}) }); - graph = iD.actions.SplitWay('b', '=')(graph); + graph = iD.actions.Split('b', '=')(graph); expect(_.pluck(graph.entity('r').members, 'id')).to.eql(['~', '=', '-']); }); @@ -184,7 +184,7 @@ describe("iD.actions.SplitWay", function () { 'r': iD.Relation({id: 'r', members: [{id: '~', type: 'way'}, {id: '-', type: 'way'}]}) }); - graph = iD.actions.SplitWay('b', '=')(graph); + graph = iD.actions.Split('b', '=')(graph); expect(_.pluck(graph.entity('r').members, 'id')).to.eql(['~', '-', '=']); }); @@ -214,7 +214,7 @@ describe("iD.actions.SplitWay", function () { {id: 'c', role: 'via'}]}) }); - graph = iD.actions.SplitWay('b', '=')(graph); + graph = iD.actions.Split('b', '=')(graph); expect(graph.entity('r').members).to.eql([ {id: '=', role: 'from'}, @@ -246,7 +246,7 @@ describe("iD.actions.SplitWay", function () { {id: 'c', role: 'via'}]}) }); - graph = iD.actions.SplitWay('b', '=')(graph); + graph = iD.actions.Split('b', '=')(graph); expect(graph.entity('r').members).to.eql([ {id: '~', role: 'from'}, @@ -278,7 +278,7 @@ describe("iD.actions.SplitWay", function () { {id: 'c', role: 'via'}]}) }); - graph = iD.actions.SplitWay('b', '=')(graph); + graph = iD.actions.Split('b', '=')(graph); expect(graph.entity('r').members).to.eql([ {id: '-', role: 'from'}, diff --git a/test/spec/behavior/hash.js b/test/spec/behavior/hash.js index 7616bf536..7f9200ae1 100644 --- a/test/spec/behavior/hash.js +++ b/test/spec/behavior/hash.js @@ -1,19 +1,18 @@ describe("iD.behavior.Hash", function () { - var hash, map, controller; + mocha.globals('__onhashchange.hash'); + + var hash, context; beforeEach(function () { - map = { - on: function () { return map; }, - zoom: function () { return arguments.length ? map : 0; }, - center: function () { return arguments.length ? map : [0, 0]; }, - centerZoom: function () { return arguments.length ? map : [0, 0]; } - }; + context = iD(); - controller = { - on: function () { return controller; } - }; + // Neuter connection + context.connection().loadTiles = function () {}; - hash = iD.behavior.Hash(controller, map); + hash = iD.behavior.Hash(context); + + d3.select(document.createElement('div')) + .call(context.map()); }); afterEach(function () { @@ -22,44 +21,41 @@ describe("iD.behavior.Hash", function () { it("sets hadHash if location.hash is present", function () { location.hash = "map=20.00/38.87952/-77.02405"; + hash(); + expect(hash.hadHash).to.be.true; }); it("centerZooms map to requested level", function () { location.hash = "map=20.00/38.87952/-77.02405"; - sinon.spy(map, 'centerZoom'); + hash(); - expect(map.centerZoom).to.have.been.calledWith([-77.02405,38.87952], 20.0); + + expect(context.map().center()[0]).to.be.closeTo(-77.02405, 0.1); + expect(context.map().center()[1]).to.be.closeTo(38.87952, 0.1); + expect(context.map().zoom()).to.equal(20.0); }); - describe("on window hashchange events", function () { - beforeEach(function () { - hash(); + it("centerZooms map at requested coordinates on hash change", function (done) { + hash(); + + d3.select(window).one('hashchange', function () { + expect(context.map().center()[0]).to.be.closeTo(-77.02405, 0.1); + expect(context.map().center()[1]).to.be.closeTo(38.87952, 0.1); + expect(context.map().zoom()).to.equal(20.0); + done(); }); - function onhashchange(fn) { - d3.select(window).one("hashchange", fn); - } - - it("centerZooms map at requested coordinates", function (done) { - onhashchange(function () { - expect(map.centerZoom).to.have.been.calledWith([-77.02405,38.87952], 20.0); - done(); - }); - - sinon.spy(map, 'centerZoom'); - location.hash = "#map=20.00/38.87952/-77.02405"; - }); + location.hash = "#map=20.00/38.87952/-77.02405"; }); - describe("on map move events", function () { - it("stores the current zoom and coordinates in location.hash", function () { - sinon.stub(map, 'on') - .withArgs("move.hash", sinon.match.instanceOf(Function)) - .yields(); - hash(); - expect(location.hash).to.equal("#map=0.00/0/0"); - }); + it("stores the current zoom and coordinates in location.hash on map move events", function () { + hash(); + + context.map().center([38.9, -77.0]); + context.map().zoom(2.0); + + expect(location.hash).to.equal("#map=2.00/-77.0/38.9"); }); }); diff --git a/test/spec/behavior/select.js b/test/spec/behavior/select.js new file mode 100644 index 000000000..42c568cca --- /dev/null +++ b/test/spec/behavior/select.js @@ -0,0 +1,54 @@ +describe("iD.behavior.Select", function() { + var a, b, context, behavior, container; + + beforeEach(function() { + container = d3.select('body').append('div'); + + context = iD().container(container); + + a = iD.Node({loc: [0, 0]}); + b = iD.Node({loc: [0, 0]}); + + context.perform(iD.actions.AddEntity(a), iD.actions.AddEntity(b)); + + container.call(context.map()) + .append('div') + .attr('class', 'inspector-wrap'); + + context.surface().selectAll('circle') + .data([a, b]) + .enter().append('circle') + .attr('class', function(d) { return d.id; }); + + behavior = iD.behavior.Select(context); + context.install(behavior); + }); + + afterEach(function() { + context.uninstall(behavior); + 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 empty space clears the selection", function() { + context.enter(iD.modes.Select(context, [a.id])); + happen.click(context.surface().node()); + expect(context.selection()).to.eql([]); + }); + + specify("shift-click on entity adds the entity to the selection", function() { + 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]); + }); + + specify("shift-click on empty space leaves the selection unchanged", function() { + context.enter(iD.modes.Select(context, [a.id])); + happen.click(context.surface().node(), {shiftKey: true}); + expect(context.selection()).to.eql([a.id]); + }); +}); diff --git a/test/spec/connection.js b/test/spec/connection.js index 652c1419b..cdc439a46 100644 --- a/test/spec/connection.js +++ b/test/spec/connection.js @@ -2,7 +2,8 @@ describe('iD.Connection', function () { var c; beforeEach(function () { - c = new iD.Connection(); + context = iD(); + c = new iD.Connection(context); }); it('is instantiated', function () { diff --git a/test/spec/graph/difference.js b/test/spec/graph/difference.js new file mode 100644 index 000000000..c9647bbea --- /dev/null +++ b/test/spec/graph/difference.js @@ -0,0 +1,229 @@ +describe("iD.Difference", function () { + describe("#changes", function () { + it("includes created entities", function () { + var node = iD.Node({id: 'n'}), + base = iD.Graph(), + head = base.replace(node), + diff = iD.Difference(base, head); + expect(diff.changes()).to.eql({n: {base: undefined, head: node}}); + }); + + it("includes undone created entities", function () { + var node = iD.Node({id: 'n'}), + base = iD.Graph(), + head = base.replace(node), + diff = iD.Difference(head, base); + expect(diff.changes()).to.eql({n: {base: node, head: undefined}}); + }); + + it("includes modified entities", function () { + var n1 = iD.Node({id: 'n'}), + n2 = n1.update(), + base = iD.Graph([n1]), + head = base.replace(n2), + diff = iD.Difference(base, head); + expect(diff.changes()).to.eql({n: {base: n1, head: n2}}); + }); + + it("includes undone modified entities", function () { + var n1 = iD.Node({id: 'n'}), + n2 = n1.update(), + base = iD.Graph([n1]), + head = base.replace(n2), + diff = iD.Difference(head, base); + expect(diff.changes()).to.eql({n: {base: n2, head: n1}}); + }); + + it("includes deleted entities", function () { + var node = iD.Node({id: 'n'}), + base = iD.Graph([node]), + head = base.remove(node), + diff = iD.Difference(base, head); + expect(diff.changes()).to.eql({n: {base: node, head: undefined}}); + }); + + it("includes undone deleted entities", function () { + var node = iD.Node({id: 'n'}), + base = iD.Graph([node]), + head = base.remove(node), + diff = iD.Difference(head, base); + expect(diff.changes()).to.eql({n: {base: undefined, head: node}}); + }); + + it("doesn't include created entities that were subsequently deleted", function () { + var node = iD.Node(), + base = iD.Graph(), + head = base.replace(node).remove(node), + diff = iD.Difference(base, head); + expect(diff.changes()).to.eql({}); + }); + }); + + describe("#extantIDs", function () { + it("includes the ids of created entities", function () { + var node = iD.Node({id: 'n'}), + base = iD.Graph(), + head = base.replace(node), + diff = iD.Difference(base, head); + expect(diff.extantIDs()).to.eql(['n']); + }); + + it("includes the ids of modified entities", function () { + var n1 = iD.Node({id: 'n'}), + n2 = n1.move([1, 2]), + base = iD.Graph([n1]), + head = base.replace(n2), + diff = iD.Difference(base, head); + expect(diff.extantIDs()).to.eql(['n']); + }); + + it("omits the ids of deleted entities", function () { + var node = iD.Node({id: 'n'}), + base = iD.Graph([node]), + head = base.remove(node), + diff = iD.Difference(base, head); + expect(diff.extantIDs()).to.eql([]); + }); + }); + + describe("#created", function () { + it("returns an array of created entities", function () { + var node = iD.Node({id: 'n'}), + base = iD.Graph(), + head = base.replace(node), + diff = iD.Difference(base, head); + expect(diff.created()).to.eql([node]); + }); + }); + + describe("#modified", function () { + it("returns an array of modified entities", function () { + var n1 = iD.Node({id: 'n'}), + n2 = n1.move([1, 2]), + base = iD.Graph([n1]), + head = base.replace(n2), + diff = iD.Difference(base, head); + expect(diff.modified()).to.eql([n2]); + }); + }); + + describe("#deleted", function () { + it("returns an array of deleted entities", function () { + var node = iD.Node({id: 'n'}), + base = iD.Graph([node]), + head = base.remove(node), + diff = iD.Difference(base, head); + expect(diff.deleted()).to.eql([node]); + }); + }); + + describe("#complete", function () { + it("includes created entities", function () { + var node = iD.Node({id: 'n'}), + base = iD.Graph(), + head = base.replace(node), + diff = iD.Difference(base, head); + expect(diff.complete()['n']).to.equal(node); + }); + + it("includes modified entities", function () { + var n1 = iD.Node({id: 'n'}), + n2 = n1.move([1, 2]), + base = iD.Graph([n1]), + head = base.replace(n2), + diff = iD.Difference(base, head); + expect(diff.complete()['n']).to.equal(n2); + }); + + it("includes deleted entities", function () { + var node = iD.Node({id: 'n'}), + base = iD.Graph([node]), + head = base.remove(node), + diff = iD.Difference(base, head); + expect(diff.complete()).to.eql({n: undefined}); + }); + + it("includes nodes added to a way", function () { + var n1 = iD.Node({id: 'n1'}), + n2 = iD.Node({id: 'n2'}), + w1 = iD.Way({id: 'w', nodes: ['n1']}), + w2 = w1.addNode('n2'), + base = iD.Graph([n1, n2, w1]), + head = base.replace(w2), + diff = iD.Difference(base, head); + + expect(diff.complete()['n2']).to.equal(n2); + }); + + it("includes nodes removed from a way", function () { + var n1 = iD.Node({id: 'n1'}), + n2 = iD.Node({id: 'n2'}), + w1 = iD.Way({id: 'w', nodes: ['n1', 'n2']}), + w2 = w1.removeNode('n2'), + base = iD.Graph([n1, n2, w1]), + head = base.replace(w2), + diff = iD.Difference(base, head); + + expect(diff.complete()['n2']).to.equal(n2); + }); + + it("includes parent ways of modified nodes", function () { + var n1 = iD.Node({id: 'n'}), + n2 = n1.move([1, 2]), + way = iD.Way({id: 'w', nodes: ['n']}), + base = iD.Graph([n1, way]), + head = base.replace(n2), + diff = iD.Difference(base, head); + + expect(diff.complete()['w']).to.equal(way); + }); + + it("includes parent relations of modified entities", function () { + var n1 = iD.Node({id: 'n'}), + n2 = n1.move([1, 2]), + rel = iD.Relation({id: 'r', members: [{id: 'n'}]}), + base = iD.Graph([n1, rel]), + head = base.replace(n2), + diff = iD.Difference(base, head); + + expect(diff.complete()['r']).to.equal(rel); + }); + + it("includes parent relations of modified entities, recursively", function () { + var n1 = iD.Node({id: 'n'}), + n2 = n1.move([1, 2]), + rel1 = iD.Relation({id: 'r1', members: [{id: 'n'}]}), + rel2 = iD.Relation({id: 'r2', members: [{id: 'r1'}]}), + base = iD.Graph([n1, rel1, rel2]), + head = base.replace(n2), + diff = iD.Difference(base, head); + + expect(diff.complete()['r2']).to.equal(rel2); + }); + + it("includes parent relations of parent ways of modified nodes", function () { + var n1 = iD.Node({id: 'n'}), + n2 = n1.move([1, 2]), + way = iD.Way({id: 'w', nodes: ['n']}), + rel = iD.Relation({id: 'r', members: [{id: 'w'}]}), + base = iD.Graph([n1, way, rel]), + head = base.replace(n2), + diff = iD.Difference(base, head); + + expect(diff.complete()['r']).to.equal(rel); + }); + + it("copes with recursive relations", function () { + var node = iD.Node({id: 'n'}), + rel1 = iD.Relation({id: 'r1', members: [{id: 'n'}, {id: 'r2'}]}), + rel2 = iD.Relation({id: 'r2', members: [{id: 'r1'}]}), + base = iD.Graph([node, rel1, rel2]), + head = base.replace(node.move([1, 2])), + diff = iD.Difference(base, head); + + expect(diff.complete()).to.be.ok; + }); + + it("limits changes to those within a given extent"); + }); +}); diff --git a/test/spec/graph/entity.js b/test/spec/graph/entity.js index a573f4c8c..f2ac85f17 100644 --- a/test/spec/graph/entity.js +++ b/test/spec/graph/entity.js @@ -52,12 +52,6 @@ describe('iD.Entity', function () { expect(e.id).to.equal('w1'); }); - it("tags the entity as updated", function () { - var tags = {foo: 'bar'}, - e = iD.Entity().update({tags: tags}); - expect(e._updated).to.to.be.true; - }); - it("doesn't modify the input", function () { var attrs = {tags: {foo: 'bar'}}, e = iD.Entity().update(attrs); @@ -69,6 +63,33 @@ describe('iD.Entity', function () { }); }); + describe("#mergeTags", function () { + it("returns a new Entity", function () { + var a = iD.Entity(), + b = a.mergeTags({}); + expect(b instanceof iD.Entity).to.be.true; + expect(a).not.to.equal(b); + }); + + it("merges tags", function () { + var a = iD.Entity({tags: {a: 'a'}}), + b = a.mergeTags({b: 'b'}); + expect(b.tags).to.eql({a: 'a', b: 'b'}); + }); + + it("combines non-conflicting tags", function () { + var a = iD.Entity({tags: {a: 'a'}}), + b = a.mergeTags({a: 'a'}); + expect(b.tags).to.eql({a: 'a'}); + }); + + it("combines conflicting tags with semicolons", function () { + var a = iD.Entity({tags: {a: 'a'}}), + b = a.mergeTags({a: 'b'}); + expect(b.tags).to.eql({a: 'a; b'}); + }); + }); + describe("#osmId", function () { it("returns an OSM ID as a string", function () { expect(iD.Entity({id: 'w1234'}).osmId()).to.eql('1234'); @@ -77,42 +98,6 @@ describe('iD.Entity', function () { }); }); - describe("#created", function () { - it("returns falsy by default", function () { - expect(iD.Entity({id: 'w1234'}).created()).not.to.be.ok; - }); - - it("returns falsy for an unmodified Entity", function () { - expect(iD.Entity({id: 'w1234'}).created()).not.to.be.ok; - }); - - it("returns falsy for a modified Entity with positive ID", function () { - expect(iD.Entity({id: 'w1234'}).update({}).created()).not.to.be.ok; - }); - - it("returns truthy for a modified Entity with negative ID", function () { - expect(iD.Entity({id: 'w-1234'}).update({}).created()).to.be.ok; - }); - }); - - describe("#modified", function () { - it("returns falsy by default", function () { - expect(iD.Entity({id: 'w1234'}).modified()).not.to.be.ok; - }); - - it("returns falsy for an unmodified Entity", function () { - expect(iD.Entity({id: 'w1234'}).modified()).not.to.be.ok; - }); - - it("returns truthy for a modified Entity with positive ID", function () { - expect(iD.Entity({id: 'w1234'}).update({}).modified()).to.be.ok; - }); - - it("returns falsy for a modified Entity with negative ID", function () { - expect(iD.Entity({id: 'w-1234'}).update({}).modified()).not.to.be.ok; - }); - }); - describe("#intersects", function () { it("returns true for a way with a node within the given extent", function () { var node = iD.Node({loc: [0, 0]}), diff --git a/test/spec/graph/graph.js b/test/spec/graph/graph.js index 05bcbbe89..a357ff360 100644 --- a/test/spec/graph/graph.js +++ b/test/spec/graph/graph.js @@ -333,86 +333,4 @@ describe('iD.Graph', function() { expect(graph.childNodes(way)).to.eql([node]); }); }); - - describe("#difference", function () { - it("returns an Array of ids of changed entities", function () { - var initial = iD.Node({id: "n1"}), - updated = initial.update({}), - created = iD.Node(), - deleted = iD.Node({id: 'n2'}), - graph1 = iD.Graph([initial, deleted]), - graph2 = graph1.replace(updated).replace(created).remove(deleted); - expect(graph2.difference(graph1)).to.eql([created.id, updated.id, deleted.id]); - }); - - - it("includes created entities, and reverse", function () { - var node = iD.Node(), - graph1 = iD.Graph(), - graph2 = graph1.replace(node); - expect(graph2.difference(graph1)).to.eql([node.id]); - expect(graph1.difference(graph2)).to.eql([node.id]); - }); - - it("includes entities changed from base, and reverse", function () { - var node = iD.Node(), - graph1 = iD.Graph(node), - graph2 = graph1.replace(node.update()); - expect(graph2.difference(graph1)).to.eql([node.id]); - expect(graph1.difference(graph2)).to.eql([node.id]); - }); - - it("includes already changed entities that were updated, and reverse", function () { - var node = iD.Node(), - graph1 = iD.Graph().replace(node), - graph2 = graph1.replace(node.update()); - expect(graph2.difference(graph1)).to.eql([node.id]); - expect(graph1.difference(graph2)).to.eql([node.id]); - }); - - it("includes affected child nodes", function () { - var n = iD.Node({id: 'n'}), - n2 = iD.Node({id: 'n2'}), - w1 = iD.Way({id: 'w1', nodes: ['n']}), - w1_ = iD.Way({id: 'w1', nodes: ['n', 'n2']}), - graph1 = iD.Graph([n, n2, w1]), - graph2 = graph1.replace(w1_); - expect(graph2.difference(graph1)).to.eql(['n2', 'w1']); - expect(graph1.difference(graph2)).to.eql(['n2', 'w1']); - }); - - }); - - describe("#modified", function () { - it("returns an Array of ids of modified entities", function () { - var node = iD.Node({id: 'n1'}), - node_ = iD.Node({id: 'n1'}), - graph = iD.Graph([node]).replace(node_); - expect(graph.modified()).to.eql([node.id]); - }); - }); - - describe("#created", function () { - it("returns an Array of ids of created entities", function () { - var node1 = iD.Node({id: 'n-1'}), - node2 = iD.Node({id: 'n2'}), - graph = iD.Graph([node2]).replace(node1); - expect(graph.created()).to.eql([node1.id]); - }); - }); - - describe("#deleted", function () { - it("returns an Array of ids of deleted entities", function () { - var node1 = iD.Node({id: "n1"}), - node2 = iD.Node(), - graph = iD.Graph([node1, node2]).remove(node1); - expect(graph.deleted()).to.eql([node1.id]); - }); - - it("doesn't include created entities that were subsequently deleted", function () { - var node = iD.Node(), - graph = iD.Graph().replace(node).remove(node); - expect(graph.deleted()).to.eql([]); - }); - }); }); diff --git a/test/spec/graph/history.js b/test/spec/graph/history.js index dc685b446..0ecbed5d3 100644 --- a/test/spec/graph/history.js +++ b/test/spec/graph/history.js @@ -13,7 +13,25 @@ describe("iD.History", function () { }); }); + describe("#merge", function () { + it("merges the entities into all graph versions", function () { + var n = iD.Node({id: 'n'}); + history.merge({n: n}); + expect(history.graph().entity('n')).to.equal(n); + }); + + it("emits a change event", function () { + history.on('change', spy); + history.merge({}); + expect(spy).to.have.been.called; + }); + }); + describe("#perform", function () { + it("returns a difference", function () { + expect(history.perform(action).changes()).to.eql({}); + }); + it("updates the graph", function () { var node = iD.Node(); history.perform(function (graph) { return graph.replace(node); }); @@ -27,8 +45,8 @@ describe("iD.History", function () { it("emits a change event", function () { history.on('change', spy); - history.perform(action); - expect(spy).to.have.been.calledWith([]); + var difference = history.perform(action); + expect(spy).to.have.been.calledWith(difference); }); it("performs multiple actions", function () { @@ -42,6 +60,10 @@ describe("iD.History", function () { }); describe("#replace", function () { + it("returns a difference", function () { + expect(history.replace(action).changes()).to.eql({}); + }); + it("updates the graph", function () { var node = iD.Node(); history.replace(function (graph) { return graph.replace(node); }); @@ -56,8 +78,8 @@ describe("iD.History", function () { it("emits a change event", function () { history.on('change', spy); - history.replace(action); - expect(spy).to.have.been.calledWith([]); + var difference = history.replace(action); + expect(spy).to.have.been.calledWith(difference); }); it("performs multiple actions", function () { @@ -71,6 +93,11 @@ describe("iD.History", function () { }); describe("#pop", function () { + it("returns a difference", function () { + history.perform(action, "annotation"); + expect(history.pop().changes()).to.eql({}); + }); + it("updates the graph", function () { history.perform(action, "annotation"); history.pop(); @@ -86,12 +113,16 @@ describe("iD.History", function () { it("emits a change event", function () { history.perform(action); history.on('change', spy); - history.pop(); - expect(spy).to.have.been.calledWith([]); + var difference = history.pop(); + expect(spy).to.have.been.calledWith(difference); }); }); describe("#undo", function () { + it("returns a difference", function () { + expect(history.undo().changes()).to.eql({}); + }); + it("pops the undo stack", function () { history.perform(action, "annotation"); history.undo(); @@ -121,12 +152,16 @@ describe("iD.History", function () { it("emits a change event", function () { history.perform(action); history.on('change', spy); - history.undo(); - expect(spy).to.have.been.calledWith([]); + var difference = history.undo(); + expect(spy).to.have.been.calledWith(difference); }); }); describe("#redo", function () { + it("returns a difference", function () { + expect(history.redo().changes()).to.eql({}); + }); + it("emits an redone event", function () { history.perform(action); history.undo(); @@ -139,8 +174,8 @@ describe("iD.History", function () { history.perform(action); history.undo(); history.on('change', spy); - history.redo(); - expect(spy).to.have.been.calledWith([]); + var difference = history.redo(); + expect(spy).to.have.been.calledWith(difference); }); }); diff --git a/test/spec/graph/node.js b/test/spec/graph/node.js index ad149261e..71a2c5e95 100644 --- a/test/spec/graph/node.js +++ b/test/spec/graph/node.js @@ -4,15 +4,6 @@ describe('iD.Node', function () { expect(iD.Node().type).to.equal("node"); }); - it("returns a created Entity if no ID is specified", function () { - expect(iD.Node().created()).to.be.ok; - }); - - it("returns an unmodified Entity if ID is specified", function () { - expect(iD.Node({id: 'n1234'}).created()).not.to.be.ok; - expect(iD.Node({id: 'n1234'}).modified()).not.to.be.ok; - }); - it("defaults tags to an empty object", function () { expect(iD.Node().tags).to.eql({}); }); diff --git a/test/spec/graph/relation.js b/test/spec/graph/relation.js index 8edf9c1e7..6e00dcacb 100644 --- a/test/spec/graph/relation.js +++ b/test/spec/graph/relation.js @@ -10,15 +10,6 @@ describe('iD.Relation', function () { expect(iD.Relation().type).to.equal("relation"); }); - it("returns a created Entity if no ID is specified", function () { - expect(iD.Relation().created()).to.be.ok; - }); - - it("returns an unmodified Entity if ID is specified", function () { - expect(iD.Relation({id: 'r1234'}).created()).not.to.be.ok; - expect(iD.Relation({id: 'r1234'}).modified()).not.to.be.ok; - }); - it("defaults members to an empty array", function () { expect(iD.Relation().members).to.eql([]); }); diff --git a/test/spec/graph/way.js b/test/spec/graph/way.js index ec41edec6..6b6b1543f 100644 --- a/test/spec/graph/way.js +++ b/test/spec/graph/way.js @@ -10,15 +10,6 @@ describe('iD.Way', function() { expect(iD.Way().type).to.equal("way"); }); - it("returns a created Entity if no ID is specified", function () { - expect(iD.Way().created()).to.be.ok; - }); - - it("returns an unmodified Entity if ID is specified", function () { - expect(iD.Way({id: 'w1234'}).created()).not.to.be.ok; - expect(iD.Way({id: 'w1234'}).modified()).not.to.be.ok; - }); - it("defaults nodes to an empty array", function () { expect(iD.Way().nodes).to.eql([]); }); diff --git a/test/spec/modes/add_point.js b/test/spec/modes/add_point.js index a284cf682..38a614198 100644 --- a/test/spec/modes/add_point.js +++ b/test/spec/modes/add_point.js @@ -1,41 +1,36 @@ describe("iD.modes.AddPoint", function () { - var container, map, history, controller, mode; + var context; beforeEach(function () { - container = d3.select('body').append('div'); - history = iD.History(); - map = iD.Map().history(history); - controller = iD.Controller(map, history); + var container = d3.select(document.createElement('div')); - container.call(map); - container.append('div') + context = iD() + .container(container); + + container.call(context.map()) + .append('div') .attr('class', 'inspector-wrap'); - mode = iD.modes.AddPoint(); - controller.enter(mode); - }); - - afterEach(function() { - container.remove(); + context.enter(iD.modes.AddPoint(context)); }); describe("clicking the map", function () { it("adds a node", function () { - happen.click(map.surface.node(), {}); - expect(history.changes().created).to.have.length(1); + happen.click(context.surface().node(), {}); + expect(context.changes().created).to.have.length(1); }); it("selects the node", function () { - happen.click(map.surface.node(), {}); - expect(controller.mode.id).to.equal('select'); - expect(controller.mode.selection()).to.eql([history.changes().created[0].id]); + happen.click(context.surface().node(), {}); + 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 () { happen.keydown(document, {keyCode: 27}); - expect(controller.mode.id).to.equal('browse'); + expect(context.mode().id).to.equal('browse'); }); }); }); diff --git a/test/spec/oauth.js b/test/spec/oauth.js index 8fd57c18c..b7ae1074f 100644 --- a/test/spec/oauth.js +++ b/test/spec/oauth.js @@ -2,7 +2,8 @@ describe('iD.OAuth', function() { var o; beforeEach(function() { - o = iD.OAuth(); + context = iD(); + o = iD.OAuth(context); }); describe('#logout', function() { diff --git a/test/spec/renderer/map.js b/test/spec/renderer/map.js index d7d0e3440..de8abdf61 100644 --- a/test/spec/renderer/map.js +++ b/test/spec/renderer/map.js @@ -1,22 +1,10 @@ describe('iD.Map', function() { - var container, map; + var map; beforeEach(function() { - container = d3.select('body').append('div'); - map = iD.Map(); - container.call(map); - }); - - afterEach(function() { - container.remove(); - }); - - describe('#connection', function() { - it('gets and sets connection', function() { - var connection = iD.Connection(); - expect(map.connection(connection)).to.equal(map); - expect(map.connection()).to.equal(connection); - }); + map = iD().map(); + d3.select(document.createElement('div')) + .call(map); }); describe('#zoom', function() { diff --git a/test/spec/ui/confirm.js b/test/spec/ui/confirm.js index a9380e1cc..428a8aa6c 100644 --- a/test/spec/ui/confirm.js +++ b/test/spec/ui/confirm.js @@ -2,10 +2,13 @@ describe("iD.ui.confirm", function () { it('can be instantiated', function () { var confirm = iD.ui.confirm(); expect(confirm).to.be.ok; + happen.keydown(document, {keyCode: 27}); // dismiss }); + it('can be dismissed', function () { var confirm = iD.ui.confirm(); happen.click(confirm.select('button').node()); expect(confirm.node().parentNode).to.be.null; + happen.keydown(document, {keyCode: 27}); // dismiss }); }); diff --git a/test/spec/ui/modal.js b/test/spec/ui/modal.js index 32a42878a..d7f818d80 100644 --- a/test/spec/ui/modal.js +++ b/test/spec/ui/modal.js @@ -4,5 +4,6 @@ describe("iD.ui.modal", function () { .select('.content') .text('foo'); expect(modal).to.be.ok; + happen.keydown(document, {keyCode: 27}); // dismiss }); });