From b7df3eda386f379269fec07895cbe40d5589a3d6 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 7 Nov 2018 23:09:11 -0500 Subject: [PATCH] Use new Mapillary API calls for map_features, image_detections (closes #5374) --- modules/services/mapillary.js | 131 +++++++++++++++----------------- test/spec/services/mapillary.js | 8 +- 2 files changed, 66 insertions(+), 73 deletions(-) diff --git a/modules/services/mapillary.js b/modules/services/mapillary.js index 0767f283d..e7afb42d4 100644 --- a/modules/services/mapillary.js +++ b/modules/services/mapillary.js @@ -2,7 +2,6 @@ import _find from 'lodash-es/find'; import _flatten from 'lodash-es/flatten'; import _forEach from 'lodash-es/forEach'; -import _isEmpty from 'lodash-es/isEmpty'; import _map from 'lodash-es/map'; import _some from 'lodash-es/some'; import _union from 'lodash-es/union'; @@ -111,6 +110,9 @@ function loadNextTilePage(which, currZoom, url, tile) { var loc = feature.geometry.coordinates; var d; + // An image (shown as a green dot on the map) is a single street photo with extra + // information such as location, camera angle (CA), camera model, and so on. + // Each image feature is a GeoJSON Point if (which === 'images') { d = { loc: loc, @@ -120,17 +122,44 @@ function loadNextTilePage(which, currZoom, url, tile) { captured_by: feature.properties.username, pano: feature.properties.pano }; + cache.forImageKey[d.key] = d; // cache imageKey -> image + // Mapillary organizes images as sequences. A sequence of images are continuously captured + // by a user at a give time. Sequences are shown on the map as green lines. + // Each sequence feature is a GeoJSON LineString } else if (which === 'sequences') { var sequenceKey = feature.properties.key; cache.lineString[sequenceKey] = feature; // cache sequenceKey -> lineString feature.properties.coordinateProperties.image_keys.forEach(function(imageKey) { cache.forImageKey[imageKey] = sequenceKey; // cache imageKey -> sequenceKey }); - return false; // because no `d` data worth loading into an rbush + return false; // because no `d` data worth loading into an rbush - } else if (which === 'objects') { + // An image detection is a semantic pixel area on an image. The area could indicate + // sky, trees, sidewalk in the image. A detection can be a polygon, a bounding box, or a point. + // Each image_detection feature is a GeoJSON Point (located where the image was taken) + } else if (which === 'image_detections') { + d = { + key: feature.properties.key, + image_key: feature.properties.image_key, + value: feature.properties.value, + package: feature.properties.package, + shape: feature.properties.shape + }; + + // cache imageKey -> image_detections + if (!cache.forImageKey[d.image_key]) { + cache.forImageKey[d.image_key] = []; + } + cache.forImageKey[d.image_key].push(d); + return false; // because no `d` data worth loading into an rbush + + + // A map feature is a real world object that can be shown on a map. It could be any object + // recognized from images, manually added in images, or added on the map. + // Each map feature is a GeoJSON Point (located where the feature is) + } else if (which === 'map_features') { d = { loc: loc, key: feature.properties.key, @@ -138,18 +167,6 @@ function loadNextTilePage(which, currZoom, url, tile) { package: feature.properties.package, detections: feature.properties.detections }; - - // cache imageKey -> detectionKey - feature.properties.detections.forEach(function(detection) { - var imageKey = detection.image_key; - var detectionKey = detection.detection_key; - if (!_mlyCache.detections[imageKey]) { - _mlyCache.detections[imageKey] = {}; - } - if (!_mlyCache.detections[imageKey][detectionKey]) { - _mlyCache.detections[imageKey][detectionKey] = {}; - } - }); } return { @@ -158,11 +175,13 @@ function loadNextTilePage(which, currZoom, url, tile) { }).filter(Boolean); - cache.rtree.load(features); + if (cache.rtree && features) { + cache.rtree.load(features); + } if (which === 'images' || which === 'sequences') { dispatch.call('loadedImages'); - } else if (which === 'objects') { + } else if (which === 'map_features') { dispatch.call('loadedSigns'); } @@ -266,8 +285,11 @@ export default { if (cache.images && cache.images.inflight) { _forEach(cache.images.inflight, abortRequest); } - if (cache.objects && cache.objects.inflight) { - _forEach(cache.objects.inflight, abortRequest); + if (cache.image_detections && cache.image_detections.inflight) { + _forEach(cache.image_detections.inflight, abortRequest); + } + if (cache.map_features && cache.map_features.inflight) { + _forEach(cache.map_features.inflight, abortRequest); } if (cache.sequences && cache.sequences.inflight) { _forEach(cache.sequences.inflight, abortRequest); @@ -276,9 +298,9 @@ export default { _mlyCache = { images: { inflight: {}, loaded: {}, nextPage: {}, nextURL: {}, rtree: rbush(), forImageKey: {} }, - objects: { inflight: {}, loaded: {}, nextPage: {}, nextURL: {}, rtree: rbush() }, - sequences: { inflight: {}, loaded: {}, nextPage: {}, nextURL: {}, rtree: rbush(), forImageKey: {}, lineString: {} }, - detections: {} + image_detections: { inflight: {}, loaded: {}, nextPage: {}, nextURL: {}, forImageKey: {} }, + map_features: { inflight: {}, loaded: {}, nextPage: {}, nextURL: {}, rtree: rbush() }, + sequences: { inflight: {}, loaded: {}, nextPage: {}, nextURL: {}, rtree: rbush(), forImageKey: {}, lineString: {} } }; _mlySelectedImage = null; @@ -294,7 +316,7 @@ export default { signs: function(projection) { var psize = 32, limit = 3; - return searchLimited(psize, limit, projection, _mlyCache.objects.rtree); + return searchLimited(psize, limit, projection, _mlyCache.map_features.rtree); }, @@ -335,7 +357,8 @@ export default { loadSigns: function(context, projection) { // if we are looking at signs, we'll actually need to fetch images too loadTiles('images', apibase + 'images?', projection); - loadTiles('objects', apibase + 'map_features?layers=trafficsigns&', projection); + loadTiles('map_features', apibase + 'map_features?layers=trafficsigns&min_nbr_image_detections=1&', projection); + loadTiles('image_detections', apibase + 'image_detections?layers=trafficsigns&', projection); }, @@ -537,6 +560,7 @@ export default { this.setStyles(null, true); + // if signs signs are shown, highlight the ones that appear in this image d3_selectAll('.layer-mapillary-signs .icon-sign') .classed('selected', function(d) { return _some(d.detections, function(detection) { @@ -620,63 +644,32 @@ export default { if (!_mlyViewer || _mlyFallback) return; var imageKey = d && d.key; - var detections = (imageKey && _mlyCache.detections[imageKey]) || []; + if (!imageKey) return; - _forEach(detections, function(data, k) { - if (_isEmpty(data)) { - loadDetection(k); - } else { - var tag = makeTag(data); - if (tag) { - var tagComponent = _mlyViewer.getComponent('tag'); - tagComponent.add([tag]); - } + var detections = _mlyCache.image_detections.forImageKey[imageKey] || []; + detections.forEach(function(data) { + var tag = makeTag(data); + if (tag) { + var tagComponent = _mlyViewer.getComponent('tag'); + tagComponent.add([tag]); } }); - - function loadDetection(detectionKey) { - var url = apibase + 'image_detections/' + - detectionKey + '?' + utilQsString({ client_id: clientId }); - - d3_request(url) - .mimeType('application/json') - .response(function(xhr) { - return JSON.parse(xhr.responseText); - }) - .get(function(err, data) { - if (!data || !data.properties) return; - - var imageKey = data.properties.image_key; - _mlyCache.detections[imageKey][detectionKey] = data; - - var selectedKey = _mlySelectedImage && _mlySelectedImage.key; - if (imageKey === selectedKey) { - var tag = makeTag(data); - if (tag) { - var tagComponent = _mlyViewer.getComponent('tag'); - tagComponent.add([tag]); - } - } - }); - } - - function makeTag(data) { - var valueParts = data.properties.value.split('--'); + var valueParts = data.value.split('--'); if (valueParts.length !== 3) return; var text = valueParts[1].replace(/-/g, ' '); var tag; // Currently only two shapes - if (data.properties.shape.type === 'Polygon') { + if (data.shape.type === 'Polygon') { var polygonGeometry = new Mapillary .TagComponent - .PolygonGeometry(data.properties.shape.coordinates[0]); + .PolygonGeometry(data.shape.coordinates[0]); tag = new Mapillary.TagComponent.OutlineTag( - data.properties.key, + data.key, polygonGeometry, { text: text, @@ -688,13 +681,13 @@ export default { } ); - } else if (data.properties.shape.type === 'Point') { + } else if (data.shape.type === 'Point') { var pointGeometry = new Mapillary .TagComponent - .PointGeometry(data.properties.shape.coordinates[0]); + .PointGeometry(data.shape.coordinates[0]); tag = new Mapillary.TagComponent.SpotTag( - data.properties.key, + data.key, pointGeometry, { text: text, diff --git a/test/spec/services/mapillary.js b/test/spec/services/mapillary.js index eaadd6ba4..f01107fbd 100644 --- a/test/spec/services/mapillary.js +++ b/test/spec/services/mapillary.js @@ -32,9 +32,9 @@ describe('iD.serviceMapillary', function() { it('Initializes cache one time', function() { var cache = mapillary.cache(); expect(cache).to.have.property('images'); - expect(cache).to.have.property('objects'); + expect(cache).to.have.property('image_detections'); + expect(cache).to.have.property('map_features'); expect(cache).to.have.property('sequences'); - expect(cache).to.have.property('detections'); mapillary.init(); var cache2 = mapillary.cache(); @@ -275,7 +275,7 @@ describe('iD.serviceMapillary', function() { { minX: 10, minY: 1, maxX: 10, maxY: 1, data: { key: '2', loc: [10,1], detections: detections } } ]; - mapillary.cache().objects.rtree.load(features); + mapillary.cache().map_features.rtree.load(features); var res = mapillary.signs(context.projection); expect(res).to.deep.eql([ @@ -297,7 +297,7 @@ describe('iD.serviceMapillary', function() { { minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '4', loc: [10,0], detections: detections } } ]; - mapillary.cache().objects.rtree.load(features); + mapillary.cache().map_features.rtree.load(features); var res = mapillary.signs(context.projection); expect(res).to.have.length.of.at.most(3); });