diff --git a/css/app.css b/css/app.css index 468478091..90401612c 100644 --- a/css/app.css +++ b/css/app.css @@ -1064,7 +1064,17 @@ div.typeahead a:first-child { } .Browse .tooltip .tooltip-arrow { - left: 30px; - } - + left: 30px; +} +.tail { + pointer-events:none; + position: absolute; + background: rgba(255, 255, 255, 0.7); + max-width: 250px; + margin-top: -15px; + padding: 5px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} diff --git a/css/map.css b/css/map.css index 590cc114e..b2929f149 100644 --- a/css/map.css +++ b/css/map.css @@ -65,9 +65,12 @@ g.vertex circle.stroke { fill:#333; } -g.vertex.shared circle { +g.vertex.shared circle.fill { fill:#aff; } +g.vertex.shared circle.stroke { + fill:#044; +} g.vertex.hover circle.fill { -webkit-transform:scale(1.5, 1.5); @@ -81,9 +84,12 @@ g.vertex.hover circle.stroke { transform:scale(1.4, 1.4); } -g.vertex circle.selected { +g.vertex circle.selected.fill { fill: #ffff00; } +g.vertex circle.selected.stroke { + fill: #38380A; +} circle.midpoint { fill:#aaa; @@ -252,9 +258,6 @@ path.casing.tag-highway-secondary_link { stroke:#444; } -path.stroke.tag-bridge-yes { - stroke:#eee; -} path.casing.tag-bridge-yes { stroke-width: 14; stroke: #000; diff --git a/index.html b/index.html index 8e86da888..9db659489 100644 --- a/index.html +++ b/index.html @@ -22,6 +22,7 @@ + @@ -31,7 +32,6 @@ - diff --git a/js/id/connection.js b/js/id/connection.js index 101c7a9c8..003750ff2 100644 --- a/js/id/connection.js +++ b/js/id/connection.js @@ -16,6 +16,7 @@ iD.Connection = function() { function bboxFromAPI(box, tile, callback) { function done(err, parsed) { loadedTiles[tile.toString()] = true; + delete inflight[tile.toString()]; callback(err, parsed); } inflight[tile.toString()] = loadFromURL(bboxUrl(box), done); @@ -26,8 +27,6 @@ iD.Connection = function() { return callback(null, parse(dom)); } return d3.xml(url).get().on('load', done); - inflight.push(d3.xml(url).get() - .on('load', done)); } function getNodes(obj) { @@ -201,7 +200,7 @@ iD.Connection = function() { } function loadTiles(projection) { - var scaleExtent = [16, 16], + var scaleExtent = [15, 15], s = projection.scale(), tiles = d3.geo.tile() .scaleExtent(scaleExtent) diff --git a/js/id/graph/graph.js b/js/id/graph/graph.js index d1f5e4cc7..74caf7ceb 100644 --- a/js/id/graph/graph.js +++ b/js/id/graph/graph.js @@ -13,6 +13,7 @@ iD.Graph = function(entities) { this.transients = {}; this._parentWays = {}; this._parentRels = {}; + this._fetches = {}; if (iD.debug) { Object.freeze(this); @@ -119,20 +120,29 @@ iD.Graph.prototype = { // Resolve the id references in a way, replacing them with actual objects. fetch: function(id) { + if (this._fetches[id]) return this._fetches[id]; var entity = this.entities[id], nodes = []; if (!entity || !entity.nodes || !entity.nodes.length) return entity; for (var i = 0, l = entity.nodes.length; i < l; i++) { nodes[i] = this.fetch(entity.nodes[i]); } - return iD.Entity(entity, {nodes: nodes}); + return (this._fetches[id] = iD.Entity(entity, {nodes: nodes})); }, difference: function (graph) { - var result = [], entity, id; + var result = [], entity, oldentity, id; for (id in this.entities) { entity = this.entities[id]; - if (entity !== graph.entities[id]) { + oldentity = graph.entities[id]; + if (entity !== oldentity) { + if (entity && entity.type === 'way') { + result = oldentity ? + result + .concat(_.difference(entity.nodes, oldentity.nodes)) + .concat(_.difference(oldentity.nodes, entity.nodes)) + : result.concat(entity.nodes); + } result.push(id); } } @@ -141,6 +151,7 @@ iD.Graph.prototype = { entity = graph.entities[id]; if (entity && !this.entities.hasOwnProperty(id)) { result.push(id); + if (entity.type === 'way') result = result.concat(entity.nodes); } } diff --git a/js/id/graph/way.js b/js/id/graph/way.js index 77858d29a..5c3e1210b 100644 --- a/js/id/graph/way.js +++ b/js/id/graph/way.js @@ -6,7 +6,8 @@ iD.Way = iD.Entity.extend({ return resolver.transient(this, 'extent', function() { var extent = [[-Infinity, Infinity], [Infinity, -Infinity]]; for (var i = 0, l = this.nodes.length; i < l; i++) { - var node = resolver.entity(this.nodes[i]); + var node = this.nodes[i]; + if (node.loc === undefined) node = resolver.entity(node); if (node.loc[0] > extent[0][0]) extent[0][0] = node.loc[0]; if (node.loc[0] < extent[1][0]) extent[1][0] = node.loc[0]; if (node.loc[1] < extent[0][1]) extent[0][1] = node.loc[1]; diff --git a/js/id/id.js b/js/id/id.js index a6af4242a..e5e6b0eb0 100644 --- a/js/id/id.js +++ b/js/id/id.js @@ -183,9 +183,9 @@ window.iD = function(container) { .call(redo ? refreshTooltip : undo_tooltip.hide); }); - window.onresize = function() { + d3.select(window).on('resize.map-size', function() { map.size(m.size()); - }; + }); map.keybinding() .on('a', function(evt, mods) { diff --git a/js/id/modes/add_area.js b/js/id/modes/add_area.js index 33facb041..a9fcb830e 100644 --- a/js/id/modes/add_area.js +++ b/js/id/modes/add_area.js @@ -12,7 +12,7 @@ iD.modes.AddArea = function() { controller = mode.controller; map.dblclickEnable(false) - .hint('Click on the map to start drawing an area, like a park, lake, or building.'); + .tail('Click on the map to start drawing an area, like a park, lake, or building.'); map.surface.on('click.addarea', function() { var datum = d3.select(d3.event.target).datum() || {}, @@ -23,8 +23,7 @@ iD.modes.AddArea = function() { history.perform( iD.actions.AddWay(way), iD.actions.AddWayNode(way.id, datum.id), - iD.actions.AddWayNode(way.id, datum.id), - 'started an area'); + iD.actions.AddWayNode(way.id, datum.id)); } else { // start from a new node @@ -33,8 +32,7 @@ iD.modes.AddArea = function() { iD.actions.AddWay(way), iD.actions.AddNode(node), iD.actions.AddWayNode(way.id, node.id), - iD.actions.AddWayNode(way.id, node.id), - 'started an area'); + iD.actions.AddWayNode(way.id, node.id)); } controller.enter(iD.modes.DrawArea(way.id)); @@ -49,7 +47,7 @@ iD.modes.AddArea = function() { window.setTimeout(function() { mode.map.dblclickEnable(true); }, 1000); - mode.map.hint(false); + mode.map.tail(false); mode.map.surface.on('click.addarea', null); mode.map.keybinding().on('⎋.addarea', null); }; diff --git a/js/id/modes/add_line.js b/js/id/modes/add_line.js index bc87238ca..b83202097 100644 --- a/js/id/modes/add_line.js +++ b/js/id/modes/add_line.js @@ -13,7 +13,7 @@ iD.modes.AddLine = function() { controller = mode.controller; map.dblclickEnable(false) - .hint('Click on the map to start drawing an road, path, or route.'); + .tail('Click on the map to start drawing an road, path, or route.'); map.surface.on('click.addline', function() { var datum = d3.select(d3.event.target).datum() || {}, @@ -33,8 +33,7 @@ iD.modes.AddLine = function() { } else { history.perform( iD.actions.AddWay(way), - iD.actions.AddWayNode(way.id, datum.id), - 'started a line'); + iD.actions.AddWayNode(way.id, datum.id)); } } else if (datum.type === 'way') { @@ -46,8 +45,7 @@ iD.modes.AddLine = function() { iD.actions.AddWay(way), iD.actions.AddNode(node), iD.actions.AddWayNode(datum.id, node.id, choice.index), - iD.actions.AddWayNode(way.id, node.id), - 'started a line'); + iD.actions.AddWayNode(way.id, node.id)); } else { // begin a new way @@ -56,8 +54,7 @@ iD.modes.AddLine = function() { history.perform( iD.actions.AddWay(way), iD.actions.AddNode(node), - iD.actions.AddWayNode(way.id, node.id), - 'started a line'); + iD.actions.AddWayNode(way.id, node.id)); } controller.enter(iD.modes.DrawLine(way.id, direction)); @@ -70,7 +67,7 @@ iD.modes.AddLine = function() { mode.exit = function() { mode.map.dblclickEnable(true); - mode.map.hint(false); + mode.map.tail(false); mode.map.surface.on('click.addline', null); mode.map.keybinding().on('⎋.addline', null); }; diff --git a/js/id/modes/add_point.js b/js/id/modes/add_point.js index 37767f3de..21d6343e0 100644 --- a/js/id/modes/add_point.js +++ b/js/id/modes/add_point.js @@ -10,7 +10,7 @@ iD.modes.AddPoint = function() { history = mode.history, controller = mode.controller; - map.hint('Click on the map to add a point.'); + map.tail('Click on the map to add a point.'); map.surface.on('click.addpoint', function() { var node = iD.Node({loc: map.mouseCoordinates(), _poi: true}); @@ -28,7 +28,7 @@ iD.modes.AddPoint = function() { }; mode.exit = function() { - mode.map.hint(false); + mode.map.tail(false); mode.map.surface.on('click.addpoint', null); mode.map.keybinding().on('⎋.addpoint', null); }; diff --git a/js/id/modes/draw_area.js b/js/id/modes/draw_area.js index d868a7620..cdb966351 100644 --- a/js/id/modes/draw_area.js +++ b/js/id/modes/draw_area.js @@ -18,8 +18,7 @@ iD.modes.DrawArea = function(wayId) { map.dblclickEnable(false) .fastEnable(false); - map.hint('Click on the map to add points to your area. Finish the ' + - 'area by clicking on your first point'); + map.tail('Click to add points to your area. Click the first point to finish the area.'); history.perform( iD.actions.AddNode(node), @@ -42,22 +41,28 @@ iD.modes.DrawArea = function(wayId) { var datum = d3.select(d3.event.target).datum() || {}; if (datum.id === tailId || datum.id === headId) { - history.replace(iD.actions.DeleteNode(node.id)); - controller.enter(iD.modes.Select(way)); + if (way.nodes.length > 3) { + history.undo(); + controller.enter(iD.modes.Select(way)); + } else { + // Areas with less than 3 nodes gets deleted + history.replace(iD.actions.DeleteWay(way.id)); + controller.enter(iD.modes.Browse()); + } } else if (datum.type === 'node' && datum.id !== node.id) { // connect the way to an existing node history.replace( iD.actions.DeleteNode(node.id), iD.actions.AddWayNode(way.id, datum.id, -1), - 'added to an area'); + way.nodes.length > 2 ? 'added to an area' : ''); controller.enter(iD.modes.DrawArea(wayId)); } else { history.replace( iD.actions.Noop(), - 'added to an area'); + way.nodes.length > 2 ? 'added to an area' : ''); controller.enter(iD.modes.DrawArea(wayId)); } @@ -110,7 +115,7 @@ iD.modes.DrawArea = function(wayId) { surface.selectAll('.way, .node') .classed('active', false); - mode.map.hint(false); + mode.map.tail(false); mode.map.fastEnable(true); surface diff --git a/js/id/modes/draw_line.js b/js/id/modes/draw_line.js index 9c3d4b38e..68130d7b8 100644 --- a/js/id/modes/draw_line.js +++ b/js/id/modes/draw_line.js @@ -19,7 +19,7 @@ iD.modes.DrawLine = function(wayId, direction) { map.dblclickEnable(false) .fastEnable(false) - .hint('Click to add more points to the line. ' + + .tail('Click to add more points to the line. ' + 'Click on other lines to connect to them, and double-click to ' + 'end the line.'); @@ -42,16 +42,22 @@ iD.modes.DrawLine = function(wayId, direction) { if (datum.id === tailId) { // connect the way in a loop - history.replace( - iD.actions.DeleteNode(node.id), - iD.actions.AddWayNode(wayId, tailId, index), - 'added to a line'); + if (way.nodes.length > 2) { + history.replace( + iD.actions.DeleteNode(node.id), + iD.actions.AddWayNode(wayId, tailId, index), + 'added to a line'); - controller.enter(iD.modes.Select(way)); + controller.enter(iD.modes.Select(way)); + + } else { + history.replace(iD.actions.DeleteWay(way.id)); + controller.enter(iD.modes.Browse()); + } } else if (datum.id === headId) { // finish the way - history.replace(iD.actions.DeleteNode(node.id)); + history.undo(); controller.enter(iD.modes.Select(way)); @@ -146,7 +152,7 @@ iD.modes.DrawLine = function(wayId, direction) { surface.selectAll('.way, .node') .classed('active', false); - mode.map.hint(false); + mode.map.tail(false); mode.map.fastEnable(true); mode.map.minzoom(0); diff --git a/js/id/modes/select.js b/js/id/modes/select.js index a1fde48ef..d091fdc57 100644 --- a/js/id/modes/select.js +++ b/js/id/modes/select.js @@ -87,11 +87,12 @@ iD.modes.Select = function (entity) { // Exit mode if selected entity gets undone mode.history.on('change.entity-undone', function() { + var old = entity; entity = mode.history.graph().entity(entity.id); if (!entity) { mode.controller.enter(iD.modes.Browse()); - } else { - d3.select('.inspector-wrap').datum(entity).call(inspector); + } else if(!_.isEqual(entity.tags, old.tags)) { + inspector.tags(entity.tags); } }); @@ -138,7 +139,7 @@ iD.modes.Select = function (entity) { mode.exit = function () { var surface = mode.map.surface; - changeTags(entity, inspector.tags()); + entity && changeTags(entity, inspector.tags()); d3.select('.inspector-wrap') .style('display', 'none') .html(''); diff --git a/js/id/renderer/background.js b/js/id/renderer/background.js index 76188c79f..f861d2226 100644 --- a/js/id/renderer/background.js +++ b/js/id/renderer/background.js @@ -55,14 +55,36 @@ iD.Background = function() { ups = {}; tiles.forEach(function(d) { - d.push(source(d)); - // this tile has not been loaded yet - if (!cache[d] && - cache[atZoom(d, -1)] && + + // if this tile has already failed, do + // not request it + if (cache[d] !== false) d.push(source(d)); + + // if this tile has failed, try to request its tile above + if (cache[d] === false && + cache[atZoom(d, -1)] !== false && !ups[atZoom(d, -1)]) { + ups[atZoom(d, -1)] = true; tiles.push(atZoom(d, -1)); - } else if (!cache[d]) { + + // if this tile has not finished, req the one above + } else if (cache[d] === undefined && + + // but the tile above is in the cache + cache[atZoom(d, -1)] && + + // and another tile has not already requested the + // tile above + !ups[atZoom(d, -1)]) { + + ups[atZoom(d, -1)] = true; + tiles.push(atZoom(d, -1)); + + // if this tile has not yet completed, try keeping the + // tiles below it + } else if (cache[d] === undefined || + cache[d] === false) { upZoom(d, 1).forEach(function(u) { if (cache[u] && !ups[u]) { ups[u] = true; @@ -79,11 +101,12 @@ iD.Background = function() { image.exit().remove(); function load(d) { - cache[d] = true; + cache[d.slice(0, 3)] = true; d3.select(this).on('load', null); } - function error() { + function error(d) { + cache[d.slice(0, 3)] = false; d3.select(this).remove(); } diff --git a/js/id/renderer/map.js b/js/id/renderer/map.js index 3f64886b6..9edb347e4 100644 --- a/js/id/renderer/map.js +++ b/js/id/renderer/map.js @@ -21,6 +21,7 @@ iD.Map = function() { lines = iD.svg.Lines(), areas = iD.svg.Areas(), midpoints = iD.svg.Midpoints(), + tail = d3.tail(), surface, tilegroup; function map(selection) { @@ -29,14 +30,14 @@ iD.Map = function() { var supersurface = selection.append('div') .style('position', 'absolute') + .on('mousedown.drag', function() { + translateStart = projection.translate(); + }) .call(zoom); surface = supersurface.append('svg') .on('mouseup.reset-transform', resetTransform) .on('touchend.reset-transform', resetTransform) - .on('mousedown.drag', function() { - translateStart = projection.translate(); - }) .on('mousedown.zoom', function() { if (d3.event.button == 2) { d3.event.stopPropagation(); @@ -44,9 +45,13 @@ iD.Map = function() { }) .call(iD.svg.Surface()); + map.size(selection.size()); map.surface = surface; + supersurface + .call(tail); + d3.select(document).call(keybinding); } @@ -62,16 +67,22 @@ iD.Map = function() { all = graph.intersects(extent); filter = d3.functor(true); } else { - var only = {}; + var only = {}, + filterOnly = {}; for (var j = 0; j < difference.length; j++) { - var id = difference[j]; - only[id] = graph.fetch(id); - if (only[id] && only[id].type === 'node') { - var parents = graph.parentWays(only[id]); - for (var k = 0; k < parents.length; k++) { - // Don't re-fetch parents - if (only[parents[k].id] === undefined) { - only[parents[k].id] = graph.fetch(parents[k].id); + var id = difference[j], + entity = graph.fetch(id); + // Even if the entity is false (deleted), it needs to be + // removed from the surface + only[id] = entity; + if (entity && entity.intersects(extent, graph)) { + if (only[id].type === 'node') { + var parents = graph.parentWays(only[id]); + for (var k = 0; k < parents.length; k++) { + // Don't re-fetch parents + if (only[parents[k].id] === undefined) { + only[parents[k].id] = graph.fetch(parents[k].id); + } } } } @@ -80,7 +91,7 @@ iD.Map = function() { filter = function(d) { return d.midpoint ? d.way in only : d.id in only; }; } - if (all.length > 10000) { + if (all.length > 100000) { editOff(); return; } @@ -113,14 +124,14 @@ iD.Map = function() { if (Math.log(d3.event.scale / Math.LN2 - 8) < minzoom + 1) { iD.flash() .select('.content') - .text('Cannot zoom out further in current mode.') + .text('Cannot zoom out further in current mode.'); return map.zoom(16); } var fast = (d3.event.scale === projection.scale() && fastEnabled); projection .translate(d3.event.translate) .scale(d3.event.scale); - if (fast) { + if (fast && translateStart) { var a = d3.event.translate, b = translateStart, translate = 'translate(' + ~~(a[0] - b[0]) + 'px,' + @@ -278,6 +289,11 @@ iD.Map = function() { return map; }; + map.tail = function (_) { + tail.text(_); + return map; + }; + map.hint = function (_) { if (_ === false) { d3.select('div.inspector-wrap') diff --git a/js/id/renderer/style.js b/js/id/renderer/style.js deleted file mode 100644 index 734ce10ca..000000000 --- a/js/id/renderer/style.js +++ /dev/null @@ -1,38 +0,0 @@ -iD.Style = {}; - -// all styling that is done outside of CSS in iD. -// -// Since SVG does not support z-index, we sort roads manually with d3's `sort` -// and the `waystack` fn. -// -// This also chooses kosher CSS classes for ways, and images for points - -iD.Style.highway_stack = { - motorway: 0, - motorway_link: 1, - trunk: 2, - trunk_link: 3, - primary: 4, - primary_link: 5, - secondary: 6, - tertiary: 7, - unclassified: 8, - residential: 9, - service: 10, - footway: 11 -}; - -iD.Style.waystack = function(a, b) { - if (!a || !b) return 0; - if (a.tags.layer !== undefined && b.tags.layer !== undefined) { - return a.tags.layer - b.tags.layer; - } - if (a.tags.bridge) return 1; - if (b.tags.bridge) return -1; - var as = 0, bs = 0; - if (a.tags.highway && b.tags.highway) { - as -= iD.Style.highway_stack[a.tags.highway]; - bs -= iD.Style.highway_stack[b.tags.highway]; - } - return as - bs; -}; diff --git a/js/id/services/taginfo.js b/js/id/services/taginfo.js index 20f1cd09a..aa89a37fa 100644 --- a/js/id/services/taginfo.js +++ b/js/id/services/taginfo.js @@ -33,6 +33,25 @@ iD.taginfo = function() { return _.omit(parameters, 'geometry'); } + function popularKeys(parameters) { + var pop_field = 'count_all_fraction'; + if (parameters.filter) pop_field = 'count_' + parameters.filter + '_fraction'; + return function(d) { return parseFloat(d[pop_field]) > 0.01; }; + } + + function popularValues(parameters) { + return function(d) { return parseFloat(d['fraction']) > 0.01; }; + } + + function valKey(d) { return { value: d.key }; } + + function valKeyDescription(d) { + return { + value: d.value, + title: d.description + }; + } + taginfo.keys = function(parameters, callback) { parameters = clean(setSort(setFilter(parameters))); d3.json(endpoint + 'keys/all?' + @@ -41,7 +60,10 @@ iD.taginfo = function() { sortname: 'count_all', sortorder: 'desc', page: 1 - }, parameters)), callback); + }, parameters)), function(err, d) { + if (err) return callback(err); + callback(null, d.data.filter(popularKeys(parameters)).map(valKey)); + }); }; taginfo.values = function(parameters, callback) { @@ -52,7 +74,10 @@ iD.taginfo = function() { sortname: 'count_all', sortorder: 'desc', page: 1 - }, parameters)), callback); + }, parameters)), function(err, d) { + if (err) return callback(err); + callback(null, d.data.filter(popularValues()).map(valKeyDescription)); + }); }; taginfo.docs = function(parameters, callback) { diff --git a/js/id/svg.js b/js/id/svg.js index fd648f5db..5860126e9 100644 --- a/js/id/svg.js +++ b/js/id/svg.js @@ -2,13 +2,13 @@ iD.svg = { RoundProjection: function (projection) { return function (d) { return iD.util.geo.roundCoords(projection(d)); - } + }; }, PointTransform: function (projection) { projection = iD.svg.RoundProjection(projection); return function (entity) { return 'translate(' + projection(entity.loc) + ')'; - } + }; } }; diff --git a/js/id/svg/areas.js b/js/id/svg/areas.js index 386f5f0c3..ff24afecc 100644 --- a/js/id/svg/areas.js +++ b/js/id/svg/areas.js @@ -1,5 +1,32 @@ iD.svg.Areas = function() { - return function(surface, graph, entities, filter, projection) { + + var area_stack = { + building: 0, + manmade: 1, + natural: 1, + boundary: 2 + }; + + function findKey(a) { + var vals = Object.keys(a.tags).filter(function(k) { + return area_stack[k] !== undefined; + }); + if (vals.length > 0) return area_stack[vals[0]]; + else return -1; + } + + function areastack(a, b) { + if (!a || !b || !a.tags || !b.tags) return 0; + if (a.tags.layer !== undefined && b.tags.layer !== undefined) { + return a.tags.layer - b.tags.layer; + } + var as = 0, bs = 0; + as -= findKey(a); + bs -= findKey(b); + return as - bs; + } + + return function drawAreas(surface, graph, entities, filter, projection) { var areas = []; for (var i = 0; i < entities.length; i++) { @@ -9,6 +36,8 @@ iD.svg.Areas = function() { } } + areas.sort(areastack); + var lineStrings = {}; function lineString(entity) { diff --git a/js/id/svg/lines.js b/js/id/svg/lines.js index 3a64c1b28..3f1b634d7 100644 --- a/js/id/svg/lines.js +++ b/js/id/svg/lines.js @@ -1,16 +1,68 @@ iD.svg.Lines = function() { - var arrowtext = '►\u3000\u3000', - alength; + var arrowtext = '►\u3000\u3000', + alength; + + var highway_stack = { + motorway: 0, + motorway_link: 1, + trunk: 2, + trunk_link: 3, + primary: 4, + primary_link: 5, + secondary: 6, + tertiary: 7, + unclassified: 8, + residential: 9, + service: 10, + footway: 11 + }; + + function waystack(a, b) { + if (!a || !b || !a.tags || !b.tags) return 0; + if (a.tags.layer !== undefined && b.tags.layer !== undefined) { + return a.tags.layer - b.tags.layer; + } + if (a.tags.bridge) return 1; + if (b.tags.bridge) return -1; + var as = 0, bs = 0; + if (a.tags.highway && b.tags.highway) { + as -= highway_stack[a.tags.highway]; + bs -= highway_stack[b.tags.highway]; + } + return as - bs; + } + + function drawPaths(group, lines, filter, classes, lineString) { + var paths = group.selectAll('path') + .filter(filter) + .data(lines, iD.Entity.key); + + paths.enter() + .append('path') + .attr('class', classes); + + paths + .order() + .attr('d', lineString) + .call(iD.svg.TagClasses()); + + paths.exit() + .remove(); + + return paths; + } + + return function drawLines(surface, graph, entities, filter, projection) { - return function(surface, graph, entities, filter, projection) { if (!alength) { var arrow = surface.append('text').text(arrowtext); alength = arrow.node().getComputedTextLength(); arrow.remove(); } - var lines = []; + var lines = [], + lineStrings = {}; for (var i = 0; i < entities.length; i++) { var entity = entities[i]; @@ -19,7 +71,7 @@ iD.svg.Lines = function() { } } - var lineStrings = {}; + lines.sort(waystack); function lineString(entity) { if (lineStrings[entity.id] !== undefined) { @@ -31,32 +83,12 @@ iD.svg.Lines = function() { 'M' + nodes.map(iD.svg.RoundProjection(projection)).join('L')); } - function drawPaths(group, lines, filter, classes) { - var paths = group.selectAll('path') - .filter(filter) - .data(lines, iD.Entity.key); - - paths.enter() - .append('path') - .attr('class', classes); - - paths - .order() - .attr('d', lineString) - .call(iD.svg.TagClasses()); - - paths.exit() - .remove(); - - return paths; - } - var casing = surface.select('.layer-casing'), stroke = surface.select('.layer-stroke'), defs = surface.select('defs'), text = surface.select('.layer-text'), - casings = drawPaths(casing, lines, filter, 'way line casing'), - strokes = drawPaths(stroke, lines, filter, 'way line stroke'); + casings = drawPaths(casing, lines, filter, 'way line casing', lineString), + strokes = drawPaths(stroke, lines, filter, 'way line stroke', lineString); // Determine the lengths of oneway paths var lengths = {}, @@ -97,5 +129,5 @@ iD.svg.Lines = function() { // adding longer text than necessary, since overflow is hidden return (new Array(Math.floor(lengths[d.id] * 1.1))).join(arrowtext); }); - } + }; }; diff --git a/js/id/svg/midpoints.js b/js/id/svg/midpoints.js index 185afc696..9c060ea3b 100644 --- a/js/id/svg/midpoints.js +++ b/js/id/svg/midpoints.js @@ -1,5 +1,5 @@ iD.svg.Midpoints = function() { - return function(surface, graph, entities, filter, projection) { + return function drawMidpoints(surface, graph, entities, filter, projection) { var midpoints = []; for (var i = 0; i < entities.length; i++) { diff --git a/js/id/svg/points.js b/js/id/svg/points.js index 9015f1d5d..7a2600814 100644 --- a/js/id/svg/points.js +++ b/js/id/svg/points.js @@ -10,7 +10,7 @@ iD.svg.Points = function() { return 'icons/unknown.png'; } - return function(surface, graph, entities, filter, projection) { + return function drawPoints(surface, graph, entities, filter, projection) { var points = []; for (var i = 0; i < entities.length; i++) { @@ -20,6 +20,10 @@ iD.svg.Points = function() { } } + if (points.length > 100) { + return surface.select('.layer-hit').selectAll('g.point').remove(); + } + var groups = surface.select('.layer-hit').selectAll('g.point') .filter(filter) .data(points, iD.Entity.key); diff --git a/js/id/svg/surface.js b/js/id/svg/surface.js index 5c19a0c65..9fb64ca0e 100644 --- a/js/id/svg/surface.js +++ b/js/id/svg/surface.js @@ -1,5 +1,5 @@ iD.svg.Surface = function() { - return function(selection) { + return function drawSurface(selection) { selection.append('defs') .append('clipPath') .attr('id', 'clip') diff --git a/js/id/svg/vertices.js b/js/id/svg/vertices.js index 36363fbff..700ef5538 100644 --- a/js/id/svg/vertices.js +++ b/js/id/svg/vertices.js @@ -1,5 +1,5 @@ iD.svg.Vertices = function() { - return function(surface, graph, entities, filter, projection) { + return function drawVertices(surface, graph, entities, filter, projection) { var vertices = []; for (var i = 0; i < entities.length; i++) { @@ -9,6 +9,10 @@ iD.svg.Vertices = function() { } } + if (vertices.length > 2000) { + return surface.select('.layer-hit').selectAll('g.vertex').remove(); + } + var groups = surface.select('.layer-hit').selectAll('g.vertex') .filter(filter) .data(vertices, iD.Entity.key); diff --git a/js/id/ui/inspector.js b/js/id/ui/inspector.js index 5edc48e9f..9078dea7b 100644 --- a/js/id/ui/inspector.js +++ b/js/id/ui/inspector.js @@ -83,6 +83,7 @@ iD.ui.inspector = function() { } function drawButtons(selection) { +<<<<<<< HEAD var inspectorButton1 = selection.append('div') .attr('class', 'button-wrap') .append('button') @@ -111,7 +112,8 @@ iD.ui.inspector = function() { tags = [{key: '', value: ''}]; } - var li = tagList.selectAll('li') + var li = tagList.html('') + .selectAll('li') .data(tags, function(d) { return d.key; }); li.exit().remove(); @@ -166,7 +168,6 @@ iD.ui.inspector = function() { if (en.on_node) types.push('point'); if (en.on_way) types.push('line'); en.types = types; - console.log(en); iD.ui.modal() .select('.content') .datum(en) @@ -225,15 +226,26 @@ iD.ui.inspector = function() { key = row.selectAll('.key'), value = row.selectAll('.value'); + function sort(value, data) { + var sameletter = [], + other = []; + for (var i = 0; i < data.length; i++) { + if (data[i].value.substring(0, value.length) === value) { + sameletter.push(data[i]); + } else { + other.push(data[i]); + } + } + return sameletter.concat(other); + } + key.call(d3.typeahead() .data(_.debounce(function(_, callback) { taginfo.keys({ geometry: geometry, query: key.property('value') }, function(err, data) { - callback(data.data.map(function (d) { - return {value: d.key}; - })); + if (!err) callback(sort(key.property('value'), data)); }); }, 500))); @@ -244,9 +256,7 @@ iD.ui.inspector = function() { geometry: geometry, query: value.property('value') }, function(err, data) { - callback(data.data.map(function (d) { - return {value: d.value, title: d.description}; - })); + if (!err) callback(sort(value.property('value'), data)); }); }, 500))); } @@ -272,15 +282,19 @@ iD.ui.inspector = function() { event.close(entity); } - inspector.tags = function () { - var tags = {}; - tagList.selectAll('li').each(function() { - var row = d3.select(this), - key = row.selectAll('.key').property('value'), - value = row.selectAll('.value').property('value'); - if (key !== '') tags[key] = value; - }); - return tags; + inspector.tags = function (tags) { + if (!arguments.length) { + var tags = {}; + tagList.selectAll('li').each(function() { + var row = d3.select(this), + key = row.selectAll('.key').property('value'), + value = row.selectAll('.value').property('value'); + if (key !== '') tags[key] = value; + }); + return tags; + } else { + drawTags(tags); + } }; return d3.rebind(inspector, event, 'on'); diff --git a/js/lib/d3.tail.js b/js/lib/d3.tail.js new file mode 100644 index 000000000..8046e4a96 --- /dev/null +++ b/js/lib/d3.tail.js @@ -0,0 +1,70 @@ +d3.tail = function() { + var text = false, + container, + xmargin = 20, + tooltip_size = [0, 0], + selection_size = [0, 0], + transformProp = iD.util.prefixCSSProperty('Transform'); + + var tail = function(selection) { + + d3.select(window).on('resize.tail-size', function() { + selection_size = selection.size(); + }); + + function setup() { + + container = d3.select(document.body) + .append('div').attr('class', 'tail'); + + selection + .on('mousemove.tail', mousemove) + .on('mouseover.tail', mouseover) + .on('mouseout.tail', mouseout); + + container + .on('mousemove.tail', mousemove); + + selection_size = selection.size(); + + } + + function mousemove() { + if (text === false) return; + var xoffset = ((d3.event.x + tooltip_size[0] + xmargin) > selection_size[0]) ? + -tooltip_size[0] - xmargin : xoffset = xmargin; + container.style(transformProp, 'translate(' + + (~~d3.event.x + xoffset) + 'px,' + + ~~d3.event.y + 'px)'); + } + + function mouseout() { + if (d3.event.relatedTarget !== container.node() && + text !== false) container.style('display', 'none'); + } + + function mouseover() { + if (d3.event.relatedTarget !== container.node() && + text !== false) container.style('display', 'block'); + } + + if (!container) setup(); + + }; + + tail.text = function(_) { + if (_ === false) { + text = _; + container.style('display', 'none'); + return tail; + } else if (container.style('display') == 'none') { + container.style('display', 'block'); + } + text = _; + container.text(text); + tooltip_size = container.size(); + return tail; + }; + + return tail; +}; diff --git a/js/lib/d3.typeahead.js b/js/lib/d3.typeahead.js index 554b89a60..08a1a36ec 100644 --- a/js/lib/d3.typeahead.js +++ b/js/lib/d3.typeahead.js @@ -2,7 +2,7 @@ d3.typeahead = function() { var data; var typeahead = function(selection) { - var container, hidden, idx = 0; + var container, hidden, idx = -1; function setup() { var rect = selection.node().getBoundingClientRect(); @@ -20,7 +20,7 @@ d3.typeahead = function() { function hide() { container.remove(); - idx = 0; + idx = -1; hidden = true; } @@ -33,14 +33,17 @@ d3.typeahead = function() { .on('blur.typeahead', slowHide); function key() { + var len = container.selectAll('a').data().length; if (d3.event.keyCode === 40) { - idx++; + idx = Math.min(idx + 1, len - 1); return highlight(); } else if (d3.event.keyCode === 38) { - idx--; + idx = Math.max(idx - 1, 0); return highlight(); } else if (d3.event.keyCode === 13) { - select(container.select('a.selected').datum()); + if (container.select('a.selected').node()) { + select(container.select('a.selected').datum()); + } hide(); } else { update(); diff --git a/test/index.html b/test/index.html index 072439887..f8d570f36 100644 --- a/test/index.html +++ b/test/index.html @@ -37,7 +37,6 @@ - @@ -55,6 +54,9 @@ + + + @@ -152,6 +154,10 @@ + + + + diff --git a/test/spec/taginfo.js b/test/spec/taginfo.js index b41c760e0..61f7bcea0 100644 --- a/test/spec/taginfo.js +++ b/test/spec/taginfo.js @@ -1,8 +1,9 @@ describe("iD.taginfo", function() { - var server; + var server, taginfo; beforeEach(function() { server = sinon.fakeServer.create(); + taginfo = iD.taginfo(); }); afterEach(function() { @@ -15,46 +16,81 @@ describe("iD.taginfo", function() { describe("#keys", function() { it("calls the given callback with the results of the keys query", function() { - var taginfo = iD.taginfo(), - callback = sinon.spy(); - + var callback = sinon.spy(); taginfo.keys({query: "amen"}, callback); server.respondWith("GET", new RegExp("http://taginfo.openstreetmap.org/api/4/keys/all"), [200, { "Content-Type": "application/json" }, - '{"data":[{"count_all":5190337,"key":"amenity"}]}']); + '{"data":[{"count_all":5190337,"key":"amenity","count_all_fraction":1.0}]}']); server.respond(); expect(query(server.requests[0].url)).to.eql( {query: "amen", page: "1", rp: "6", sortname: "count_all", sortorder: "desc"}); - expect(callback).to.have.been.calledWith(null, - {"data":[{"count_all":5190337,"key":"amenity"}]}); + expect(callback).to.have.been.calledWith(null, [{"value":"amenity"}]); + }); + + it("filters only popular nodes", function() { + var callback = sinon.spy(); + taginfo.keys({query: "amen"}, callback); + + server.respondWith("GET", new RegExp("http://taginfo.openstreetmap.org/api/4/keys/all"), + [200, { "Content-Type": "application/json" }, + '{"data":[{"count_all":5190337,"key":"amenity","count_all_fraction":1.0, "count_nodes_fraction":1.0},\ + {"count_all":1,"key":"amenityother","count_all_fraction":0.0, "count_nodes_fraction":0.0}]}']); + server.respond(); + + expect(callback).to.have.been.calledWith(null, [{"value":"amenity"}]); + }); + + it("filters only popular nodes with an entity type filter", function() { + var callback = sinon.spy(); + + taginfo.keys({query: "amen", filter: "nodes"}, callback); + + server.respondWith("GET", new RegExp("http://taginfo.openstreetmap.org/api/4/keys/all"), + [200, { "Content-Type": "application/json" }, + '{"data":[{"count_all":5190337,"key":"amenity","count_all_fraction":1.0, "count_nodes_fraction":1.0},\ + {"count_all":1,"key":"amenityother","count_all_fraction":0.0, "count_nodes_fraction":1.0}]}']); + server.respond(); + + expect(callback).to.have.been.calledWith(null, [{"value":"amenity"},{"value":"amenityother"}]); }); }); describe("#values", function() { it("calls the given callback with the results of the values query", function() { - var taginfo = iD.taginfo(), - callback = sinon.spy(); + var callback = sinon.spy(); taginfo.values({key: "amenity", query: "par"}, callback); server.respondWith("GET", new RegExp("http://taginfo.openstreetmap.org/api/4/key/values"), [200, { "Content-Type": "application/json" }, - '{"data":[{"value":"parking","description":"A place for parking cars"}]}']); + '{"data":[{"value":"parking","description":"A place for parking cars", "fraction":0.1}]}']); server.respond(); expect(query(server.requests[0].url)).to.eql( {key: "amenity", query: "par", page: "1", rp: "20", sortname: 'count_all', sortorder: 'desc'}); - expect(callback).to.have.been.calledWith(null, - {"data":[{"value":"parking","description":"A place for parking cars"}]}); + expect(callback).to.have.been.calledWith(null, [{"value":"parking","title":"A place for parking cars"}]); + }); + + it("filters popular values", function() { + var callback = sinon.spy(); + + taginfo.values({key: "amenity", query: "par"}, callback); + + server.respondWith("GET", new RegExp("http://taginfo.openstreetmap.org/api/4/key/values"), + [200, { "Content-Type": "application/json" }, + '{"data":[{"value":"parking","description":"A place for parking cars", "fraction":1.0},\ + {"value":"party","description":"A place for partying", "fraction":0.0}]}']); + server.respond(); + + expect(callback).to.have.been.calledWith(null, [{"value":"parking","title":"A place for parking cars"}]); }); }); describe("#docs", function() { it("calls the given callback with the results of the docs query", function() { - var taginfo = iD.taginfo(), - callback = sinon.spy(); + var callback = sinon.spy(); taginfo.docs({key: "amenity", value: "parking"}, callback); diff --git a/test/spec/ui/confirm.js b/test/spec/ui/confirm.js new file mode 100644 index 000000000..a9380e1cc --- /dev/null +++ b/test/spec/ui/confirm.js @@ -0,0 +1,11 @@ +describe("iD.ui.confirm", function () { + it('can be instantiated', function () { + var confirm = iD.ui.confirm(); + expect(confirm).to.be.ok; + }); + it('can be dismissed', function () { + var confirm = iD.ui.confirm(); + happen.click(confirm.select('button').node()); + expect(confirm.node().parentNode).to.be.null; + }); +}); diff --git a/test/spec/ui/flash.js b/test/spec/ui/flash.js new file mode 100644 index 000000000..0426d1734 --- /dev/null +++ b/test/spec/ui/flash.js @@ -0,0 +1,13 @@ +describe("iD.ui.flash", function () { + it('can be instantiated', function () { + var flash = iD.ui.flash(); + expect(flash).to.be.ok; + }); + it('leaves after 1000 ms', function (done) { + var flash = iD.ui.flash(); + window.setTimeout(function() { + expect(flash.node().parentNode).to.be.null; + done(); + }, 1200); + }); +}); diff --git a/test/spec/ui/modal.js b/test/spec/ui/modal.js new file mode 100644 index 000000000..32a42878a --- /dev/null +++ b/test/spec/ui/modal.js @@ -0,0 +1,8 @@ +describe("iD.ui.modal", function () { + it('can be instantiated', function () { + var modal = iD.ui.modal() + .select('.content') + .text('foo'); + expect(modal).to.be.ok; + }); +}); diff --git a/test/spec/util.js b/test/spec/util.js index e6663ade2..3ac1cdda2 100644 --- a/test/spec/util.js +++ b/test/spec/util.js @@ -61,5 +61,36 @@ describe('Util', function() { expect(iD.util.geo.dist(a, b)).to.eql(5); }); }); + + describe('#pointInPolygon', function() { + it('says a point in a polygon is on a polygon', function() { + var poly = [[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]; + var point = [0.5, 0.5]; + expect(iD.util.geo.pointInPolygon(point, poly)).to.be.true; + }); + it('says a point outside of a polygon is outside', function() { + var poly = [ + [0, 0], + [0, 1], + [1, 1], + [1, 0], + [0, 0]]; + var point = [0.5, 1.5]; + expect(iD.util.geo.pointInPolygon(point, poly)).to.be.false; + }); + }); + + describe('#polygonContainsPolygon', function() { + it('says a polygon in a polygon is in', function() { + var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; + var inner = [[1, 1], [1, 2], [2, 2], [2, 1], [1, 1]]; + expect(iD.util.geo.polygonContainsPolygon(outer, inner)).to.be.true; + }); + it('says a polygon outside of a polygon is out', function() { + var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; + var inner = [[1, 1], [1, 9], [2, 2], [2, 1], [1, 1]]; + expect(iD.util.geo.polygonContainsPolygon(outer, inner)).to.be.false; + }); + }); }); });