diff --git a/css/app.css b/css/app.css index ede9e7eae..432cbee0c 100644 --- a/css/app.css +++ b/css/app.css @@ -340,6 +340,7 @@ a.hide { right: -100%; } + .pane { position:absolute; width:50%; @@ -716,6 +717,52 @@ a:hover .icon.out-link { background-position: -500px -14px;} bottom: 0; } +#mapillaryImage { + position: absolute; + right: 0; + bottom: 30px; + padding: 8px; + background-color: #fff; +} +#mapillaryImage > div { + position: relative; +} + +#mapillaryImage > a { + display: block; + width: 100%; + height: auto; + color: white; + position: relative; +} + +#mapillaryImage .link { + background-color: black; + position: absolute; + bottom: 0; + width: 100%; + height: 20px; + text-align: center; +} + +#mapillaryImage img { + width: 100%; + heigth: auto; + display: block; +} + +#mapillaryImage.hidden { + visibility: hidden; +} + +#mapillaryImage .close { + cursor: pointer; + position: absolute; + right: 0; + top: 0; + background-color: #f2f2f2; +} + .feature-list-pane .inspector-body { top: 120px; } diff --git a/css/map.css b/css/map.css index 8c013fbec..d88b460ae 100644 --- a/css/map.css +++ b/css/map.css @@ -1140,6 +1140,46 @@ text.gpx { fill:#FF26D4; } +/* Mapillary Sequences */ +.sequence { + stroke: red; + stroke-width: 4; + fill: none; + pointer-events: visibleStroke; +} + +g.image.point.hover { + stroke-opacity: 0.3; +} + +g.image.point{ + stroke: orange; + stroke-width: 2; + fill: none; + cursor: pointer; /* Opera */ + cursor: url(img/cursor-select-mapillary.png) 6 1, pointer; /* FF */ +} + +g.image.point path{ + stroke-width: 3; + stroke: orange; + fill: orange; + /*marker-end:url(#mapillary_direction_arrow);*/ +} + +g.image.point circle{ + fill: orange; + fill-opacity: 0.3; +} +g.image.point.selected circle{ + fill: yellow; + fill-opacity: 0.9; +} + +#mapillary_direction_arrow { + fill: orange; +} + /* Modes */ .mode-draw-line .vertex.active, diff --git a/data/core.yaml b/data/core.yaml index 4c9afb483..ecf603004 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -13,13 +13,15 @@ en: description: "Add restaurants, monuments, postal boxes or other points to the map." tail: Click on the map to add a point. browse: - title: Browse description: Pan and zoom the map. draw_area: 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: @@ -293,6 +295,12 @@ 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: + tooltip: "Mapillary street photos" + title: "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/img/arrow-icon.png b/dist/img/arrow-icon.png new file mode 100644 index 000000000..891492c1b Binary files /dev/null and b/dist/img/arrow-icon.png differ diff --git a/dist/img/cursor-select-mapillary.png b/dist/img/cursor-select-mapillary.png new file mode 100644 index 000000000..bdfecf6a6 Binary files /dev/null and b/dist/img/cursor-select-mapillary.png differ diff --git a/dist/img/cursor-select-mapillary2x.png b/dist/img/cursor-select-mapillary2x.png new file mode 100644 index 000000000..971c12e83 Binary files /dev/null and b/dist/img/cursor-select-mapillary2x.png differ diff --git a/dist/locales/en.json b/dist/locales/en.json index 78205649d..2342ca3f2 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -24,6 +24,10 @@ }, "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": { @@ -354,6 +358,12 @@ "zoom": "Zoom to GPX track", "browse": "Browse for a .gpx file" }, + "mapillary": { + "tooltip": "Mapillary street photos", + "title": "Mapillary", + "view_on_mapillary": "View this image on Mapillary.", + "no_image_found": "No image found on Mapillary. Go and take some!\n" + }, "help": { "title": "Help", "help": "# Help\n\nThis is an editor for [OpenStreetMap](http://www.openstreetmap.org/), the\nfree and editable map of the world. You can use it to add and update\ndata in your area, making an open-source and open-data map of the world\nbetter for everyone.\n\nEdits that you make on this map will be visible to everyone who uses\nOpenStreetMap. In order to make an edit, you'll need a\n[free OpenStreetMap account](https://www.openstreetmap.org/user/new).\n\nThe [iD editor](http://ideditor.com/) is a collaborative project with [source\ncode available on GitHub](https://github.com/openstreetmap/iD).\n", diff --git a/index.html b/index.html index 69768a885..f267f5c3b 100644 --- a/index.html +++ b/index.html @@ -66,6 +66,7 @@ + @@ -76,6 +77,7 @@ + @@ -188,6 +190,7 @@ + diff --git a/js/id/id.js b/js/id/id.js index 3d135de22..0fe52e675 100644 --- a/js/id/id.js +++ b/js/id/id.js @@ -214,6 +214,13 @@ 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 new file mode 100644 index 000000000..483ca0d21 --- /dev/null +++ b/js/id/modes/select_image.js @@ -0,0 +1,100 @@ +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; + + var behaviors = [ + ]; + + function click() { + var datum = d3.event.target.__data__; + if (isImage(datum)) { + if (currentImage) { + context.surface().selectAll('.key_' + currentImage.properties.key) + .classed('selected', false); + } + if(currentImage === datum) { + context.surface().selectAll('.key_' + currentImage.properties.key) + .classed('selected', false); + currentImage = undefined; + + } else { + currentImage = datum; + context.surface().selectAll('.key_' + currentImage.properties.key) + .classed('selected', true); + } + imageView.show(currentImage); + } + } + + function isImage(datum) { + return datum !== undefined && datum && datum.properties !== undefined && datum.properties.entityType === 'image'; + } + + mode.enter = function () { +// console.log('selectImage.enter'); + context.map().enableSequences(true); + context.container().select('#select_image_checkbox') + .attr('checked','checked'); + behaviors.forEach(function (behavior) { + context.install(behavior); + }); + + // 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); + context.surface() + .on('mouseover.image', function () { + var datum = d3.event.target.__data__; + if (isImage(datum)) { + imageView.show(datum); + } + }) + .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); + + } + behaviors.forEach(function (behavior) { + context.uninstall(behavior); + }); + + 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/map.js b/js/id/renderer/map.js index d5d3dd9de..3ebc18354 100644 --- a/js/id/renderer/map.js +++ b/js/id/renderer/map.js @@ -18,6 +18,7 @@ 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; @@ -113,6 +114,7 @@ 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); @@ -400,5 +402,9 @@ iD.Map = function(context) { return map; }; + map.enableSequences = function (enable) { + sequences.enable(enable); + }; + return d3.rebind(map, dispatch, 'on'); }; diff --git a/js/id/svg/sequences.js b/js/id/svg/sequences.js new file mode 100644 index 000000000..fef3a41ff --- /dev/null +++ b/js/id/svg/sequences.js @@ -0,0 +1,109 @@ +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 image = images.enter() + .append('g') + .attr('class', function (d) { + return 'image point key_' + d.properties.key; + }) + .attr('transform', function (d) { + var translate = iD.svg.PointTransform(context.projection)({loc: d.geometry.coordinates}); + if (d.properties.ca) { + return translate + 'rotate(' + d.properties.ca + ',0,0)'; + } + return translate; + }) + .on('mouseover', function (d) { + surface.select('.key_' + d.properties.key).classed('hover', true); + }) + .on('mouseout', function (d) { + surface.select('.key_' + d.properties.key).classed('hover', false); + }); + + + image.append('path') + .call(drawSequences.markerPath, 'stroke'); + + image.append('circle') + .attr('dx', '0') + .attr('dy', '0') + .attr('r', '10'); + + // 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,-10 l 0,-20 l -5,20 l 10,0 l -5,-20'); + }; + + + return drawSequences; +}; diff --git a/js/id/ui.js b/js/id/ui.js index eacf19f8f..614858cf5 100644 --- a/js/id/ui.js +++ b/js/id/ui.js @@ -31,6 +31,10 @@ 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 4b7d8e39b..dd5d2535d 100644 --- a/js/id/ui/background.js +++ b/js/id/ui/background.js @@ -312,6 +312,34 @@ iD.ui.Background = function(context) { label.append('span') .text(t('gpx.local_layer')); + var mapillaryLayerItem = content.append('ul') + .attr('class', 'layer-list') + .append('li') + .classed('layer-toggle-gpx', true); + + var mapillaryLabel = mapillaryLayerItem.append('label') + .call(bootstrap.tooltip() + .title(t('modes.selectImage.description')) + .placement('top')); + + mapillaryLabel.append('input') + .attr('type', 'checkbox') + .attr('id', 'select_image_checkbox') + .on('change', function(){ + if(this.checked) { + context.enter(iD.modes.SelectImage(context)); + } else { + context.enter(iD.modes.Browse(context)); + } + update(); + + }); + + mapillaryLabel.append('span') + .text(t('mapillary.title')); + + + var adjustments = content.append('div') .attr('class', 'adjustments'); @@ -358,6 +386,10 @@ iD.ui.Background = function(context) { var keybinding = d3.keybinding('background'); keybinding.on(key, toggle); + keybinding.on('m', function() { + context.enter(iD.modes.SelectImage(context)); + }); + d3.select(document) .call(keybinding); diff --git a/js/id/ui/image_view.js b/js/id/ui/image_view.js new file mode 100644 index 000000000..36adccd79 --- /dev/null +++ b/js/id/ui/image_view.js @@ -0,0 +1,36 @@ +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('div') + .attr('class', 'icon close') + .on('click', function(){ + imageWrapper.classed('hidden', true); + }); + content.append('div').html('