From 4571fd05638152d7b629a24464feee098721b74b Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 8 Nov 2017 00:35:44 -0500 Subject: [PATCH] Highlight selected/hovered streetview tracks, fade unselected Also some code cleanups and nitpicky variable renames More consistency in Mapillary/OpenStreetCam services --- css/60_photos.css | 60 +++++-- modules/services/mapillary.js | 239 +++++++++++++++++----------- modules/services/openstreetcam.js | 155 ++++++++++++------ modules/svg/mapillary_images.js | 64 +++++--- modules/svg/mapillary_signs.js | 33 ++-- modules/svg/openstreetcam_images.js | 71 ++++++--- test/spec/services/mapillary.js | 20 +-- test/spec/services/openstreetcam.js | 39 ++--- 8 files changed, 435 insertions(+), 246 deletions(-) diff --git a/css/60_photos.css b/css/60_photos.css index a251a2658..54056a300 100644 --- a/css/60_photos.css +++ b/css/60_photos.css @@ -24,41 +24,71 @@ overflow: hidden; } +/* markers and sequences */ .viewfield-group { pointer-events: visible; cursor: pointer; } .viewfield-group * { - stroke-width: 1; - stroke: #444; z-index: 50; } - .viewfield-group.selected * { - stroke-width: 2; - stroke: #222; - fill: #ff5800 !important; + fill: #ffee00 !important; + z-index: 60; +} +.viewfield-group.hovered * { + fill: #eebb00 !important; + z-index: 70; +} +.viewfield-group.highlighted * { z-index: 60; } -.viewfield-group:hover * { - stroke-width: 1; +.viewfield-group circle { stroke: #333; - fill: #ff9900 !important; - z-index: 70; + stroke-width: 1; + stroke-opacity: 0.4; + fill-opacity: 0.4; +} +.viewfield-group.highlighted circle { + stroke-opacity: 0.9; + fill-opacity: 0.9; +} +.viewfield-group.highlighted.hovered circle { + stroke-width: 2; + stroke-opacity: 0.9; + fill-opacity: 0.9; +} +.viewfield-group.highlighted.selected circle { + stroke-width: 2; + stroke-opacity: 1; + fill-opacity: 1; } -.viewfield-group:hover path.viewfield, -.viewfield-group.selected path.viewfield, -.viewfield-group path.viewfield { +.viewfield-group .viewfield { stroke-width: 0; - fill-opacity: 0.6; + fill-opacity: 0.4; +} +.viewfield-group.highlighted .viewfield { + fill-opacity: 0.8; +} +.viewfield-group.highlighted.hovered .viewfield { + fill-opacity: 0.8; +} +.viewfield-group.highlighted.selected .viewfield { + fill-opacity: 0.9; } .sequence { - stroke-width: 2; fill: none; + stroke-width: 2; + stroke-opacity: 0.4; +} +.sequence.highlighted, +.sequence.selected { + stroke-width: 4; + stroke-opacity: 1; } diff --git a/modules/services/mapillary.js b/modules/services/mapillary.js index 932911d8a..d7d28619d 100644 --- a/modules/services/mapillary.js +++ b/modules/services/mapillary.js @@ -6,6 +6,7 @@ import _forEach from 'lodash-es/forEach'; import _isEmpty from 'lodash-es/isEmpty'; import _map from 'lodash-es/map'; import _some from 'lodash-es/some'; +import _union from 'lodash-es/union'; import { range as d3_range } from 'd3-array'; import { dispatch as d3_dispatch } from 'd3-dispatch'; @@ -35,12 +36,12 @@ var apibase = 'https://a.mapillary.com/v3/', maxResults = 1000, tileZoom = 14, dispatch = d3_dispatch('loadedImages', 'loadedSigns'), - mapillaryCache, - mapillaryClicks, - mapillaryImage, - mapillarySignDefs, - mapillarySignSprite, - mapillaryViewer; + _mlyCache, + _mlyClicks, + _mlySelectedImage, + _mlySignDefs, + _mlySignSprite, + _mlyViewer; function abortRequest(i) { @@ -120,7 +121,7 @@ function loadTiles(which, url, projection) { function loadNextTilePage(which, currZoom, url, tile) { - var cache = mapillaryCache[which], + var cache = _mlyCache[which], rect = tile.extent.rectangle(), maxPages = maxPageAtZoom(currZoom), nextPage = cache.nextPage[tile.id] || 0, @@ -165,14 +166,15 @@ function loadNextTilePage(which, currZoom, url, tile) { captured_at: feature.properties.captured_at, pano: feature.properties.pano }; + cache.forImageKey[d.key] = d; // cache imageKey -> image } else if (which === 'sequences') { - var sk = feature.properties.key; - cache.lineString[sk] = feature; // cache sequence_key -> linestring - feature.properties.coordinateProperties.image_keys.forEach(function(ik) { - cache.forImage[ik] = sk; // cache image_key -> sequence_key + var sequenceKey = feature.properties.key; + cache.lineString[sequenceKey] = feature; // cache sequenceKey -> lineString + feature.properties.coordinateProperties.image_keys.forEach(function(imageKey) { + cache.forImageKey[imageKey] = sequenceKey; // cache imageKey -> sequenceKey }); - return false; // nothing to actually insert + return false; // because no `d` data worth loading into an rbush } else if (which === 'objects') { d = { @@ -183,15 +185,15 @@ function loadNextTilePage(which, currZoom, url, tile) { detections: feature.properties.detections }; - // cache image_key -> detection_key + // cache imageKey -> detectionKey feature.properties.detections.forEach(function(detection) { - var ik = detection.image_key; - var dk = detection.detection_key; - if (!mapillaryCache.detections[ik]) { - mapillaryCache.detections[ik] = {}; + var imageKey = detection.image_key; + var detectionKey = detection.detection_key; + if (!_mlyCache.detections[imageKey]) { + _mlyCache.detections[imageKey] = {}; } - if (!mapillaryCache.detections[ik][dk]) { - mapillaryCache.detections[ik][dk] = {}; + if (!_mlyCache.detections[imageKey][detectionKey]) { + _mlyCache.detections[imageKey][detectionKey] = {}; } }); } @@ -199,6 +201,7 @@ function loadNextTilePage(which, currZoom, url, tile) { return { minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d }; + }).filter(Boolean); cache.rtree.load(features); @@ -295,7 +298,7 @@ function searchLimited(psize, limit, projection, rtree) { export default { init: function() { - if (!mapillaryCache) { + if (!_mlyCache) { this.reset(); } @@ -303,7 +306,7 @@ export default { }, reset: function() { - var cache = mapillaryCache; + var cache = _mlyCache; if (cache) { if (cache.images && cache.images.inflight) { @@ -317,27 +320,27 @@ export default { } } - mapillaryCache = { - images: { inflight: {}, loaded: {}, nextPage: {}, nextURL: {}, rtree: rbush() }, + _mlyCache = { + images: { inflight: {}, loaded: {}, nextPage: {}, nextURL: {}, rtree: rbush(), forImageKey: {} }, objects: { inflight: {}, loaded: {}, nextPage: {}, nextURL: {}, rtree: rbush() }, - sequences: { inflight: {}, loaded: {}, nextPage: {}, nextURL: {}, rtree: rbush(), forImage: {}, lineString: {} }, + sequences: { inflight: {}, loaded: {}, nextPage: {}, nextURL: {}, rtree: rbush(), forImageKey: {}, lineString: {} }, detections: {} }; - mapillaryImage = null; - mapillaryClicks = []; + _mlySelectedImage = null; + _mlyClicks = []; }, images: function(projection) { var psize = 16, limit = 3; - return searchLimited(psize, limit, projection, mapillaryCache.images.rtree); + return searchLimited(psize, limit, projection, _mlyCache.images.rtree); }, signs: function(projection) { var psize = 32, limit = 3; - return searchLimited(psize, limit, projection, mapillaryCache.objects.rtree); + return searchLimited(psize, limit, projection, _mlyCache.objects.rtree); }, @@ -349,17 +352,17 @@ export default { var sequenceKeys = {}; // all sequences for images in viewport - mapillaryCache.images.rtree.search(bbox) + _mlyCache.images.rtree.search(bbox) .forEach(function(d) { - var sk = mapillaryCache.sequences.forImage[d.data.key]; - if (sk) { - sequenceKeys[sk] = true; + var sequenceKey = _mlyCache.sequences.forImageKey[d.data.key]; + if (sequenceKey) { + sequenceKeys[sequenceKey] = true; } }); - // Return linestrings for the sequences - return Object.keys(sequenceKeys).map(function(sk) { - return mapillaryCache.sequences.lineString[sk]; + // Return lineStrings for the sequences + return Object.keys(sequenceKeys).map(function(sequenceKey) { + return _mlyCache.sequences.lineString[sequenceKey]; }); }, @@ -373,11 +376,11 @@ export default { signHTML: function(d) { - if (!mapillarySignDefs || !mapillarySignSprite) return; - var position = mapillarySignDefs[d.value]; + if (!_mlySignDefs || !_mlySignSprite) return; + var position = _mlySignDefs[d.value]; if (!position) return '
'; var iconStyle = [ - 'background-image:url(' + mapillarySignSprite + ')', + 'background-image:url(' + _mlySignSprite + ')', 'background-repeat:no-repeat', 'height:' + position.height + 'px', 'width:' + position.width + 'px', @@ -400,12 +403,12 @@ export default { loadTiles('objects', url, projection); // load traffic sign defs - if (!mapillarySignDefs) { - mapillarySignSprite = context.asset('img/traffic-signs/traffic-signs.png'); - mapillarySignDefs = {}; + if (!_mlySignDefs) { + _mlySignSprite = context.asset('img/traffic-signs/traffic-signs.png'); + _mlySignDefs = {}; d3_json(context.asset('img/traffic-signs/traffic-signs.json'), function(err, data) { if (err) return; - mapillarySignDefs = data; + _mlySignDefs = data; }); } }, @@ -455,7 +458,7 @@ export default { .selectAll('.photo-wrapper.mly-wrapper') .classed('hide', false); - mapillaryViewer.resize(); + _mlyViewer.resize(); } return this; @@ -468,10 +471,10 @@ export default { .selectAll('.photo-wrapper') .classed('hide', true); - d3_selectAll('.layer-mapillary-images .viewfield-group, .layer-mapillary-signs .icon-sign') + d3_selectAll('.viewfield-group, .sequence, .icon-sign') .classed('selected', false); - mapillaryImage = null; + _mlySelectedImage = null; return this; }, @@ -479,13 +482,13 @@ export default { parsePagination: parsePagination, - updateViewer: function(imageKey, context) { - if (!imageKey) return; + updateViewer: function(d, context) { + if (!d || !d.key) return; - if (!mapillaryViewer) { - this.initViewer(imageKey, context); + if (!_mlyViewer) { + this.initViewer(d.key, context); } else { - mapillaryViewer.moveToKey(imageKey); + _mlyViewer.moveToKey(d.key); } return this; @@ -504,51 +507,54 @@ export default { } }; - mapillaryViewer = new Mapillary.Viewer('mly', clientId, imageKey, opts); - mapillaryViewer.on('nodechanged', nodeChanged); + _mlyViewer = new Mapillary.Viewer('mly', clientId, imageKey, opts); + _mlyViewer.on('nodechanged', nodeChanged); } // nodeChanged: called after the viewer has changed images and is ready. // - // There is some logic here to batch up clicks into a mapillaryClicks array + // There is some logic here to batch up clicks into a _mlyClicks array // because the user might click on a lot of markers quickly and nodechanged // may be called out of order asychronously. // // Clicks are added to the array in `selectedImage` and removed here. // function nodeChanged(node) { - mapillaryViewer.getComponent('tag').removeAll(); // remove previous detections + _mlyViewer.getComponent('tag').removeAll(); // remove previous detections - var clicks = mapillaryClicks; + var clicks = _mlyClicks; var index = clicks.indexOf(node.key); - if (index > -1) { // `nodechanged` initiated from clicking on a marker.. - clicks.splice(index, 1); - // If `node.key` matches the current mapillaryImage, call `selectedImage()` + var selectedKey = _mlySelectedImage && _mlySelectedImage.key; + + if (index > -1) { // `nodechanged` initiated from clicking on a marker.. + clicks.splice(index, 1); // remove the click + // If `node.key` matches the current _mlySelectedImage, call `selectImage()` // one more time to update the detections and attribution.. - if (node.key === mapillaryImage) { - that.selectedImage(node.key, false); + if (node.key === selectedKey) { + that.selectImage(_mlySelectedImage, node.key, true); } } else { // `nodechanged` initiated from the Mapillary viewer controls.. var loc = node.computedLatLon ? [node.computedLatLon.lon, node.computedLatLon.lat] : [node.latLon.lon, node.latLon.lat]; context.map().centerEase(loc); - that.selectedImage(node.key, false); + that.selectImage(undefined, node.key, true); } } }, - selectedImage: function(imageKey, fromClick) { - if (!arguments.length) return mapillaryImage; - mapillaryImage = imageKey; - - if (fromClick) { - mapillaryClicks.push(imageKey); + selectImage: function(d, imageKey, fromViewer) { + if (!d && imageKey) { + d = _mlyCache.images.forImageKey[imageKey]; } - d3_selectAll('.viewfield-group') - .classed('selected', function(d) { - return d.key === imageKey; - }); + _mlySelectedImage = d; + imageKey = d && d.key; + + if (!fromViewer && imageKey) { + _mlyClicks.push(imageKey); + } + + this.setStyles(null, _mlySelectedImage, true); d3_selectAll('.layer-mapillary-signs .icon-sign') .classed('selected', function(d) { @@ -557,7 +563,7 @@ export default { }); }); - if (!imageKey) return this; + if (!d) return this; function localeTimestamp(s) { @@ -570,14 +576,14 @@ export default { var selected = d3_selectAll('.layer-mapillary-images .viewfield-group.selected'); if (selected.empty()) return this; - var datum = selected.datum(); - var timestamp = localeTimestamp(datum.captured_at); + var timestamp = localeTimestamp(d.captured_at); var attribution = d3_select('.mapillary-js-dom .Attribution'); var capturedAt = attribution.selectAll('.captured-at'); if (capturedAt.empty()) { capturedAt = attribution .insert('span', ':last-child') .attr('class', 'captured-at'); + attribution .insert('span', ':last-child') .text('|'); @@ -585,23 +591,79 @@ export default { capturedAt .text(timestamp); - this.updateDetections(); + this.updateDetections(d); return this; }, - updateDetections: function() { - if (!mapillaryViewer) return; + getSelectedImage: function() { + return _mlySelectedImage; + }, + + + getSequenceKeyForImage: function(d) { + var imageKey = d && d.key; + return imageKey && _mlyCache.sequences.forImageKey[imageKey]; + }, + + + getSelectedSequenceKey: function() { + return this.getSequenceKeyForImage(_mlySelectedImage); + }, + + + setStyles: function(hovered, selected, reset) { + if (reset) { // reset all layers + d3_selectAll('.viewfield-group') + .classed('highlighted', false) + .classed('hovered', false) + .classed('selected', false); + + d3_selectAll('.sequence') + .classed('highlighted', false) + .classed('selected', false); + } + + var hoveredImageKey = hovered && hovered.key; + var hoveredSequenceKey = this.getSequenceKeyForImage(hovered); + var hoveredLineString = hoveredSequenceKey && _mlyCache.sequences.lineString[hoveredSequenceKey]; + var hoveredImageKeys = (hoveredLineString && hoveredLineString.properties.coordinateProperties.image_keys) || []; + + var selectedImageKey = selected && selected.key; + var selectedSequenceKey = this.getSequenceKeyForImage(selected); + var selectedLineString = selectedSequenceKey && _mlyCache.sequences.lineString[selectedSequenceKey]; + var selectedImageKeys = (selectedLineString && selectedLineString.properties.coordinateProperties.image_keys) || []; + + // highlight sibling viewfields on either the selected or the hovered sequences + var highlightedImageKeys = _union(hoveredImageKeys, selectedImageKeys); + + d3_selectAll('.layer-mapillary-images .viewfield-group') + .classed('highlighted', function(d) { return highlightedImageKeys.indexOf(d.key) !== -1; }) + .classed('hovered', function(d) { return d.key === hoveredImageKey; }) + .classed('selected', function(d) { return d.key === selectedImageKey; }); + + d3_selectAll('.layer-mapillary-images .sequence') + .classed('highlighted', function(d) { return d.properties.key === hoveredSequenceKey; }) + .classed('selected', function(d) { return d.properties.key === selectedSequenceKey; }); + + return this; + }, + + + updateDetections: function(d) { + if (!_mlyViewer) return; + + var imageKey = d && d.key; + var detections = (imageKey && _mlyCache.detections[imageKey]) || []; - var detections = mapillaryCache.detections[mapillaryImage]; _forEach(detections, function(data, k) { if (_isEmpty(data)) { loadDetection(k); } else { var tag = makeTag(data); if (tag) { - var tagComponent = mapillaryViewer.getComponent('tag'); + var tagComponent = _mlyViewer.getComponent('tag'); tagComponent.add([tag]); } } @@ -622,13 +684,14 @@ export default { .get(function(err, data) { if (!data || !data.properties) return; - var ik = data.properties.image_key; - mapillaryCache.detections[ik][detectionKey] = data; + var imageKey = data.properties.image_key; + _mlyCache.detections[imageKey][detectionKey] = data; - if (mapillaryImage === ik) { + var selectedKey = _mlySelectedImage && _mlySelectedImage.key; + if (imageKey === selectedKey) { var tag = makeTag(data); if (tag) { - var tagComponent = mapillaryViewer.getComponent('tag'); + var tagComponent = _mlyViewer.getComponent('tag'); tagComponent.add([tag]); } } @@ -683,16 +746,14 @@ export default { }, - cache: function(_) { - if (!arguments.length) return mapillaryCache; - mapillaryCache = _; - return this; + cache: function() { + return _mlyCache; }, signDefs: function(_) { - if (!arguments.length) return mapillarySignDefs; - mapillarySignDefs = _; + if (!arguments.length) return _mlySignDefs; + _mlySignDefs = _; return this; } diff --git a/modules/services/openstreetcam.js b/modules/services/openstreetcam.js index 01320c78f..9d76176a1 100644 --- a/modules/services/openstreetcam.js +++ b/modules/services/openstreetcam.js @@ -3,6 +3,7 @@ import _find from 'lodash-es/find'; import _flatten from 'lodash-es/flatten'; import _forEach from 'lodash-es/forEach'; import _map from 'lodash-es/map'; +import _union from 'lodash-es/union'; import { range as d3_range } from 'd3-array'; import { dispatch as d3_dispatch } from 'd3-dispatch'; @@ -24,8 +25,8 @@ var apibase = 'http://openstreetcam.org', maxResults = 1000, tileZoom = 14, dispatch = d3_dispatch('loadedImages'), - openstreetcamCache, - openstreetcamImage; + _oscCache, + _oscSelectedImage; function abortRequest(i) { @@ -105,7 +106,7 @@ function loadTiles(which, url, projection) { function loadNextTilePage(which, currZoom, url, tile) { - var cache = openstreetcamCache[which]; + var cache = _oscCache[which]; var bbox = tile.extent.bbox(); var maxPages = maxPageAtZoom(currZoom); var nextPage = cache.nextPage[tile.id] || 1; @@ -150,15 +151,15 @@ function loadNextTilePage(which, currZoom, url, tile) { captured_at: localeDateString(item.shot_date || item.date_added), captured_by: item.username, imagePath: item.lth_name, - sequence_id: +item.sequence_id, + sequence_id: item.sequence_id, sequence_index: +item.sequence_index }; // cache sequence info - var seq = openstreetcamCache.sequences[d.sequence_id]; + var seq = _oscCache.sequences[d.sequence_id]; if (!seq) { seq = { rotation: 0, images: [] }; - openstreetcamCache.sequences[d.sequence_id] = seq; + _oscCache.sequences[d.sequence_id] = seq; } seq.images[d.sequence_index] = d; } @@ -225,7 +226,7 @@ function searchLimited(psize, limit, projection, rtree) { export default { init: function() { - if (!openstreetcamCache) { + if (!_oscCache) { this.reset(); } @@ -233,7 +234,7 @@ export default { }, reset: function() { - var cache = openstreetcamCache; + var cache = _oscCache; if (cache) { if (cache.images && cache.images.inflight) { @@ -241,18 +242,18 @@ export default { } } - openstreetcamCache = { + _oscCache = { images: { inflight: {}, loaded: {}, nextPage: {}, rtree: rbush() }, sequences: {} }; - openstreetcamImage = null; + _oscSelectedImage = null; }, images: function(projection) { var psize = 16, limit = 3; - return searchLimited(psize, limit, projection, openstreetcamCache.images.rtree); + return searchLimited(psize, limit, projection, _oscCache.images.rtree); }, @@ -261,24 +262,26 @@ export default { var min = [viewport[0][0], viewport[1][1]]; var max = [viewport[1][0], viewport[0][1]]; var bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox(); - var seq_ids = {}; + var sequenceKeys = {}; // all sequences for images in viewport - openstreetcamCache.images.rtree.search(bbox) - .forEach(function(d) { seq_ids[d.data.sequence_id] = true; }); + _oscCache.images.rtree.search(bbox) + .forEach(function(d) { sequenceKeys[d.data.sequence_id] = true; }); // make linestrings from those sequences var lineStrings = []; - Object.keys(seq_ids).forEach(function(seq_id) { - var seq = openstreetcamCache.sequences[seq_id]; - var images = seq && seq.images; - if (images) { - lineStrings.push({ - type: 'LineString', - coordinates: images.map(function (d) { return d.loc; }).filter(Boolean) - }); - } - }); + Object.keys(sequenceKeys) + .forEach(function(sequenceKey) { + var seq = _oscCache.sequences[sequenceKey]; + var images = seq && seq.images; + if (images) { + lineStrings.push({ + type: 'LineString', + coordinates: images.map(function (d) { return d.loc; }).filter(Boolean), + properties: { key: sequenceKey } + }); + } + }); return lineStrings; }, @@ -334,14 +337,14 @@ export default { function rotate(deg) { return function() { - if (!openstreetcamImage) return; - var seq_id = openstreetcamImage.sequence_id; - var seq = openstreetcamCache.sequences[seq_id]; - if (!seq) return; + if (!_oscSelectedImage) return; + var sequenceKey = _oscSelectedImage.sequence_id; + var sequence = _oscCache.sequences[sequenceKey]; + if (!sequence) return; - var r = seq.rotation || 0; + var r = sequence.rotation || 0; r += deg; - seq.rotation = r; + sequence.rotation = r; d3_select('#photoviewer .osc-wrapper .osc-image') .transition() @@ -352,19 +355,19 @@ export default { function step(stepBy) { return function() { - if (!openstreetcamImage) return; - var seq_id = openstreetcamImage.sequence_id; - var seq = openstreetcamCache.sequences[seq_id]; - if (!seq) return; + if (!_oscSelectedImage) return; + var sequenceKey = _oscSelectedImage.sequence_id; + var sequence = _oscCache.sequences[sequenceKey]; + if (!sequence) return; - var nextIndex = openstreetcamImage.sequence_index + stepBy; - var nextImage = seq.images[nextIndex]; + var nextIndex = _oscSelectedImage.sequence_index + stepBy; + var nextImage = sequence.images[nextIndex]; if (!nextImage) return; context.map().centerEase(nextImage.loc); that - .selectedImage(nextImage) + .selectImage(nextImage) .updateViewer(nextImage); }; } @@ -397,10 +400,10 @@ export default { .selectAll('.photo-wrapper') .classed('hide', true); - d3_selectAll('.layer-openstreetcam-images .viewfield-group') + d3_selectAll('.viewfield-group, .sequence, .icon-sign') .classed('selected', false); - openstreetcamImage = null; + _oscSelectedImage = null; return this; }, @@ -412,8 +415,8 @@ export default { .remove(); if (d) { - var seq = openstreetcamCache.sequences[d.sequence_id]; - var r = (seq && seq.rotation) || 0; + var sequence = _oscCache.sequences[d.sequence_id]; + var r = (sequence && sequence.rotation) || 0; wrap.append('img') .attr('class', 'osc-image') @@ -457,23 +460,73 @@ export default { }, - selectedImage: function(d) { - if (!arguments.length) return openstreetcamImage; - openstreetcamImage = d; + selectImage: function(d) { + _oscSelectedImage = d; - d3_selectAll('.viewfield-group') - .classed('selected', function(d) { - return d.key === openstreetcamImage.key; - }); + this.setStyles(null, _oscSelectedImage, true); + + d3_selectAll('.icon-sign') + .classed('selected', false); return this; }, - cache: function(_) { - if (!arguments.length) return openstreetcamCache; - openstreetcamCache = _; + getSelectedImage: function() { + return _oscSelectedImage; + }, + + + getSequenceKeyForImage: function(d) { + return d && d.sequence_id; + }, + + + getSelectedSequenceKey: function() { + return this.getSequenceKeyForImage(_oscSelectedImage); + }, + + + setStyles: function(hovered, selected, reset) { + if (reset) { // reset all layers + d3_selectAll('.viewfield-group') + .classed('highlighted', false) + .classed('hovered', false) + .classed('selected', false); + + d3_selectAll('.sequence') + .classed('highlighted', false) + .classed('selected', false); + } + + var hoveredImageKey = hovered && hovered.key; + var hoveredSequenceKey = this.getSequenceKeyForImage(hovered); + var hoveredSequence = hoveredSequenceKey && _oscCache.sequences[hoveredSequenceKey]; + var hoveredImageKeys = (hoveredSequence && hoveredSequence.images.map(function (d) { return d.key; })) || []; + + var selectedImageKey = selected && selected.key; + var selectedSequenceKey = this.getSequenceKeyForImage(selected); + var selectedSequence = selectedSequenceKey && _oscCache.sequences[selectedSequenceKey]; + var selectedImageKeys = (selectedSequence && selectedSequence.images.map(function (d) { return d.key; })) || []; + + // highlight sibling viewfields on either the selected or the hovered sequences + var highlightedImageKeys = _union(hoveredImageKeys, selectedImageKeys); + + d3_selectAll('.layer-openstreetcam-images .viewfield-group') + .classed('highlighted', function(d) { return highlightedImageKeys.indexOf(d.key) !== -1; }) + .classed('hovered', function(d) { return d.key === hoveredImageKey; }) + .classed('selected', function(d) { return d.key === selectedImageKey; }); + + d3_selectAll('.layer-openstreetcam-images .sequence') + .classed('highlighted', function(d) { return d.properties.key === hoveredSequenceKey; }) + .classed('selected', function(d) { return d.properties.key === selectedSequenceKey; }); + return this; + }, + + + cache: function() { + return _oscCache; } }; diff --git a/modules/svg/mapillary_images.js b/modules/svg/mapillary_images.js index 18da5c84a..d84de323a 100644 --- a/modules/svg/mapillary_images.js +++ b/modules/svg/mapillary_images.js @@ -14,7 +14,7 @@ import { services } from '../services'; export function svgMapillaryImages(projection, context, dispatch) { var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000), minZoom = 12, - minViewfieldZoom = 17, + minViewfieldZoom = 18, layer = d3_select(null), _mapillary; @@ -26,7 +26,7 @@ export function svgMapillaryImages(projection, context, dispatch) { } - function getMapillary() { + function getService() { if (services.mapillary && !_mapillary) { _mapillary = services.mapillary; _mapillary.event.on('loadedImages', throttledRedraw); @@ -39,10 +39,10 @@ export function svgMapillaryImages(projection, context, dispatch) { function showLayer() { - var mapillary = getMapillary(); - if (!mapillary) return; + var service = getService(); + if (!service) return; - mapillary.loadViewer(context); + service.loadViewer(context); editOn(); layer @@ -55,9 +55,9 @@ export function svgMapillaryImages(projection, context, dispatch) { function hideLayer() { - var mapillary = getMapillary(); - if (mapillary) { - mapillary.hideViewer(); + var service = getService(); + if (service) { + service.hideViewer(); } throttledRedraw.cancel(); @@ -82,18 +82,34 @@ export function svgMapillaryImages(projection, context, dispatch) { function click(d) { - var mapillary = getMapillary(); - if (!mapillary) return; + var service = getService(); + if (!service) return; context.map().centerEase(d.loc); - mapillary - .selectedImage(d.key, true) - .updateViewer(d.key, context) + service + .selectImage(d) + .updateViewer(d, context) .showViewer(); } + function mouseover(d) { + var service = getService(); + var selected = d3_select('.viewfield-group.selected'); + var datum = selected.size() && selected.datum(); + if (service) service.setStyles(d, datum); + } + + + function mouseout() { + var service = getService(); + var selected = d3_select('.viewfield-group.selected'); + var datum = selected.size() && selected.datum(); + if (service) service.setStyles(null, datum); + } + + function transform(d) { var t = svgPointTransform(projection)(d); if (d.ca) t += ' rotate(' + Math.floor(d.ca) + ',0,0)'; @@ -103,10 +119,9 @@ export function svgMapillaryImages(projection, context, dispatch) { function update() { var highZoom = ~~context.map().zoom() >= minViewfieldZoom; - var mapillary = getMapillary(); - var images = (mapillary ? mapillary.images(projection) : []); - var sequences = (mapillary && highZoom ? mapillary.sequences(projection) : []); - var imageKey = mapillary ? mapillary.selectedImage() : null; + var service = getService(); + var images = (service ? service.images(projection) : []); + var sequences = (service && highZoom ? service.sequences(projection) : []); var clip = d3_geoIdentity().clipExtent(projection.clipExtent()).stream; var project = projection.stream; @@ -115,7 +130,7 @@ export function svgMapillaryImages(projection, context, dispatch) { }}); var lineStrings = layer.selectAll('.sequences').selectAll('.sequence') - .data(sequences); + .data(sequences, function(d) { return d.properties.key; }); lineStrings.exit() .remove(); @@ -138,7 +153,8 @@ export function svgMapillaryImages(projection, context, dispatch) { var enter = markers.enter() .append('g') .attr('class', 'viewfield-group') - .classed('selected', function(d) { return d.key === imageKey; }) + .on('mouseover', mouseover) + .on('mouseout', mouseout) .on('click', click); markers = markers @@ -166,6 +182,12 @@ export function svgMapillaryImages(projection, context, dispatch) { .attr('dy', '0') .attr('r', '6'); + + var selected = d3_select('.viewfield-group.selected'); + var datum = selected.size() && selected.datum(); + if (service) service.setStyles(null, datum); + + function viewfieldPath() { var d = this.parentNode.__data__; if (d.pano) { @@ -179,7 +201,7 @@ export function svgMapillaryImages(projection, context, dispatch) { function drawImages(selection) { var enabled = svgMapillaryImages.enabled, - mapillary = getMapillary(); + mapillary = getService(); layer = selection.selectAll('.layer-mapillary-images') .data(mapillary ? [0] : []); @@ -229,7 +251,7 @@ export function svgMapillaryImages(projection, context, dispatch) { drawImages.supported = function() { - return !!getMapillary(); + return !!getService(); }; diff --git a/modules/svg/mapillary_signs.js b/modules/svg/mapillary_signs.js index 5137c5a8f..bbf9124a3 100644 --- a/modules/svg/mapillary_signs.js +++ b/modules/svg/mapillary_signs.js @@ -18,7 +18,7 @@ export function svgMapillarySigns(projection, context, dispatch) { } - function getMapillary() { + function getService() { if (services.mapillary && !_mapillary) { _mapillary = services.mapillary; _mapillary.event.on('loadedSigns', throttledRedraw); @@ -52,12 +52,12 @@ export function svgMapillarySigns(projection, context, dispatch) { function click(d) { - var mapillary = getMapillary(); - if (!mapillary) return; + var service = getService(); + if (!service) return; context.map().centerEase(d.loc); - var selected = mapillary.selectedImage(), + var selected = service.selectedImage(), imageKey; // Pick one of the images the sign was detected in, @@ -68,17 +68,18 @@ export function svgMapillarySigns(projection, context, dispatch) { } }); - mapillary - .selectedImage(imageKey, true) + service + .selectImage(null, imageKey) .updateViewer(imageKey, context) .showViewer(); } function update() { - var mapillary = getMapillary(), - data = (mapillary ? mapillary.signs(projection) : []), - imageKey = mapillary ? mapillary.selectedImage() : null; + var service = getService(); + var data = (service ? service.signs(projection) : []); + var image = service && service.getSelectedImage(); + var imageKey = image && image.key; var signs = layer.selectAll('.icon-sign') .data(data, function(d) { return d.key; }); @@ -101,7 +102,7 @@ export function svgMapillarySigns(projection, context, dispatch) { enter .append('xhtml:body') .attr('class', 'icon-sign-body') - .html(mapillary.signHTML); + .html(service.signHTML); signs .merge(enter) @@ -112,10 +113,10 @@ export function svgMapillarySigns(projection, context, dispatch) { function drawSigns(selection) { var enabled = svgMapillarySigns.enabled, - mapillary = getMapillary(); + service = getService(); layer = selection.selectAll('.layer-mapillary-signs') - .data(mapillary ? [0] : []); + .data(service ? [0] : []); layer.exit() .remove(); @@ -127,10 +128,10 @@ export function svgMapillarySigns(projection, context, dispatch) { .merge(layer); if (enabled) { - if (mapillary && ~~context.map().zoom() >= minZoom) { + if (service && ~~context.map().zoom() >= minZoom) { editOn(); update(); - mapillary.loadSigns(context, projection); + service.loadSigns(context, projection); } else { editOff(); } @@ -152,8 +153,8 @@ export function svgMapillarySigns(projection, context, dispatch) { drawSigns.supported = function() { - var mapillary = getMapillary(); - return (mapillary && mapillary.signsSupported()); + var service = getService(); + return (service && service.signsSupported()); }; diff --git a/modules/svg/openstreetcam_images.js b/modules/svg/openstreetcam_images.js index 458bec47c..477bced21 100644 --- a/modules/svg/openstreetcam_images.js +++ b/modules/svg/openstreetcam_images.js @@ -1,11 +1,12 @@ import _throttle from 'lodash-es/throttle'; -import { select as d3_select } from 'd3-selection'; import { geoIdentity as d3_geoIdentity, geoPath as d3_geoPath } from 'd3-geo'; +import { select as d3_select } from 'd3-selection'; + import { svgPointTransform } from './point_transform'; import { services } from '../services'; @@ -13,7 +14,7 @@ import { services } from '../services'; export function svgOpenstreetcamImages(projection, context, dispatch) { var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000), minZoom = 12, - minViewfieldZoom = 17, + minViewfieldZoom = 18, layer = d3_select(null), _openstreetcam; @@ -25,7 +26,7 @@ export function svgOpenstreetcamImages(projection, context, dispatch) { } - function getOpenstreetcam() { + function getService() { if (services.openstreetcam && !_openstreetcam) { _openstreetcam = services.openstreetcam; _openstreetcam.event.on('loadedImages', throttledRedraw); @@ -38,10 +39,10 @@ export function svgOpenstreetcamImages(projection, context, dispatch) { function showLayer() { - var openstreetcam = getOpenstreetcam(); - if (!openstreetcam) return; + var service = getService(); + if (!service) return; - openstreetcam.loadViewer(context); + service.loadViewer(context); editOn(); layer @@ -54,9 +55,9 @@ export function svgOpenstreetcamImages(projection, context, dispatch) { function hideLayer() { - var openstreetcam = getOpenstreetcam(); - if (openstreetcam) { - openstreetcam.hideViewer(); + var service = getService(); + if (service) { + service.hideViewer(); } throttledRedraw.cancel(); @@ -81,18 +82,34 @@ export function svgOpenstreetcamImages(projection, context, dispatch) { function click(d) { - var openstreetcam = getOpenstreetcam(); - if (!openstreetcam) return; + var service = getService(); + if (!service) return; context.map().centerEase(d.loc); - openstreetcam - .selectedImage(d) + service + .selectImage(d) .updateViewer(d) .showViewer(); } + function mouseover(d) { + var service = getService(); + var selected = d3_select('.viewfield-group.selected'); + var datum = selected.size() && selected.datum(); + if (service) service.setStyles(d, datum); + } + + + function mouseout() { + var service = getService(); + var selected = d3_select('.viewfield-group.selected'); + var datum = selected.size() && selected.datum(); + if (service) service.setStyles(null, datum); + } + + function transform(d) { var t = svgPointTransform(projection)(d); if (d.ca) t += ' rotate(' + Math.floor(d.ca) + ',0,0)'; @@ -102,11 +119,9 @@ export function svgOpenstreetcamImages(projection, context, dispatch) { function update() { var highZoom = ~~context.map().zoom() >= minViewfieldZoom; - var openstreetcam = getOpenstreetcam(); - var sequences = (openstreetcam && highZoom ? openstreetcam.sequences(projection) : []); - var images = (openstreetcam ? openstreetcam.images(projection) : []); - var selectedImage = openstreetcam && openstreetcam.selectedImage(); - var imageKey = selectedImage && selectedImage.key; + var service = getService(); + var sequences = (service && highZoom ? service.sequences(projection) : []); + var images = (service ? service.images(projection) : []); var clip = d3_geoIdentity().clipExtent(projection.clipExtent()).stream; var project = projection.stream; @@ -115,7 +130,7 @@ export function svgOpenstreetcamImages(projection, context, dispatch) { }}); var lineStrings = layer.selectAll('.sequences').selectAll('.sequence') - .data(sequences); + .data(sequences, function(d) { return d.properties.key; }); lineStrings.exit() .remove(); @@ -138,7 +153,8 @@ export function svgOpenstreetcamImages(projection, context, dispatch) { var enter = markers.enter() .append('g') .attr('class', 'viewfield-group') - .classed('selected', function(d) { return d.key === imageKey; }) + .on('mouseover', mouseover) + .on('mouseout', mouseout) .on('click', click); markers = markers @@ -165,15 +181,20 @@ export function svgOpenstreetcamImages(projection, context, dispatch) { .attr('dx', '0') .attr('dy', '0') .attr('r', '6'); + + + var selected = d3_select('.viewfield-group.selected'); + var datum = selected.size() && selected.datum(); + if (service) service.setStyles(null, datum); } function drawImages(selection) { var enabled = svgOpenstreetcamImages.enabled, - openstreetcam = getOpenstreetcam(); + service = getService(); layer = selection.selectAll('.layer-openstreetcam-images') - .data(openstreetcam ? [0] : []); + .data(service ? [0] : []); layer.exit() .remove(); @@ -195,10 +216,10 @@ export function svgOpenstreetcamImages(projection, context, dispatch) { .merge(layer); if (enabled) { - if (openstreetcam && ~~context.map().zoom() >= minZoom) { + if (service && ~~context.map().zoom() >= minZoom) { editOn(); update(); - openstreetcam.loadImages(projection); + service.loadImages(projection); } else { editOff(); } @@ -220,7 +241,7 @@ export function svgOpenstreetcamImages(projection, context, dispatch) { drawImages.supported = function() { - return !!getOpenstreetcam(); + return !!getService(); }; diff --git a/test/spec/services/mapillary.js b/test/spec/services/mapillary.js index e70d896f1..8fb7aef15 100644 --- a/test/spec/services/mapillary.js +++ b/test/spec/services/mapillary.js @@ -65,12 +65,12 @@ describe('iD.serviceMapillary', function() { describe('#reset', function() { it('resets cache and image', function() { - mapillary.cache({foo: 'bar'}); - mapillary.selectedImage('baz'); + mapillary.cache().foo = 'bar'; + mapillary.selectImage({key: 'baz'}); mapillary.reset(); expect(mapillary.cache()).to.not.have.property('foo'); - expect(mapillary.selectedImage()).to.be.null; + expect(mapillary.getSelectedImage()).to.be.null; }); }); @@ -377,9 +377,9 @@ describe('iD.serviceMapillary', function() { }; mapillary.cache().sequences.lineString['-'] = gj; - mapillary.cache().sequences.forImage['0'] = '-'; - mapillary.cache().sequences.forImage['1'] = '-'; - mapillary.cache().sequences.forImage['2'] = '-'; + mapillary.cache().sequences.forImageKey['0'] = '-'; + mapillary.cache().sequences.forImageKey['1'] = '-'; + mapillary.cache().sequences.forImageKey['2'] = '-'; var res = mapillary.sequences(context.projection); expect(res).to.deep.eql([gj]); @@ -430,10 +430,10 @@ describe('iD.serviceMapillary', function() { }); }); - describe('#selectedImage', function() { - it('sets and gets selected image', function() { - mapillary.selectedImage('foo'); - expect(mapillary.selectedImage()).to.eql('foo'); + describe('#selectImage', function() { + it('gets and sets the selected image', function() { + mapillary.selectImage('foo'); + expect(mapillary.getSelectedImage()).to.eql('foo'); }); }); diff --git a/test/spec/services/openstreetcam.js b/test/spec/services/openstreetcam.js index facdd55ac..e9d8dbc27 100644 --- a/test/spec/services/openstreetcam.js +++ b/test/spec/services/openstreetcam.js @@ -62,12 +62,12 @@ describe('iD.serviceOpenstreetcam', function() { describe('#reset', function() { it('resets cache and image', function() { - openstreetcam.cache({foo: 'bar'}); - openstreetcam.selectedImage('baz'); + openstreetcam.cache().foo = 'bar'; + openstreetcam.selectImage({key: 'baz'}); openstreetcam.reset(); expect(openstreetcam.cache()).to.not.have.property('foo'); - expect(openstreetcam.selectedImage()).to.be.null; + expect(openstreetcam.getSelectedImage()).to.be.null; }); }); @@ -251,27 +251,27 @@ describe('iD.serviceOpenstreetcam', function() { describe('#images', function() { it('returns images in the visible map area', function() { var features = [ - { minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '0', loc: [10,0], ca: 90, sequence_id: 100, sequence_index: 0 } }, - { minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '1', loc: [10,0], ca: 90, sequence_id: 100, sequence_index: 1 } }, - { minX: 10, minY: 1, maxX: 10, maxY: 1, data: { key: '2', loc: [10,1], ca: 90, sequence_id: 100, sequence_index: 2 } } + { minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '0', loc: [10,0], ca: 90, sequence_id: '100', sequence_index: 0 } }, + { minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '1', loc: [10,0], ca: 90, sequence_id: '100', sequence_index: 1 } }, + { minX: 10, minY: 1, maxX: 10, maxY: 1, data: { key: '2', loc: [10,1], ca: 90, sequence_id: '100', sequence_index: 2 } } ]; openstreetcam.cache().images.rtree.load(features); var res = openstreetcam.images(context.projection); expect(res).to.deep.eql([ - { key: '0', loc: [10,0], ca: 90, sequence_id: 100, sequence_index: 0 }, - { key: '1', loc: [10,0], ca: 90, sequence_id: 100, sequence_index: 1 } + { key: '0', loc: [10,0], ca: 90, sequence_id: '100', sequence_index: 0 }, + { key: '1', loc: [10,0], ca: 90, sequence_id: '100', sequence_index: 1 } ]); }); it('limits results no more than 3 stacked images in one spot', function() { var features = [ - { minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '0', loc: [10,0], ca: 90, sequence_id: 100, sequence_index: 0 } }, - { minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '1', loc: [10,0], ca: 90, sequence_id: 100, sequence_index: 1 } }, - { minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '2', loc: [10,0], ca: 90, sequence_id: 100, sequence_index: 2 } }, - { minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '3', loc: [10,0], ca: 90, sequence_id: 100, sequence_index: 3 } }, - { minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '4', loc: [10,0], ca: 90, sequence_id: 100, sequence_index: 4 } } + { minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '0', loc: [10,0], ca: 90, sequence_id: '100', sequence_index: 0 } }, + { minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '1', loc: [10,0], ca: 90, sequence_id: '100', sequence_index: 1 } }, + { minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '2', loc: [10,0], ca: 90, sequence_id: '100', sequence_index: 2 } }, + { minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '3', loc: [10,0], ca: 90, sequence_id: '100', sequence_index: 3 } }, + { minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '4', loc: [10,0], ca: 90, sequence_id: '100', sequence_index: 4 } } ]; openstreetcam.cache().images.rtree.load(features); @@ -284,9 +284,9 @@ describe('iD.serviceOpenstreetcam', function() { describe('#sequences', function() { it('returns sequence linestrings in the visible map area', function() { var features = [ - { minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '0', loc: [10,0], ca: 90, sequence_id: 100, sequence_index: 0 } }, - { minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '1', loc: [10,0], ca: 90, sequence_id: 100, sequence_index: 1 } }, - { minX: 10, minY: 1, maxX: 10, maxY: 1, data: { key: '2', loc: [10,1], ca: 90, sequence_id: 100, sequence_index: 2 } } + { minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '0', loc: [10,0], ca: 90, sequence_id: '100', sequence_index: 0 } }, + { minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '1', loc: [10,0], ca: 90, sequence_id: '100', sequence_index: 1 } }, + { minX: 10, minY: 1, maxX: 10, maxY: 1, data: { key: '2', loc: [10,1], ca: 90, sequence_id: '100', sequence_index: 2 } } ]; openstreetcam.cache().images.rtree.load(features); @@ -295,15 +295,16 @@ describe('iD.serviceOpenstreetcam', function() { var res = openstreetcam.sequences(context.projection); expect(res).to.deep.eql([{ type: 'LineString', - coordinates: [[10,0], [10,0], [10,1]] + coordinates: [[10,0], [10,0], [10,1]], + properties: { key: '100' } }]); }); }); describe('#selectedImage', function() { it('sets and gets selected image', function() { - openstreetcam.selectedImage('foo'); - expect(openstreetcam.selectedImage()).to.eql('foo'); + openstreetcam.selectImage('foo'); + expect(openstreetcam.getSelectedImage()).to.eql('foo'); }); });