mirror of
https://github.com/FoggedLens/iD.git
synced 2026-03-13 22:56:12 +00:00
re: #5125 There is some cleverness here to search for the best nearby bubble instead of just relying on the next and previous sequence keys.
1023 lines
32 KiB
JavaScript
1023 lines
32 KiB
JavaScript
import _extend from 'lodash-es/extend';
|
|
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 { dispatch as d3_dispatch } from 'd3-dispatch';
|
|
import { range as d3_range } from 'd3-array';
|
|
import { timer as d3_timer } from 'd3-timer';
|
|
|
|
import {
|
|
event as d3_event,
|
|
select as d3_select,
|
|
selectAll as d3_selectAll
|
|
} from 'd3-selection';
|
|
|
|
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,
|
|
geoMetersToLat,
|
|
geoMetersToLon,
|
|
geoPointInPolygon,
|
|
geoRotate,
|
|
geoVecLength
|
|
} from '../geo';
|
|
|
|
import { utilDetect } from '../util/detect';
|
|
import { utilQsString, utilRebind } from '../util';
|
|
|
|
import Q from 'q';
|
|
|
|
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 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 = [];
|
|
|
|
/**
|
|
* abortRequest().
|
|
*/
|
|
function abortRequest(i) {
|
|
i.abort();
|
|
}
|
|
|
|
/**
|
|
* nearNullIsland().
|
|
*/
|
|
function nearNullIsland(x, y, z) {
|
|
if (z >= 7) {
|
|
var center = Math.pow(2, z - 1);
|
|
var width = Math.pow(2, z - 6);
|
|
var min = center - (width / 2);
|
|
var max = center + (width / 2) - 1;
|
|
return x >= min && x <= max && y >= min && y <= max;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
/**
|
|
* getTiles() returns array of d3 geo tiles.
|
|
* Using d3.geo.tiles.js from lib, gets tile extents for each grid tile in a grid created from
|
|
* an area around (and including) the current map view extents.
|
|
*/
|
|
function getTiles(projection, margin) {
|
|
// s is the current map scale
|
|
// z is the 'Level of Detail', or zoom-level, where Level 1 is far from the earth, and Level 23 is close to the ground.
|
|
// ts ('tile size') here is the formula for determining the width/height of the map in pixels, but with a modification.
|
|
// See 'Ground Resolution and Map Scale': //https://msdn.microsoft.com/en-us/library/bb259689.aspx.
|
|
// As used here, by subtracting constant 'tileZoom' from z (the level), you end up with a much smaller value for the tile size (in pixels).
|
|
var s = projection.scale() * 2 * Math.PI;
|
|
var z = Math.max(Math.log(s) / Math.log(2) - 8, 0);
|
|
var ts = 256 * Math.pow(2, z - tileZoom);
|
|
var origin = [
|
|
s / 2 - projection.translate()[0],
|
|
s / 2 - projection.translate()[1]
|
|
];
|
|
|
|
var tiler = d3_geoTile()
|
|
.scaleExtent([tileZoom, tileZoom])
|
|
.scale(s)
|
|
.size(projection.clipExtent()[1])
|
|
.translate(projection.translate())
|
|
.margin(margin || 0); // request nearby tiles so we can connect sequences.
|
|
|
|
return tiler()
|
|
.map(function(tile) {
|
|
var x = tile[0] * ts - origin[0];
|
|
var y = tile[1] * ts - origin[1];
|
|
return {
|
|
id: tile.toString(),
|
|
xyz: tile,
|
|
extent: geoExtent(
|
|
projection.invert([x, y + ts]),
|
|
projection.invert([x + ts, y])
|
|
)
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* loadTiles() wraps the process of generating tiles and then fetching image points for each tile.
|
|
*/
|
|
function loadTiles(which, url, projection, margin) {
|
|
var s = projection.scale() * 2 * Math.PI;
|
|
var currZoom = Math.floor(Math.max(Math.log(s) / Math.log(2) - 8, 0));
|
|
|
|
// breakup the map view into tiles
|
|
var tiles = getTiles(projection, margin).filter(function (t) {
|
|
return !nearNullIsland(t.xyz[0], t.xyz[1], t.xyz[2]);
|
|
});
|
|
|
|
tiles.forEach(function (tile) {
|
|
loadNextTilePage(which, currZoom, url, tile);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* loadNextTilePage() load data for the next tile page in line.
|
|
*/
|
|
function loadNextTilePage(which, currZoom, 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;
|
|
|
|
cache.inflight[id] = getBubbles(url, tile, function(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();
|
|
|
|
var features = bubbles.map(function (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
|
|
};
|
|
|
|
cache.points[bubble.id] = d;
|
|
|
|
// 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
|
|
};
|
|
|
|
}).filter(Boolean);
|
|
|
|
cache.rtree.load(features);
|
|
|
|
connectSequences();
|
|
|
|
if (which === 'bubbles') {
|
|
dispatch.call('loadedBubbles');
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
// call this sometimes to connect the bubbles into sequences
|
|
function connectSequences() {
|
|
var cache = _ssCache.bubbles;
|
|
var keepLeaders = [];
|
|
|
|
for (var i = 0; i < cache.leaders.length; i++) {
|
|
var bubble = cache.points[cache.leaders[i]];
|
|
var seen = {};
|
|
|
|
// try to make a sequence.. use the key of the leader bubble.
|
|
var sequence = { key: bubble.key, bubbles: [] };
|
|
var complete = false;
|
|
|
|
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 (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;
|
|
}
|
|
|
|
// create a GeoJSON LineString
|
|
sequence.geojson = {
|
|
type: 'LineString',
|
|
properties: { key: sequence.key },
|
|
coordinates: sequence.bubbles.map(function (d) { return d.loc; })
|
|
};
|
|
|
|
} else {
|
|
keepLeaders.push(cache.leaders[i]);
|
|
}
|
|
}
|
|
|
|
// 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}'
|
|
});
|
|
|
|
return jsonpRequest(urlForRequest, function (data) {
|
|
if (!data || data.error) {
|
|
callback(null);
|
|
} else {
|
|
callback(data);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* partitionViewport() partition viewport into `psize` x `psize` regions.
|
|
*/
|
|
function partitionViewport(psize, projection) {
|
|
var dimensions = projection.clipExtent()[1];
|
|
psize = psize || 16;
|
|
|
|
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];
|
|
var max = [x + psize, y];
|
|
partitions.push(geoExtent(projection.invert(min), projection.invert(max)));
|
|
});
|
|
});
|
|
|
|
return partitions;
|
|
}
|
|
|
|
|
|
/**
|
|
* searchLimited().
|
|
*/
|
|
function searchLimited(psize, limit, projection, rtree) {
|
|
limit = limit || 3;
|
|
|
|
var partitions = partitionViewport(psize, projection);
|
|
var results;
|
|
|
|
results = _flatten(_map(partitions, function (extent) {
|
|
return rtree.search(extent.bbox())
|
|
.slice(0, limit)
|
|
.map(function (d) { return d.data; });
|
|
}));
|
|
|
|
return results;
|
|
}
|
|
|
|
|
|
/**
|
|
* getImage()
|
|
*/
|
|
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'});
|
|
};
|
|
img.onerror = function() {
|
|
response.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);
|
|
});
|
|
|
|
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()
|
|
*/
|
|
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 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];
|
|
}
|
|
|
|
|
|
function getQuadKeys() {
|
|
var dim = _resolution / 256;
|
|
var 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',
|
|
|
|
'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',
|
|
|
|
'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',
|
|
|
|
'20','21', '30','31',
|
|
'22','23', '32','33'
|
|
];
|
|
|
|
} else { // dim === 2
|
|
quadKeys = [
|
|
'0', '1',
|
|
'2', '3'
|
|
];
|
|
}
|
|
|
|
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 () {
|
|
var cache = _ssCache;
|
|
|
|
if (cache) {
|
|
if (cache.bubbles && cache.bubbles.inflight) {
|
|
_forEach(cache.bubbles.inflight, abortRequest);
|
|
}
|
|
}
|
|
|
|
_ssCache = {
|
|
bubbles: { inflight: {}, loaded: {}, nextPage: {}, rtree: rbush(), points: {}, leaders: [] },
|
|
sequences: {}
|
|
};
|
|
},
|
|
|
|
/**
|
|
* bubbles()
|
|
*/
|
|
bubbles: function (projection) {
|
|
var psize = 32, limit = 3;
|
|
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 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));
|
|
|
|
|
|
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('selected', 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;
|
|
}
|
|
|
|
// Add hires checkbox
|
|
var label = attribution
|
|
.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 = _extend(_sceneOptions, viewstate);
|
|
that.showViewer();
|
|
}
|
|
});
|
|
});
|
|
|
|
label
|
|
.append('span')
|
|
.text(t('streetside.hires'));
|
|
|
|
|
|
// Add capture date
|
|
if (d.captured_by) {
|
|
var yyyy = (new Date()).getFullYear();
|
|
|
|
attribution
|
|
.append('a')
|
|
.attr('class', 'captured_by')
|
|
.attr('target', '_blank')
|
|
.attr('href', 'https://www.microsoft.com/en-us/maps/streetside')
|
|
.text('©' + yyyy + ' Microsoft');
|
|
|
|
attribution
|
|
.append('span')
|
|
.text('|');
|
|
}
|
|
|
|
if (d.captured_at) {
|
|
attribution
|
|
.append('span')
|
|
.attr('class', 'captured_at')
|
|
.text(localeTimestamp(d.captured_at));
|
|
}
|
|
|
|
// Add image link
|
|
attribution
|
|
.append('a')
|
|
.attr('class', 'image_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;
|
|
},
|
|
|
|
|
|
/**
|
|
* setStyles().
|
|
*/
|
|
setStyles: function (hovered, 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 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 = _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; });
|
|
|
|
// 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;
|
|
}
|
|
};
|