diff --git a/css/60_photos.css b/css/60_photos.css index 63173726d..14c201ae5 100644 --- a/css/60_photos.css +++ b/css/60_photos.css @@ -241,39 +241,6 @@ label.streetside-hires { color: #20c4ff; } -.osc-controls-wrap { - text-align: center; - position: absolute; - top: 10px; - width: 100%; - z-index: 10; -} - -.osc-controls { - display: inline-block; - z-index: 10; -} - -.osc-controls button { - height: 18px; - width: 18px; - background: rgba(0,0,0,0.65); - color: #eee; - border-radius: 0; -} -.osc-controls button:first-of-type { - border-radius: 3px 0 0 3px; -} -.osc-controls button:last-of-type { - border-radius: 0 3px 3px 0; -} -.osc-controls button:hover, -.osc-controls button:active, -.osc-controls button:focus { - background: rgba(0,0,0,0.85); - color: #fff; -} - .osc-image-wrap { transform-origin:0 0; -ms-transform-origin:0 0; @@ -281,3 +248,39 @@ label.streetside-hires { -moz-transform-origin:0 0; -o-transform-origin:0 0; } + + +/* photo-controls (step forward, back, rotate) */ +.photo-controls-wrap { + text-align: center; + position: absolute; + top: 10px; + width: 100%; + z-index: 10; +} + +.photo-controls { + display: inline-block; + z-index: 10; +} + +.photo-controls button, +.photo-controls button:focus { + height: 18px; + width: 18px; + background: rgba(0,0,0,0.65); + color: #eee; + border-radius: 0; +} +.photo-controls button:first-of-type { + border-radius: 3px 0 0 3px; +} +.photo-controls button:last-of-type { + border-radius: 0 3px 3px 0; +} +.photo-controls button:hover, +.photo-controls button:active { + background: rgba(0,0,0,0.85); + color: #fff; +} + diff --git a/modules/services/openstreetcam.js b/modules/services/openstreetcam.js index 271fee008..dd7639e25 100644 --- a/modules/services/openstreetcam.js +++ b/modules/services/openstreetcam.js @@ -338,9 +338,9 @@ export default { var controlsEnter = wrapEnter .append('div') - .attr('class', 'osc-controls-wrap') + .attr('class', 'photo-controls-wrap') .append('div') - .attr('class', 'osc-controls'); + .attr('class', 'photo-controls'); controlsEnter .append('button') diff --git a/modules/services/streetside.js b/modules/services/streetside.js index 00be81690..64b76b318 100644 --- a/modules/services/streetside.js +++ b/modules/services/streetside.js @@ -18,7 +18,16 @@ import rbush from 'rbush'; import { t } from '../util/locale'; import { jsonpRequest } from '../util/jsonp_request'; import { d3geoTile as d3_geoTile } from '../lib/d3.geo.tile'; -import { geoExtent } from '../geo'; + +import { + geoExtent, + geoMetersToLat, + geoMetersToLon, + geoPointInPolygon, + geoRotate, + geoVecLength +} from '../geo'; + import { utilDetect } from '../util/detect'; import { utilQsString, utilRebind } from '../util'; @@ -599,6 +608,8 @@ export default { * loadViewer() create the streeside viewer. */ loadViewer: function (context) { + var that = this; + // create ms-wrapper, a photo wrapper class var wrap = d3_select('#photoviewer').selectAll('.ms-wrapper') .data([0]); @@ -618,6 +629,23 @@ export default { .append('div') .attr('class', 'photo-attribution fillD'); + var controlsEnter = wrapEnter + .append('div') + .attr('class', 'photo-controls-wrap') + .append('div') + .attr('class', 'photo-controls'); + + controlsEnter + .append('button') + .on('click.back', step(-1)) + .text('◄'); + + controlsEnter + .append('button') + .on('click.forward', step(1)) + .text('►'); + + // create working canvas for stitching together images wrap = wrap .merge(wrapEnter) @@ -639,6 +667,82 @@ export default { .append('script') .attr('id', 'streetside-viewerjs') .attr('src', context.asset(pannellumViewerJS)); + + + function step(stepBy) { + return function() { + var viewer = d3_select('#photoviewer'); + var selected = viewer.empty() ? undefined : viewer.datum(); + if (!selected) return; + + var nextID = (stepBy === 1 ? selected.ne : selected.pr); + var yaw = _pannellumViewer.getYaw(); + var ca = selected.ca + yaw; + var origin = selected.loc; + + // construct a search trapezoid pointing out from current bubble + var meters = 35; + var p1 = [ + origin[0] + geoMetersToLon(meters / 5, origin[1]), + origin[1] + ]; + var p2 = [ + origin[0] + geoMetersToLon(meters / 2, origin[1]), + origin[1] + geoMetersToLat(meters) + ]; + var p3 = [ + origin[0] - geoMetersToLon(meters / 2, origin[1]), + origin[1] + geoMetersToLat(meters) + ]; + var p4 = [ + origin[0] - geoMetersToLon(meters / 5, origin[1]), + origin[1] + ]; + + var poly = [p1, p2, p3, p4, p1]; + + // rotate it to face forward/backward + var angle = (stepBy === 1 ? ca : ca + 180) * (Math.PI / 180); + poly = geoRotate(poly, -angle, origin); + + var extent = poly.reduce(function(extent, point) { + return extent.extend(geoExtent(point)); + }, geoExtent()); + + // find nearest other bubble in the search polygon + var minDist = Infinity; + _ssCache.bubbles.rtree.search(extent.bbox()) + .forEach(function(d) { + if (d.data.key === selected.key) return; + if (!geoPointInPolygon(d.data.loc, poly)) return; + + var dist = geoVecLength(d.data.loc, selected.loc); + var theta = selected.ca - d.data.ca; + var minTheta = Math.min(Math.abs(theta), 360 - Math.abs(theta)); + if (minTheta > 20) { + dist += 5; // penalize distance if camera angles don't match + } + + if (dist < minDist) { + nextID = d.data.key; + minDist = dist; + } + }); + + var nextBubble = nextID && _ssCache.bubbles.points[nextID]; + if (!nextBubble) return; + + context.map().centerEase(nextBubble.loc); + + that.selectImage(nextBubble) + .then(function(r) { + if (r.status === 'ok') { + _sceneOptions.yaw = yaw; + that.showViewer(); + } + }); + }; + } }, /**