- }
- }
-
- if (result.wiki) break;
- }
-
- callback(null, result); // Helper method to get wiki info if a given language exists
-
- function getWikiInfo(wiki, langCode, tKey) {
- if (wiki && wiki[langCode]) {
- return {
- title: wiki[langCode],
- text: tKey,
- url: 'https://wiki.openstreetmap.org/wiki/' + wiki[langCode]
- };
- }
- }
- });
- },
- addLocale: function addLocale(langCode, qid) {
- // Makes it easier to unit test
- _localeIDs[langCode] = qid;
- },
- apibase: function apibase(val) {
- if (!arguments.length) return _apibase;
- _apibase = val;
- return this;
- }
- };
-
- var jsonpCache = {};
- window.jsonpCache = jsonpCache;
- function jsonpRequest(url, callback) {
- var request = {
- abort: function abort() {}
- };
-
- if (window.JSONP_FIX) {
- if (window.JSONP_DELAY === 0) {
- callback(window.JSONP_FIX);
- } else {
- var t = window.setTimeout(function () {
- callback(window.JSONP_FIX);
- }, window.JSONP_DELAY || 0);
-
- request.abort = function () {
- window.clearTimeout(t);
- };
- }
-
- return request;
- }
-
- function rand() {
- var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
- var c = '';
- var i = -1;
-
- while (++i < 15) {
- c += chars.charAt(Math.floor(Math.random() * 52));
- }
-
- return c;
- }
-
- function create(url) {
- var e = url.match(/callback=(\w+)/);
- var c = e ? e[1] : rand();
-
- jsonpCache[c] = function (data) {
- if (jsonpCache[c]) {
- callback(data);
- }
-
- finalize();
- };
-
- function finalize() {
- delete jsonpCache[c];
- script.remove();
- }
-
- request.abort = finalize;
- return 'jsonpCache.' + c;
- }
-
- var cb = create(url);
- var script = select('head').append('script').attr('type', 'text/javascript').attr('src', url.replace(/(\{|%7B)callback(\}|%7D)/, cb));
- return request;
- }
-
- 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$2 = 2000;
- var tileZoom$2 = 16.5;
- var tiler$6 = utilTiler().zoomExtent([tileZoom$2, tileZoom$2]).skipNullIsland(true);
- var dispatch$7 = dispatch('loadedImages', '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 = {
- showFullscreenCtrl: false,
- autoLoad: true,
- compass: true,
- yaw: 0,
- minHfov: minHfov,
- maxHfov: maxHfov,
- hfov: defaultHfov,
- type: 'cubemap',
- cubeMap: []
- };
-
- var _loadViewerPromise$2;
- /**
- * abortRequest().
- */
-
-
- function abortRequest$6(i) {
- i.abort();
- }
- /**
- * localeTimeStamp().
- */
-
-
- function localeTimestamp(s) {
- if (!s) return null;
- var options = {
- day: 'numeric',
- month: 'short',
- year: 'numeric'
- };
- var d = new Date(s);
- if (isNaN(d.getTime())) return null;
- return d.toLocaleString(_mainLocalizer.localeCode(), options);
- }
- /**
- * loadTiles() wraps the process of generating tiles and then fetching image points for each tile.
- */
-
-
- function loadTiles$2(which, url, projection, margin) {
- var tiles = tiler$6.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$6(cache.inflight[k]);
- delete cache.inflight[k];
- }
- });
- tiles.forEach(function (tile) {
- return loadNextTilePage$2(which, url, tile);
- });
- }
- /**
- * loadNextTilePage() load data for the next tile page in line.
- */
-
-
- function loadNextTilePage$2(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;
- 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$7.call('loadedImages');
- }
- });
- } // 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: {
- captured_at: sequence.bubbles[0] ? sequence.bubbles[0].captured_at : null,
- captured_by: sequence.bubbles[0] ? sequence.bubbles[0].captured_by : null,
- 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$2,
- appkey: bubbleAppKey,
- jsCallback: '{callback}'
- });
- return jsonpRequest(urlForRequest, function (data) {
- if (!data || data.error) {
- callback(null);
- } else {
- callback(data);
- }
- });
- } // partition viewport into higher zoom tiles
-
-
- function partitionViewport$2(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]);
- return tiler.getTiles(projection).map(function (tile) {
- return tile.extent;
- });
- } // no more than `limit` results per partition.
-
-
- function searchLimited$2(limit, projection, rtree) {
- limit = limit || 5;
- return partitionViewport$2(projection).reduce(function (result, extent) {
- var found = rtree.search(extent.bbox()).slice(0, limit).map(function (d) {
- return d.data;
- });
- return found.length ? result.concat(found) : result;
- }, []);
- }
- /**
- * loadImage()
- */
-
-
- function loadImage(imgInfo) {
- return new Promise(function (resolve) {
- var img = new Image();
-
- img.onload = function () {
- var canvas = document.getElementById('ideditor-canvas' + imgInfo.face);
- var ctx = canvas.getContext('2d');
- ctx.drawImage(img, imgInfo.x, imgInfo.y);
- resolve({
- imgInfo: imgInfo,
- status: 'ok'
- });
- };
-
- img.onerror = function () {
- resolve({
- data: imgInfo,
- status: 'error'
- });
- };
-
- img.setAttribute('crossorigin', '');
- img.src = imgInfo.url;
- });
- }
- /**
- * loadCanvas()
- */
-
-
- function loadCanvas(imageGroup) {
- return Promise.all(imageGroup.map(loadImage)).then(function (data) {
- var canvas = document.getElementById('ideditor-canvas' + data[0].imgInfo.face);
- var which = {
- '01': 0,
- '02': 1,
- '03': 2,
- '10': 3,
- '11': 4,
- '12': 5
- };
- var face = data[0].imgInfo.face;
- _sceneOptions.cubeMap[which[face]] = canvas.toDataURL('image/jpeg', 1.0);
- return {
- status: 'loadCanvas for face ' + data[0].imgInfo.face + 'ok'
- };
- });
- }
- /**
- * loadFaces()
- */
-
-
- function loadFaces(faceGroup) {
- return Promise.all(faceGroup.map(loadCanvas)).then(function () {
- return {
- status: 'loadFaces done'
- };
- });
- }
-
- function setupCanvas(selection, reset) {
- if (reset) {
- selection.selectAll('#ideditor-stitcher-canvases').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('#ideditor-stitcher-canvases').data([0]).enter().append('div').attr('id', 'ideditor-stitcher-canvases').attr('display', 'none').selectAll('canvas').data(['canvas01', 'canvas02', 'canvas03', 'canvas10', 'canvas11', 'canvas12']).enter().append('canvas').attr('id', function (d) {
- return 'ideditor-' + 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];
- }
-
- 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;
- }
-
- var serviceStreetside = {
- /**
- * init() initialize streetside.
- */
- init: function init() {
- if (!_ssCache) {
- this.reset();
- }
-
- this.event = utilRebind(this, dispatch$7, 'on');
- },
-
- /**
- * reset() reset the cache.
- */
- reset: function reset() {
- if (_ssCache) {
- Object.values(_ssCache.bubbles.inflight).forEach(abortRequest$6);
- }
-
- _ssCache = {
- bubbles: {
- inflight: {},
- loaded: {},
- nextPage: {},
- rtree: new RBush(),
- points: {},
- leaders: []
- },
- sequences: {}
- };
- },
-
- /**
- * bubbles()
- */
- bubbles: function bubbles(projection) {
- var limit = 5;
- return searchLimited$2(limit, projection, _ssCache.bubbles.rtree);
- },
- cachedImage: function cachedImage(imageKey) {
- return _ssCache.bubbles.points[imageKey];
- },
- sequences: function sequences(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 loadBubbles(projection, margin) {
- // by default: request 2 nearby tiles so we can connect sequences.
- if (margin === undefined) margin = 2;
- loadTiles$2('bubbles', bubbleApi, projection, margin);
- },
- viewer: function viewer() {
- return _pannellumViewer;
- },
- initViewer: function initViewer() {
- if (!window.pannellum) return;
- if (_pannellumViewer) return;
- _currScene += 1;
-
- var sceneID = _currScene.toString();
-
- var options = {
- 'default': {
- firstScene: sceneID
- },
- scenes: {}
- };
- options.scenes[sceneID] = _sceneOptions;
- _pannellumViewer = window.pannellum.viewer('ideditor-viewer-streetside', options);
- },
- ensureViewerLoaded: function ensureViewerLoaded(context) {
- if (_loadViewerPromise$2) return _loadViewerPromise$2; // create ms-wrapper, a photo wrapper class
-
- var wrap = context.container().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('class', 'photo-wrapper ms-wrapper').classed('hide', true);
- var that = this;
- var pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse'; // inject div to support streetside viewer (pannellum) and attribution line
-
- wrapEnter.append('div').attr('id', 'ideditor-viewer-streetside').on(pointerPrefix + 'down.streetside', function () {
- select(window).on(pointerPrefix + 'move.streetside', function () {
- dispatch$7.call('viewerChanged');
- }, true);
- }).on(pointerPrefix + 'up.streetside pointercancel.streetside', function () {
- select(window).on(pointerPrefix + 'move.streetside', null); // continue dispatching events for a few seconds, in case viewer has inertia.
-
- var t = timer(function (elapsed) {
- dispatch$7.call('viewerChanged');
-
- if (elapsed > 2000) {
- t.stop();
- }
- });
- }).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)).html('◄');
- controlsEnter.append('button').on('click.forward', step(1)).html('►'); // create working canvas for stitching together images
-
- wrap = wrap.merge(wrapEnter).call(setupCanvas, true); // Register viewer resize handler
-
- context.ui().photoviewer.on('resize.streetside', function () {
- if (_pannellumViewer) {
- _pannellumViewer.resize();
- }
- });
- _loadViewerPromise$2 = new Promise(function (resolve, reject) {
- var loadedCount = 0;
-
- function loaded() {
- loadedCount += 1; // wait until both files are loaded
-
- if (loadedCount === 2) resolve();
- }
-
- var head = select('head'); // load streetside pannellum viewer css
-
- head.selectAll('#ideditor-streetside-viewercss').data([0]).enter().append('link').attr('id', 'ideditor-streetside-viewercss').attr('rel', 'stylesheet').attr('crossorigin', 'anonymous').attr('href', context.asset(pannellumViewerCSS)).on('load.serviceStreetside', loaded).on('error.serviceStreetside', function () {
- reject();
- }); // load streetside pannellum viewer js
-
- head.selectAll('#ideditor-streetside-viewerjs').data([0]).enter().append('script').attr('id', 'ideditor-streetside-viewerjs').attr('crossorigin', 'anonymous').attr('src', context.asset(pannellumViewerJS)).on('load.serviceStreetside', loaded).on('error.serviceStreetside', function () {
- reject();
- });
- })["catch"](function () {
- _loadViewerPromise$2 = null;
- });
- return _loadViewerPromise$2;
-
- function step(stepBy) {
- return function () {
- var viewer = context.container().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 && that.cachedImage(nextID);
- if (!nextBubble) return;
- context.map().centerEase(nextBubble.loc);
- that.selectImage(context, nextBubble.key).yaw(yaw).showViewer(context);
- };
- }
- },
- yaw: function yaw(_yaw) {
- if (typeof _yaw !== 'number') return _yaw;
- _sceneOptions.yaw = _yaw;
- return this;
- },
-
- /**
- * showViewer()
- */
- showViewer: function showViewer(context) {
- var wrap = context.container().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 hideViewer(context) {
- var viewer = context.container().select('.photoviewer');
- if (!viewer.empty()) viewer.datum(null);
- viewer.classed('hide', true).selectAll('.photo-wrapper').classed('hide', true);
- context.container().selectAll('.viewfield-group, .sequence, .icon-sign').classed('currentView', false);
- this.updateUrlImage(null);
- return this.setStyles(context, null, true);
- },
-
- /**
- * selectImage().
- */
- selectImage: function selectImage(context, key) {
- var that = this;
- var d = this.cachedImage(key);
- var viewer = context.container().select('.photoviewer');
- if (!viewer.empty()) viewer.datum(d);
- this.setStyles(context, null, true);
- var wrap = context.container().select('.photoviewer .ms-wrapper');
- var attribution = wrap.selectAll('.photo-attribution').html('');
- wrap.selectAll('.pnlm-load-box') // display "loading.."
- .style('display', 'block');
- if (!d) return this;
- this.updateUrlImage(key);
- _sceneOptions.northOffset = d.ca;
- var line1 = attribution.append('div').attr('class', 'attribution-row');
- var hiresDomId = utilUniqueDomId('streetside-hires'); // Add hires checkbox
-
- var label = line1.append('label').attr('for', hiresDomId).attr('class', 'streetside-hires');
- label.append('input').attr('type', 'checkbox').attr('id', hiresDomId).property('checked', _hires).on('click', function (d3_event) {
- d3_event.stopPropagation();
- _hires = !_hires;
- _resolution = _hires ? 1024 : 512;
- wrap.call(setupCanvas, true);
- var viewstate = {
- yaw: _pannellumViewer.getYaw(),
- pitch: _pannellumViewer.getPitch(),
- hfov: _pannellumViewer.getHfov()
- };
- _sceneOptions = Object.assign(_sceneOptions, viewstate);
- that.selectImage(context, d.key).showViewer(context);
- });
- label.append('span').html(_t.html('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').html('©' + yyyy + ' Microsoft');
- captureInfo.append('span').html('|');
- }
-
- if (d.captured_at) {
- captureInfo.append('span').attr('class', 'captured_at').html(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').html(_t.html('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').html(_t.html('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]
- };
- });
- });
- loadFaces(faces).then(function () {
- if (!_pannellumViewer) {
- that.initViewer();
- } else {
- // make a new scene
- _currScene += 1;
-
- var sceneID = _currScene.toString();
-
- _pannellumViewer.addScene(sceneID, _sceneOptions).loadScene(sceneID); // remove previous scene
-
-
- if (_currScene > 2) {
- sceneID = (_currScene - 1).toString();
-
- _pannellumViewer.removeScene(sceneID);
- }
- }
- });
- return this;
- },
- getSequenceKeyForBubble: function getSequenceKeyForBubble(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 setStyles(context, hovered, reset) {
- if (reset) {
- // reset all layers
- context.container().selectAll('.viewfield-group').classed('highlighted', false).classed('hovered', false).classed('currentView', false);
- context.container().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 = context.container().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);
- context.container().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;
- });
- context.container().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
-
- context.container().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;
- },
- updateUrlImage: function updateUrlImage(imageKey) {
- if (!window.mocha) {
- var hash = utilStringQs(window.location.hash);
-
- if (imageKey) {
- hash.photo = 'streetside/' + imageKey;
- } else {
- delete hash.photo;
- }
-
- window.location.replace('#' + utilQsString(hash, true));
- }
- },
-
- /**
- * cache().
- */
- cache: function cache() {
- return _ssCache;
- }
- };
-
- var _apibase$1 = 'https://taginfo.openstreetmap.org/api/4/';
- var _inflight$2 = {};
- var _popularKeys = {};
- var _taginfoCache = {};
- var tag_sorts = {
- point: 'count_nodes',
- vertex: 'count_nodes',
- area: 'count_ways',
- line: 'count_ways'
- };
- var tag_sort_members = {
- point: 'count_node_members',
- vertex: 'count_node_members',
- area: 'count_way_members',
- line: 'count_way_members',
- relation: 'count_relation_members'
- };
- var tag_filters = {
- point: 'nodes',
- vertex: 'nodes',
- area: 'ways',
- line: 'ways'
- };
- var tag_members_fractions = {
- point: 'count_node_members_fraction',
- vertex: 'count_node_members_fraction',
- area: 'count_way_members_fraction',
- line: 'count_way_members_fraction',
- relation: 'count_relation_members_fraction'
- };
-
- function sets(params, n, o) {
- if (params.geometry && o[params.geometry]) {
- params[n] = o[params.geometry];
- }
-
- return params;
- }
-
- function setFilter(params) {
- return sets(params, 'filter', tag_filters);
- }
-
- function setSort(params) {
- return sets(params, 'sortname', tag_sorts);
- }
-
- function setSortMembers(params) {
- return sets(params, 'sortname', tag_sort_members);
- }
-
- function clean(params) {
- return utilObjectOmit(params, ['geometry', 'debounce']);
- }
-
- function filterKeys(type) {
- var count_type = type ? 'count_' + type : 'count_all';
- return function (d) {
- return parseFloat(d[count_type]) > 2500 || d.in_wiki;
- };
- }
-
- function filterMultikeys(prefix) {
- return function (d) {
- // d.key begins with prefix, and d.key contains no additional ':'s
- var re = new RegExp('^' + prefix + '(.*)$');
- var matches = d.key.match(re) || [];
- return matches.length === 2 && matches[1].indexOf(':') === -1;
- };
- }
-
- function filterValues(allowUpperCase) {
- return function (d) {
- if (d.value.match(/[;,]/) !== null) return false; // exclude some punctuation
-
- if (!allowUpperCase && d.value.match(/[A-Z*]/) !== null) return false; // exclude uppercase letters
-
- return parseFloat(d.fraction) > 0.0;
- };
- }
-
- function filterRoles(geometry) {
- return function (d) {
- if (d.role === '') return false; // exclude empty role
-
- if (d.role.match(/[A-Z*;,]/) !== null) return false; // exclude uppercase letters and some punctuation
-
- return parseFloat(d[tag_members_fractions[geometry]]) > 0.0;
- };
- }
-
- function valKey(d) {
- return {
- value: d.key,
- title: d.key
- };
- }
-
- function valKeyDescription(d) {
- var obj = {
- value: d.value,
- title: d.description || d.value
- };
-
- if (d.count) {
- obj.count = d.count;
- }
-
- return obj;
- }
-
- function roleKey(d) {
- return {
- value: d.role,
- title: d.role
- };
- } // sort keys with ':' lower than keys without ':'
-
-
- function sortKeys(a, b) {
- return a.key.indexOf(':') === -1 && b.key.indexOf(':') !== -1 ? -1 : a.key.indexOf(':') !== -1 && b.key.indexOf(':') === -1 ? 1 : 0;
- }
-
- var debouncedRequest$1 = debounce(request$1, 300, {
- leading: false
- });
-
- function request$1(url, params, exactMatch, callback, loaded) {
- if (_inflight$2[url]) return;
- if (checkCache(url, params, exactMatch, callback)) return;
- var controller = new AbortController();
- _inflight$2[url] = controller;
- d3_json(url, {
- signal: controller.signal
- }).then(function (result) {
- delete _inflight$2[url];
- if (loaded) loaded(null, result);
- })["catch"](function (err) {
- delete _inflight$2[url];
- if (err.name === 'AbortError') return;
- if (loaded) loaded(err.message);
- });
- }
-
- function checkCache(url, params, exactMatch, callback) {
- var rp = params.rp || 25;
- var testQuery = params.query || '';
- var testUrl = url;
-
- do {
- var hit = _taginfoCache[testUrl]; // exact match, or shorter match yielding fewer than max results (rp)
-
- if (hit && (url === testUrl || hit.length < rp)) {
- callback(null, hit);
- return true;
- } // don't try to shorten the query
-
-
- if (exactMatch || !testQuery.length) return false; // do shorten the query to see if we already have a cached result
- // that has returned fewer than max results (rp)
-
- testQuery = testQuery.slice(0, -1);
- testUrl = url.replace(/&query=(.*?)&/, '&query=' + testQuery + '&');
- } while (testQuery.length >= 0);
-
- return false;
- }
-
- var serviceTaginfo = {
- init: function init() {
- _inflight$2 = {};
- _taginfoCache = {};
- _popularKeys = {
- // manually exclude some keys – #5377, #7485
- postal_code: true,
- full_name: true,
- loc_name: true,
- reg_name: true,
- short_name: true,
- sorting_name: true,
- artist_name: true,
- nat_name: true,
- long_name: true,
- 'bridge:name': true
- }; // Fetch popular keys. We'll exclude these from `values`
- // lookups because they stress taginfo, and they aren't likely
- // to yield meaningful autocomplete results.. see #3955
-
- var params = {
- rp: 100,
- sortname: 'values_all',
- sortorder: 'desc',
- page: 1,
- debounce: false,
- lang: _mainLocalizer.languageCode()
- };
- this.keys(params, function (err, data) {
- if (err) return;
- data.forEach(function (d) {
- if (d.value === 'opening_hours') return; // exception
-
- _popularKeys[d.value] = true;
- });
- });
- },
- reset: function reset() {
- Object.values(_inflight$2).forEach(function (controller) {
- controller.abort();
- });
- _inflight$2 = {};
- },
- keys: function keys(params, callback) {
- var doRequest = params.debounce ? debouncedRequest$1 : request$1;
- params = clean(setSort(params));
- params = Object.assign({
- rp: 10,
- sortname: 'count_all',
- sortorder: 'desc',
- page: 1,
- lang: _mainLocalizer.languageCode()
- }, params);
- var url = _apibase$1 + 'keys/all?' + utilQsString(params);
- doRequest(url, params, false, callback, function (err, d) {
- if (err) {
- callback(err);
- } else {
- var f = filterKeys(params.filter);
- var result = d.data.filter(f).sort(sortKeys).map(valKey);
- _taginfoCache[url] = result;
- callback(null, result);
- }
- });
- },
- multikeys: function multikeys(params, callback) {
- var doRequest = params.debounce ? debouncedRequest$1 : request$1;
- params = clean(setSort(params));
- params = Object.assign({
- rp: 25,
- sortname: 'count_all',
- sortorder: 'desc',
- page: 1,
- lang: _mainLocalizer.languageCode()
- }, params);
- var prefix = params.query;
- var url = _apibase$1 + 'keys/all?' + utilQsString(params);
- doRequest(url, params, true, callback, function (err, d) {
- if (err) {
- callback(err);
- } else {
- var f = filterMultikeys(prefix);
- var result = d.data.filter(f).map(valKey);
- _taginfoCache[url] = result;
- callback(null, result);
- }
- });
- },
- values: function values(params, callback) {
- // Exclude popular keys from values lookups.. see #3955
- var key = params.key;
-
- if (key && _popularKeys[key]) {
- callback(null, []);
- return;
- }
-
- var doRequest = params.debounce ? debouncedRequest$1 : request$1;
- params = clean(setSort(setFilter(params)));
- params = Object.assign({
- rp: 25,
- sortname: 'count_all',
- sortorder: 'desc',
- page: 1,
- lang: _mainLocalizer.languageCode()
- }, params);
- var url = _apibase$1 + 'key/values?' + utilQsString(params);
- doRequest(url, params, false, callback, function (err, d) {
- if (err) {
- callback(err);
- } else {
- // In most cases we prefer taginfo value results with lowercase letters.
- // A few OSM keys expect values to contain uppercase values (see #3377).
- // This is not an exhaustive list (e.g. `name` also has uppercase values)
- // but these are the fields where taginfo value lookup is most useful.
- var re = /network|taxon|genus|species|brand|grape_variety|royal_cypher|listed_status|booth|rating|stars|:output|_hours|_times|_ref|manufacturer|country|target|brewery/;
- var allowUpperCase = re.test(params.key);
- var f = filterValues(allowUpperCase);
- var result = d.data.filter(f).map(valKeyDescription);
- _taginfoCache[url] = result;
- callback(null, result);
- }
- });
- },
- roles: function roles(params, callback) {
- var doRequest = params.debounce ? debouncedRequest$1 : request$1;
- var geometry = params.geometry;
- params = clean(setSortMembers(params));
- params = Object.assign({
- rp: 25,
- sortname: 'count_all_members',
- sortorder: 'desc',
- page: 1,
- lang: _mainLocalizer.languageCode()
- }, params);
- var url = _apibase$1 + 'relation/roles?' + utilQsString(params);
- doRequest(url, params, true, callback, function (err, d) {
- if (err) {
- callback(err);
- } else {
- var f = filterRoles(geometry);
- var result = d.data.filter(f).map(roleKey);
- _taginfoCache[url] = result;
- callback(null, result);