diff --git a/modules/services/streetside.js b/modules/services/streetside.js index e88aff0a6..8c390dd31 100644 --- a/modules/services/streetside.js +++ b/modules/services/streetside.js @@ -2,9 +2,9 @@ import { dispatch as d3_dispatch } from 'd3-dispatch'; import { timer as d3_timer } from 'd3-timer'; import { - event as d3_event, - select as d3_select, - selectAll as d3_selectAll + event as d3_event, + select as d3_select, + selectAll as d3_selectAll } from 'd3-selection'; import RBush from 'rbush'; @@ -12,48 +12,41 @@ import { t } from '../util/locale'; import { jsonpRequest } from '../util/jsonp_request'; import { - geoExtent, - geoMetersToLat, - geoMetersToLon, - geoPointInPolygon, - geoRotate, - geoScaleToZoom, - geoVecLength + geoExtent, geoMetersToLat, geoMetersToLon, geoPointInPolygon, + geoRotate, geoScaleToZoom, geoVecLength } from '../geo'; import { utilDetect } from '../util/detect'; import { utilArrayUnion, utilQsString, utilRebind, utilTiler } from '../util'; -import Q from 'q'; +const bubbleApi = 'https://dev.virtualearth.net/mapcontrol/HumanScaleServices/GetBubbles.ashx?'; +const streetsideImagesApi = 'https://t.ssl.ak.tiles.virtualearth.net/tiles/'; +const bubbleAppKey = 'AuftgJsO0Xs8Ts4M1xZUQJQXJNsvmh3IV8DkNieCiy3tCwCUMq76-WpkrBtNAuEm'; +const pannellumViewerCSS = 'pannellum-streetside/pannellum.css'; +const pannellumViewerJS = 'pannellum-streetside/pannellum.js'; +const maxResults = 2000; +const tileZoom = 16.5; +const tiler = utilTiler().zoomExtent([tileZoom, tileZoom]).skipNullIsland(true); +const dispatch = d3_dispatch('loadedBubbles', 'viewerChanged'); +const minHfov = 10; // zoom in degrees: 20, 10, 5 +const maxHfov = 90; // zoom out degrees +const defaultHfov = 45; -var bubbleApi = 'https://dev.virtualearth.net/mapcontrol/HumanScaleServices/GetBubbles.ashx?'; -var streetsideImagesApi = 'https://t.ssl.ak.tiles.virtualearth.net/tiles/'; -var bubbleAppKey = 'AuftgJsO0Xs8Ts4M1xZUQJQXJNsvmh3IV8DkNieCiy3tCwCUMq76-WpkrBtNAuEm'; -var pannellumViewerCSS = 'pannellum-streetside/pannellum.css'; -var pannellumViewerJS = 'pannellum-streetside/pannellum.js'; -var maxResults = 2000; -var tileZoom = 16.5; -var tiler = utilTiler().zoomExtent([tileZoom, tileZoom]).skipNullIsland(true); -var dispatch = d3_dispatch('loadedBubbles', 'viewerChanged'); -var minHfov = 10; // zoom in degrees: 20, 10, 5 -var maxHfov = 90; // zoom out degrees -var defaultHfov = 45; - -var _hires = false; -var _resolution = 512; // higher numbers are slower - 512, 1024, 2048, 4096 -var _currScene = 0; -var _ssCache; -var _pannellumViewer; -var _sceneOptions; -var _dataUrlArray = []; +let _hires = false; +let _resolution = 512; // higher numbers are slower - 512, 1024, 2048, 4096 +let _currScene = 0; +let _ssCache; +let _pannellumViewer; +let _sceneOptions; +let _dataUrlArray = []; /** * abortRequest(). */ function abortRequest(i) { - i.abort(); + i.abort(); } @@ -61,12 +54,12 @@ function abortRequest(i) { * localeTimeStamp(). */ function localeTimestamp(s) { - if (!s) return null; - var detected = utilDetect(); - var options = { day: 'numeric', month: 'short', year: 'numeric' }; - var d = new Date(s); - if (isNaN(d.getTime())) return null; - return d.toLocaleString(detected.locale, options); + if (!s) return null; + const detected = utilDetect(); + const options = { day: 'numeric', month: 'short', year: 'numeric' }; + const d = new Date(s); + if (isNaN(d.getTime())) return null; + return d.toLocaleString(detected.locale, options); } @@ -74,906 +67,873 @@ function localeTimestamp(s) { * loadTiles() wraps the process of generating tiles and then fetching image points for each tile. */ function loadTiles(which, url, projection, margin) { - var tiles = tiler.margin(margin).getTiles(projection); + const tiles = tiler.margin(margin).getTiles(projection); - // abort inflight requests that are no longer needed - var cache = _ssCache[which]; - Object.keys(cache.inflight).forEach(function(k) { - var wanted = tiles.find(function(tile) { return k.indexOf(tile.id + ',') === 0; }); - if (!wanted) { - abortRequest(cache.inflight[k]); - delete cache.inflight[k]; - } - }); + // abort inflight requests that are no longer needed + const cache = _ssCache[which]; + Object.keys(cache.inflight).forEach(k => { + const wanted = tiles.find(tile => k.indexOf(tile.id + ',') === 0); + if (!wanted) { + abortRequest(cache.inflight[k]); + delete cache.inflight[k]; + } + }); - tiles.forEach(function (tile) { - loadNextTilePage(which, url, tile); - }); + tiles.forEach(tile => loadNextTilePage(which, url, tile)); } + /** * loadNextTilePage() load data for the next tile page in line. */ function loadNextTilePage(which, url, tile) { - var cache = _ssCache[which]; - var nextPage = cache.nextPage[tile.id] || 0; - var id = tile.id + ',' + String(nextPage); - if (cache.loaded[id] || cache.inflight[id]) return; + const cache = _ssCache[which]; + const nextPage = cache.nextPage[tile.id] || 0; + const id = tile.id + ',' + String(nextPage); + if (cache.loaded[id] || cache.inflight[id]) return; - cache.inflight[id] = getBubbles(url, tile, function(bubbles) { - cache.loaded[id] = true; - delete cache.inflight[id]; - if (!bubbles) return; + cache.inflight[id] = getBubbles(url, tile, (bubbles) => { + cache.loaded[id] = true; + delete cache.inflight[id]; + if (!bubbles) return; - // [].shift() removes the first element, some statistics info, not a bubble point - bubbles.shift(); + // [].shift() removes the first element, some statistics info, not a bubble point + bubbles.shift(); - var features = bubbles.map(function(bubble) { - if (cache.points[bubble.id]) return null; // skip duplicates + const features = bubbles.map(bubble => { + if (cache.points[bubble.id]) return null; // skip duplicates - var loc = [bubble.lo, bubble.la]; - var d = { - loc: loc, - key: bubble.id, - ca: bubble.he, - captured_at: bubble.cd, - captured_by: 'microsoft', - // nbn: bubble.nbn, - // pbn: bubble.pbn, - // ad: bubble.ad, - // rn: bubble.rn, - pr: bubble.pr, // previous - ne: bubble.ne, // next - pano: true, - sequenceKey: null - }; + const loc = [bubble.lo, bubble.la]; + const d = { + loc: loc, + key: bubble.id, + ca: bubble.he, + captured_at: bubble.cd, + captured_by: 'microsoft', + // nbn: bubble.nbn, + // pbn: bubble.pbn, + // ad: bubble.ad, + // rn: bubble.rn, + pr: bubble.pr, // previous + ne: bubble.ne, // next + pano: true, + sequenceKey: null + }; - cache.points[bubble.id] = d; + cache.points[bubble.id] = d; - // a sequence starts here - if (bubble.pr === undefined) { - cache.leaders.push(bubble.id); - } + // a sequence starts here + if (bubble.pr === undefined) { + cache.leaders.push(bubble.id); + } - return { - minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d - }; + return { + minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d + }; - }).filter(Boolean); + }).filter(Boolean); - cache.rtree.load(features); + cache.rtree.load(features); - connectSequences(); + connectSequences(); - if (which === 'bubbles') { - dispatch.call('loadedBubbles'); - } - }); + if (which === 'bubbles') { + dispatch.call('loadedBubbles'); + } + }); } // call this sometimes to connect the bubbles into sequences function connectSequences() { - var cache = _ssCache.bubbles; - var keepLeaders = []; + let cache = _ssCache.bubbles; + let keepLeaders = []; - for (var i = 0; i < cache.leaders.length; i++) { - var bubble = cache.points[cache.leaders[i]]; - var seen = {}; + for (let i = 0; i < cache.leaders.length; i++) { + let bubble = cache.points[cache.leaders[i]]; + let seen = {}; - // try to make a sequence.. use the key of the leader bubble. - var sequence = { key: bubble.key, bubbles: [] }; - var complete = false; + // try to make a sequence.. use the key of the leader bubble. + let sequence = { key: bubble.key, bubbles: [] }; + let complete = false; - do { - sequence.bubbles.push(bubble); - seen[bubble.key] = true; + do { + sequence.bubbles.push(bubble); + seen[bubble.key] = true; - if (bubble.ne === undefined) { - complete = true; - } else { - bubble = cache.points[bubble.ne]; // advance to next - } - } while (bubble && !seen[bubble.key] && !complete); + if (bubble.ne === undefined) { + complete = true; + } else { + bubble = cache.points[bubble.ne]; // advance to next + } + } while (bubble && !seen[bubble.key] && !complete); - if (complete) { - _ssCache.sequences[sequence.key] = sequence; + if (complete) { + _ssCache.sequences[sequence.key] = sequence; - // assign bubbles to the sequence - for (var j = 0; j < sequence.bubbles.length; j++) { - sequence.bubbles[j].sequenceKey = sequence.key; - } + // assign bubbles to the sequence + for (let j = 0; j < sequence.bubbles.length; j++) { + sequence.bubbles[j].sequenceKey = sequence.key; + } - // create a GeoJSON LineString - sequence.geojson = { - type: 'LineString', - properties: { key: sequence.key }, - coordinates: sequence.bubbles.map(function (d) { return d.loc; }) - }; + // create a GeoJSON LineString + sequence.geojson = { + type: 'LineString', + properties: { key: sequence.key }, + coordinates: sequence.bubbles.map(d => d.loc) + }; - } else { - keepLeaders.push(cache.leaders[i]); - } + } else { + keepLeaders.push(cache.leaders[i]); } + } - // couldn't complete these, save for later - cache.leaders = keepLeaders; + // couldn't complete these, save for later + cache.leaders = keepLeaders; } + /** * getBubbles() handles the request to the server for a tile extent of 'bubbles' (streetside image locations). */ function getBubbles(url, tile, callback) { - var rect = tile.extent.rectangle(); - var urlForRequest = url + utilQsString({ - n: rect[3], - s: rect[1], - e: rect[2], - w: rect[0], - c: maxResults, - appkey: bubbleAppKey, - jsCallback: '{callback}' - }); + let rect = tile.extent.rectangle(); + let urlForRequest = url + utilQsString({ + n: rect[3], + s: rect[1], + e: rect[2], + w: rect[0], + c: maxResults, + appkey: bubbleAppKey, + jsCallback: '{callback}' + }); - return jsonpRequest(urlForRequest, function (data) { - if (!data || data.error) { - callback(null); - } else { - callback(data); - } - }); + return jsonpRequest(urlForRequest, (data) => { + if (!data || data.error) { + callback(null); + } else { + callback(data); + } + }); } // partition viewport into higher zoom tiles function partitionViewport(projection) { - var z = geoScaleToZoom(projection.scale()); - var z2 = (Math.ceil(z * 2) / 2) + 2.5; // round to next 0.5 and add 2.5 - var tiler = utilTiler().zoomExtent([z2, z2]); + let z = geoScaleToZoom(projection.scale()); + let z2 = (Math.ceil(z * 2) / 2) + 2.5; // round to next 0.5 and add 2.5 + let tiler = utilTiler().zoomExtent([z2, z2]); - return tiler.getTiles(projection) - .map(function(tile) { return tile.extent; }); + return tiler.getTiles(projection) + .map(tile => tile.extent); } // no more than `limit` results per partition. function searchLimited(limit, projection, rtree) { - limit = limit || 5; + limit = limit || 5; - return partitionViewport(projection) - .reduce(function(result, extent) { - var found = rtree.search(extent.bbox()) - .slice(0, limit) - .map(function(d) { return d.data; }); + return partitionViewport(projection) + .reduce((result, extent) => { + let found = rtree.search(extent.bbox()) + .slice(0, limit) + .map(d => d.data); - return (found.length ? result.concat(found) : result); - }, []); + return (found.length ? result.concat(found) : result); + }, []); } /** - * getImage() + * loadImage() */ -function getImage(imgInfo) { - var response = Q.defer(); - var img = new Image(); - - img.onload = function() { - var canvas = document.getElementById('canvas' + imgInfo.face); - var ctx = canvas.getContext('2d'); - ctx.drawImage(img, imgInfo.x, imgInfo.y); - response.resolve({imgInfo:imgInfo, status: 'ok'}); +function loadImage(imgInfo) { + return new Promise(resolve => { + let img = new Image(); + img.onload = () => { + let canvas = document.getElementById('canvas' + imgInfo.face); + let ctx = canvas.getContext('2d'); + ctx.drawImage(img, imgInfo.x, imgInfo.y); + resolve({ imgInfo: imgInfo, status: 'ok' }); }; - img.onerror = function() { - response.resolve({data: imgInfo, status: 'error'}); + img.onerror = () => { + resolve({ data: imgInfo, status: 'error' }); }; img.setAttribute('crossorigin', ''); img.src = imgInfo.url; - - return response.promise; + }); } /** * loadCanvas() */ -function loadCanvas(imgInfoGroup) { - var response = Q.defer(); - var getImagePromises = imgInfoGroup.map(function(imgInfo) { - return getImage(imgInfo); +function loadCanvas(imageGroup) { + return Promise.all(imageGroup.map(loadImage)) + .then((data) => { + let canvas = document.getElementById('canvas' + data[0].imgInfo.face); + const which = { '01': 0, '02': 1, '03': 2, '10': 3, '11': 4, '12': 5 }; + let face = data[0].imgInfo.face; + _dataUrlArray[which[face]] = canvas.toDataURL('image/jpeg', 1.0); + return { status: 'loadCanvas for face ' + data[0].imgInfo.face + 'ok'}; }); - - Q.all(getImagePromises).then(function(data) { - var canvas = document.getElementById('canvas' + data[0].imgInfo.face); - switch (data[0].imgInfo.face) { - case '01': - _dataUrlArray[0] = canvas.toDataURL('image/jpeg', 1.0); - break; - case '02': - _dataUrlArray[1] = canvas.toDataURL('image/jpeg', 1.0); - break; - case '03': - _dataUrlArray[2] = canvas.toDataURL('image/jpeg', 1.0); - break; - case '10': - _dataUrlArray[3] = canvas.toDataURL('image/jpeg', 1.0); - break; - case '11': - _dataUrlArray[4] = canvas.toDataURL('image/jpeg', 1.0); - break; - case '12': - _dataUrlArray[5] = canvas.toDataURL('image/jpeg', 1.0); - break; - } - response.resolve({status:'loadCanvas for face ' + data[0].imgInfo.face + 'ok'}); - }); - - return response.promise; -} - - -function setupCanvas(selection, reset) { - if (reset) { - selection.selectAll('#divForCanvasWork') - .remove(); - } - - // Add the Streetside working canvases. These are used for 'stitching', or combining, - // multiple images for each of the six faces, before passing to the Pannellum control as DataUrls - selection.selectAll('#divForCanvasWork') - .data([0]) - .enter() - .append('div') - .attr('id', 'divForCanvasWork') - .attr('display', 'none') - .selectAll('canvas') - .data(['canvas01', 'canvas02', 'canvas03', 'canvas10', 'canvas11', 'canvas12']) - .enter() - .append('canvas') - .attr('id', function(d) { return d; }) - .attr('width', _resolution) - .attr('height', _resolution); } /** - * processFaces() + * loadFaces() */ -function processFaces(imgFaceInfoGroups) { - var response = Q.defer(); - var loadCanvasPromises = imgFaceInfoGroups.map(function(faceImgGroup) { - return loadCanvas(faceImgGroup); - }); - - Q.all(loadCanvasPromises).then(function() { - response.resolve({status: 'processFaces done'}); - }); - - return response.promise; +function loadFaces(faceGroup) { + return Promise.all(faceGroup.map(loadCanvas)) + .then(() => { return { status: 'loadFaces done' }; }); } +function setupCanvas(selection, reset) { + if (reset) { + selection.selectAll('#divForCanvasWork') + .remove(); + } + + // Add the Streetside working canvases. These are used for 'stitching', or combining, + // multiple images for each of the six faces, before passing to the Pannellum control as DataUrls + selection.selectAll('#divForCanvasWork') + .data([0]) + .enter() + .append('div') + .attr('id', 'divForCanvasWork') + .attr('display', 'none') + .selectAll('canvas') + .data(['canvas01', 'canvas02', 'canvas03', 'canvas10', 'canvas11', 'canvas12']) + .enter() + .append('canvas') + .attr('id', d => d) + .attr('width', _resolution) + .attr('height', _resolution); +} + function qkToXY(qk) { - var x = 0; - var y = 0; - var scale = 256; - for (var i = qk.length; i > 0; i--) { - var key = qk[i-1]; - x += (+(key === '1' || key === '3')) * scale; - y += (+(key === '2' || key === '3')) * scale; - scale *= 2; - } - return [x, y]; + let x = 0; + let y = 0; + let scale = 256; + for (let i = qk.length; i > 0; i--) { + const key = qk[i-1]; + x += (+(key === '1' || key === '3')) * scale; + y += (+(key === '2' || key === '3')) * scale; + scale *= 2; + } + return [x, y]; } function getQuadKeys() { - var dim = _resolution / 256; - var quadKeys; + let dim = _resolution / 256; + let quadKeys; - if (dim === 16) { - quadKeys = [ - '0000','0001','0010','0011','0100','0101','0110','0111', '1000','1001','1010','1011','1100','1101','1110','1111', - '0002','0003','0012','0013','0102','0103','0112','0113', '1002','1003','1012','1013','1102','1103','1112','1113', - '0020','0021','0030','0031','0120','0121','0130','0131', '1020','1021','1030','1031','1120','1121','1130','1131', - '0022','0023','0032','0033','0122','0123','0132','0133', '1022','1023','1032','1033','1122','1123','1132','1133', - '0200','0201','0210','0211','0300','0301','0310','0311', '1200','1201','1210','1211','1300','1301','1310','1311', - '0202','0203','0212','0213','0302','0303','0312','0313', '1202','1203','1212','1213','1302','1303','1312','1313', - '0220','0221','0230','0231','0320','0321','0330','0331', '1220','1221','1230','1231','1320','1321','1330','1331', - '0222','0223','0232','0233','0322','0323','0332','0333', '1222','1223','1232','1233','1322','1323','1332','1333', + if (dim === 16) { + quadKeys = [ + '0000','0001','0010','0011','0100','0101','0110','0111', '1000','1001','1010','1011','1100','1101','1110','1111', + '0002','0003','0012','0013','0102','0103','0112','0113', '1002','1003','1012','1013','1102','1103','1112','1113', + '0020','0021','0030','0031','0120','0121','0130','0131', '1020','1021','1030','1031','1120','1121','1130','1131', + '0022','0023','0032','0033','0122','0123','0132','0133', '1022','1023','1032','1033','1122','1123','1132','1133', + '0200','0201','0210','0211','0300','0301','0310','0311', '1200','1201','1210','1211','1300','1301','1310','1311', + '0202','0203','0212','0213','0302','0303','0312','0313', '1202','1203','1212','1213','1302','1303','1312','1313', + '0220','0221','0230','0231','0320','0321','0330','0331', '1220','1221','1230','1231','1320','1321','1330','1331', + '0222','0223','0232','0233','0322','0323','0332','0333', '1222','1223','1232','1233','1322','1323','1332','1333', - '2000','2001','2010','2011','2100','2101','2110','2111', '3000','3001','3010','3011','3100','3101','3110','3111', - '2002','2003','2012','2013','2102','2103','2112','2113', '3002','3003','3012','3013','3102','3103','3112','3113', - '2020','2021','2030','2031','2120','2121','2130','2131', '3020','3021','3030','3031','3120','3121','3130','3131', - '2022','2023','2032','2033','2122','2123','2132','2133', '3022','3023','3032','3033','3122','3123','3132','3133', - '2200','2201','2210','2211','2300','2301','2310','2311', '3200','3201','3210','3211','3300','3301','3310','3311', - '2202','2203','2212','2213','2302','2303','2312','2313', '3202','3203','3212','3213','3302','3303','3312','3313', - '2220','2221','2230','2231','2320','2321','2330','2331', '3220','3221','3230','3231','3320','3321','3330','3331', - '2222','2223','2232','2233','2322','2323','2332','2333', '3222','3223','3232','3233','3322','3323','3332','3333' - ]; + '2000','2001','2010','2011','2100','2101','2110','2111', '3000','3001','3010','3011','3100','3101','3110','3111', + '2002','2003','2012','2013','2102','2103','2112','2113', '3002','3003','3012','3013','3102','3103','3112','3113', + '2020','2021','2030','2031','2120','2121','2130','2131', '3020','3021','3030','3031','3120','3121','3130','3131', + '2022','2023','2032','2033','2122','2123','2132','2133', '3022','3023','3032','3033','3122','3123','3132','3133', + '2200','2201','2210','2211','2300','2301','2310','2311', '3200','3201','3210','3211','3300','3301','3310','3311', + '2202','2203','2212','2213','2302','2303','2312','2313', '3202','3203','3212','3213','3302','3303','3312','3313', + '2220','2221','2230','2231','2320','2321','2330','2331', '3220','3221','3230','3231','3320','3321','3330','3331', + '2222','2223','2232','2233','2322','2323','2332','2333', '3222','3223','3232','3233','3322','3323','3332','3333' + ]; - } else if (dim === 8) { - quadKeys = [ - '000','001','010','011', '100','101','110','111', - '002','003','012','013', '102','103','112','113', - '020','021','030','031', '120','121','130','131', - '022','023','032','033', '122','123','132','133', + } else if (dim === 8) { + quadKeys = [ + '000','001','010','011', '100','101','110','111', + '002','003','012','013', '102','103','112','113', + '020','021','030','031', '120','121','130','131', + '022','023','032','033', '122','123','132','133', - '200','201','210','211', '300','301','310','311', - '202','203','212','213', '302','303','312','313', - '220','221','230','231', '320','321','330','331', - '222','223','232','233', '322','323','332','333' - ]; + '200','201','210','211', '300','301','310','311', + '202','203','212','213', '302','303','312','313', + '220','221','230','231', '320','321','330','331', + '222','223','232','233', '322','323','332','333' + ]; - } else if (dim === 4) { - quadKeys = [ - '00','01', '10','11', - '02','03', '12','13', + } else if (dim === 4) { + quadKeys = [ + '00','01', '10','11', + '02','03', '12','13', - '20','21', '30','31', - '22','23', '32','33' - ]; + '20','21', '30','31', + '22','23', '32','33' + ]; - } else { // dim === 2 - quadKeys = [ - '0', '1', - '2', '3' - ]; - } + } else { // dim === 2 + quadKeys = [ + '0', '1', + '2', '3' + ]; + } - return quadKeys; + return quadKeys; } export default { - /** - * init() initialize streetside. - */ - init: function () { - if (!_ssCache) { - this.reset(); - } - - this.event = utilRebind(this, dispatch, 'on'); - }, - - /** - * reset() reset the cache. - */ - reset: function () { - if (_ssCache) { - Object.values(_ssCache.bubbles.inflight).forEach(abortRequest); - } - - _ssCache = { - bubbles: { inflight: {}, loaded: {}, nextPage: {}, rtree: new RBush(), points: {}, leaders: [] }, - sequences: {} - }; - }, - - /** - * bubbles() - */ - bubbles: function (projection) { - var limit = 5; - return searchLimited(limit, projection, _ssCache.bubbles.rtree); - }, - - - sequences: function(projection) { - var viewport = projection.clipExtent(); - 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 seen = {}; - var results = []; - - // all sequences for bubbles in viewport - _ssCache.bubbles.rtree.search(bbox) - .forEach(function(d) { - var key = d.data.sequenceKey; - if (key && !seen[key]) { - seen[key] = true; - results.push(_ssCache.sequences[key].geojson); - } - }); - - return results; - }, - - - /** - * loadBubbles() - */ - loadBubbles: function (projection, margin) { - // by default: request 2 nearby tiles so we can connect sequences. - if (margin === undefined) margin = 2; - - loadTiles('bubbles', bubbleApi, projection, margin); - }, - - - viewer: function() { - return _pannellumViewer; - }, - - - initViewer: function () { - if (!window.pannellum) return; - if (_pannellumViewer) return; - - var sceneID = ++_currScene + ''; - var options = { - 'default': { firstScene: sceneID }, - scenes: {} - }; - options.scenes[sceneID] = _sceneOptions; - - _pannellumViewer = window.pannellum.viewer('viewer-streetside', options); - - _pannellumViewer - .on('mousedown', function() { - d3_select(window).on('mousemove.pannellum', function() { - dispatch.call('viewerChanged'); - }); - }) - .on('mouseup', function() { - d3_select(window).on('mousemove.pannellum', null); - // continue dispatching events for a few seconds, in case viewer has inertia. - var t = d3_timer(function(elapsed) { - dispatch.call('viewerChanged'); - if (elapsed > 2000) { - t.stop(); - } - }); - }); - }, - - - /** - * 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]); - - // inject ms-wrapper into the photoviewer div - // (used by all to house each custom photo viewer) - var wrapEnter = wrap.enter() - .append('div') - .attr('id', 'ms') - .attr('class', 'photo-wrapper ms-wrapper') - .classed('hide', true); - - // inject div to support streetside viewer (pannellum) and attribution line - wrapEnter - .append('div') - .attr('id', 'viewer-streetside') - .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) - .call(setupCanvas, true); - - // load streetside pannellum viewer css - d3_select('head').selectAll('#streetside-viewercss') - .data([0]) - .enter() - .append('link') - .attr('id', 'streetside-viewercss') - .attr('rel', 'stylesheet') - .attr('href', context.asset(pannellumViewerCSS)); - - // load streetside pannellum viewer js - d3_select('head').selectAll('#streetside-viewerjs') - .data([0]) - .enter() - .append('script') - .attr('id', 'streetside-viewerjs') - .attr('src', context.asset(pannellumViewerJS)); - - - // Register viewer resize handler - context.ui().photoviewer.on('resize.streetside', function() { - if (_pannellumViewer) { - _pannellumViewer.resize(); - } - }); - - - 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(); - } - }); - }; - } - }, - - /** - * showViewer() - */ - showViewer: function (yaw) { - if (!_sceneOptions) return; - - if (yaw !== undefined) { - _sceneOptions.yaw = yaw; - } - - if (!_pannellumViewer) { - this.initViewer(); - } else { - // make a new scene - var sceneID = ++_currScene + ''; - _pannellumViewer - .addScene(sceneID, _sceneOptions) - .loadScene(sceneID); - - // remove previous scene - if (_currScene > 2) { - sceneID = (_currScene - 1) + ''; - _pannellumViewer - .removeScene(sceneID); - } - } - - var wrap = d3_select('#photoviewer') - .classed('hide', false); - - var isHidden = wrap.selectAll('.photo-wrapper.ms-wrapper.hide').size(); - - if (isHidden) { - wrap - .selectAll('.photo-wrapper:not(.ms-wrapper)') - .classed('hide', true); - - wrap - .selectAll('.photo-wrapper.ms-wrapper') - .classed('hide', false); - } - - return this; - }, - - /** - * hideViewer() - */ - hideViewer: function () { - var viewer = d3_select('#photoviewer'); - if (!viewer.empty()) viewer.datum(null); - - viewer - .classed('hide', true) - .selectAll('.photo-wrapper') - .classed('hide', true); - - d3_selectAll('.viewfield-group, .sequence, .icon-sign') - .classed('currentView', false); - - return this.setStyles(null, true); - }, - - /** - * selectImage(). - */ - selectImage: function (d) { - var response = Q.defer(); - var that = this; - - var viewer = d3_select('#photoviewer'); - if (!viewer.empty()) viewer.datum(d); - - this.setStyles(null, true); - - var wrap = d3_select('#photoviewer .ms-wrapper'); - var attribution = wrap.selectAll('.photo-attribution').html(''); - - wrap.selectAll('.pnlm-load-box') // display "loading.." - .style('display', 'block'); - - if (!d) { - response.resolve({status: 'ok'}); - return response.promise; - } - - var line1 = attribution - .append('div') - .attr('class', 'attribution-row'); - - // Add hires checkbox - var label = line1 - .append('label') - .attr('class', 'streetside-hires'); - - label - .append('input') - .attr('type', 'checkbox') - .attr('id', 'streetside-hires-input') - .property('checked', _hires) - .on('click', function() { - d3_event.stopPropagation(); - - _hires = !_hires; - _resolution = _hires ? 1024 : 512; - wrap.call(setupCanvas, true); - - var viewstate = { - yaw: _pannellumViewer.getYaw(), - pitch: _pannellumViewer.getPitch(), - hfov: _pannellumViewer.getHfov() - }; - - that.selectImage(d) - .then(function(r) { - if (r.status === 'ok') { - _sceneOptions = Object.assign(_sceneOptions, viewstate); - that.showViewer(); - } - }); - }); - - label - .append('span') - .text(t('streetside.hires')); - - - var captureInfo = line1 - .append('div') - .attr('class', 'attribution-capture-info'); - - // Add capture date - if (d.captured_by) { - var yyyy = (new Date()).getFullYear(); - - captureInfo - .append('a') - .attr('class', 'captured_by') - .attr('target', '_blank') - .attr('href', 'https://www.microsoft.com/en-us/maps/streetside') - .text('©' + yyyy + ' Microsoft'); - - captureInfo - .append('span') - .text('|'); - } - - if (d.captured_at) { - captureInfo - .append('span') - .attr('class', 'captured_at') - .text(localeTimestamp(d.captured_at)); - } - - // Add image links - var line2 = attribution - .append('div') - .attr('class', 'attribution-row'); - - line2 - .append('a') - .attr('class', 'image-view-link') - .attr('target', '_blank') - .attr('href', 'https://www.bing.com/maps?cp=' + d.loc[1] + '~' + d.loc[0] + - '&lvl=17&dir=' + d.ca + '&style=x&v=2&sV=1') - .text(t('streetside.view_on_bing')); - - line2 - .append('a') - .attr('class', 'image-report-link') - .attr('target', '_blank') - .attr('href', 'https://www.bing.com/maps/privacyreport/streetsideprivacyreport?bubbleid=' + encodeURIComponent(d.key) + - '&focus=photo&lat=' + d.loc[1] + '&lng=' + d.loc[0] + '&z=17') - .text(t('streetside.report')); - - - var bubbleIdQuadKey = d.key.toString(4); - var paddingNeeded = 16 - bubbleIdQuadKey.length; - for (var i = 0; i < paddingNeeded; i++) { - bubbleIdQuadKey = '0' + bubbleIdQuadKey; - } - var imgUrlPrefix = streetsideImagesApi + 'hs' + bubbleIdQuadKey; - var imgUrlSuffix = '.jpg?g=6338&n=z'; - - // Cubemap face code order matters here: front=01, right=02, back=03, left=10, up=11, down=12 - var faceKeys = ['01','02','03','10','11','12']; - - // Map images to cube faces - var quadKeys = getQuadKeys(); - var faces = faceKeys.map(function(faceKey) { - return quadKeys.map(function(quadKey) { - var xy = qkToXY(quadKey); - return { - face: faceKey, - url: imgUrlPrefix + faceKey + quadKey + imgUrlSuffix, - x: xy[0], - y: xy[1] - }; - }); - }); - - processFaces(faces).then(function() { - _sceneOptions = { - showFullscreenCtrl: false, - autoLoad: true, - compass: true, - northOffset: d.ca, - yaw: 0, - minHfov: minHfov, - maxHfov: maxHfov, - hfov: defaultHfov, - type: 'cubemap', - cubeMap: [ - _dataUrlArray[0], - _dataUrlArray[1], - _dataUrlArray[2], - _dataUrlArray[3], - _dataUrlArray[4], - _dataUrlArray[5] - ] - }; - response.resolve({status: 'ok'}); - }); - - return response.promise; - }, - - - getSequenceKeyForBubble: function(d) { - return d && d.sequenceKey; - }, - - - // Updates the currently highlighted sequence and selected bubble. - // Reset is only necessary when interacting with the viewport because - // this implicitly changes the currently selected bubble/sequence - setStyles: function (hovered, reset) { - if (reset) { // reset all layers - d3_selectAll('.viewfield-group') - .classed('highlighted', false) - .classed('hovered', false) - .classed('currentView', false); - - d3_selectAll('.sequence') - .classed('highlighted', false) - .classed('currentView', false); - } - - var hoveredBubbleKey = hovered && hovered.key; - var hoveredSequenceKey = this.getSequenceKeyForBubble(hovered); - var hoveredSequence = hoveredSequenceKey && _ssCache.sequences[hoveredSequenceKey]; - var hoveredBubbleKeys = (hoveredSequence && hoveredSequence.bubbles.map(function (d) { return d.key; })) || []; - - var viewer = d3_select('#photoviewer'); - var selected = viewer.empty() ? undefined : viewer.datum(); - var selectedBubbleKey = selected && selected.key; - var selectedSequenceKey = this.getSequenceKeyForBubble(selected); - var selectedSequence = selectedSequenceKey && _ssCache.sequences[selectedSequenceKey]; - var selectedBubbleKeys = (selectedSequence && selectedSequence.bubbles.map(function (d) { return d.key; })) || []; - - // highlight sibling viewfields on either the selected or the hovered sequences - var highlightedBubbleKeys = utilArrayUnion(hoveredBubbleKeys, selectedBubbleKeys); - - d3_selectAll('.layer-streetside-images .viewfield-group') - .classed('highlighted', function (d) { return highlightedBubbleKeys.indexOf(d.key) !== -1; }) - .classed('hovered', function (d) { return d.key === hoveredBubbleKey; }) - .classed('currentView', function (d) { return d.key === selectedBubbleKey; }); - - d3_selectAll('.layer-streetside-images .sequence') - .classed('highlighted', function (d) { return d.properties.key === hoveredSequenceKey; }) - .classed('currentView', function (d) { return d.properties.key === selectedSequenceKey; }); - - // update viewfields if needed - d3_selectAll('.viewfield-group .viewfield') - .attr('d', viewfieldPath); - - function viewfieldPath() { - var d = this.parentNode.__data__; - if (d.pano && d.key !== selectedBubbleKey) { - return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0'; - } else { - return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z'; - } - } - - return this; - }, - - /** - * cache(). - */ - cache: function () { - return _ssCache; + /** + * init() initialize streetside. + */ + init: function() { + if (!_ssCache) { + this.reset(); } + + this.event = utilRebind(this, dispatch, 'on'); + }, + + /** + * reset() reset the cache. + */ + reset: function() { + if (_ssCache) { + Object.values(_ssCache.bubbles.inflight).forEach(abortRequest); + } + + _ssCache = { + bubbles: { inflight: {}, loaded: {}, nextPage: {}, rtree: new RBush(), points: {}, leaders: [] }, + sequences: {} + }; + }, + + /** + * bubbles() + */ + bubbles: function(projection) { + const limit = 5; + return searchLimited(limit, projection, _ssCache.bubbles.rtree); + }, + + + sequences: function(projection) { + const viewport = projection.clipExtent(); + const min = [viewport[0][0], viewport[1][1]]; + const max = [viewport[1][0], viewport[0][1]]; + const bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox(); + let seen = {}; + let results = []; + + // all sequences for bubbles in viewport + _ssCache.bubbles.rtree.search(bbox) + .forEach(d => { + const key = d.data.sequenceKey; + if (key && !seen[key]) { + seen[key] = true; + results.push(_ssCache.sequences[key].geojson); + } + }); + + return results; + }, + + + /** + * loadBubbles() + */ + loadBubbles: function(projection, margin) { + // by default: request 2 nearby tiles so we can connect sequences. + if (margin === undefined) margin = 2; + + loadTiles('bubbles', bubbleApi, projection, margin); + }, + + + viewer: function() { + return _pannellumViewer; + }, + + + initViewer: function () { + if (!window.pannellum) return; + if (_pannellumViewer) return; + + const sceneID = ++_currScene + ''; + const options = { + 'default': { firstScene: sceneID }, + scenes: {} + }; + options.scenes[sceneID] = _sceneOptions; + + _pannellumViewer = window.pannellum.viewer('viewer-streetside', options); + + _pannellumViewer + .on('mousedown', () => { + d3_select(window) + .on('mousemove.pannellum', () => { dispatch.call('viewerChanged'); }); + }) + .on('mouseup', () => { + d3_select(window) + .on('mousemove.pannellum', null); + + // continue dispatching events for a few seconds, in case viewer has inertia. + let t = d3_timer(elapsed => { + dispatch.call('viewerChanged'); + if (elapsed > 2000) { + t.stop(); + } + }); + }); + }, + + + /** + * loadViewer() create the streeside viewer. + */ + loadViewer: function(context) { + let that = this; + + // create ms-wrapper, a photo wrapper class + let wrap = d3_select('#photoviewer').selectAll('.ms-wrapper') + .data([0]); + + // inject ms-wrapper into the photoviewer div + // (used by all to house each custom photo viewer) + let wrapEnter = wrap.enter() + .append('div') + .attr('id', 'ms') + .attr('class', 'photo-wrapper ms-wrapper') + .classed('hide', true); + + // inject div to support streetside viewer (pannellum) and attribution line + wrapEnter + .append('div') + .attr('id', 'viewer-streetside') + .append('div') + .attr('class', 'photo-attribution fillD'); + + let 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) + .call(setupCanvas, true); + + // load streetside pannellum viewer css + d3_select('head').selectAll('#streetside-viewercss') + .data([0]) + .enter() + .append('link') + .attr('id', 'streetside-viewercss') + .attr('rel', 'stylesheet') + .attr('href', context.asset(pannellumViewerCSS)); + + // load streetside pannellum viewer js + d3_select('head').selectAll('#streetside-viewerjs') + .data([0]) + .enter() + .append('script') + .attr('id', 'streetside-viewerjs') + .attr('src', context.asset(pannellumViewerJS)); + + + // Register viewer resize handler + context.ui().photoviewer.on('resize.streetside', () => { + if (_pannellumViewer) { + _pannellumViewer.resize(); + } + }); + + + function step(stepBy) { + return () => { + let viewer = d3_select('#photoviewer'); + let selected = viewer.empty() ? undefined : viewer.datum(); + if (!selected) return; + + let nextID = (stepBy === 1 ? selected.ne : selected.pr); + let yaw = _pannellumViewer.getYaw(); + let ca = selected.ca + yaw; + let origin = selected.loc; + + // construct a search trapezoid pointing out from current bubble + const meters = 35; + let p1 = [ + origin[0] + geoMetersToLon(meters / 5, origin[1]), + origin[1] + ]; + let p2 = [ + origin[0] + geoMetersToLon(meters / 2, origin[1]), + origin[1] + geoMetersToLat(meters) + ]; + let p3 = [ + origin[0] - geoMetersToLon(meters / 2, origin[1]), + origin[1] + geoMetersToLat(meters) + ]; + let p4 = [ + origin[0] - geoMetersToLon(meters / 5, origin[1]), + origin[1] + ]; + + let poly = [p1, p2, p3, p4, p1]; + + // rotate it to face forward/backward + let angle = (stepBy === 1 ? ca : ca + 180) * (Math.PI / 180); + poly = geoRotate(poly, -angle, origin); + + let extent = poly.reduce((extent, point) => { + return extent.extend(geoExtent(point)); + }, geoExtent()); + + // find nearest other bubble in the search polygon + let minDist = Infinity; + _ssCache.bubbles.rtree.search(extent.bbox()) + .forEach(d => { + if (d.data.key === selected.key) return; + if (!geoPointInPolygon(d.data.loc, poly)) return; + + let dist = geoVecLength(d.data.loc, selected.loc); + let theta = selected.ca - d.data.ca; + let 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; + } + }); + + let nextBubble = nextID && _ssCache.bubbles.points[nextID]; + if (!nextBubble) return; + + context.map().centerEase(nextBubble.loc); + + that.selectImage(nextBubble) + .then(response => { + if (response.status === 'ok') { + _sceneOptions.yaw = yaw; + that.showViewer(); + } + }); + }; + } + }, + + + /** + * showViewer() + */ + showViewer: function(yaw) { + if (!_sceneOptions) return; + + if (yaw !== undefined) { + _sceneOptions.yaw = yaw; + } + + if (!_pannellumViewer) { + this.initViewer(); + } else { + // make a new scene + let sceneID = ++_currScene + ''; + _pannellumViewer + .addScene(sceneID, _sceneOptions) + .loadScene(sceneID); + + // remove previous scene + if (_currScene > 2) { + sceneID = (_currScene - 1) + ''; + _pannellumViewer + .removeScene(sceneID); + } + } + + let wrap = d3_select('#photoviewer') + .classed('hide', false); + + let isHidden = wrap.selectAll('.photo-wrapper.ms-wrapper.hide').size(); + + if (isHidden) { + wrap + .selectAll('.photo-wrapper:not(.ms-wrapper)') + .classed('hide', true); + + wrap + .selectAll('.photo-wrapper.ms-wrapper') + .classed('hide', false); + } + + return this; + }, + + + /** + * hideViewer() + */ + hideViewer: function () { + let viewer = d3_select('#photoviewer'); + if (!viewer.empty()) viewer.datum(null); + + viewer + .classed('hide', true) + .selectAll('.photo-wrapper') + .classed('hide', true); + + d3_selectAll('.viewfield-group, .sequence, .icon-sign') + .classed('currentView', false); + + return this.setStyles(null, true); + }, + + + /** + * selectImage(). + */ + selectImage: function (d) { + let that = this; + let viewer = d3_select('#photoviewer'); + if (!viewer.empty()) viewer.datum(d); + + this.setStyles(null, true); + + let wrap = d3_select('#photoviewer .ms-wrapper'); + let attribution = wrap.selectAll('.photo-attribution').html(''); + + wrap.selectAll('.pnlm-load-box') // display "loading.." + .style('display', 'block'); + + if (!d) { + return Promise.resolve({ status: 'ok' }); + } + + let line1 = attribution + .append('div') + .attr('class', 'attribution-row'); + + // Add hires checkbox + let label = line1 + .append('label') + .attr('class', 'streetside-hires'); + + label + .append('input') + .attr('type', 'checkbox') + .attr('id', 'streetside-hires-input') + .property('checked', _hires) + .on('click', () => { + d3_event.stopPropagation(); + + _hires = !_hires; + _resolution = _hires ? 1024 : 512; + wrap.call(setupCanvas, true); + + let viewstate = { + yaw: _pannellumViewer.getYaw(), + pitch: _pannellumViewer.getPitch(), + hfov: _pannellumViewer.getHfov() + }; + + that.selectImage(d) + .then(response => { + if (response.status === 'ok') { + _sceneOptions = Object.assign(_sceneOptions, viewstate); + that.showViewer(); + } + }); + }); + + label + .append('span') + .text(t('streetside.hires')); + + + let captureInfo = line1 + .append('div') + .attr('class', 'attribution-capture-info'); + + // Add capture date + if (d.captured_by) { + const yyyy = (new Date()).getFullYear(); + + captureInfo + .append('a') + .attr('class', 'captured_by') + .attr('target', '_blank') + .attr('href', 'https://www.microsoft.com/en-us/maps/streetside') + .text('©' + yyyy + ' Microsoft'); + + captureInfo + .append('span') + .text('|'); + } + + if (d.captured_at) { + captureInfo + .append('span') + .attr('class', 'captured_at') + .text(localeTimestamp(d.captured_at)); + } + + // Add image links + let line2 = attribution + .append('div') + .attr('class', 'attribution-row'); + + line2 + .append('a') + .attr('class', 'image-view-link') + .attr('target', '_blank') + .attr('href', 'https://www.bing.com/maps?cp=' + d.loc[1] + '~' + d.loc[0] + + '&lvl=17&dir=' + d.ca + '&style=x&v=2&sV=1') + .text(t('streetside.view_on_bing')); + + line2 + .append('a') + .attr('class', 'image-report-link') + .attr('target', '_blank') + .attr('href', 'https://www.bing.com/maps/privacyreport/streetsideprivacyreport?bubbleid=' + + encodeURIComponent(d.key) + '&focus=photo&lat=' + d.loc[1] + '&lng=' + d.loc[0] + '&z=17') + .text(t('streetside.report')); + + + let bubbleIdQuadKey = d.key.toString(4); + const paddingNeeded = 16 - bubbleIdQuadKey.length; + for (let i = 0; i < paddingNeeded; i++) { + bubbleIdQuadKey = '0' + bubbleIdQuadKey; + } + const imgUrlPrefix = streetsideImagesApi + 'hs' + bubbleIdQuadKey; + const imgUrlSuffix = '.jpg?g=6338&n=z'; + + // Cubemap face code order matters here: front=01, right=02, back=03, left=10, up=11, down=12 + const faceKeys = ['01','02','03','10','11','12']; + + // Map images to cube faces + let quadKeys = getQuadKeys(); + let faces = faceKeys.map((faceKey) => { + return quadKeys.map((quadKey) =>{ + const xy = qkToXY(quadKey); + return { + face: faceKey, + url: imgUrlPrefix + faceKey + quadKey + imgUrlSuffix, + x: xy[0], + y: xy[1] + }; + }); + }); + + return loadFaces(faces) + .then(() => { + _sceneOptions = { + showFullscreenCtrl: false, + autoLoad: true, + compass: true, + northOffset: d.ca, + yaw: 0, + minHfov: minHfov, + maxHfov: maxHfov, + hfov: defaultHfov, + type: 'cubemap', + cubeMap: [ + _dataUrlArray[0], + _dataUrlArray[1], + _dataUrlArray[2], + _dataUrlArray[3], + _dataUrlArray[4], + _dataUrlArray[5] + ] + }; + return { status: 'ok' }; + }); + }, + + + getSequenceKeyForBubble: function(d) { + return d && d.sequenceKey; + }, + + + // Updates the currently highlighted sequence and selected bubble. + // Reset is only necessary when interacting with the viewport because + // this implicitly changes the currently selected bubble/sequence + setStyles: function (hovered, reset) { + if (reset) { // reset all layers + d3_selectAll('.viewfield-group') + .classed('highlighted', false) + .classed('hovered', false) + .classed('currentView', false); + + d3_selectAll('.sequence') + .classed('highlighted', false) + .classed('currentView', false); + } + + let hoveredBubbleKey = hovered && hovered.key; + let hoveredSequenceKey = this.getSequenceKeyForBubble(hovered); + let hoveredSequence = hoveredSequenceKey && _ssCache.sequences[hoveredSequenceKey]; + let hoveredBubbleKeys = (hoveredSequence && hoveredSequence.bubbles.map(d => d.key)) || []; + + let viewer = d3_select('#photoviewer'); + let selected = viewer.empty() ? undefined : viewer.datum(); + let selectedBubbleKey = selected && selected.key; + let selectedSequenceKey = this.getSequenceKeyForBubble(selected); + let selectedSequence = selectedSequenceKey && _ssCache.sequences[selectedSequenceKey]; + let selectedBubbleKeys = (selectedSequence && selectedSequence.bubbles.map(d => d.key)) || []; + + // highlight sibling viewfields on either the selected or the hovered sequences + let highlightedBubbleKeys = utilArrayUnion(hoveredBubbleKeys, selectedBubbleKeys); + + d3_selectAll('.layer-streetside-images .viewfield-group') + .classed('highlighted', d => highlightedBubbleKeys.indexOf(d.key) !== -1) + .classed('hovered', d => d.key === hoveredBubbleKey) + .classed('currentView', d => d.key === selectedBubbleKey); + + d3_selectAll('.layer-streetside-images .sequence') + .classed('highlighted', d => d.properties.key === hoveredSequenceKey) + .classed('currentView', d => d.properties.key === selectedSequenceKey); + + // update viewfields if needed + d3_selectAll('.viewfield-group .viewfield') + .attr('d', viewfieldPath); + + function viewfieldPath() { + let d = this.parentNode.__data__; + if (d.pano && d.key !== selectedBubbleKey) { + return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0'; + } else { + return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z'; + } + } + + return this; + }, + + + /** + * cache(). + */ + cache: function () { + return _ssCache; + } }; diff --git a/modules/svg/streetside.js b/modules/svg/streetside.js index 32c11c938..60bc3de21 100644 --- a/modules/svg/streetside.js +++ b/modules/svg/streetside.js @@ -99,8 +99,8 @@ export function svgStreetside(projection, context, dispatch) { service .selectImage(d) - .then(function(r) { - if (r.status === 'ok'){ + .then(response => { + if (response.status === 'ok'){ service.showViewer(_viewerYaw); } }); diff --git a/package.json b/package.json index 5cdc41aa8..332b55f46 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,6 @@ "node-diff3": "1.0.0", "osm-auth": "1.0.2", "pannellum": "2.4.1", - "q": "1.5.1", "rbush": "3.0.1", "string.fromcodepoint": "0.2.1", "which-polygon": "2.2.0",