diff --git a/API.md b/API.md index b88592cae..e0981f8e0 100644 --- a/API.md +++ b/API.md @@ -137,7 +137,7 @@ iD can use external presets exclusively or along with the default OpenStreetMap var iD = iD() .presets(customPresets) - .taginfo(iD.taginfo()) + .taginfo(iD.services.taginfo()) .imagery(iD.data.imagery); ``` @@ -152,7 +152,7 @@ Just like Presets, Imagery can be configured using the `iD().imagery` accessor. var iD = iD() .presets(customPresets) - .taginfo(iD.taginfo()) + .taginfo(iD.services.taginfo()) .imagery(customImagery); ``` @@ -168,7 +168,7 @@ The Imagery object should follow the structure defined by [editor-imagery-index] var iD = iD() .presets(customPresets) - .taginfo(iD.taginfo().endpoint('url')) + .taginfo(iD.services.taginfo().endpoint('url')) .imagery(customImagery); ``` diff --git a/Makefile b/Makefile index 4ad6861eb..65a4f96a5 100644 --- a/Makefile +++ b/Makefile @@ -63,6 +63,7 @@ dist/iD.js: \ js/lib/marked.js \ js/id/start.js \ js/id/id.js \ + js/id/services.js \ js/id/services/*.js \ js/id/util.js \ js/id/util/*.js \ diff --git a/css/app.css b/css/app.css index a83bb2847..243b1c43c 100644 --- a/css/app.css +++ b/css/app.css @@ -295,6 +295,8 @@ ul li { list-style: none;} .fl { float: left;} .fr { float: right;} +.al { left: 0; } +.ar { right: 0; } div.hide, form.hide, @@ -655,7 +657,6 @@ button.save.has-count .count::before { .mapillary-image { position: absolute; - right: 0; bottom: 30px; width: 330px; height: 250px; @@ -669,7 +670,6 @@ button.save.has-count .count::before { height: auto; background-color: rgba(0,0,0,.5); bottom: 0; - right: 0; padding: 5px 10px; } @@ -1797,6 +1797,10 @@ div.full-screen > button:hover { color: #7092FF; } +.layer-list:empty { + display: none; +} + .layer-list > li:first-child { border-radius: 3px 3px 0 0; } diff --git a/css/map.css b/css/map.css index 624aa97a2..88fcb6003 100644 --- a/css/map.css +++ b/css/map.css @@ -1514,43 +1514,84 @@ text.gpx { fill: #FF26D4; } -/* Mapillary Layer */ +/* Mapillary Image Layer */ -.layer-mapillary { +.layer-mapillary-images { pointer-events: none; } -.layer-mapillary g { +.layer-mapillary-images .viewfield-group { pointer-events: visible; cursor: pointer; /* Opera */ cursor: url(img/cursor-select-mapillary.png) 6 1, pointer; /* FF */ } -.layer-mapillary g * { +.layer-mapillary-images .viewfield-group * { stroke-width: 1; stroke: #444; fill: #ffc600; + z-index: 50; } -.layer-mapillary g:hover * { +.layer-mapillary-images .viewfield-group:hover * { stroke-width: 1; stroke: #333; fill: #ff9900; + z-index: 60; } -.layer-mapillary g.selected * { +.layer-mapillary-images .viewfield-group.selected * { stroke-width: 2; stroke: #222; fill: #ff5800; + z-index: 60; } -.layer-mapillary g:hover path.viewfield, -.layer-mapillary g.selected path.viewfield, -.layer-mapillary g path.viewfield { +.layer-mapillary-images .viewfield-group:hover path.viewfield, +.layer-mapillary-images .viewfield-group.selected path.viewfield, +.layer-mapillary-images .viewfield-group path.viewfield { stroke-width: 0; fill-opacity: 0.6; } +/* Mapillary Sign Layer */ + +.layer-mapillary-signs { + pointer-events: none; +} + +.layer-mapillary-signs .icon-sign body { + min-width: 20px; + height: 28px; + width: 28px; + border: 2px solid transparent; + pointer-events: visible; + cursor: pointer; /* Opera */ + cursor: url(img/cursor-select-mapillary.png) 6 1, pointer; /* FF */ + z-index: 70; +} + +.layer-mapillary-signs .icon-sign:hover body { + border: 2px solid rgba(255,198,0,0.8); + z-index: 80; + } + +.layer-mapillary-signs .icon-sign.selected body { + border: 2px solid rgba(255,0,0,0.8); + z-index: 80; + } + +.layer-mapillary-signs .icon-sign .t { + font-size: 28px; + z-index: 70; + position: absolute; +} + +.layer-mapillary-signs .icon-sign:hover .t, +.layer-mapillary-signs .icon-sign.selected .t { + z-index: 80; +} + /* Modes */ .mode-draw-line .vertex.active, diff --git a/data/core.yaml b/data/core.yaml index 0bed7af65..2a6864488 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -415,9 +415,13 @@ en: drag_drop: "Drag and drop a .gpx file on the page, or click the button to the right to browse" zoom: "Zoom to GPX track" browse: "Browse for a .gpx file" - mapillary: + mapillary_images: tooltip: "Street-level photos from Mapillary" title: "Photo Overlay (Mapillary)" + mapillary_signs: + tooltip: "Traffic signs from Mapillary" + title: "Traffic Sign Overlay (Mapillary)" + mapillary: view_on_mapillary: "View this image on Mapillary" help: title: "Help" diff --git a/dist/locales/en.json b/dist/locales/en.json index ad4ddff19..35e4b7329 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -501,9 +501,15 @@ "zoom": "Zoom to GPX track", "browse": "Browse for a .gpx file" }, - "mapillary": { + "mapillary_images": { "tooltip": "Street-level photos from Mapillary", - "title": "Photo Overlay (Mapillary)", + "title": "Photo Overlay (Mapillary)" + }, + "mapillary_signs": { + "tooltip": "Traffic signs from Mapillary", + "title": "Traffic Sign Overlay (Mapillary)" + }, + "mapillary": { "view_on_mapillary": "View this image on Mapillary" }, "help": { diff --git a/dist/traffico b/dist/traffico new file mode 160000 index 000000000..2ff4ac82f --- /dev/null +++ b/dist/traffico @@ -0,0 +1 @@ +Subproject commit 2ff4ac82f199f5e793e45e2326c24228db0d7726 diff --git a/index.html b/index.html index 9bb30b26b..798ce1acc 100644 --- a/index.html +++ b/index.html @@ -6,6 +6,7 @@ + @@ -39,7 +40,9 @@ - + + + @@ -57,7 +60,8 @@ - + + @@ -249,7 +253,7 @@ id = iD() .presets(iD.data.presets) .imagery(iD.data.imagery) - .taginfo(iD.taginfo()) + .taginfo(iD.services.taginfo()) .assetPath('dist/'); d3.select('#id-container') diff --git a/js/id/core/tree.js b/js/id/core/tree.js index 6bc40c33d..05bbc2230 100644 --- a/js/id/core/tree.js +++ b/js/id/core/tree.js @@ -2,17 +2,8 @@ iD.Tree = function(head) { var rtree = rbush(), rectangles = {}; - function extentRectangle(extent) { - return [ - extent[0][0], - extent[0][1], - extent[1][0], - extent[1][1] - ]; - } - function entityRectangle(entity) { - var rect = extentRectangle(entity.extent(head)); + var rect = entity.extent(head).rectangle(); rect.id = entity.id; rectangles[entity.id] = rect; return rect; @@ -90,7 +81,7 @@ iD.Tree = function(head) { rtree.load(_.map(insertions, entityRectangle)); } - return rtree.search(extentRectangle(extent)).map(function(rect) { + return rtree.search(extent.rectangle()).map(function(rect) { return head.entity(rect.id); }); }; diff --git a/js/id/geo/extent.js b/js/id/geo/extent.js index 912bcf08a..685c22282 100644 --- a/js/id/geo/extent.js +++ b/js/id/geo/extent.js @@ -45,6 +45,10 @@ _.extend(iD.geo.Extent.prototype, { (this[0][1] + this[1][1]) / 2]; }, + rectangle: function() { + return [this[0][0], this[0][1], this[1][0], this[1][1]]; + }, + polygon: function() { return [ [this[0][0], this[0][1]], @@ -100,7 +104,7 @@ _.extend(iD.geo.Extent.prototype, { }, toParam: function() { - return [this[0][0], this[0][1], this[1][0], this[1][1]].join(','); + return this.rectangle().join(','); } }); diff --git a/js/id/id.js b/js/id/id.js index 0ff24a6f6..b0d3dc83d 100644 --- a/js/id/id.js +++ b/js/id/id.js @@ -2,11 +2,12 @@ window.iD = function () { window.locale.en = iD.data.en; window.locale.current('en'); - var context = {}, - storage; + var dispatch = d3.dispatch('enter', 'exit'), + context = {}; // https://github.com/openstreetmap/iD/issues/772 // http://mathiasbynens.be/notes/localstorage-pattern#comment-9 + var storage; try { storage = localStorage; } catch (e) {} // eslint-disable-line no-empty storage = storage || (function() { var s = {}; @@ -30,34 +31,7 @@ window.iD = function () { } }; - /* Accessor for setting minimum zoom for editing features. */ - - var minEditableZoom = 16; - context.minEditableZoom = function(_) { - if (!arguments.length) return minEditableZoom; - minEditableZoom = _; - connection.tileZoom(_); - return context; - }; - - var history = iD.History(context), - dispatch = d3.dispatch('enter', 'exit'), - mode, - container, - ui = iD.ui(context), - connection = iD.Connection(), - locale = iD.detect().locale, - localePath; - - if (locale && iD.data.locales.indexOf(locale) === -1) { - locale = locale.split('-')[0]; - } - - context.preauth = function(options) { - connection.switch(options); - return context; - }; - + var locale, localePath; context.locale = function(loc, path) { locale = loc; localePath = path; @@ -83,16 +57,24 @@ window.iD = function () { } }; + /* Straight accessors. Avoid using these if you can. */ + var ui, connection, history; context.ui = function() { return ui; }; context.connection = function() { return connection; }; context.history = function() { return history; }; + /* Connection */ function entitiesLoaded(err, result) { if (!err) history.merge(result.data, result.extent); } + context.preauth = function(options) { + connection.switch(options); + return context; + }; + context.loadTiles = function(projection, dimensions, callback) { function done(err, result) { entitiesLoaded(err, result); @@ -133,13 +115,17 @@ window.iD = function () { }); }; + var minEditableZoom = 16; + context.minEditableZoom = function(_) { + if (!arguments.length) return minEditableZoom; + minEditableZoom = _; + connection.tileZoom(_); + return context; + }; + + /* History */ - context.graph = history.graph; - context.changes = history.changes; - context.intersects = history.intersects; - var inIntro = false; - context.inIntro = function(_) { if (!arguments.length) return inIntro; inIntro = _; @@ -157,45 +143,34 @@ window.iD = function () { connection.flush(); features.reset(); history.reset(); + _.each(iD.services, function(service) { + var reset = service().reset; + if (reset) reset(context); + }); return context; }; - // Debounce save, since it's a synchronous localStorage write, - // and history changes can happen frequently (e.g. when dragging). - context.debouncedSave = _.debounce(context.save, 350); - function withDebouncedSave(fn) { - return function() { - var result = fn.apply(history, arguments); - context.debouncedSave(); - return result; - }; - } - - context.perform = withDebouncedSave(history.perform); - context.replace = withDebouncedSave(history.replace); - context.pop = withDebouncedSave(history.pop); - context.overwrite = withDebouncedSave(history.overwrite); - context.undo = withDebouncedSave(history.undo); - context.redo = withDebouncedSave(history.redo); /* Graph */ context.hasEntity = function(id) { return history.graph().hasEntity(id); }; - context.entity = function(id) { return history.graph().entity(id); }; - context.childNodes = function(way) { return history.graph().childNodes(way); }; - context.geometry = function(id) { return context.entity(id).geometry(history.graph()); }; + /* Modes */ + var mode; + context.mode = function() { + return mode; + }; context.enter = function(newMode) { if (mode) { mode.exit(); @@ -207,10 +182,6 @@ window.iD = function () { dispatch.enter(mode); }; - context.mode = function() { - return mode; - }; - context.selectedIDs = function() { if (mode && mode.selectedIDs) { return mode.selectedIDs(); @@ -219,15 +190,16 @@ window.iD = function () { } }; + /* Behaviors */ context.install = function(behavior) { context.surface().call(behavior); }; - context.uninstall = function(behavior) { context.surface().call(behavior.off); }; + /* Copy/Paste */ var copyIDs = [], copyGraph; context.copyGraph = function() { return copyGraph; }; @@ -238,15 +210,14 @@ window.iD = function () { return context; }; - /* Projection */ - context.projection = iD.geo.RawMercator(); /* Background */ - var background = iD.Background(context); + var background; context.background = function() { return background; }; + /* Features */ - var features = iD.Features(context); + var features; context.features = function() { return features; }; context.hasHiddenConnections = function(id) { var graph = history.graph(), @@ -254,20 +225,13 @@ window.iD = function () { return features.hasHiddenConnections(entity, graph); }; + /* Map */ - var map = iD.Map(context); + var map; context.map = function() { return map; }; context.layers = function() { return map.layers; }; context.surface = function() { return map.surface; }; context.editable = function() { return map.editable(); }; - context.mouse = map.mouse; - context.extent = map.extent; - context.pan = map.pan; - context.zoomIn = map.zoomIn; - context.zoomOut = map.zoomOut; - context.zoomInFurther = map.zoomInFurther; - context.zoomOutFurther = map.zoomOutFurther; - context.redrawEnable = map.redrawEnable; context.surfaceRect = function() { // Work around a bug in Firefox. @@ -276,9 +240,9 @@ window.iD = function () { return context.surface().node().parentNode.getBoundingClientRect(); }; - /* Presets */ - var presets = iD.presets(); + /* Presets */ + var presets; context.presets = function(_) { if (!arguments.length) return presets; presets.load(_); @@ -286,17 +250,28 @@ window.iD = function () { return context; }; + + /* Imagery */ context.imagery = function(_) { background.load(_); return context; }; + + /* Container */ + var container, embed; context.container = function(_) { if (!arguments.length) return container; container = _; container.classed('id-container', true); return context; }; + context.embed = function(_) { + if (!arguments.length) return embed; + embed = _; + return context; + }; + /* Taginfo */ var taginfo; @@ -306,13 +281,8 @@ window.iD = function () { return context; }; - var embed = false; - context.embed = function(_) { - if (!arguments.length) return embed; - embed = _; - return context; - }; + /* Assets */ var assetPath = ''; context.assetPath = function(_) { if (!arguments.length) return assetPath; @@ -332,9 +302,63 @@ window.iD = function () { return assetMap[asset] || assetPath + asset; }; + + /* Init */ + + context.projection = iD.geo.RawMercator(); + + locale = iD.detect().locale; + if (locale && iD.data.locales.indexOf(locale) === -1) { + locale = locale.split('-')[0]; + } + + history = iD.History(context); + context.graph = history.graph; + context.changes = history.changes; + context.intersects = history.intersects; + + // Debounce save, since it's a synchronous localStorage write, + // and history changes can happen frequently (e.g. when dragging). + context.debouncedSave = _.debounce(context.save, 350); + function withDebouncedSave(fn) { + return function() { + var result = fn.apply(history, arguments); + context.debouncedSave(); + return result; + }; + } + + context.perform = withDebouncedSave(history.perform); + context.replace = withDebouncedSave(history.replace); + context.pop = withDebouncedSave(history.pop); + context.overwrite = withDebouncedSave(history.overwrite); + context.undo = withDebouncedSave(history.undo); + context.redo = withDebouncedSave(history.redo); + + ui = iD.ui(context); + + connection = iD.Connection(); + + background = iD.Background(context); + + features = iD.Features(context); + + map = iD.Map(context); + context.mouse = map.mouse; + context.extent = map.extent; + context.pan = map.pan; + context.zoomIn = map.zoomIn; + context.zoomOut = map.zoomOut; + context.zoomInFurther = map.zoomInFurther; + context.zoomOutFurther = map.zoomOutFurther; + context.redrawEnable = map.redrawEnable; + + presets = iD.presets(); + return d3.rebind(context, dispatch, 'on'); }; + iD.version = '1.8.5'; (function() { diff --git a/js/id/renderer/background.js b/js/id/renderer/background.js index ce478e220..60f3e3449 100644 --- a/js/id/renderer/background.js +++ b/js/id/renderer/background.js @@ -4,7 +4,8 @@ iD.Background = function(context) { .projection(context.projection), gpxLayer = iD.GpxLayer(context, dispatch) .projection(context.projection), - mapillaryLayer = iD.MapillaryLayer(context), + mapillaryImageLayer, + mapillarySignLayer, overlayLayers = []; var backgroundSources; @@ -85,13 +86,43 @@ iD.Background = function(context) { gpx.call(gpxLayer); - var mapillary = selection.selectAll('.layer-mapillary') - .data([0]); - mapillary.enter().insert('div') - .attr('class', 'layer-layer layer-mapillary'); + var mapillary = iD.services.mapillary, + supportsMapillaryImages = !!mapillary, + supportsMapillarySigns = !!mapillary && mapillary().signsSupported(); - mapillary.call(mapillaryLayer); + var mapillaryImages = selection.selectAll('.layer-mapillary-images') + .data(supportsMapillaryImages ? [0] : []); + + mapillaryImages.enter().insert('div') + .attr('class', 'layer-layer layer-mapillary-images'); + + if (supportsMapillaryImages) { + if (!mapillaryImageLayer) { mapillaryImageLayer = iD.MapillaryImageLayer(context); } + mapillaryImages.call(mapillaryImageLayer); + } else { + mapillaryImageLayer = null; + } + + mapillaryImages.exit() + .remove(); + + + var mapillarySigns = selection.selectAll('.layer-mapillary-signs') + .data(supportsMapillarySigns ? [0] : []); + + mapillarySigns.enter().insert('div') + .attr('class', 'layer-layer layer-mapillary-signs'); + + if (supportsMapillarySigns) { + if (!mapillarySignLayer) { mapillarySignLayer = iD.MapillarySignLayer(context); } + mapillarySigns.call(mapillarySignLayer); + } else { + mapillarySignLayer = null; + } + + mapillarySigns.exit() + .remove(); } background.sources = function(extent) { @@ -103,7 +134,8 @@ iD.Background = function(context) { background.dimensions = function(_) { baseLayer.dimensions(_); gpxLayer.dimensions(_); - mapillaryLayer.dimensions(_); + if (mapillaryImageLayer) mapillaryImageLayer.dimensions(_); + if (mapillarySignLayer) mapillarySignLayer.dimensions(_); overlayLayers.forEach(function(layer) { layer.dimensions(_); @@ -172,12 +204,23 @@ iD.Background = function(context) { dispatch.change(); }; - background.showsMapillaryLayer = function() { - return mapillaryLayer.enable(); + background.showsMapillaryImageLayer = function() { + return mapillaryImageLayer && mapillaryImageLayer.enable(); }; - background.toggleMapillaryLayer = function() { - mapillaryLayer.enable(!mapillaryLayer.enable()); + background.showsMapillarySignLayer = function() { + return mapillarySignLayer && mapillarySignLayer.enable(); + }; + + background.toggleMapillaryImageLayer = function() { + if (!mapillaryImageLayer) return; + mapillaryImageLayer.enable(!mapillaryImageLayer.enable()); + dispatch.change(); + }; + + background.toggleMapillarySignLayer = function() { + if (!mapillarySignLayer) return; + mapillarySignLayer.enable(!mapillarySignLayer.enable()); dispatch.change(); }; diff --git a/js/id/renderer/map.js b/js/id/renderer/map.js index 94023f33e..a8d70826d 100644 --- a/js/id/renderer/map.js +++ b/js/id/renderer/map.js @@ -37,6 +37,10 @@ iD.Map = function(context) { supersurface = selection.append('div') .attr('id', 'supersurface'); + // Mapillary streetsigns require supersurface transform to have + // a value in order to do correct foreignObject positioning in Chrome + iD.util.setTransform(supersurface, 0, 0); + // Need a wrapper div because Opera can't cope with an absolutely positioned // SVG element: http://bl.ocks.org/jfirebaugh/6fbfbd922552bf776c16 var dataLayer = supersurface.append('div') diff --git a/js/id/renderer/mapillary_image_layer.js b/js/id/renderer/mapillary_image_layer.js new file mode 100644 index 000000000..544ef011b --- /dev/null +++ b/js/id/renderer/mapillary_image_layer.js @@ -0,0 +1,182 @@ +iD.MapillaryImageLayer = function(context) { + var debouncedRedraw = _.debounce(function () { context.pan([0,0]); }, 1000), + enabled = false, + minZoom = 12, + layer = d3.select(null), + _mapillary; + + function getMapillary() { + if (iD.services.mapillary && !_mapillary) { + _mapillary = iD.services.mapillary().on('loadedImages', debouncedRedraw); + } else if (!iD.services.mapillary && _mapillary) { + _mapillary = null; + } + return _mapillary; + } + + function showThumbnail(image) { + var mapillary = getMapillary(); + if (!mapillary) return; + + var thumb = mapillary.selectedThumbnail(), + posX = context.projection(image.loc)[0], + width = layer.dimensions()[0], + position = (posX < width / 2) ? 'right' : 'left'; + + if (thumb) { + d3.selectAll('.layer-mapillary-images .viewfield-group, .layer-mapillary-signs .icon-sign') + .classed('selected', function(d) { return d.key === thumb.key; }); + } + + mapillary.showThumbnail(image.key, position); + } + + function hideThumbnail() { + d3.selectAll('.layer-mapillary-images .viewfield-group, .layer-mapillary-signs .icon-sign') + .classed('selected', false); + + var mapillary = getMapillary(); + if (mapillary) { + mapillary.hideThumbnail(); + } + } + + function showLayer() { + editOn(); + layer + .style('opacity', 0) + .transition() + .duration(500) + .style('opacity', 1) + .each('end', debouncedRedraw); + } + + function hideLayer() { + debouncedRedraw.cancel(); + hideThumbnail(); + layer + .transition() + .duration(500) + .style('opacity', 0) + .each('end', editOff); + } + + function editOn() { + layer.style('display', 'block'); + } + + function editOff() { + layer.selectAll('.viewfield-group').remove(); + layer.style('display', 'none'); + } + + function transform(d) { + var t = iD.svg.PointTransform(context.projection)(d); + if (d.ca) t += ' rotate(' + Math.floor(d.ca) + ',0,0)'; + return t; + } + + function drawMarkers() { + var mapillary = getMapillary(), + data = (mapillary ? mapillary.images(context.projection, layer.dimensions()) : []); + + var markers = layer.selectAll('.viewfield-group') + .data(data, function(d) { return d.key; }); + + // Enter + var enter = markers.enter() + .append('g') + .attr('class', 'viewfield-group'); + + enter.append('path') + .attr('class', 'viewfield') + .attr('transform', 'scale(1.5,1.5),translate(-8, -13)') + .attr('d', 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z'); + + enter.append('circle') + .attr('dx', '0') + .attr('dy', '0') + .attr('r', '6'); + + // Exit + markers.exit() + .remove(); + + // Update + markers + .attr('transform', transform); + } + + function render(selection) { + var mapillary = getMapillary(); + + layer = selection.selectAll('svg') + .data(mapillary ? [0] : []); + + layer.enter() + .append('svg') + .style('display', enabled ? 'block' : 'none') + .dimensions(context.map().dimensions()) + .on('click', function() { // deselect/select + var mapillary = getMapillary(); + if (!mapillary) return; + var d = d3.event.target.__data__, + thumb = mapillary.selectedThumbnail(); + if (thumb && thumb.key === d.key) { + hideThumbnail(); + } else { + mapillary.selectedThumbnail(d); + context.map().centerEase(d.loc); + showThumbnail(d); + } + }) + .on('mouseover', function() { + var mapillary = getMapillary(); + if (!mapillary) return; + showThumbnail(d3.event.target.__data__); + }) + .on('mouseout', function() { + var mapillary = getMapillary(); + if (!mapillary) return; + var thumb = mapillary.selectedThumbnail(); + if (thumb) { + showThumbnail(thumb); + } else { + hideThumbnail(); + } + }); + + layer.exit() + .remove(); + + if (enabled) { + if (mapillary && ~~context.map().zoom() >= minZoom) { + editOn(); + drawMarkers(); + mapillary.loadImages(context.projection, layer.dimensions()); + } else { + editOff(); + } + } + } + + render.enable = function(_) { + if (!arguments.length) return enabled; + enabled = _; + if (enabled) { + showLayer(); + } else { + hideLayer(); + } + return render; + }; + + render.dimensions = function(_) { + if (layer.empty()) return null; + if (!arguments.length) return layer.dimensions(); + layer.dimensions(_); + return render; + }; + + return render; +}; diff --git a/js/id/renderer/mapillary_layer.js b/js/id/renderer/mapillary_layer.js deleted file mode 100644 index 05b6e4ed7..000000000 --- a/js/id/renderer/mapillary_layer.js +++ /dev/null @@ -1,159 +0,0 @@ -iD.MapillaryLayer = function (context) { - var enable = false, - currentImage, - svg, div, request; - - function show(image) { - svg.selectAll('g') - .classed('selected', function(d) { - return currentImage && d.key === currentImage.key; - }); - - div.classed('hidden', false) - .classed('temp', image !== currentImage); - - div.selectAll('img') - .attr('src', 'https://d1cuyjsrcm0gby.cloudfront.net/' + image.key + '/thumb-320.jpg'); - - div.selectAll('a') - .attr('href', 'https://www.mapillary.com/map/im/' + image.key); - } - - function hide() { - currentImage = undefined; - - svg.selectAll('g') - .classed('selected', false); - - div.classed('hidden', true); - } - - function transform(image) { - var t = 'translate(' + context.projection(image.loc) + ')'; - if (image.ca) t += 'rotate(' + image.ca + ',0,0)'; - return t; - } - - function render(selection) { - svg = selection.selectAll('svg') - .data([0]); - - svg.enter().append('svg') - .on('click', function() { - var image = d3.event.target.__data__; - if (currentImage === image) { - hide(); - } else { - currentImage = image; - show(image); - } - }) - .on('mouseover', function() { - show(d3.event.target.__data__); - }) - .on('mouseout', function() { - if (currentImage) { - show(currentImage); - } else { - hide(); - } - }); - - svg.style('display', enable ? 'block' : 'none'); - - div = context.container().selectAll('.mapillary-image') - .data([0]); - - var enter = div.enter().append('div') - .attr('class', 'mapillary-image'); - - enter.append('button') - .on('click', hide) - .append('div') - .call(iD.svg.Icon('#icon-close')); - - enter.append('img'); - - enter - .append('a') - .attr('class', 'link') - .attr('target', '_blank') - .call(iD.svg.Icon('#icon-out-link', 'inline')) - .append('span') - .text(t('mapillary.view_on_mapillary')); - - if (!enable) { - hide(); - - svg.selectAll('g') - .remove(); - - return; - } - - // Update existing images while waiting for new ones to load. - svg.selectAll('g') - .attr('transform', transform); - - var extent = context.map().extent(); - - if (request) - request.abort(); - - request = d3.json('https://a.mapillary.com/v2/search/s/geojson?client_id=NzNRM2otQkR2SHJzaXJmNmdQWVQ0dzoxNjQ3MDY4ZTUxY2QzNGI2&min_lat=' + - extent[0][1] + '&max_lat=' + extent[1][1] + '&min_lon=' + - extent[0][0] + '&max_lon=' + extent[1][0] + '&max_results=100&geojson=true', - function (error, data) { - if (error) return; - - var images = []; - - for (var i = 0; i < data.features.length; i++) { - var sequence = data.features[i]; - for (var j = 0; j < sequence.geometry.coordinates.length; j++) { - images.push({ - key: sequence.properties.keys[j], - ca: sequence.properties.cas[j], - loc: sequence.geometry.coordinates[j] - }); - if (images.length >= 1000) break; - } - } - - var g = svg.selectAll('g') - .data(images, function(d) { return d.key; }); - - var enter = g.enter().append('g') - .attr('class', 'image'); - - enter.append('path') - .attr('class', 'viewfield') - .attr('transform', 'scale(1.5,1.5),translate(-8, -13)') - .attr('d', 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z'); - - enter.append('circle') - .attr('dx', '0') - .attr('dy', '0') - .attr('r', '6'); - - g.attr('transform', transform); - - g.exit() - .remove(); - }); - } - - render.enable = function(_) { - if (!arguments.length) return enable; - enable = _; - return render; - }; - - render.dimensions = function(_) { - if (!arguments.length) return svg.dimensions(); - svg.dimensions(_); - return render; - }; - - return render; -}; diff --git a/js/id/renderer/mapillary_sign_layer.js b/js/id/renderer/mapillary_sign_layer.js new file mode 100644 index 000000000..88dea7292 --- /dev/null +++ b/js/id/renderer/mapillary_sign_layer.js @@ -0,0 +1,164 @@ +iD.MapillarySignLayer = function(context) { + var debouncedRedraw = _.debounce(function () { context.pan([0,0]); }, 1000), + enabled = false, + minZoom = 12, + layer = d3.select(null), + _mapillary; + + function getMapillary() { + if (iD.services.mapillary && !_mapillary) { + _mapillary = iD.services.mapillary().on('loadedSigns', debouncedRedraw); + } else if (!iD.services.mapillary && _mapillary) { + _mapillary = null; + } + return _mapillary; + } + + function showThumbnail(image) { + var mapillary = getMapillary(); + if (!mapillary) return; + + var thumb = mapillary.selectedThumbnail(), + posX = context.projection(image.loc)[0], + width = layer.dimensions()[0], + position = (posX < width / 2) ? 'right' : 'left'; + + if (thumb) { + d3.selectAll('.layer-mapillary-images .viewfield-group, .layer-mapillary-signs .icon-sign') + .classed('selected', function(d) { return d.key === thumb.key; }); + } + + mapillary.showThumbnail(image.key, position); + } + + function hideThumbnail() { + d3.selectAll('.layer-mapillary-images .viewfield-group, .layer-mapillary-signs .icon-sign') + .classed('selected', false); + + var mapillary = getMapillary(); + if (mapillary) { + mapillary.hideThumbnail(); + } + } + + function showLayer() { + editOn(); + debouncedRedraw(); + } + + function hideLayer() { + debouncedRedraw.cancel(); + hideThumbnail(); + editOff(); + } + + function editOn() { + layer.style('display', 'block'); + } + + function editOff() { + layer.selectAll('.icon-sign').remove(); + layer.style('display', 'none'); + } + + function drawSigns() { + var mapillary = getMapillary(), + data = (mapillary ? mapillary.signs(context.projection, layer.dimensions()) : []); + + var signs = layer.select('.mapillary-sign-offset') + .selectAll('.icon-sign') + .data(data, function(d) { return d.key; }); + + // Enter + var enter = signs.enter() + .append('foreignObject') + .attr('class', 'icon-sign') + .attr('width', '32px') // for Firefox + .attr('height', '32px'); // for Firefox + + enter + .append('xhtml:body') + .html(mapillary.signHTML); + + enter + .on('click', function(d) { // deselect/select + var mapillary = getMapillary(); + if (!mapillary) return; + var thumb = mapillary.selectedThumbnail(); + if (thumb && thumb.key === d.key) { + hideThumbnail(); + } else { + mapillary.selectedThumbnail(d); + context.map().centerEase(d.loc); + showThumbnail(d); + } + }) + .on('mouseover', showThumbnail) + .on('mouseout', function() { + var mapillary = getMapillary(); + if (!mapillary) return; + var thumb = mapillary.selectedThumbnail(); + if (thumb) { + showThumbnail(thumb); + } else { + hideThumbnail(); + } + }); + + // Exit + signs.exit() + .remove(); + + // Update + signs + .attr('transform', iD.svg.PointTransform(context.projection)); + } + + function render(selection) { + var mapillary = getMapillary(); + + layer = selection.selectAll('svg') + .data(mapillary ? [0] : []); + + layer.enter() + .append('svg') + .style('display', enabled ? 'block' : 'none') + .dimensions(context.map().dimensions()) + .append('g') + .attr('class', 'mapillary-sign-offset') + .attr('transform', 'translate(-16, -16)'); // center signs on loc + + layer.exit() + .remove(); + + if (enabled) { + if (mapillary && ~~context.map().zoom() >= minZoom) { + editOn(); + drawSigns(); + mapillary.loadSigns(context, context.projection, layer.dimensions()); + } else { + editOff(); + } + } + } + + render.enable = function(_) { + if (!arguments.length) return enabled; + enabled = _; + if (enabled) { + showLayer(); + } else { + hideLayer(); + } + return render; + }; + + render.dimensions = function(_) { + if (layer.empty()) return null; + if (!arguments.length) return layer.dimensions(); + layer.dimensions(_); + return render; + }; + + return render; +}; diff --git a/js/id/services.js b/js/id/services.js new file mode 100644 index 000000000..c11dc788f --- /dev/null +++ b/js/id/services.js @@ -0,0 +1 @@ +iD.services = {}; diff --git a/js/id/services/mapillary.js b/js/id/services/mapillary.js new file mode 100644 index 000000000..8a98e9dbb --- /dev/null +++ b/js/id/services/mapillary.js @@ -0,0 +1,274 @@ +iD.services.mapillary = function() { + var mapillary = {}, + dispatch = d3.dispatch('loadedImages', 'loadedSigns'), + apibase = 'https://a.mapillary.com/v2/', + urlImage = 'https://www.mapillary.com/map/im/', + urlThumb = 'https://d1cuyjsrcm0gby.cloudfront.net/', + clientId = 'NzNRM2otQkR2SHJzaXJmNmdQWVQ0dzo1ZWYyMmYwNjdmNDdlNmVi', + maxResults = 1000, + tileZoom = 14; + + + function loadSignDefs(context) { + if (!iD.services.mapillary.sign_defs) { + iD.services.mapillary.sign_defs = {}; + _.each(['au', 'br', 'ca', 'de', 'us'], function(region) { + d3.json(context.assetPath() + 'traffico/string-maps/' + region + '-map.json', function(err, data) { + if (err) return; + if (region === 'de') region = 'eu'; + iD.services.mapillary.sign_defs[region] = data; + }); + }); + } + } + + function abortRequest(i) { + i.abort(); + } + + function getTiles(projection, dimensions) { + var s = projection.scale() * 2 * Math.PI, + z = Math.max(Math.log(s) / Math.log(2) - 8, 0), + ts = 256 * Math.pow(2, z - tileZoom), + origin = [ + s / 2 - projection.translate()[0], + s / 2 - projection.translate()[1]]; + + return d3.geo.tile() + .scaleExtent([tileZoom, tileZoom]) + .scale(s) + .size(dimensions) + .translate(projection.translate())() + .map(function(tile) { + var x = tile[0] * ts - origin[0], + y = tile[1] * ts - origin[1]; + + return { + id: tile.toString(), + extent: iD.geo.Extent( + projection.invert([x, y + ts]), + projection.invert([x + ts, y])) + }; + }); + } + + + function loadTiles(which, url, projection, dimensions) { + var tiles = getTiles(projection, dimensions); + + _.filter(which.inflight, function(v, k) { + var wanted = _.find(tiles, function(tile) { return k === (tile.id + ',0'); }); + if (!wanted) delete which.inflight[k]; + return !wanted; + }).map(abortRequest); + + tiles.forEach(function(tile) { + loadTilePage(which, url, tile, 0); + }); + } + + function loadTilePage(which, url, tile, page) { + var cache = iD.services.mapillary.cache[which], + id = tile.id + ',' + String(page), + rect = tile.extent.rectangle(); + + if (cache.loaded[id] || cache.inflight[id]) return; + + cache.inflight[id] = d3.json(url + + iD.util.qsString({ + geojson: 'true', + limit: maxResults, + page: page, + client_id: clientId, + min_lon: rect[0], + min_lat: rect[1], + max_lon: rect[2], + max_lat: rect[3] + }), function(err, data) { + cache.loaded[id] = true; + delete cache.inflight[id]; + if (err || !data.features || !data.features.length) return; + + var features = [], + feature, loc, d; + + for (var i = 0; i < data.features.length; i++) { + feature = data.features[i]; + loc = feature.geometry.coordinates; + d = { key: feature.properties.key, loc: loc }; + if (which === 'images') d.ca = feature.properties.ca; + if (which === 'signs') d.signs = feature.properties.rects; + + features.push([loc[0], loc[1], loc[0], loc[1], d]); + } + + cache.rtree.load(features); + + if (which === 'images') dispatch.loadedImages(); + if (which === 'signs') dispatch.loadedSigns(); + + if (data.features.length === maxResults) { + loadTilePage(which, url, tile, ++page); + } + } + ); + } + + mapillary.loadImages = function(projection, dimensions) { + var url = apibase + 'search/im/geojson?'; + loadTiles('images', url, projection, dimensions); + }; + + mapillary.loadSigns = function(context, projection, dimensions) { + var url = apibase + 'search/im/geojson/or?'; + loadSignDefs(context); + loadTiles('signs', url, projection, dimensions); + }; + + + // partition viewport into `psize` x `psize` regions + function partitionViewport(psize, projection, dimensions) { + psize = psize || 16; + var cols = d3.range(0, dimensions[0], psize), + rows = d3.range(0, dimensions[1], psize), + partitions = []; + + rows.forEach(function(y) { + cols.forEach(function(x) { + var min = [x, y + psize], + max = [x + psize, y]; + partitions.push( + iD.geo.Extent(projection.invert(min), projection.invert(max))); + }); + }); + + return partitions; + } + + // no more than `limit` results per partition. + function searchLimited(psize, limit, projection, dimensions, rtree) { + limit = limit || 3; + + var partitions = partitionViewport(psize, projection, dimensions); + return _.flatten(_.compact(_.map(partitions, function(extent) { + return rtree.search(extent.rectangle()) + .slice(0, limit) + .map(function(d) { return d[4]; }); + }))); + } + + mapillary.images = function(projection, dimensions) { + var psize = 16, limit = 3; + return searchLimited(psize, limit, projection, dimensions, iD.services.mapillary.cache.images.rtree); + }; + + mapillary.signs = function(projection, dimensions) { + var psize = 32, limit = 3; + return searchLimited(psize, limit, projection, dimensions, iD.services.mapillary.cache.signs.rtree); + }; + + mapillary.signsSupported = function() { + var detected = iD.detect(); + return (!(detected.ie || detected.browser.toLowerCase() === 'safari')); + }; + + mapillary.signHTML = function(d) { + if (!iD.services.mapillary.sign_defs) return; + + var detectionPackage = d.signs[0].package, + type = d.signs[0].type, + country = detectionPackage.split('_')[1]; + return iD.services.mapillary.sign_defs[country][type]; + }; + + mapillary.showThumbnail = function(imageKey, position) { + if (!imageKey) return; + + var positionClass = { + 'ar': (position !== 'left'), + 'al': (position === 'left') + }; + + var thumbnail = d3.select('#content').selectAll('.mapillary-image') + .data([0]); + + // Enter + var enter = thumbnail.enter().append('div') + .attr('class', 'mapillary-image ar'); + + enter.append('button') + .on('click', function () { + mapillary.hideThumbnail(); + }) + .append('div') + .call(iD.svg.Icon('#icon-close')); + + enter.append('img'); + + enter.append('a') + .attr('class', 'link ar') + .attr('target', '_blank') + .call(iD.svg.Icon('#icon-out-link', 'inline')) + .append('span') + .text(t('mapillary.view_on_mapillary')); + + // Update + thumbnail.selectAll('img') + .attr('src', urlThumb + imageKey + '/thumb-320.jpg'); + + var link = thumbnail.selectAll('a') + .attr('href', urlImage + imageKey); + + if (position) { + thumbnail.classed(positionClass); + link.classed(positionClass); + } + + thumbnail + .transition() + .duration(200) + .style('opacity', 1); + }; + + mapillary.hideThumbnail = function() { + if (iD.services.mapillary) { + iD.services.mapillary.thumb = null; + } + d3.select('#content').selectAll('.mapillary-image') + .transition() + .duration(200) + .style('opacity', 0) + .remove(); + }; + + mapillary.selectedThumbnail = function(d) { + if (!iD.services.mapillary) return null; + if (!arguments.length) return iD.services.mapillary.thumb; + iD.services.mapillary.thumb = d; + }; + + mapillary.reset = function() { + var cache = iD.services.mapillary.cache; + + if (cache) { + _.forEach(cache.images.inflight, abortRequest); + _.forEach(cache.signs.inflight, abortRequest); + } + + iD.services.mapillary.cache = { + images: { inflight: {}, loaded: {}, rtree: rbush() }, + signs: { inflight: {}, loaded: {}, rtree: rbush() } + }; + + iD.services.mapillary.thumb = null; + + return mapillary; + }; + + + if (!iD.services.mapillary.cache) { + mapillary.reset(); + } + + return d3.rebind(mapillary, dispatch, 'on'); +}; diff --git a/js/id/services/countrycode.js b/js/id/services/nominatim.js similarity index 59% rename from js/id/services/countrycode.js rename to js/id/services/nominatim.js index c324cf64e..e57921331 100644 --- a/js/id/services/countrycode.js +++ b/js/id/services/nominatim.js @@ -1,15 +1,11 @@ -iD.countryCode = function() { - var countryCode = {}, +iD.services.nominatim = function() { + var nominatim = {}, endpoint = 'https://nominatim.openstreetmap.org/reverse?'; - if (!iD.countryCode.cache) { - iD.countryCode.cache = rbush(); - } - var cache = iD.countryCode.cache; - - countryCode.search = function(location, callback) { - var countryCodes = cache.search([location[0], location[1], location[0], location[1]]); + nominatim.countryCode = function(location, callback) { + var cache = iD.services.nominatim.cache, + countryCodes = cache.search([location[0], location[1], location[0], location[1]]); if (countryCodes.length > 0) return callback(null, countryCodes[0][4]); @@ -28,11 +24,21 @@ iD.countryCode = function() { var extent = iD.geo.Extent(location).padByMeters(1000); - cache.insert([extent[0][0], extent[0][1], extent[1][0], extent[1][1], result.address.country_code]); + cache.insert(extent.rectangle().concat(result.address.country_code)); callback(null, result.address.country_code); }); }; - return countryCode; + nominatim.reset = function() { + iD.services.nominatim.cache = rbush(); + return nominatim; + }; + + + if (!iD.services.nominatim.cache) { + nominatim.reset(); + } + + return nominatim; }; diff --git a/js/id/services/taginfo.js b/js/id/services/taginfo.js index ade77ada7..b71661d25 100644 --- a/js/id/services/taginfo.js +++ b/js/id/services/taginfo.js @@ -1,4 +1,4 @@ -iD.taginfo = function() { +iD.services.taginfo = function() { var taginfo = {}, endpoint = 'https://taginfo.openstreetmap.org/api/4/', tag_sorts = { @@ -14,11 +14,6 @@ iD.taginfo = function() { line: 'ways' }; - if (!iD.taginfo.cache) { - iD.taginfo.cache = {}; - } - - var cache = iD.taginfo.cache; function sets(parameters, n, o) { if (parameters.geometry && o[parameters.geometry]) { @@ -68,6 +63,8 @@ iD.taginfo = function() { var debounced = _.debounce(d3.json, 100, true); function request(url, debounce, callback) { + var cache = iD.services.taginfo.cache; + if (cache[url]) { callback(null, cache[url]); } else if (debounce) { @@ -132,5 +129,15 @@ iD.taginfo = function() { return taginfo; }; + taginfo.reset = function() { + iD.services.taginfo.cache = {}; + return taginfo; + }; + + + if (!iD.services.taginfo.cache) { + taginfo.reset(); + } + return taginfo; }; diff --git a/js/id/services/wikipedia.js b/js/id/services/wikipedia.js index eb121f57a..a33143a07 100644 --- a/js/id/services/wikipedia.js +++ b/js/id/services/wikipedia.js @@ -1,4 +1,4 @@ -iD.wikipedia = function() { +iD.services.wikipedia = function() { var wiki = {}, endpoint = 'https://en.wikipedia.org/w/api.php?'; diff --git a/js/id/ui/map_data.js b/js/id/ui/map_data.js index 903860137..0298910e4 100644 --- a/js/id/ui/map_data.js +++ b/js/id/ui/map_data.js @@ -5,6 +5,7 @@ iD.ui.MapData = function(context) { fillDefault = context.storage('area-fill') || 'partial', fillSelected = fillDefault; + function map_data(selection) { function showsFeature(d) { @@ -42,16 +43,168 @@ iD.ui.MapData = function(context) { update(); } - function clickMapillary() { - context.background().toggleMapillaryLayer(); + function clickMapillaryImages() { + context.background().toggleMapillaryImageLayer(); update(); } + function clickMapillarySigns() { + context.background().toggleMapillarySignLayer(); + update(); + } + + + function drawMapillaryItems(selection) { + var mapillary = iD.services.mapillary, + supportsMapillaryImages = !!mapillary, + supportsMapillarySigns = !!mapillary && mapillary().signsSupported(); + + var mapillaryList = selection + .selectAll('.mapillary-list') + .data([0]); + + // Enter + mapillaryList + .enter() + .append('ul') + .attr('class', 'layer-list mapillary-list'); + + var mapillaryImageLayerItem = mapillaryList + .selectAll('.item-mapillary-images') + .data(supportsMapillaryImages ? [0] : []); + + var enterImages = mapillaryImageLayerItem.enter() + .append('li') + .attr('class', 'item-mapillary-images'); + + var labelImages = enterImages.append('label') + .call(bootstrap.tooltip() + .title(t('mapillary_images.tooltip')) + .placement('top')); + + labelImages.append('input') + .attr('type', 'checkbox') + .on('change', clickMapillaryImages); + + labelImages.append('span') + .text(t('mapillary_images.title')); + + + var mapillarySignLayerItem = mapillaryList + .selectAll('.item-mapillary-signs') + .data(supportsMapillarySigns ? [0] : []); + + var enterSigns = mapillarySignLayerItem.enter() + .append('li') + .attr('class', 'item-mapillary-signs'); + + var labelSigns = enterSigns.append('label') + .call(bootstrap.tooltip() + .title(t('mapillary_signs.tooltip')) + .placement('top')); + + labelSigns.append('input') + .attr('type', 'checkbox') + .on('change', clickMapillarySigns); + + labelSigns.append('span') + .text(t('mapillary_signs.title')); + + // Update + var showsMapillaryImages = supportsMapillaryImages && context.background().showsMapillaryImageLayer(), + showsMapillarySigns = supportsMapillarySigns && context.background().showsMapillarySignLayer(); + + mapillaryImageLayerItem + .classed('active', showsMapillaryImages) + .selectAll('input') + .property('checked', showsMapillaryImages); + + mapillarySignLayerItem + .classed('active', showsMapillarySigns) + .selectAll('input') + .property('checked', showsMapillarySigns); + + // Exit + mapillaryImageLayerItem.exit() + .remove(); + mapillarySignLayerItem.exit() + .remove(); + } + + + function drawGpxItem(selection) { + var supportsGpx = iD.detect().filedrop, + gpxLayerItem = selection + .selectAll('.layer-gpx') + .data(supportsGpx ? [0] : []); + + // Enter + var enter = gpxLayerItem.enter() + .append('ul') + .attr('class', 'layer-list layer-gpx') + .append('li') + .classed('layer-toggle-gpx', true); + + enter.append('button') + .attr('class', 'layer-extent') + .call(bootstrap.tooltip() + .title(t('gpx.zoom')) + .placement('left')) + .on('click', function() { + d3.event.preventDefault(); + d3.event.stopPropagation(); + context.background().zoomToGpxLayer(); + }) + .call(iD.svg.Icon('#icon-search')); + + enter.append('button') + .attr('class', 'layer-browse') + .call(bootstrap.tooltip() + .title(t('gpx.browse')) + .placement('left')) + .on('click', function() { + d3.select(document.createElement('input')) + .attr('type', 'file') + .on('change', function() { + context.background().gpxLayerFiles(d3.event.target.files); + }) + .node().click(); + }) + .call(iD.svg.Icon('#icon-geolocate')); + + var labelGpx = enter.append('label') + .call(bootstrap.tooltip() + .title(t('gpx.drag_drop')) + .placement('top')); + + labelGpx.append('input') + .attr('type', 'checkbox') + .on('change', clickGpx); + + labelGpx.append('span') + .text(t('gpx.local_layer')); + + // Update + var hasGpx = supportsGpx && context.background().hasGpxLayer(), + showsGpx = supportsGpx && context.background().showsGpxLayer(); + + gpxLayerItem + .classed('active', showsGpx) + .selectAll('input') + .property('disabled', !hasGpx) + .property('checked', showsGpx); + + // Exit + gpxLayerItem.exit() + .remove(); + } + + function drawList(selection, data, type, name, change, active) { var items = selection.selectAll('li') .data(data); - //enter + // Enter var enter = items.enter() .append('li') .attr('class', 'layer') @@ -79,7 +232,7 @@ iD.ui.MapData = function(context) { label.append('span') .text(function(d) { return t(name + '.' + d + '.description'); }); - //update + // Update items .classed('active', active) .selectAll('input') @@ -88,32 +241,24 @@ iD.ui.MapData = function(context) { return (name === 'feature' && autoHiddenFeature(d)); }); - //exit + // Exit items.exit() .remove(); } + function update() { - featureList.call(drawList, features, 'checkbox', 'feature', clickFeature, showsFeature); + dataLayerContainer.call(drawMapillaryItems); + dataLayerContainer.call(drawGpxItem); + fillList.call(drawList, fills, 'radio', 'area_fill', setFill, showsFill); - var hasGpx = context.background().hasGpxLayer(), - showsGpx = context.background().showsGpxLayer(), - showsMapillary = context.background().showsMapillaryLayer(); - - gpxLayerItem - .classed('active', showsGpx) - .selectAll('input') - .property('disabled', !hasGpx) - .property('checked', showsGpx); - - mapillaryLayerItem - .classed('active', showsMapillary) - .selectAll('input') - .property('checked', showsMapillary); + featureList.call(drawList, features, 'checkbox', 'feature', clickFeature, showsFeature); } - function hidePanel() { setVisible(false); } + function hidePanel() { + setVisible(false); + } function togglePanel() { if (d3.event) d3.event.preventDefault(); @@ -136,6 +281,7 @@ iD.ui.MapData = function(context) { shown = show; if (show) { + update(); selection.on('mousedown.map_data-inside', function() { return d3.event.stopPropagation(); }); @@ -184,79 +330,15 @@ iD.ui.MapData = function(context) { .classed('expanded', true) .on('click', function() { var exp = d3.select(this).classed('expanded'); - layerContainer.style('display', exp ? 'none' : 'block'); + dataLayerContainer.style('display', exp ? 'none' : 'block'); d3.select(this).classed('expanded', !exp); d3.event.preventDefault(); }); - var layerContainer = content.append('div') - .attr('class', 'filters') + var dataLayerContainer = content.append('div') + .attr('class', 'data-data-layers') .style('display', 'block'); - // mapillary - var mapillaryLayerItem = layerContainer.append('ul') - .attr('class', 'layer-list') - .append('li'); - - var label = mapillaryLayerItem.append('label') - .call(bootstrap.tooltip() - .title(t('mapillary.tooltip')) - .placement('top')); - - label.append('input') - .attr('type', 'checkbox') - .on('change', clickMapillary); - - label.append('span') - .text(t('mapillary.title')); - - // gpx - var gpxLayerItem = layerContainer.append('ul') - .style('display', iD.detect().filedrop ? 'block' : 'none') - .attr('class', 'layer-list') - .append('li') - .classed('layer-toggle-gpx', true); - - gpxLayerItem.append('button') - .attr('class', 'layer-extent') - .call(bootstrap.tooltip() - .title(t('gpx.zoom')) - .placement('left')) - .on('click', function() { - d3.event.preventDefault(); - d3.event.stopPropagation(); - context.background().zoomToGpxLayer(); - }) - .call(iD.svg.Icon('#icon-search')); - - gpxLayerItem.append('button') - .attr('class', 'layer-browse') - .call(bootstrap.tooltip() - .title(t('gpx.browse')) - .placement('left')) - .on('click', function() { - d3.select(document.createElement('input')) - .attr('type', 'file') - .on('change', function() { - context.background().gpxLayerFiles(d3.event.target.files); - }) - .node().click(); - }) - .call(iD.svg.Icon('#icon-geolocate')); - - label = gpxLayerItem.append('label') - .call(bootstrap.tooltip() - .title(t('gpx.drag_drop')) - .placement('top')); - - label.append('input') - .attr('type', 'checkbox') - .property('disabled', true) - .on('change', clickGpx); - - label.append('span') - .text(t('gpx.local_layer')); - // area fills content.append('a') @@ -272,11 +354,11 @@ iD.ui.MapData = function(context) { }); var fillContainer = content.append('div') - .attr('class', 'filters') + .attr('class', 'data-area-fills') .style('display', 'none'); var fillList = fillContainer.append('ul') - .attr('class', 'layer-list'); + .attr('class', 'layer-list layer-fill-list'); // feature filters @@ -293,11 +375,11 @@ iD.ui.MapData = function(context) { }); var featureContainer = content.append('div') - .attr('class', 'filters') + .attr('class', 'data-feature-filters') .style('display', 'none'); var featureList = featureContainer.append('ul') - .attr('class', 'layer-list'); + .attr('class', 'layer-list layer-feature-list'); context.features() diff --git a/js/id/ui/preset/address.js b/js/id/ui/preset/address.js index d19fe9213..823b0e7f4 100644 --- a/js/id/ui/preset/address.js +++ b/js/id/ui/preset/address.js @@ -109,7 +109,7 @@ iD.ui.preset.address = function(field, context) { var center = entity.extent(context.graph()).center(), addressFormat; - iD.countryCode().search(center, function (err, countryCode) { + iD.services.nominatim().countryCode(center, function (err, countryCode) { addressFormat = _.find(iD.data.addressFormats, function (a) { return a && a.countryCodes && _.contains(a.countryCodes, countryCode); }) || _.first(iD.data.addressFormats); diff --git a/js/id/ui/preset/localized.js b/js/id/ui/preset/localized.js index 35e6307c7..18d27ff70 100644 --- a/js/id/ui/preset/localized.js +++ b/js/id/ui/preset/localized.js @@ -1,6 +1,6 @@ iD.ui.preset.localized = function(field, context) { var dispatch = d3.dispatch('change', 'input'), - wikipedia = iD.wikipedia(), + wikipedia = iD.services.wikipedia(), input, localizedInputs, wikiTitles, entity; diff --git a/js/id/ui/preset/wikipedia.js b/js/id/ui/preset/wikipedia.js index 4b069cc2b..5ac04cef3 100644 --- a/js/id/ui/preset/wikipedia.js +++ b/js/id/ui/preset/wikipedia.js @@ -1,6 +1,6 @@ iD.ui.preset.wikipedia = function(field, context) { var dispatch = d3.dispatch('change'), - wikipedia = iD.wikipedia(), + wikipedia = iD.services.wikipedia(), link, entity, lang, title; function i(selection) { diff --git a/js/lib/d3.dimensions.js b/js/lib/d3.dimensions.js index 7f9ef0521..eef257401 100644 --- a/js/lib/d3.dimensions.js +++ b/js/lib/d3.dimensions.js @@ -1,8 +1,17 @@ d3.selection.prototype.dimensions = function (dimensions) { if (!arguments.length) { var node = this.node(); - return [node.offsetWidth, - node.offsetHeight]; + if (!node) return; + + var prop = this.property('__dimensions__'); + if (!prop) { + var cr = node.getBoundingClientRect(); + prop = [cr.width, cr.height]; + this.property('__dimensions__', prop); + } + return prop; } + + this.property('__dimensions__', [dimensions[0], dimensions[1]]); return this.attr({width: dimensions[0], height: dimensions[1]}); }; diff --git a/js/lib/rbush.js b/js/lib/rbush.js index 19b841a8a..f6975dfe8 100644 --- a/js/lib/rbush.js +++ b/js/lib/rbush.js @@ -1,10 +1,11 @@ /* - (c) 2013, Vladimir Agafonkin + (c) 2015, Vladimir Agafonkin RBush, a JavaScript library for high-performance 2D spatial indexing of points and rectangles. https://github.com/mourner/rbush */ -(function () { 'use strict'; +(function () { +'use strict'; function rbush(maxEntries, format) { @@ -57,6 +58,33 @@ rbush.prototype = { return result; }, + collides: function (bbox) { + + var node = this.data, + toBBox = this.toBBox; + + if (!intersects(bbox, node.bbox)) return false; + + var nodesToSearch = [], + i, len, child, childBBox; + + while (node) { + for (i = 0, len = node.children.length; i < len; i++) { + + child = node.children[i]; + childBBox = node.leaf ? toBBox(child) : child.bbox; + + if (intersects(bbox, childBBox)) { + if (node.leaf || contains(bbox, childBBox)) return true; + nodesToSearch.push(child); + } + } + node = nodesToSearch.pop(); + } + + return false; + }, + load: function (data) { if (!(data && data.length)) return this; @@ -180,13 +208,14 @@ rbush.prototype = { return result; }, - _build: function (items, left, right, level, height) { + _build: function (items, left, right, height) { var N = right - left + 1, M = this._maxEntries, node; if (N <= M) { + // reached leaf level; return leaf node = { children: items.slice(left, right + 1), height: 1, @@ -197,7 +226,7 @@ rbush.prototype = { return node; } - if (!level) { + if (!height) { // target height of the bulk-loaded tree height = Math.ceil(Math.log(N) / Math.log(M)); @@ -205,31 +234,33 @@ rbush.prototype = { M = Math.ceil(N / Math.pow(M, height - 1)); } - // TODO eliminate recursion? - node = { children: [], height: height, - bbox: null + bbox: null, + leaf: false }; + // split the items into M mostly square tiles + var N2 = Math.ceil(N / M), N1 = N2 * Math.ceil(Math.sqrt(M)), - i, j, right2, childNode; + i, j, right2, right3; + + multiSelect(items, left, right, N1, this.compareMinX); - // split the items into M mostly square tiles for (i = left; i <= right; i += N1) { - if (i + N1 <= right) partitionSort(items, i, right, i + N1, this.compareMinX); right2 = Math.min(i + N1 - 1, right); + multiSelect(items, i, right2, N2, this.compareMinY); + for (j = i; j <= right2; j += N2) { - if (j + N2 <= right2) partitionSort(items, j, right2, j + N2, this.compareMinY); + right3 = Math.min(j + N2 - 1, right2); // pack each entry recursively - childNode = this._build(items, j, Math.min(j + N2 - 1, right2), level + 1, height - 1); - node.children.push(childNode); + node.children.push(this._build(items, j, right3, height - 1)); } } @@ -309,9 +340,13 @@ rbush.prototype = { this._chooseSplitAxis(node, m, M); + var splitIndex = this._chooseSplitIndex(node, m, M); + var newNode = { - children: node.children.splice(this._chooseSplitIndex(node, m, M)), - height: node.height + children: node.children.splice(splitIndex, node.children.length - splitIndex), + height: node.height, + bbox: null, + leaf: false }; if (node.leaf) newNode.leaf = true; @@ -327,7 +362,9 @@ rbush.prototype = { // split root node this.data = { children: [node, newNode], - height: node.height + 1 + height: node.height + 1, + bbox: null, + leaf: false }; calcBBox(this.data, this.toBBox); }, @@ -442,6 +479,7 @@ rbush.prototype = { } }; + // calculate node's bbox from bboxes of its children function calcBBox(node, toBBox) { node.bbox = distBBox(node, 0, node.children.length, toBBox); @@ -459,7 +497,6 @@ function distBBox(node, k, p, toBBox) { return bbox; } - function empty() { return [Infinity, Infinity, -Infinity, -Infinity]; } function extend(a, b) { @@ -481,7 +518,7 @@ function enlargedArea(a, b) { (Math.max(b[3], a[3]) - Math.min(b[1], a[1])); } -function intersectionArea (a, b) { +function intersectionArea(a, b) { var minX = Math.max(a[0], b[0]), minY = Math.max(a[1], b[1]), maxX = Math.min(a[2], b[2]), @@ -498,44 +535,74 @@ function contains(a, b) { b[3] <= a[3]; } -function intersects (a, b) { +function intersects(a, b) { return b[0] <= a[2] && b[1] <= a[3] && b[2] >= a[0] && b[3] >= a[1]; } +// sort an array so that items come in groups of n unsorted items, with groups sorted between each other; +// combines selection algorithm with binary divide & conquer approach -function partitionSort(arr, left, right, k, compare) { - var pivot; +function multiSelect(arr, left, right, n, compare) { + var stack = [left, right], + mid; - while (true) { - pivot = Math.floor((left + right) / 2); - pivot = partition(arr, left, right, pivot, compare); + while (stack.length) { + right = stack.pop(); + left = stack.pop(); - if (k === pivot) break; - else if (k < pivot) right = pivot - 1; - else left = pivot + 1; + if (right - left <= n) continue; + + mid = left + Math.ceil((right - left) / n / 2) * n; + select(arr, left, right, mid, compare); + + stack.push(left, mid, mid, right); } - - partition(arr, left, right, k, compare); } -function partition(arr, left, right, pivot, compare) { - var k = left, - value = arr[pivot]; +// Floyd-Rivest selection algorithm: +// sort an array between left and right (inclusive) so that the smallest k elements come first (unordered) +function select(arr, left, right, k, compare) { + var n, i, z, s, sd, newLeft, newRight, t, j; - swap(arr, pivot, right); - - for (var i = left; i < right; i++) { - if (compare(arr[i], value) < 0) { - swap(arr, k, i); - k++; + while (right > left) { + if (right - left > 600) { + n = right - left + 1; + i = k - left + 1; + z = Math.log(n); + s = 0.5 * Math.exp(2 * z / 3); + sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (i - n / 2 < 0 ? -1 : 1); + newLeft = Math.max(left, Math.floor(k - i * s / n + sd)); + newRight = Math.min(right, Math.floor(k + (n - i) * s / n + sd)); + select(arr, newLeft, newRight, k, compare); } - } - swap(arr, right, k); - return k; + t = arr[k]; + i = left; + j = right; + + swap(arr, left, k); + if (compare(arr[right], t) > 0) swap(arr, left, right); + + while (i < j) { + swap(arr, i, j); + i++; + j--; + while (compare(arr[i], t) < 0) i++; + while (compare(arr[j], t) > 0) j--; + } + + if (compare(arr[left], t) === 0) swap(arr, left, j); + else { + j++; + swap(arr, j, right); + } + + if (j <= k) left = j + 1; + if (k <= j) right = j - 1; + } } function swap(arr, i, j) { @@ -546,9 +613,9 @@ function swap(arr, i, j) { // export as AMD/CommonJS module or global variable -if (typeof define === 'function' && define.amd) define(function() { return rbush; }); +if (typeof define === 'function' && define.amd) define('rbush', function () { return rbush; }); else if (typeof module !== 'undefined') module.exports = rbush; else if (typeof self !== 'undefined') self.rbush = rbush; else window.rbush = rbush; -})(); \ No newline at end of file +})(); diff --git a/test/index.html b/test/index.html index 1de031b1b..c8b018623 100644 --- a/test/index.html +++ b/test/index.html @@ -38,7 +38,9 @@ - + + + @@ -56,7 +58,8 @@ - + + @@ -261,6 +264,7 @@ + @@ -300,9 +304,9 @@ - - - + + + diff --git a/test/index_packaged.html b/test/index_packaged.html index 5af0b5d99..bb2b50d29 100644 --- a/test/index_packaged.html +++ b/test/index_packaged.html @@ -59,6 +59,7 @@ + @@ -98,9 +99,9 @@ - - - + + + diff --git a/test/spec/geo/extent.js b/test/spec/geo/extent.js index cd2e84cb0..b58dbefec 100644 --- a/test/spec/geo/extent.js +++ b/test/spec/geo/extent.js @@ -57,7 +57,20 @@ describe("iD.geo.Extent", function () { describe("#center", function () { it("returns the center point", function () { - expect(iD.geo.Extent([0, 0], [5, 10]).center()).to.eql([2.5, 5]); + expect(iD.geo.Extent([0, 0], [5, 10]).center()).to.eql([2.5, 5]); + }); + }); + + describe("#rectangle", function () { + it("returns the extent as a rectangle", function () { + expect(iD.geo.Extent([0, 0], [5, 10]).rectangle()).to.eql([0, 0, 5, 10]); + }); + }); + + describe("#polygon", function () { + it("returns the extent as a polygon", function () { + expect(iD.geo.Extent([0, 0], [5, 10]).polygon()) + .to.eql([[0, 0], [0, 10], [5, 10], [5, 0], [0, 0]]); }); }); diff --git a/test/spec/services/mapillary.js b/test/spec/services/mapillary.js new file mode 100644 index 000000000..cea1274ed --- /dev/null +++ b/test/spec/services/mapillary.js @@ -0,0 +1,336 @@ +describe('iD.services.mapillary', function() { + var dimensions = [64, 64], + context, server, mapillary; + + beforeEach(function() { + context = iD(); + context.projection.scale(667544.214430109); // z14 + + server = sinon.fakeServer.create(); + mapillary = iD.services.mapillary(); + mapillary.reset(); + }); + + afterEach(function() { + server.restore(); + }); + + + describe('Mapillary service', function() { + it('Initializes cache one time', function() { + var cache = iD.services.mapillary.cache; + expect(cache).to.have.property('images'); + expect(cache).to.have.property('signs'); + + var mapillary2 = iD.services.mapillary(); + var cache2 = iD.services.mapillary.cache; + expect(cache).to.equal(cache2); + }); + }); + + describe('#loadImages', function() { + it('fires loadedImages when images are loaded', function() { + var spy = sinon.spy(); + mapillary.on('loadedImages', spy); + mapillary.loadImages(context.projection, dimensions); + + var match = /search\/im\/geojson/, + features = [{ + type: 'Feature', + geometry: { type: 'Point', coordinates: [0,0] }, + properties: { ca: 90, key: '0' } + }], + response = { type: 'FeatureCollection', features: features }; + + server.respondWith('GET', match, + [200, { 'Content-Type': 'application/json' }, JSON.stringify(response) ]); + server.respond(); + + expect(spy).to.have.been.calledOnce; + }); + + it('loads multiple pages of image results', function() { + var spy = sinon.spy(); + mapillary.on('loadedImages', spy); + mapillary.loadImages(context.projection, dimensions); + + var features0 = [], + features1 = [], + i; + + for (i = 0; i < 1000; i++) { + features0.push({ + type: 'Feature', + geometry: { type: 'Point', coordinates: [0,0] }, + properties: { ca: 90, key: String(i) } + }); + } + for (i = 0; i < 500; i++) { + features1.push({ + type: 'Feature', + geometry: { type: 'Point', coordinates: [0,0] }, + properties: { ca: 90, key: String(1000 + i) } + }); + } + + var match0 = /page=0/, + response0 = { type: 'FeatureCollection', features: features0 }, + match1 = /page=1/, + response1 = { type: 'FeatureCollection', features: features1 } + + server.respondWith('GET', match0, + [200, { 'Content-Type': 'application/json' }, JSON.stringify(response0) ]); + server.respondWith('GET', match1, + [200, { 'Content-Type': 'application/json' }, JSON.stringify(response1) ]); + server.respond(); + + expect(spy).to.have.been.calledTwice; + }); + }); + + describe('#loadSigns', function() { + it('loads sign_defs', function() { + mapillary.loadSigns(context, context.projection, dimensions); + + var base = 'regulatory--maximum-speed-limit-65--', + match = /traffico\/string-maps\/(\w+)-map.json/; + + server.respondWith('GET', match, function (xhr, id) { + xhr.respond(200, { 'Content-Type': 'application/json' }, + '{ "' + base + id + '": true }'); + }); + server.respond(); + + var sign_defs = iD.services.mapillary.sign_defs; + + expect(sign_defs).to.have.property('au') + .that.is.an('object') + .that.deep.equals({'regulatory--maximum-speed-limit-65--au': true}); + expect(sign_defs).to.have.property('br') + .that.is.an('object') + .that.deep.equals({'regulatory--maximum-speed-limit-65--br': true}); + expect(sign_defs).to.have.property('ca') + .that.is.an('object') + .that.deep.equals({'regulatory--maximum-speed-limit-65--ca': true}); + expect(sign_defs).to.have.property('eu') + .that.is.an('object') + .that.deep.equals({'regulatory--maximum-speed-limit-65--de': true}); + expect(sign_defs).to.have.property('us') + .that.is.an('object') + .that.deep.equals({'regulatory--maximum-speed-limit-65--us': true}); + }); + + it('fires loadedSigns when signs are loaded', function() { + var spy = sinon.spy(); + mapillary.on('loadedSigns', spy); + mapillary.loadSigns(context, context.projection, dimensions); + + var match = /search\/im\/geojson\/or/, + rects = [{ + 'package': 'trafficsign_us_3.0', + rect: [ 0.805, 0.463, 0.833, 0.502 ], + length: 4, + score: '1.27', + type: 'regulatory--maximum-speed-limit-65--us' + }], + features = [{ + type: 'Feature', + geometry: { type: 'Point', coordinates: [0,0] }, + properties: { rects: rects, key: '0' } + }], + response = { type: 'FeatureCollection', features: features }; + + server.respondWith('GET', match, + [200, { 'Content-Type': 'application/json' }, JSON.stringify(response) ]); + server.respond(); + + expect(spy).to.have.been.calledOnce; + }); + + it('loads multiple pages of signs results', function() { + var spy = sinon.spy(); + mapillary.on('loadedSigns', spy); + mapillary.loadSigns(context, context.projection, dimensions); + + var rects = [{ + 'package': 'trafficsign_us_3.0', + rect: [ 0.805, 0.463, 0.833, 0.502 ], + length: 4, + score: '1.27', + type: 'regulatory--maximum-speed-limit-65--us' + }], + features0 = [], + features1 = [], + i; + + for (i = 0; i < 1000; i++) { + features0.push({ + type: 'Feature', + geometry: { type: 'Point', coordinates: [0,0] }, + properties: { rects: rects, key: String(i) } + }); + } + for (i = 0; i < 500; i++) { + features1.push({ + type: 'Feature', + geometry: { type: 'Point', coordinates: [0,0] }, + properties: { rects: rects, key: String(1000 + i) } + }); + } + + var match0 = /page=0/, + response0 = { type: 'FeatureCollection', features: features0 }, + match1 = /page=1/, + response1 = { type: 'FeatureCollection', features: features1 } + + server.respondWith('GET', match0, + [200, { 'Content-Type': 'application/json' }, JSON.stringify(response0) ]); + server.respondWith('GET', match1, + [200, { 'Content-Type': 'application/json' }, JSON.stringify(response1) ]); + server.respond(); + + expect(spy).to.have.been.calledTwice; + }); + }); + + + describe('#images', function() { + it('returns images in the visible map area', function() { + var features = [ + [0, 0, 0, 0, { key: '0', loc: [0,0], ca: 90 }], + [0, 0, 0, 0, { key: '1', loc: [0,0], ca: 90 }], + [0, 1, 0, 1, { key: '2', loc: [0,1], ca: 90 }] + ]; + + iD.services.mapillary.cache.images.rtree.load(features); + var res = mapillary.images(context.projection, dimensions); + + expect(res).to.deep.eql([ + { key: '0', loc: [0,0], ca: 90 }, + { key: '1', loc: [0,0], ca: 90 } + ]); + }); + + it('limits results no more than 3 stacked images in one spot', function() { + var features = [ + [0, 0, 0, 0, { key: '0', loc: [0,0], ca: 90 }], + [0, 0, 0, 0, { key: '1', loc: [0,0], ca: 90 }], + [0, 0, 0, 0, { key: '2', loc: [0,0], ca: 90 }], + [0, 0, 0, 0, { key: '3', loc: [0,0], ca: 90 }], + [0, 0, 0, 0, { key: '4', loc: [0,0], ca: 90 }] + ]; + + iD.services.mapillary.cache.images.rtree.load(features); + var res = mapillary.images(context.projection, dimensions); + expect(res).to.have.length.of.at.most(3); + }); + }); + + describe('#signs', function() { + it('returns signs in the visible map area', function() { + var signs = [{ + 'package': 'trafficsign_us_3.0', + rect: [ 0.805, 0.463, 0.833, 0.502 ], + length: 4, + score: '1.27', + type: 'regulatory--maximum-speed-limit-65--us' + }], + features = [ + [0, 0, 0, 0, { key: '0', loc: [0,0], signs: signs }], + [0, 0, 0, 0, { key: '1', loc: [0,0], signs: signs }], + [0, 1, 0, 1, { key: '2', loc: [0,1], signs: signs }] + ]; + + iD.services.mapillary.cache.signs.rtree.load(features); + var res = mapillary.signs(context.projection, dimensions); + + expect(res).to.deep.eql([ + { key: '0', loc: [0,0], signs: signs }, + { key: '1', loc: [0,0], signs: signs } + ]); + }); + + it('limits results no more than 3 stacked signs in one spot', function() { + var signs = [{ + 'package': 'trafficsign_us_3.0', + rect: [ 0.805, 0.463, 0.833, 0.502 ], + length: 4, + score: '1.27', + type: 'regulatory--maximum-speed-limit-65--us' + }], + features = [ + [0, 0, 0, 0, { key: '0', loc: [0,0], signs: signs }], + [0, 0, 0, 0, { key: '1', loc: [0,0], signs: signs }], + [0, 0, 0, 0, { key: '2', loc: [0,0], signs: signs }], + [0, 0, 0, 0, { key: '3', loc: [0,0], signs: signs }], + [0, 0, 0, 0, { key: '4', loc: [0,0], signs: signs }] + ]; + + iD.services.mapillary.cache.signs.rtree.load(features); + var res = mapillary.signs(context.projection, dimensions); + expect(res).to.have.length.of.at.most(3); + }); + }); + + describe('#signsSupported', function() { + it('returns false for Internet Explorer', function() { + var detect = iD.detect; + iD.detect = function() { return { ie: true, browser: 'Internet Explorer' }; }; + expect(mapillary.signsSupported()).to.be.false; + iD.detect = detect; + }); + + it('returns false for Safari', function() { + var detect = iD.detect; + iD.detect = function() { return { ie: false, browser: 'Safari' }; }; + expect(mapillary.signsSupported()).to.be.false; + iD.detect = detect; + }); + }); + + describe('#signHTML', function() { + it('returns sign HTML', function() { + iD.services.mapillary.sign_defs = { + us: {'regulatory--maximum-speed-limit-65--us': '65'} + }; + + var signdata = { + key: '0', + loc: [0,0], + signs: [{ + 'package': 'trafficsign_us_3.0', + rect: [ 0.805, 0.463, 0.833, 0.502 ], + length: 4, + score: '1.27', + type: 'regulatory--maximum-speed-limit-65--us' + }] + }; + + expect(mapillary.signHTML(signdata)).to.eql('65') + }); + }); + + describe('#selectedThumbnail', function() { + it('sets thumbnail image', function() { + mapillary.selectedThumbnail('foo'); + expect(iD.services.mapillary.thumb).to.eql('foo'); + }); + + it('gets thumbnail image', function() { + iD.services.mapillary.thumb = 'bar'; + expect(mapillary.selectedThumbnail()).to.eql('bar'); + }); + }); + + describe('#reset', function() { + it('resets cache and thumbnail image', function() { + iD.services.mapillary.cache.foo = 'bar'; + iD.services.mapillary.thumb = 'bar'; + + mapillary.reset(); + expect(iD.services.mapillary.cache).to.not.have.property('foo'); + expect(iD.services.mapillary.thumb).to.be.null; + }); + }); + +}); diff --git a/test/spec/countrycode.js b/test/spec/services/nominatim.js similarity index 81% rename from test/spec/countrycode.js rename to test/spec/services/nominatim.js index 1b519d3f4..e77dbd725 100644 --- a/test/spec/countrycode.js +++ b/test/spec/services/nominatim.js @@ -1,10 +1,10 @@ -describe("iD.countryCode", function() { - var server, countryCode; +describe("iD.services.nominatim", function() { + var server, nominatim; beforeEach(function() { server = sinon.fakeServer.create(); - iD.countryCode.cache = null; - countryCode = iD.countryCode(); + nominatim = iD.services.nominatim(); + nominatim.reset(); }); afterEach(function() { @@ -15,10 +15,10 @@ describe("iD.countryCode", function() { return iD.util.stringQs(url.substring(url.indexOf('?') + 1)); } - describe("#search", function() { - it("calls the given callback with the results of the search query", function() { + describe("#countryCode", function() { + it("calls the given callback with the results of the country code query", function() { var callback = sinon.spy(); - countryCode.search([16, 48], callback); + nominatim.countryCode([16, 48], callback); server.respondWith("GET", "https://nominatim.openstreetmap.org/reverse?addressdetails=1&format=json&lat=48&lon=16", [200, { "Content-Type": "application/json" }, @@ -29,9 +29,9 @@ describe("iD.countryCode", function() { {format: "json", addressdetails: "1", lat: "48", lon: "16"}); expect(callback).to.have.been.calledWith(null, "at"); }); - it("should not cache the first search result", function() { + it("should not cache the first country code result", function() { var callback = sinon.spy(); - countryCode.search([16, 48], callback); + nominatim.countryCode([16, 48], callback); server.respondWith("GET", "https://nominatim.openstreetmap.org/reverse?addressdetails=1&format=json&lat=48&lon=16", [200, { "Content-Type": "application/json" }, @@ -45,7 +45,7 @@ describe("iD.countryCode", function() { server.restore(); server = sinon.fakeServer.create(); - countryCode.search([17, 49], callback); + nominatim.countryCode([17, 49], callback); server.respondWith("GET", "https://nominatim.openstreetmap.org/reverse?addressdetails=1&format=json&lat=49&lon=17", [200, { "Content-Type": "application/json" }, @@ -56,9 +56,9 @@ describe("iD.countryCode", function() { {format: "json", addressdetails: "1", lat: "49", lon: "17"}); expect(callback).to.have.been.calledWith(null, "cz"); }); - it("should cache the first search result", function() { + it("should cache the first country code result", function() { var callback = sinon.spy(); - countryCode.search([16, 48], callback); + nominatim.countryCode([16, 48], callback); server.respondWith("GET", "https://nominatim.openstreetmap.org/reverse?addressdetails=1&format=json&lat=48&lon=16", [200, { "Content-Type": "application/json" }, @@ -72,7 +72,7 @@ describe("iD.countryCode", function() { server.restore(); server = sinon.fakeServer.create(); - countryCode.search([16.01, 48.01], callback); + nominatim.countryCode([16.01, 48.01], callback); server.respondWith("GET", "https://nominatim.openstreetmap.org/reverse?addressdetails=1&format=json&lat=48.01&lon=16.01", [200, { "Content-Type": "application/json" }, @@ -83,7 +83,7 @@ describe("iD.countryCode", function() { }); it("calls the given callback with an error", function() { var callback = sinon.spy(); - countryCode.search([1000, 1000], callback); + nominatim.countryCode([1000, 1000], callback); server.respondWith("GET", "https://nominatim.openstreetmap.org/reverse?addressdetails=1&format=json&lat=1000&lon=1000", [200, { "Content-Type": "application/json" }, diff --git a/test/spec/taginfo.js b/test/spec/services/taginfo.js similarity index 98% rename from test/spec/taginfo.js rename to test/spec/services/taginfo.js index 7d91d2f5b..df66510c6 100644 --- a/test/spec/taginfo.js +++ b/test/spec/services/taginfo.js @@ -1,9 +1,9 @@ -describe("iD.taginfo", function() { +describe("iD.services.taginfo", function() { var server, taginfo; beforeEach(function() { server = sinon.fakeServer.create(); - taginfo = iD.taginfo(); + taginfo = iD.services.taginfo(); }); afterEach(function() {