mirror of
https://github.com/FoggedLens/iD.git
synced 2026-05-15 13:38:26 +02:00
switch to Bing Maps API for Bing Streetside layer, fixes #10074
replaces the use of undocumented APIs for Microsoft's street level imagery service and gets rid of hardcoded values, e.g. for the `g` ("generation") parameter
see https://learn.microsoft.com/en-us/bingmaps/rest-services/imagery/get-imagery-metadata#get-streetside-metadata-centered-at-a-point for API docs
see also #10100
This commit is contained in:
@@ -46,6 +46,7 @@ _Breaking developer changes, which may affect downstream projects or sites that
|
||||
* Add support for coordinates in `<degree> <minutes>[ <seconds>]` format to search bar ([#10066], thanks [@NaVis0mple])
|
||||
#### :scissors: Operations
|
||||
#### :camera: Street-Level
|
||||
* Fetch Microsoft Bing Streetlevel imagery layer via the Bing Maps API (from the previously used undocumented internal API endpoints), fixing reliability issues with the service's integration ([#10074])
|
||||
#### :white_check_mark: Validation
|
||||
#### :bug: Bugfixes
|
||||
* Show turn restriction editor also when there is only one possible "to" way, as there might exist restrictions with that way as _via_ ([#9983])
|
||||
@@ -70,6 +71,7 @@ _Breaking developer changes, which may affect downstream projects or sites that
|
||||
[#9992]: https://github.com/openstreetmap/iD/issues/9992
|
||||
[#10062]: https://github.com/openstreetmap/iD/pull/10062
|
||||
[#10066]: https://github.com/openstreetmap/iD/pull/10066
|
||||
[#10074]: https://github.com/openstreetmap/iD/issues/10074
|
||||
[id-tagging-schema#1076]: https://github.com/openstreetmap/id-tagging-schema/pull/1076
|
||||
[@ramith-kulal]: https://github.com/ramith-kulal
|
||||
[@mangerlahn]: https://github.com/mangerlahn
|
||||
|
||||
@@ -132,6 +132,16 @@ Object.assign(geoExtent.prototype, {
|
||||
|
||||
toParam: function() {
|
||||
return this.rectangle().join(',');
|
||||
},
|
||||
|
||||
split: function() {
|
||||
const center = this.center();
|
||||
return [
|
||||
geoExtent(this[0], center),
|
||||
geoExtent([center[0], this[0][1]], [this[1][0], center[1]]),
|
||||
geoExtent(center, this[1]),
|
||||
geoExtent([this[0][0], center[1]], [center[0], this[1][1]])
|
||||
];
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
+54
-112
@@ -7,22 +7,20 @@ import {
|
||||
|
||||
import RBush from 'rbush';
|
||||
import { t, localizer } from '../core/localizer';
|
||||
import { jsonpRequest } from '../util/jsonp_request';
|
||||
|
||||
import {
|
||||
geoExtent, geoMetersToLat, geoMetersToLon, geoPointInPolygon,
|
||||
geoRotate, geoScaleToZoom, geoVecLength
|
||||
} from '../geo';
|
||||
|
||||
import { utilArrayUnion, utilQsString, utilRebind, utilStringQs, utilTiler, utilUniqueDomId } from '../util';
|
||||
import { utilAesDecrypt, utilArrayUnion, utilQsString, utilRebind, utilStringQs, utilTiler, utilUniqueDomId } from '../util';
|
||||
|
||||
|
||||
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 streetsideApi = 'http://dev.virtualearth.net/REST/v1/Imagery/MetaData/Streetside?mapArea={bbox}&key={key}&count={count}';
|
||||
const maxResults = 500;
|
||||
const bubbleAppKey = utilAesDecrypt('5c875730b09c6b422433e807e1ff060b6536c791dbfffcffc4c6b18a1bdba1f14593d151adb50e19e1be1ab19aef813bf135d0f103475e5c724dec94389e45d0');
|
||||
const pannellumViewerCSS = 'pannellum/pannellum.css';
|
||||
const pannellumViewerJS = 'pannellum/pannellum.js';
|
||||
const maxResults = 2000;
|
||||
const tileZoom = 16.5;
|
||||
const tiler = utilTiler().zoomExtent([tileZoom, tileZoom]).skipNullIsland(true);
|
||||
const dispatch = d3_dispatch('loadedImages', 'viewerChanged');
|
||||
@@ -98,40 +96,39 @@ function loadNextTilePage(which, url, tile) {
|
||||
const id = tile.id + ',' + String(nextPage);
|
||||
if (cache.loaded[id] || cache.inflight[id]) return;
|
||||
|
||||
cache.inflight[id] = getBubbles(url, tile, (bubbles) => {
|
||||
cache.inflight[id] = getBubbles(url, tile, response => {
|
||||
cache.loaded[id] = true;
|
||||
delete cache.inflight[id];
|
||||
if (!bubbles) return;
|
||||
if (!response) return;
|
||||
|
||||
// [].shift() removes the first element, some statistics info, not a bubble point
|
||||
bubbles.shift();
|
||||
if (response.resourceSets[0].resources.length === maxResults) {
|
||||
// there are more bubbles than the response can fit: re-fetch using tile split into 4
|
||||
const split = tile.extent.split();
|
||||
loadNextTilePage(which, url, { id: tile.id + ',a', extent: split[0] });
|
||||
loadNextTilePage(which, url, { id: tile.id + ',b', extent: split[1] });
|
||||
loadNextTilePage(which, url, { id: tile.id + ',c', extent: split[2] });
|
||||
loadNextTilePage(which, url, { id: tile.id + ',d', extent: split[3] });
|
||||
}
|
||||
|
||||
const features = bubbles.map(bubble => {
|
||||
if (cache.points[bubble.id]) return null; // skip duplicates
|
||||
const features = response.resourceSets[0].resources.map(bubble => {
|
||||
const bubbleId = bubble.imageUrl;
|
||||
if (cache.points[bubbleId]) return null; // skip duplicates
|
||||
|
||||
const loc = [bubble.lo, bubble.la];
|
||||
const loc = [bubble.lon, bubble.lat];
|
||||
const d = {
|
||||
loc: loc,
|
||||
key: bubble.id,
|
||||
key: bubbleId,
|
||||
imageUrl: bubble.imageUrl.replace('{subdomain}',
|
||||
bubble.imageUrlSubdomains[0]
|
||||
),
|
||||
ca: bubble.he,
|
||||
captured_at: bubble.cd,
|
||||
captured_at: bubble.vintageEnd,
|
||||
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);
|
||||
}
|
||||
cache.points[bubbleId] = d;
|
||||
|
||||
return {
|
||||
minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d
|
||||
@@ -141,8 +138,6 @@ function loadNextTilePage(which, url, tile) {
|
||||
|
||||
cache.rtree.load(features);
|
||||
|
||||
connectSequences();
|
||||
|
||||
if (which === 'bubbles') {
|
||||
dispatch.call('loadedImages');
|
||||
}
|
||||
@@ -150,83 +145,37 @@ function loadNextTilePage(which, url, tile) {
|
||||
}
|
||||
|
||||
|
||||
// call this sometimes to connect the bubbles into sequences
|
||||
function connectSequences() {
|
||||
let cache = _ssCache.bubbles;
|
||||
let keepLeaders = [];
|
||||
|
||||
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.
|
||||
let sequence = { key: bubble.key, bubbles: [] };
|
||||
let 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 (let 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(d => 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) {
|
||||
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}'
|
||||
});
|
||||
let urlForRequest = url
|
||||
.replace('{key}', bubbleAppKey)
|
||||
.replace('{bbox}', [rect[1], rect[0], rect[3], rect[2]].join(','))
|
||||
.replace('{count}', maxResults);
|
||||
|
||||
return jsonpRequest(urlForRequest, (data) => {
|
||||
if (!data || data.error) {
|
||||
callback(null);
|
||||
} else {
|
||||
callback(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
const controller = new AbortController();
|
||||
fetch(urlForRequest, { signal: controller.signal })
|
||||
.then(function(response) {
|
||||
if (!response.ok) {
|
||||
throw new Error(response.status + ' ' + response.statusText);
|
||||
}
|
||||
return response.json();
|
||||
}).then(function(result) {
|
||||
if (!result) {
|
||||
callback(null);
|
||||
}
|
||||
return callback(result || []);
|
||||
}).catch(function(err) {
|
||||
if (err.name === 'AbortError') {
|
||||
// ignore aborted requests, e.g. from duplicate requests while zooming/panning the map
|
||||
} else {
|
||||
throw new Error(err);
|
||||
}
|
||||
});
|
||||
return controller;
|
||||
}
|
||||
|
||||
|
||||
// partition viewport into higher zoom tiles
|
||||
@@ -418,7 +367,7 @@ export default {
|
||||
}
|
||||
|
||||
_ssCache = {
|
||||
bubbles: { inflight: {}, loaded: {}, nextPage: {}, rtree: new RBush(), points: {}, leaders: [] },
|
||||
bubbles: { inflight: {}, loaded: {}, nextPage: {}, rtree: new RBush(), points: {} },
|
||||
sequences: {}
|
||||
};
|
||||
},
|
||||
@@ -466,7 +415,7 @@ export default {
|
||||
// by default: request 2 nearby tiles so we can connect sequences.
|
||||
if (margin === undefined) margin = 2;
|
||||
|
||||
loadTiles('bubbles', bubbleApi, projection, margin);
|
||||
loadTiles('bubbles', streetsideApi, projection, margin);
|
||||
},
|
||||
|
||||
|
||||
@@ -845,15 +794,6 @@ export default {
|
||||
encodeURIComponent(d.key) + '&focus=photo&lat=' + d.loc[1] + '&lng=' + d.loc[0] + '&z=17')
|
||||
.call(t.append('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=13515&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'];
|
||||
|
||||
@@ -864,7 +804,9 @@ export default {
|
||||
const xy = qkToXY(quadKey);
|
||||
return {
|
||||
face: faceKey,
|
||||
url: imgUrlPrefix + faceKey + quadKey + imgUrlSuffix,
|
||||
url: d.imageUrl
|
||||
.replace('{faceId}', faceKey)
|
||||
.replace('{tileId}', quadKey),
|
||||
x: xy[0],
|
||||
y: xy[1]
|
||||
};
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import { select as d3_select } from 'd3-selection';
|
||||
|
||||
var jsonpCache = {};
|
||||
window.jsonpCache = jsonpCache;
|
||||
|
||||
export function jsonpRequest(url, callback) {
|
||||
var request = {
|
||||
abort: function() {}
|
||||
};
|
||||
|
||||
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 = d3_select('head')
|
||||
.append('script')
|
||||
.attr('type', 'text/javascript')
|
||||
.attr('src', url.replace(/(\{|%7B)callback(\}|%7D)/, cb));
|
||||
|
||||
return request;
|
||||
}
|
||||
@@ -247,4 +247,15 @@ describe('iD.geoExtent', function () {
|
||||
|
||||
});
|
||||
|
||||
describe('#split', function () {
|
||||
it('splits into four parts', function () {
|
||||
var splits = iD.geoExtent([0, 10], [5, 20]).split();
|
||||
expect(splits).to.have.length(4);
|
||||
expect(splits[0]).to.eql([[0, 10], [2.5, 15]]);
|
||||
expect(splits[1]).to.eql([[2.5, 10], [5, 15]]);
|
||||
expect(splits[2]).to.eql([[2.5, 15], [5, 20]]);
|
||||
expect(splits[3]).to.eql([[0, 15], [2.5, 20]]);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -58,23 +58,16 @@ describe('iD.serviceStreetside', function() {
|
||||
var spy = sinon.spy();
|
||||
streetside.on('loadedImages', spy);
|
||||
|
||||
window.JSONP_DELAY = 0;
|
||||
window.JSONP_FIX = [{
|
||||
elapsed: 0.001
|
||||
}, {
|
||||
id: 1, la: 0, lo: 10.001, al: 0, ro: 0, pi: 0, he: 0, bl: '',
|
||||
cd: '1/1/2018 12:00:00 PM', ml: 3, nbn: [], pbn: [], rn: [],
|
||||
pr: undefined, ne: 2
|
||||
}, {
|
||||
id: 2, la: 0, lo: 10.002, al: 0, ro: 0, pi: 0, he: 0, bl: '',
|
||||
cd: '1/1/2018 12:00:01 PM', ml: 3, nbn: [], pbn: [], rn: [],
|
||||
pr: 1, ne: 3
|
||||
}, {
|
||||
id: 3, la: 0, lo: 10.003, al: 0, ro: 0, pi: 0, he: 0, bl: '',
|
||||
cd: '1/1/2018 12:00:02 PM', ml: 3, nbn: [], pbn: [], rn: [],
|
||||
pr: 2, ne: undefined
|
||||
}
|
||||
];
|
||||
var mockData = {
|
||||
resourceSets: [{
|
||||
resources: []
|
||||
}]
|
||||
};
|
||||
|
||||
fetchMock.mock(/MetaData\/Streetside/, {
|
||||
body: JSON.stringify(mockData),
|
||||
status: 200
|
||||
});
|
||||
|
||||
streetside.loadBubbles(context.projection, 0); // 0 = don't fetch margin tiles
|
||||
|
||||
@@ -93,23 +86,16 @@ describe('iD.serviceStreetside', function() {
|
||||
var spy = sinon.spy();
|
||||
streetside.on('loadedImages', spy);
|
||||
|
||||
window.JSONP_DELAY = 0;
|
||||
window.JSONP_FIX = [{
|
||||
elapsed: 0.001
|
||||
}, {
|
||||
id: 1, la: 0, lo: 0, al: 0, ro: 0, pi: 0, he: 0, bl: '',
|
||||
cd: '1/1/2018 12:00:00 PM', ml: 3, nbn: [], pbn: [], rn: [],
|
||||
pr: undefined, ne: 2
|
||||
}, {
|
||||
id: 2, la: 0, lo: 0, al: 0, ro: 0, pi: 0, he: 0, bl: '',
|
||||
cd: '1/1/2018 12:00:01 PM', ml: 3, nbn: [], pbn: [], rn: [],
|
||||
pr: 1, ne: 3
|
||||
}, {
|
||||
id: 3, la: 0, lo: 0, al: 0, ro: 0, pi: 0, he: 0, bl: '',
|
||||
cd: '1/1/2018 12:00:02 PM', ml: 3, nbn: [], pbn: [], rn: [],
|
||||
pr: 2, ne: undefined
|
||||
}
|
||||
];
|
||||
var mockData = {
|
||||
resourceSets: [{
|
||||
resources: [{}]
|
||||
}]
|
||||
};
|
||||
|
||||
fetchMock.mock(/MetaData\/Streetside/, {
|
||||
body: JSON.stringify(mockData),
|
||||
status: 200
|
||||
});
|
||||
|
||||
streetside.loadBubbles(context.projection, 0); // 0 = don't fetch margin tiles
|
||||
|
||||
|
||||
Reference in New Issue
Block a user