From 0b1ec49402aa9a8ccd1aebeac9e7468586168221 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Sun, 14 Sep 2014 14:28:08 -0700 Subject: [PATCH] Rewrite as a separate layer --- css/app.css | 29 +++--- css/map.css | 40 ++++---- data/core.yaml | 9 +- dist/locales/en.json | 11 +-- index.html | 4 +- js/id/id.js | 7 -- js/id/modes/select_image.js | 97 ------------------ js/id/renderer/background.js | 19 ++++ js/id/renderer/map.js | 10 +- js/id/renderer/mapillary_layer.js | 159 ++++++++++++++++++++++++++++++ js/id/svg/sequences.js | 100 ------------------- js/id/ui.js | 4 - js/id/ui/background.js | 32 +++--- js/id/ui/image_view.js | 50 ---------- test/index.html | 4 +- test/spec/behavior/hash.js | 1 + test/spec/behavior/lasso.js | 1 + test/spec/renderer/map.js | 6 +- 18 files changed, 242 insertions(+), 341 deletions(-) delete mode 100644 js/id/modes/select_image.js create mode 100644 js/id/renderer/mapillary_layer.js delete mode 100644 js/id/svg/sequences.js delete mode 100644 js/id/ui/image_view.js diff --git a/css/app.css b/css/app.css index b280730c6..cd55957d1 100644 --- a/css/app.css +++ b/css/app.css @@ -717,48 +717,41 @@ a:hover .icon.out-link { background-position: -500px -14px;} bottom: 0; } -#mapillaryImage { +.mapillary-image { position: absolute; right: 0; bottom: 30px; + width: 330px; + height: 250px; padding: 5px; background-color: #fff; } -#mapillaryImage > div { - position: relative; -} -#mapillaryImage > a { +.mapillary-image a { display: block; - width: 100%; - height: auto; - color: white; - position: relative; -} - -#mapillaryImage .link { - background-color: rgba(0,0,0,.5); position: absolute; + height: auto; + background-color: rgba(0,0,0,.5); bottom: 0; right: 0; padding: 5px 10px; } -#mapillaryImage img { +.mapillary-image img { width: 100%; height: auto; display: block; } -#mapillaryImage.hidden { - visibility: hidden; +.mapillary-image.hidden { + visibility: hidden; } -#mapillaryImage.temp button { +.mapillary-image.temp button { display: none; } -#mapillaryImage button { +.mapillary-image button { border-radius: 0; padding: 5px; position: absolute; diff --git a/css/map.css b/css/map.css index 7e5ffe792..1329cbf44 100644 --- a/css/map.css +++ b/css/map.css @@ -1140,32 +1140,34 @@ text.gpx { fill:#FF26D4; } -/* Mapillary Sequences */ +/* Mapillary Layer */ -g.image.point { - cursor: pointer; /* Opera */ - cursor: url(img/cursor-select-mapillary.png) 6 1, pointer; /* FF */ +.layer-mapillary { + pointer-events: none; } -g.image.point path, -g.image.point circle { - stroke-width: 2; - stroke: #ffc600; - fill: #ffc600; +.layer-mapillary g { + pointer-events: visible; + cursor: pointer; /* Opera */ + cursor: url(img/cursor-select-mapillary.png) 6 1, pointer; /* FF */ } -g.image.point:hover circle, -g.image.point:hover path { - fill: #ff9900; - stroke-width: 2; - stroke: #ff9900; +.layer-mapillary g * { + stroke-width: 2; + stroke: #ffc600; + fill: #ffc600; } -g.image.point.selected circle, -g.image.point.selected path { - fill: #ff5800; - stroke-width: 4; - stroke: #ff5800; +.layer-mapillary g:hover * { + stroke-width: 2; + stroke: #ff9900; + fill: #ff9900; +} + +.layer-mapillary g.selected * { + stroke-width: 4; + stroke: #ff5800; + fill: #ff5800; } /* Modes */ diff --git a/data/core.yaml b/data/core.yaml index d1118ef48..efb948d32 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -19,9 +19,6 @@ en: tail: Click to add nodes to your area. Click the first node to finish the area. draw_line: tail: "Click to add more nodes to the line. Click on other lines to connect to them, and double-click to end the line." - selectImage: - title: Photos - description: "Choose and fix a Mapillary image for mapping. Shortcut: 'm'" operations: add: annotation: @@ -296,11 +293,9 @@ en: zoom: "Zoom to GPX track" browse: "Browse for a .gpx file" mapillary: - tooltip: "Mapillary street photos" - title: "Photo overlay" + tooltip: "Street-level photos from Mapillary" + title: "Photo Overlay (Mapillary)" view_on_mapillary: "View this image on Mapillary" - no_image_found: | - No image found on Mapillary. Go and take some! help: title: "Help" help: | diff --git a/dist/locales/en.json b/dist/locales/en.json index 164deac2f..7df4440a2 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -24,10 +24,6 @@ }, "draw_line": { "tail": "Click to add more nodes to the line. Click on other lines to connect to them, and double-click to end the line." - }, - "selectImage": { - "title": "Photos", - "description": "Choose and fix a Mapillary image for mapping. Shortcut: 'm'" } }, "operations": { @@ -359,10 +355,9 @@ "browse": "Browse for a .gpx file" }, "mapillary": { - "tooltip": "Mapillary street photos", - "title": "Photo overlay", - "view_on_mapillary": "View this image on Mapillary", - "no_image_found": "No image found on Mapillary. Go and take some!\n" + "tooltip": "Street-level photos from Mapillary", + "title": "Photo Overlay (Mapillary)", + "view_on_mapillary": "View this image on Mapillary" }, "help": { "title": "Help", diff --git a/index.html b/index.html index f267f5c3b..b50fa5e49 100644 --- a/index.html +++ b/index.html @@ -58,6 +58,7 @@ + @@ -66,7 +67,6 @@ - @@ -77,7 +77,6 @@ - @@ -190,7 +189,6 @@ - diff --git a/js/id/id.js b/js/id/id.js index 0fe52e675..3d135de22 100644 --- a/js/id/id.js +++ b/js/id/id.js @@ -214,13 +214,6 @@ window.iD = function () { context.zoomIn = map.zoomIn; context.zoomOut = map.zoomOut; - - /* Mapillary image view */ - var imageView = iD.ui.ImageView(context); - context.imageView = function() { - return imageView; - }; - context.surfaceRect = function() { // Work around a bug in Firefox. // http://stackoverflow.com/questions/18153989/ diff --git a/js/id/modes/select_image.js b/js/id/modes/select_image.js deleted file mode 100644 index 13230dfc4..000000000 --- a/js/id/modes/select_image.js +++ /dev/null @@ -1,97 +0,0 @@ -iD.modes.SelectImage = function (context) { - var mode = { - button: 'selectImage', - id: 'selectImage', - title: t('modes.selectImage.title'), - description: t('modes.selectImage.description'), - key: 'm' - }, imageView, currentImage; - - function click() { - var datum = d3.event.target.__data__; - if (isImage(datum)) { - if (currentImage === datum) { - context.surface().selectAll('.image.point') - .classed('selected', false); - currentImage = undefined; - } else { - currentImage = datum; - context.surface().selectAll('.image.point') - .classed('selected', function(d) { - return d === datum; - }); - context.container() - .select('#mapillaryImage') - .classed('temp', false); - imageView.show(currentImage); - } - } - } - - function isImage(datum) { - return datum && - datum.properties !== undefined && - datum.properties.entityType === 'image'; - } - - mode.enter = function () { - context.map().enableSequences(true); - context.container() - .select('#select_image_checkbox') - .attr('checked','checked'); - - // Get focus on the body. - if (document.activeElement && document.activeElement.blur) { - document.activeElement.blur(); - } - - imageView = context.imageView(); - imageView.showEmpty(); - - context.surface() - .on('click.image', click) - .on('mouseover.image', function () { - var datum = d3.event.target.__data__; - if (isImage(datum)) { - imageView.show(datum); - if (currentImage !== datum) { - context.container() - .select('#mapillaryImage') - .classed('temp', true); - } - } - }) - .on('mouseout.image', function () { - var datum = d3.event.target.__data__; - if (isImage(datum)) { - if (currentImage) { - imageView.show(currentImage); - } else { - imageView.showEmpty(); - } - } - }); - }; - - mode.exit = function () { - context.map().enableSequences(false); - context.container().select('#select_image_checkbox') - .attr('checked', null); - - if (!currentImage) { - context.container() - .select('#mapillaryImage') - .classed('hidden', true) - .classed('temp', false); - } - - context.surface().select('defs').selectAll('marker.arrow') - .remove(); - context.surface().select('.layer-hit').selectAll('g.image') - .remove(); - context.surface().select('.layer-hit').selectAll('g.sequence') - .remove(); - }; - - return mode; -}; diff --git a/js/id/renderer/background.js b/js/id/renderer/background.js index f548d3606..c1f182b6b 100644 --- a/js/id/renderer/background.js +++ b/js/id/renderer/background.js @@ -4,6 +4,7 @@ iD.Background = function(context) { .projection(context.projection), gpxLayer = iD.GpxLayer(context, dispatch) .projection(context.projection), + mapillaryLayer = iD.MapillaryLayer(context), overlayLayers = []; var backgroundSources = iD.data.imagery.map(function(source) { @@ -91,6 +92,14 @@ iD.Background = function(context) { overlays.exit() .remove(); + + var mapillary = selection.selectAll('.layer-mapillary') + .data([0]); + + mapillary.enter().insert('div') + .attr('class', 'layer-layer layer-mapillary'); + + mapillary.call(mapillaryLayer); } background.sources = function(extent) { @@ -102,6 +111,7 @@ iD.Background = function(context) { background.dimensions = function(_) { baseLayer.dimensions(_); gpxLayer.dimensions(_); + mapillaryLayer.dimensions(_); overlayLayers.forEach(function(layer) { layer.dimensions(_); @@ -166,6 +176,15 @@ iD.Background = function(context) { dispatch.change(); }; + background.showsMapillaryLayer = function() { + return mapillaryLayer.enable(); + }; + + background.toggleMapillaryLayer = function() { + mapillaryLayer.enable(!mapillaryLayer.enable()); + dispatch.change(); + }; + background.showsLayer = function(d) { return d === baseLayer.source() || (d.id === 'custom' && baseLayer.source().id === 'custom') || diff --git a/js/id/renderer/map.js b/js/id/renderer/map.js index 3ebc18354..bf88b9491 100644 --- a/js/id/renderer/map.js +++ b/js/id/renderer/map.js @@ -18,7 +18,6 @@ iD.Map = function(context) { areas = iD.svg.Areas(projection), midpoints = iD.svg.Midpoints(roundedProjection, context), labels = iD.svg.Labels(projection, context), - sequences = iD.svg.Sequences(projection, context), supersurface, surface, mouse, mousemove; @@ -34,8 +33,6 @@ iD.Map = function(context) { supersurface = selection.append('div') .attr('id', 'supersurface'); - supersurface.call(context.background()); - // 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') @@ -53,6 +50,8 @@ iD.Map = function(context) { .attr('id', 'surface') .call(iD.svg.Surface(context)); + supersurface.call(context.background()); + surface.on('mousemove.map', function() { mousemove = d3.event; }); @@ -114,7 +113,6 @@ iD.Map = function(context) { .call(vertices, graph, all, filter, map.extent(), map.zoom()) .call(lines, graph, all, filter) .call(areas, graph, all, filter) - .call(sequences, surface) .call(midpoints, graph, all, filter, map.trimmedExtent()) .call(labels, graph, all, filter, dimensions, !difference && !extent); @@ -402,9 +400,5 @@ iD.Map = function(context) { return map; }; - map.enableSequences = function (enable) { - sequences.enable(enable); - }; - return d3.rebind(map, dispatch, 'on'); }; diff --git a/js/id/renderer/mapillary_layer.js b/js/id/renderer/mapillary_layer.js new file mode 100644 index 000000000..4213fc8f7 --- /dev/null +++ b/js/id/renderer/mapillary_layer.js @@ -0,0 +1,159 @@ +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', 'http://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') + .attr('class', 'icon close'); + + enter.append('img'); + + var link = enter.append('a') + .attr('class', 'link') + .attr('target', '_blank'); + + link.append('span') + .attr('class', 'icon icon-pre-text out-link'); + + link.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://mapillary-read-api.herokuapp.com/v1/s/search?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('d', 'M 0,-5 l 0,-20 l -5,30 l 10,0 l -5,-30'); + + enter.append('circle') + .attr('dx', '0') + .attr('dy', '0') + .attr('r', '8'); + + 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/svg/sequences.js b/js/id/svg/sequences.js deleted file mode 100644 index 29b620c04..000000000 --- a/js/id/svg/sequences.js +++ /dev/null @@ -1,100 +0,0 @@ -iD.svg.Sequences = function (projection, context) { - var surface, enabled = false; - - function drawSequences(_surface) { - surface = _surface; - - if (enabled) { - drawSequences.reloadMapillaryImages(); - } else { - drawSequences.removeAll(); - } - } - - drawSequences.removeAll = function () { - var hit_layer = surface.select('.layer-hit'); - if (hit_layer) { - hit_layer.selectAll('g.image').remove(); - hit_layer.selectAll('g.sequence').remove(); - } - }; - - drawSequences.enable = function (enable) { - enabled = enable; - drawSequences(surface); - }; - - drawSequences.plotSequences = function (surface, context, sequences) { - var imagePoints = drawSequences.images(sequences, 1000); - var images = surface.select('.layer-hit').selectAll('g.image') - .data(imagePoints); - var pointTransform = iD.svg.PointTransform(context.projection); - - var image = images.enter() - .append('g') - .attr('class', 'image point') - .attr('transform', function (d) { - var translate = pointTransform({ loc: d.geometry.coordinates }); - if (d.properties.ca) { - return translate + 'rotate(' + d.properties.ca + ',0,0)'; - } - return translate; - }); - - image.append('path') - .call(drawSequences.markerPath, 'stroke'); - - image.append('circle') - .attr('dx', '0') - .attr('dy', '0') - .attr('r', '8'); - - // Selecting the following implicitly - // sets the data (point entity) on the element - images.select('.shadow'); - images.select('.stroke'); - }; - - drawSequences.reloadMapillaryImages = function () { - var extent = context.map().extent(); - d3.json('https://mapillary-read-api.herokuapp.com/v1/s/search?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) { - drawSequences.plotSequences(context.surface(), context, data); - }); - }; - - drawSequences.images = function (sequences, limit) { - var images = []; - - for (var i = 0; i < sequences.features.length; i++) { - var sequence = sequences.features[i]; - for (var j = 0; j < sequence.geometry.coordinates.length; j++) { - images.push({ - geometry: { - type: 'Point', - coordinates: sequence.geometry.coordinates[j] - }, - properties: { - key: sequence.properties.keys[j], - ca: sequence.properties.cas[j], - entityType: 'image' - } - }); - if (limit && images.length >= limit) break; - } - } - - return images; - }; - - drawSequences.markerPath = function (selection, klass) { - selection - .attr('class', klass) - .attr('transform', 'translate(0, 0)') - .attr('d', 'M 0,-5 l 0,-20 l -5,30 l 10,0 l -5,-30'); - }; - - return drawSequences; -}; diff --git a/js/id/ui.js b/js/id/ui.js index 614858cf5..eacf19f8f 100644 --- a/js/id/ui.js +++ b/js/id/ui.js @@ -31,10 +31,6 @@ iD.ui = function(context) { var m = content.append('div') .attr('id', 'map') .call(map); - content.append('div') - .attr('id', 'mapillaryImage') - .classed('hidden', true) - .call(iD.ui.ImageView(context)); bar.append('div') .attr('class', 'spacer col4'); diff --git a/js/id/ui/background.js b/js/id/ui/background.js index 2cf0b2c4d..9e88acef7 100644 --- a/js/id/ui/background.js +++ b/js/id/ui/background.js @@ -77,6 +77,11 @@ iD.ui.Background = function(context) { update(); } + function clickMapillary() { + context.background().toggleMapillaryLayer(); + update(); + } + function drawList(layerList, type, change, filter) { var sources = context.background() .sources(context.map().extent()) @@ -124,6 +129,13 @@ iD.ui.Background = function(context) { .property('disabled', !hasGpx) .property('checked', showsGpx); + var showsMapillary = context.background().showsMapillaryLayer(); + + mapillaryLayerItem + .classed('active', showsMapillary) + .selectAll('input') + .property('checked', showsMapillary); + selectLayer(); var source = context.background().baseLayerSource(); @@ -266,26 +278,16 @@ iD.ui.Background = function(context) { var mapillaryLayerItem = overlayList.append('li'); - var mapillaryLabel = mapillaryLayerItem.append('label') + label = mapillaryLayerItem.append('label') .call(bootstrap.tooltip() - .title(t('modes.selectImage.description')) + .title(t('mapillary.tooltip')) .placement('top')); - mapillaryLabel.append('input') + label.append('input') .attr('type', 'checkbox') - .attr('id', 'select_image_checkbox') - .on('change', function(){ - if (this.checked) { - mapillaryLayerItem.classed('active',true); - context.enter(iD.modes.SelectImage(context)); - } else { - mapillaryLayerItem.classed('active',false); - context.enter(iD.modes.Browse(context)); - } - update(); - }); + .on('change', clickMapillary); - mapillaryLabel.append('span') + label.append('span') .text(t('mapillary.title')); var gpxLayerItem = content.append('ul') diff --git a/js/id/ui/image_view.js b/js/id/ui/image_view.js deleted file mode 100644 index a8a67b20d..000000000 --- a/js/id/ui/image_view.js +++ /dev/null @@ -1,50 +0,0 @@ -iD.ui.ImageView = function (context) { - function imageView() { } - - imageView.showEmpty = function () { - var imageWrapper = context.container() - .select('#mapillaryImage'); - - imageWrapper.html(''); - - var content = imageWrapper - .append('div'); - - content.append('div') - .on('click', function(){ - imageWrapper.classed('hidden', true); - }); - - content.append('div') - .html(marked(t('mapillary.no_image_found'))); - }; - - imageView.show = function (imageToShow) { - var key = imageToShow.properties.key; - var imageWrapper = context.container().select('#mapillaryImage'); - imageWrapper.classed('hidden', false); - imageWrapper.html(''); - var content = imageWrapper - .append('div'); - content.append('button') - .on('click', function(){ - imageWrapper.classed('hidden', true); - }) - .append('div') - .attr('class', 'icon close'); - var wrap = content.append('div'); - wrap.append('div') - .append('img') - .attr('src', 'https://d1cuyjsrcm0gby.cloudfront.net/KEY/thumb-320.jpg'.replace('KEY', key)); - var wrapLink = wrap.append('a') - .attr('class', 'link') - .attr('target', '_blank') - .attr('src', 'http://mapillary.com/map/im/KEY'.replace('KEY', key)); - wrapLink.append('span') - .attr('class','icon icon-pre-text out-link'); - wrapLink.append('span') - .text(t('mapillary.view_on_mapillary')); - }; - - return imageView; -}; diff --git a/test/index.html b/test/index.html index 02acf487d..81cc2ae67 100644 --- a/test/index.html +++ b/test/index.html @@ -55,12 +55,12 @@ + - @@ -72,7 +72,6 @@ - @@ -166,7 +165,6 @@ - diff --git a/test/spec/behavior/hash.js b/test/spec/behavior/hash.js index 82177f034..707c1687d 100644 --- a/test/spec/behavior/hash.js +++ b/test/spec/behavior/hash.js @@ -5,6 +5,7 @@ describe('iD.behavior.Hash', function () { beforeEach(function () { context = iD(); + context.container(d3.select(document.createElement('div'))); // Neuter connection context.connection().loadTiles = function () {}; diff --git a/test/spec/behavior/lasso.js b/test/spec/behavior/lasso.js index bf92f04d9..c0bf77a74 100644 --- a/test/spec/behavior/lasso.js +++ b/test/spec/behavior/lasso.js @@ -3,6 +3,7 @@ describe("iD.behavior.Lasso", function () { beforeEach(function () { context = iD(); + context.container(d3.select(document.createElement('div'))); // Neuter connection context.connection().loadTiles = function () {}; diff --git a/test/spec/renderer/map.js b/test/spec/renderer/map.js index f00ce1f28..663030246 100644 --- a/test/spec/renderer/map.js +++ b/test/spec/renderer/map.js @@ -1,8 +1,10 @@ describe('iD.Map', function() { - var map; + var context, map; beforeEach(function() { - map = iD().map(); + context = iD(); + context.container(d3.select(document.createElement('div'))); + map = context.map(); d3.select(document.createElement('div')) .call(map); });