diff --git a/modules/services/streetside.js b/modules/services/streetside.js index 6eac992a8..dbfd10c71 100644 --- a/modules/services/streetside.js +++ b/modules/services/streetside.js @@ -26,10 +26,11 @@ 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 tileZoom = 16.5; +var tileZoom = 15; var dispatch = d3_dispatch('loadedBubbles', 'viewerChanged'); var _currScene = 0; -var _bubbleCache; +var _currSequence = 0; +var _ssCache; var _pannellumViewer; var _sceneOptions; @@ -60,11 +61,7 @@ function nearNullIsland(x, y, z) { function localeTimestamp(s) { if (!s) return null; var detected = utilDetect(); - var options = { - day: 'numeric', month: 'short', year: 'numeric' - //hour: 'numeric', minute: 'numeric', second: 'numeric', - //timeZone: 'UTC' - }; + 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); @@ -112,7 +109,6 @@ function getTiles(projection) { * loadTiles() wraps the process of generating tiles and then fetching image points for each tile. */ function loadTiles(which, url, projection) { - // console.log('loadTiles() for: ', which); var s = projection.scale() * 2 * Math.PI; var currZoom = Math.floor(Math.max(Math.log(s) / Math.log(2) - 8, 0)); @@ -130,11 +126,11 @@ function loadTiles(which, url, projection) { * loadNextTilePage() load data for the next tile page in line. */ function loadNextTilePage(which, currZoom, url, tile) { - // console.log('loadNextTilePage()'); - var cache = _bubbleCache[which]; + 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; + cache.inflight[id] = getBubbles(url, tile, function(bubbles) { cache.loaded[id] = true; delete cache.inflight[id]; @@ -144,8 +140,7 @@ function loadNextTilePage(which, currZoom, url, tile) { bubbles.shift(); var features = bubbles.map(function (bubble) { - var bubbleId = bubble.id; - if (cache.points[bubbleId]) return null; // skip duplicates + if (cache.points[bubble.id]) return null; // skip duplicates var loc = [bubble.lo, bubble.la]; var d = { @@ -154,22 +149,48 @@ function loadNextTilePage(which, currZoom, url, tile) { ca: bubble.he, captured_at: bubble.cd, captured_by: 'microsoft', - nbn: bubble.nbn, - pbn: bubble.pbn, - rn: bubble.rn, - pano: true - }; - var feature = { - geometry: { - coordinates: [bubble.lo, bubble.la], - type: 'Point' - }, - properties: d, - type: 'Feature' + // nbn: bubble.nbn, + // pbn: bubble.pbn, + // ad: bubble.ad, + // rn: bubble.rn, + pr: bubble.pr, // previous + ne: bubble.ne, // next + pano: true, + sequenceID: null }; - cache.points[bubbleId] = feature; - cache.forImageKey[bubbleId] = bubbleId; + // determine (or create) a squence to attach this bubble to + var seqID = findSequenceID(d); + var seq; + if (seqID) { + seq = _ssCache.sequences[seqID]; + } else { + seq = { id: ++_currSequence, min: bubble.id, max: bubble.id, bubbles: [] }; + _ssCache.sequences[seq.id] = seq; + } + d.sequenceID = seq.id; + + // expand the range of the sequence to include this bubble + if (d.pr === undefined || bubble.id < seq.min) { + seq.min = bubble.id; + } + if (d.ne === undefined || bubble.id > seq.max) { + seq.max = bubble.id; + } + seq.bubbles.push(bubble.id); + + // if next or previous bubble is already assigned a different sequence, + // merge the sequences + var prev = d.pr && _ssCache.bubbles.points[d.pr]; + if (prev && prev.sequenceID !== seq.id) { + mergeSequences(prev.sequenceID, seq.id); + } + var next = d.ne && _ssCache.bubbles.points[d.ne]; + if (next && next.sequenceID !== seq.id) { + mergeSequences(next.sequenceID, seq.id); + } + + cache.points[bubble.id] = d; return { minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d @@ -184,11 +205,49 @@ function loadNextTilePage(which, currZoom, url, tile) { }); } + +function findSequenceID(bubble) { + var prev = bubble.pr && _ssCache.bubbles.points[bubble.pr]; + if (prev && prev.sequenceID) { + return prev.sequenceID; + } + var next = bubble.ne && _ssCache.bubbles.points[bubble.ne]; + if (next && next.sequenceID) { + return next.sequenceID; + } + + var seqIDs = Object.keys(_ssCache.sequences); + for (var i = 0; i < seqIDs.length; i++) { + var seq = _ssCache.sequences[seqIDs[i]]; + if ((bubble.id >= seq.min && bubble.id <= seq.max) || + (bubble.pr >= seq.min && bubble.pr <= seq.max) || + (bubble.ne >= seq.min && bubble.ne <= seq.max)) { + return seq.id; + } + } +} + + +function mergeSequences(oldID, newID) { + var oldSeq = _ssCache.sequences[oldID]; + var newSeq = _ssCache.sequences[newID]; + + newSeq.min = Math.min(oldSeq.min, newSeq.min); + newSeq.max = Math.max(oldSeq.max, newSeq.max); + + oldSeq.bubbles.forEach(function (bubbleID) { + _ssCache.bubbles.points[bubbleID].sequenceID = newID; + }); + + newSeq.bubbles = newSeq.bubbles.concat(oldSeq.bubbles); + delete _ssCache.sequences[oldID]; +} + + /** * getBubbles() handles the request to the server for a tile extent of 'bubbles' (streetside image locations). */ function getBubbles(url, tile, callback) { - //console.log('services - streetside - getBubbles()'); var rect = tile.extent.rectangle(); var urlForRequest = url + utilQsString({ n: rect[3], @@ -198,6 +257,7 @@ function getBubbles(url, tile, callback) { appkey: bubbleAppKey, jsCallback: '{callback}' }); + jsonpRequest(urlForRequest, function (data) { if (!data || data.error) { callback(null); @@ -217,6 +277,7 @@ function partitionViewport(psize, projection) { var cols = d3_range(0, dimensions[0], psize); var rows = d3_range(0, dimensions[1], psize); var partitions = []; + rows.forEach(function (y) { cols.forEach(function (x) { var min = [x, y + psize]; @@ -228,17 +289,16 @@ function partitionViewport(psize, projection) { return partitions; } + /** * searchLimited(). */ function searchLimited(psize, limit, projection, rtree) { - //console.log('services - streetside - searchLimited()'); limit = limit || 3; var partitions = partitionViewport(psize, projection); var results; - // console.time('previous'); results = _flatten(_map(partitions, function (extent) { return rtree.search(extent.bbox()) .slice(0, limit) @@ -254,7 +314,7 @@ export default { * init() initialize streetside. */ init: function () { - if (!_bubbleCache) { + if (!_ssCache) { this.reset(); } @@ -265,7 +325,7 @@ export default { * reset() reset the cache. */ reset: function () { - var cache = _bubbleCache; + var cache = _ssCache; if (cache) { if (cache.bubbles && cache.bubbles.inflight) { @@ -273,8 +333,9 @@ export default { } } - _bubbleCache = { - bubbles: { inflight: {}, loaded: {}, nextPage: {}, nextURL: {}, rtree: rbush(), forImageKey: {}, points: {} } + _ssCache = { + bubbles: { inflight: {}, loaded: {}, nextPage: {}, rtree: rbush(), points: {} }, + sequences: {} }; }, @@ -283,9 +344,51 @@ export default { */ bubbles: function (projection) { var psize = 32, limit = 3; - return searchLimited(psize, limit, projection, _bubbleCache.bubbles.rtree); + return searchLimited(psize, 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 sequenceIDs = {}; + + // all sequences for bubbles in viewport + _ssCache.bubbles.rtree.search(bbox) + .forEach(function(d) { + if (d.data.sequenceID) { + sequenceIDs[d.data.sequenceID] = true; + } + }); + + // make linestrings from those sequences + var lineStrings = []; + Object.keys(sequenceIDs) + .forEach(function(sequenceID) { + var seq = _ssCache.sequences[sequenceID]; + if (seq) { + var coords = []; + for (var i = seq.min; i <= seq.max; i++) { + var point = _ssCache.bubbles.points[i]; + if (point) { + coords.push(point.loc); + } + } + + lineStrings.push({ + type: 'LineString', + properties: { key: sequenceID }, + coordinates: coords + }); + } + }); + + return lineStrings; + }, + + /** * loadBubbles() */ @@ -507,6 +610,12 @@ export default { return this; }, + + getSequenceKeyForBubble: function(d) { + return d && d.sequenceID; + }, + + /** * setStyles(). */ @@ -521,20 +630,30 @@ export default { .classed('highlighted', false) .classed('selected', false); } + var hoveredBubbleKey = hovered && hovered.key; + var hoveredSequenceKey = this.getSequenceKeyForBubble(hovered); + var hoveredSequence = hoveredSequenceKey && _ssCache.sequences[hoveredSequenceKey]; + var hoveredBubbleKeys = (hoveredSequence && hoveredSequence.bubbles) || []; + var viewer = d3_select('#photoviewer'); var selected = viewer.empty() ? undefined : viewer.datum(); var selectedBubbleKey = selected && selected.key; - var highlightedBubbleKeys = _union([hoveredBubbleKey], [selectedBubbleKey]); + var selectedSequenceKey = this.getSequenceKeyForBubble(selected); + var selectedSequence = selectedSequenceKey && _ssCache.sequences[selectedSequenceKey]; + var selectedBubbleKeys = (selectedSequence && selectedSequence.bubbles) || []; + + // highlight sibling viewfields on either the selected or the hovered sequences + var highlightedBubbleKeys = _union(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('selected', function (d) { return d.key === selectedBubbleKey; }); - // d3_selectAll('.layer-streetside-images .sequence') - // .classed('highlighted', function (d) { return d.properties.key === hoveredSequenceKey; }) - // .classed('selected', function (d) { return d.properties.key === selectedSequenceKey; }); + d3_selectAll('.layer-streetside-images .sequence') + .classed('highlighted', function (d) { return d.properties.key === hoveredSequenceKey; }) + .classed('selected', function (d) { return d.properties.key === selectedSequenceKey; }); return this; }, @@ -543,6 +662,6 @@ export default { * cache(). */ cache: function () { - return _bubbleCache; + return _ssCache; } }; diff --git a/modules/svg/streetside.js b/modules/svg/streetside.js index 9e00da9fb..c9d26fb05 100644 --- a/modules/svg/streetside.js +++ b/modules/svg/streetside.js @@ -6,7 +6,7 @@ import { services } from '../services'; export function svgStreetside(projection, context, dispatch) { var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000); - var minZoom = 16; + var minZoom = 14; var minMarkerZoom = 16; var minViewfieldZoom = 19; var layer = d3_select(null); @@ -167,8 +167,24 @@ export function svgStreetside(projection, context, dispatch) { var showViewfields = (z >= minViewfieldZoom); var service = getService(); - // gets the features from service cache + var sequences = (service ? service.sequences(projection) : []); var bubbles = (service && showMarkers ? service.bubbles(projection) : []); + + var traces = layer.selectAll('.sequences').selectAll('.sequence') + .data(sequences, function(d) { return d.properties.key; }); + + // exit + traces.exit() + .remove(); + + // enter/update + traces = traces.enter() + .append('path') + .attr('class', 'sequence') + .merge(traces) + .attr('d', svgPath(projection).geojson); + + var groups = layer.selectAll('.markers').selectAll('.viewfield-group') .data(bubbles, function(d) { return d.key; }); @@ -243,6 +259,10 @@ export function svgStreetside(projection, context, dispatch) { .attr('class', 'layer-streetside-images') .style('display', enabled ? 'block' : 'none'); + layerEnter + .append('g') + .attr('class', 'sequences'); + layerEnter .append('g') .attr('class', 'markers');