feat: updates for mapillary map features and traffic signs

This commit is contained in:
Nikola Plesa
2020-07-31 14:55:17 +02:00
parent bd9d4bce74
commit e87b0b9432
6 changed files with 367 additions and 96 deletions
+185 -77
View File
@@ -6,7 +6,7 @@ import RBush from 'rbush';
import { geoExtent, geoScaleToZoom } from '../geo';
import { svgDefs } from '../svg/defs';
import { utilArrayUnion, utilQsString, utilRebind, utilTiler } from '../util';
import { utilArrayUnion, utilQsString, utilRebind, utilTiler, utilStringQs } from '../util';
var apibase = 'https://a.mapillary.com/v3/';
@@ -40,29 +40,23 @@ var mapFeatureConfig = {
var maxResults = 1000;
var tileZoom = 14;
var tiler = utilTiler().zoomExtent([tileZoom, tileZoom]).skipNullIsland(true);
var dispatch = d3_dispatch('loadedImages', 'loadedSigns', 'loadedMapFeatures', 'bearingChanged');
var dispatch = d3_dispatch('change', 'loadedImages', 'loadedSigns', 'loadedMapFeatures', 'bearingChanged', 'nodeChanged');
var _mlyFallback = false;
var _mlyCache;
var _mlyClicks;
var _mlySelectedImage;
var _mlySelectedImageKey;
var _mlyViewer;
var _mlyViewerFilter = ['all'];
var _mlyHighlightedDetection;
var _mlyShowFeatureDetections = false;
var _mlyShowSignDetections = false;
function abortRequest(controller) {
controller.abort();
}
function maxPageAtZoom(z) {
if (z < 15) return 2;
if (z === 15) return 5;
if (z === 16) return 10;
if (z === 17) return 20;
if (z === 18) return 40;
if (z > 18) return 80;
}
function loadTiles(which, url, projection) {
var currZoom = Math.floor(geoScaleToZoom(projection.scale()));
var tiles = tiler.getTiles(projection);
@@ -161,26 +155,6 @@ function loadNextTilePage(which, currZoom, url, tile) {
});
return false; // because no `d` data worth loading into an rbush
// 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)
@@ -225,6 +199,57 @@ function loadNextTilePage(which, currZoom, url, tile) {
});
}
function loadData(which, url) {
var cache = _mlyCache[which];
var options = {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
};
var nextUrl = url + '&client_id=' + clientId;
return fetch(nextUrl, options)
.then(function(response) {
if (!response.ok) {
throw new Error(response.status + ' ' + response.statusText);
}
return response.json();
})
.then(function(data) {
if (!data || !data.features || !data.features.length) {
throw new Error('No Data');
}
data.features.forEach(function(feature) {
var d;
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
};
if (!cache.forImageKey[d.image_key]) {
cache.forImageKey[d.image_key] = [];
}
cache.forImageKey[d.image_key].push(d);
}
});
});
}
function maxPageAtZoom(z) {
if (z < 15) return 2;
if (z === 15) return 5;
if (z === 16) return 10;
if (z === 17) return 20;
if (z === 18) return 40;
if (z > 18) return 80;
}
// extract links to pages of API results
function parsePagination(links) {
return links.split(',').map(function(rel) {
@@ -270,7 +295,6 @@ function searchLimited(limit, projection, rtree) {
}
export default {
init: function() {
@@ -299,6 +323,7 @@ export default {
};
_mlySelectedImageKey = null;
_mlySelectedImage = null;
_mlyClicks = [];
},
@@ -361,18 +386,12 @@ export default {
loadSigns: function(projection) {
// if we are looking at signs, we'll actually need to fetch images too
loadTiles('images', apibase + 'images?sort_by=key&', projection);
loadTiles('map_features', apibase + 'map_features?layers=trafficsigns&min_nbr_image_detections=2&sort_by=key&', projection);
loadTiles('image_detections', apibase + 'image_detections?layers=trafficsigns&sort_by=key&', projection);
},
loadMapFeatures: function(projection) {
// if we are looking at signs, we'll actually need to fetch images too
loadTiles('images', apibase + 'images?sort_by=key', projection);
loadTiles('points', apibase + 'map_features?layers=points&min_nbr_image_detections=2&sort_by=key&values=' + mapFeatureConfig.values + '&', projection);
loadTiles('image_detections', apibase + 'image_detections?layers=points&sort_by=key&values=' + mapFeatureConfig.values + '&', projection);
},
@@ -415,6 +434,37 @@ export default {
_mlyViewer.resize();
}
});
var hash = utilStringQs(window.location.hash);
if (hash.photo) {
this.whenViewerAvailable()
.then(() => {
this.updateViewer(context, hash.photo);
this.showViewer(context);
});
}
},
whenViewerAvailable() {
return new Promise((resolve) => {
var intervalId = window.setInterval(() => {
if (window.Mapillary) {
clearInterval(intervalId);
resolve();
}
}, 1000);
});
},
showFeatureDetections: function(value) {
_mlyShowFeatureDetections = value;
},
showSignDetections: function(value) {
_mlyShowSignDetections = value;
},
@@ -442,6 +492,7 @@ export default {
hideViewer: function(context) {
_mlySelectedImageKey = null;
_mlySelectedImage = null;
if (!_mlyFallback && _mlyViewer) {
_mlyViewer.getComponent('sequence').stop();
@@ -454,9 +505,10 @@ export default {
.classed('hide', true)
.selectAll('.photo-wrapper')
.classed('hide', true);
this.updateUrlImage(null);
context.container().selectAll('.viewfield-group, .sequence, .icon-detected')
.classed('currentView', false);
dispatch.call('nodeChanged');
return this.setStyles(context, null, true);
},
@@ -465,6 +517,19 @@ export default {
parsePagination: parsePagination,
updateUrlImage: function(imageKey) {
if (!window.mocha) {
var hash = utilStringQs(window.location.hash);
if (imageKey) {
hash.photo = imageKey;
} else {
delete hash.photo
}
window.location.replace('#' + utilQsString(hash, true));
}
},
updateViewer: function(context, imageKey) {
if (!imageKey) return this;
@@ -479,6 +544,15 @@ export default {
},
highlightDetection: function(detection) {
if (detection) {
_mlyHighlightedDetection = detection.detection_key;
}
return this;
},
initViewer: function(context, imageKey) {
var that = this;
if (window.Mapillary && imageKey) {
@@ -530,6 +604,7 @@ export default {
var clicks = _mlyClicks;
var index = clicks.indexOf(node.key);
var selectedKey = _mlySelectedImageKey;
that.setSelectedImage(node);
if (index > -1) { // `nodechanged` initiated from clicking on a marker..
clicks.splice(index, 1); // remove the click
@@ -543,6 +618,9 @@ export default {
context.map().centerEase(loc);
that.selectImage(context, node.key, true);
}
that.updateUrlImage(node.key);
dispatch.call('nodeChanged');
}
function bearingChanged(e) {
@@ -558,8 +636,6 @@ export default {
_mlySelectedImageKey = imageKey;
// Note the datum could be missing, but we'll try to carry on anyway.
// There just might be a delay before user sees detections, captured_at, etc.
var d = _mlyCache.images.forImageKey[imageKey];
var viewer = context.container().select('.photoviewer');
@@ -572,22 +648,23 @@ export default {
this.setStyles(context, null, true);
// if signs signs are shown, highlight the ones that appear in this image
context.container().selectAll('.layer-mapillary-signs .icon-detected')
.classed('currentView', function(d) {
return d.detections.some(function(detection) {
return detection.image_key === imageKey;
});
});
if (_mlyShowFeatureDetections) {
this.updateDetections(imageKey, apibase + 'image_detections?layers=points&values=' + mapFeatureConfig.values + '&image_keys=' + imageKey);
}
if (d) {
this.updateDetections(d);
if (_mlyShowSignDetections) {
this.updateDetections(imageKey, apibase + 'image_detections?layers=trafficsigns&image_keys=' + imageKey);
}
return this;
},
getSelectedImage: function() {
return _mlySelectedImage;
},
getSelectedImageKey: function() {
return _mlySelectedImageKey;
},
@@ -598,6 +675,21 @@ export default {
},
setSelectedImage: function(node) {
if (node) {
_mlySelectedImage = {
ca: node.originalCA,
key: node.key,
loc: [node.originalLatLon.lon, node.originalLatLon.lat],
pano: node.pano
};
} else {
_mlySelectedImage = null;
}
},
// 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
@@ -605,8 +697,7 @@ export default {
if (reset) { // reset all layers
context.container().selectAll('.viewfield-group')
.classed('highlighted', false)
.classed('hovered', false)
.classed('currentView', false);
.classed('hovered', false);
context.container().selectAll('.sequence')
.classed('highlighted', false)
@@ -628,8 +719,7 @@ export default {
context.container().selectAll('.layer-mapillary .viewfield-group')
.classed('highlighted', function(d) { return highlightedImageKeys.indexOf(d.key) !== -1; })
.classed('hovered', function(d) { return d.key === hoveredImageKey; })
.classed('currentView', function(d) { return d.key === selectedImageKey; });
.classed('hovered', function(d) { return d.key === hoveredImageKey; });
context.container().selectAll('.layer-mapillary .sequence')
.classed('highlighted', function(d) { return d.properties.key === hoveredSequenceKey; })
@@ -652,29 +742,48 @@ export default {
},
updateDetections: function(d) {
updateDetections: function(imageKey, url) {
if (!_mlyViewer || _mlyFallback) return;
var imageKey = d && d.key;
if (!imageKey) return;
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]);
}
});
if (!_mlyCache.image_detections.forImageKey[imageKey]) {
loadData('image_detections', url)
.then(() => {
showDetections(_mlyCache.image_detections.forImageKey[imageKey] || []);
})
} else {
showDetections(_mlyCache.image_detections.forImageKey[imageKey]);
}
function showDetections(detections) {
detections.forEach(function(data) {
var tag = makeTag(data);
if (tag) {
var tagComponent = _mlyViewer.getComponent('tag');
tagComponent.add([tag]);
}
});
}
function makeTag(data) {
var valueParts = data.value.split('--');
if (valueParts.length !== 3) return;
if (!valueParts.length) return;
var text = valueParts[1].replace(/-/g, ' ');
var text = valueParts[1];
if (text === 'flat' || text === 'discrete' || text === 'sign' || text === 'traffic-light') {
text = valueParts[2];
}
text = text.replace(/-/g, ' ');
text = text.charAt(0).toUpperCase() + text.slice(1)
var tag;
// Currently only two shapes <Polygon|Point>
var color = 0xffffff;
if (_mlyHighlightedDetection === data.key) {
color = 0xffff00;
_mlyHighlightedDetection = null;
}
if (data.shape.type === 'Polygon') {
var polygonGeometry = new Mapillary
.TagComponent
@@ -685,10 +794,10 @@ export default {
polygonGeometry,
{
text: text,
textColor: 0xffff00,
lineColor: 0xffff00,
textColor: color,
lineColor: color,
lineWidth: 2,
fillColor: 0xffff00,
fillColor: color,
fillOpacity: 0.3,
}
);
@@ -703,8 +812,8 @@ export default {
pointGeometry,
{
text: text,
color: 0xffff00,
textColor: 0xffff00
color: color,
textColor: color
}
);
}
@@ -713,7 +822,6 @@ export default {
}
},
cache: function() {
return _mlyCache;
}
+2
View File
@@ -9,6 +9,7 @@ import { svgImproveOSM } from './improveOSM';
import { svgOsmose } from './osmose';
import { svgStreetside } from './streetside';
import { svgMapillaryImages } from './mapillary_images';
import { svgMapillaryPosition } from './mapillary_position';
import { svgMapillarySigns } from './mapillary_signs';
import { svgMapillaryMapFeatures } from './mapillary_map_features';
import { svgOpenstreetcamImages } from './openstreetcam_images';
@@ -31,6 +32,7 @@ export function svgLayers(projection, context) {
{ id: 'osmose', layer: svgOsmose(projection, context, dispatch) },
{ id: 'streetside', layer: svgStreetside(projection, context, dispatch)},
{ id: 'mapillary', layer: svgMapillaryImages(projection, context, dispatch) },
{ id: 'mapillary-position', layer: svgMapillaryPosition(projection, context, dispatch) },
{ id: 'mapillary-map-features', layer: svgMapillaryMapFeatures(projection, context, dispatch) },
{ id: 'mapillary-signs', layer: svgMapillarySigns(projection, context, dispatch) },
{ id: 'openstreetcam', layer: svgOpenstreetcamImages(projection, context, dispatch) },
+1 -16
View File
@@ -26,19 +26,6 @@ export function svgMapillaryImages(projection, context, dispatch) {
if (services.mapillary && !_mapillary) {
_mapillary = services.mapillary;
_mapillary.event.on('loadedImages', throttledRedraw);
_mapillary.event.on('bearingChanged', function(e) {
viewerCompassAngle = e;
// avoid updating if the map is currently transformed
// e.g. during drags or easing.
if (context.map().isTransformed()) return;
layer.selectAll('.viewfield-group.currentView')
.filter(function(d) {
return d.pano;
})
.attr('transform', transform);
});
} else if (!services.mapillary && _mapillary) {
_mapillary = null;
}
@@ -212,9 +199,7 @@ export function svgMapillaryImages(projection, context, dispatch) {
var markers = groups
.merge(groupsEnter)
.sort(function(a, b) {
return (a.key === selectedKey) ? 1
: (b.key === selectedKey) ? -1
: b.loc[1] - a.loc[1]; // sort Y
return b.loc[1] - a.loc[1]; // sort Y
})
.attr('transform', transform)
.select('.viewfield-scale');
+4 -2
View File
@@ -62,17 +62,18 @@ export function svgMapillaryMapFeatures(projection, context, dispatch) {
var selectedImageKey = service.getSelectedImageKey();
var imageKey;
var highlightedDetection;
// Pick one of the images the map feature was detected in,
// preference given to an image already selected.
d.detections.forEach(function(detection) {
if (!imageKey || selectedImageKey === detection.image_key) {
imageKey = detection.image_key;
highlightedDetection = detection
}
});
service
.selectImage(context, imageKey)
.highlightDetection(highlightedDetection)
.updateViewer(context, imageKey)
.showViewer(context);
}
@@ -176,6 +177,7 @@ export function svgMapillaryMapFeatures(projection, context, dispatch) {
editOff();
}
}
service.showFeatureDetections(enabled);
}
+173
View File
@@ -0,0 +1,173 @@
import _throttle from 'lodash-es/throttle';
import { select as d3_select } from 'd3-selection';
import { svgPointTransform } from './helpers';
import { services } from '../services';
export function svgMapillaryPosition(projection, context, dispatch) {
var throttledRedraw = _throttle(function () { update(); }, 1000);
var minZoom = 12;
var minViewfieldZoom = 18;
var layer = d3_select(null);
var _mapillary;
var viewerCompassAngle;
function init() {
if (svgMapillaryPosition.initialized) return; // run once
svgMapillaryPosition.initialized = true;
}
function getService() {
if (services.mapillary && !_mapillary) {
_mapillary = services.mapillary;
_mapillary.event.on('nodeChanged', throttledRedraw);
_mapillary.event.on('bearingChanged', function(e) {
viewerCompassAngle = e;
if (context.map().isTransformed()) return;
layer.selectAll('.viewfield-group.currentView')
.filter(function(d) {
return d.pano;
})
.attr('transform', transform);
});
} else if (!services.mapillary && _mapillary) {
_mapillary = null;
}
return _mapillary;
}
function editOn() {
layer.style('display', 'block');
}
function editOff() {
layer.selectAll('.viewfield-group').remove();
layer.style('display', 'none');
}
function transform(d) {
var t = svgPointTransform(projection)(d);
if (d.pano && viewerCompassAngle !== null && isFinite(viewerCompassAngle)) {
t += ' rotate(' + Math.floor(viewerCompassAngle) + ',0,0)';
} else if (d.ca) {
t += ' rotate(' + Math.floor(d.ca) + ',0,0)';
}
return t;
}
function update() {
var z = ~~context.map().zoom();
var showViewfields = (z >= minViewfieldZoom);
var service = getService();
var node = service && service.getSelectedImage();
var groups = layer.selectAll('.markers').selectAll('.viewfield-group')
.data(node ? [node] : [], function(d) { return d.key; });
// exit
groups.exit()
.remove();
// enter
var groupsEnter = groups.enter()
.append('g')
.attr('class', 'viewfield-group currentView highlighted');
groupsEnter
.append('g')
.attr('class', 'viewfield-scale');
// update
var markers = groups
.merge(groupsEnter)
.attr('transform', transform)
.select('.viewfield-scale');
markers.selectAll('circle')
.data([0])
.enter()
.append('circle')
.attr('dx', '0')
.attr('dy', '0')
.attr('r', '6');
var viewfields = markers.selectAll('.viewfield')
.data(showViewfields ? [0] : []);
viewfields.exit()
.remove();
viewfields.enter()
.insert('path', 'circle')
.attr('class', 'viewfield')
.classed('pano', function() { return this.parentNode.__data__.pano; })
.attr('transform', 'scale(1.5,1.5),translate(-8, -13)')
.attr('d', viewfieldPath);
function viewfieldPath() {
var d = this.parentNode.__data__;
if (d.pano) {
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';
}
}
}
function drawImages(selection) {
var service = getService();
layer = selection.selectAll('.layer-mapillary-position')
.data(service ? [0] : []);
layer.exit()
.remove();
var layerEnter = layer.enter()
.append('g')
.attr('class', 'layer-mapillary-position');
layerEnter
.append('g')
.attr('class', 'markers');
layer = layerEnter
.merge(layer);
if (service && ~~context.map().zoom() >= minZoom) {
editOn();
update();
} else {
editOff();
}
}
drawImages.enabled = function(_) {
update();
return this;
};
drawImages.supported = function() {
return !!getService();
};
init();
return drawImages;
}
+2 -1
View File
@@ -72,7 +72,7 @@ export function svgMapillarySigns(projection, context, dispatch) {
});
service
.selectImage(context, imageKey)
.highlightDetection(d)
.updateViewer(context, imageKey)
.showViewer(context);
}
@@ -163,6 +163,7 @@ export function svgMapillarySigns(projection, context, dispatch) {
editOff();
}
}
service.showSignDetections(enabled);
}