Switch to Mapillary API v4

This commit is contained in:
Nikola Pleša
2021-06-15 10:13:07 +02:00
parent 6cd17713fa
commit 7a0d8adb7b
13 changed files with 344 additions and 412 deletions

View File

@@ -305,17 +305,23 @@ label.streetside-hires {
top: -25px;
}
.mly-wrapper .AttributionContainer .AttributionIconContainer .AttributionMapillaryLogo {
margin-top: 3px;
.mly-wrapper .mapillary-attribution-container {
display: flex;
align-items: center;
}
.mly-wrapper .AttributionContainer .AttributionImageContainer {
color: #fff;
font-size: 10px;
font-weight: 300;
overflow: hidden;
.mly-wrapper .mapillary-attribution-container .mapillary-attribution-icon-container {
display: flex;
align-items: center;
}
.mly-wrapper .mapillary-attribution-container .mapillary-attribution-username {
display: none;
}
.mly-wrapper .mapillary-attribution-container .mapillary-attribution-date {
margin-right: 6px;
}
/* OpenStreetCam viewer */
.osc-wrapper {

File diff suppressed because one or more lines are too long

View File

@@ -128,7 +128,7 @@ export function rendererPhotos(context) {
};
photos.shouldFilterByUsername = function() {
return showsLayer('mapillary') || showsLayer('openstreetcam') || showsLayer('streetside');
return !showsLayer('mapillary') && showsLayer('openstreetcam') && !showsLayer('streetside');
};
photos.showsPhotoType = function(val) {

View File

@@ -1,50 +1,30 @@
/* global Mapillary:false */
/* global mapillary:false */
import { dispatch as d3_dispatch } from 'd3-dispatch';
import { select as d3_select } from 'd3-selection';
import base64 from 'base64-js';
import Protobuf from 'pbf';
import RBush from 'rbush';
import vt from '@mapbox/vector-tile';
import { VectorTile } from '@mapbox/vector-tile';
import { geoExtent, geoScaleToZoom } from '../geo';
import { utilQsString, utilRebind, utilTiler, utilStringQs } from '../util';
const imageDetectionUrl = 'https://a.mapillary.com/v3/image_detections';
const tileUrl = 'https://tiles3.mapillary.com/v0.1/{z}/{x}/{y}.mvt';
const mapFeatureTileUrl = 'https://a.mapillary.com/v3/tiles/map_features/{z}/{x}/{y}.mvt';
const clientId = 'NzNRM2otQkR2SHJzaXJmNmdQWVQ0dzo1ZWYyMmYwNjdmNDdlNmVi';
const mapFeatureValues = [
'construction--flat--crosswalk-plain',
'marking--discrete--crosswalk-zebra',
'object--banner',
'object--bench',
'object--bike-rack',
'object--billboard',
'object--catch-basin',
'object--cctv-camera',
'object--fire-hydrant',
'object--mailbox',
'object--manhole',
'object--phone-booth',
'object--sign--advertisement',
'object--sign--information',
'object--sign--store',
'object--street-light',
'object--support--utility-pole',
'object--traffic-light--*',
'object--traffic-light--pedestrians',
'object--trash-can'
].join(',');
const accessToken = 'MLY|4100327730013843|5bb78b81720791946a9a7b956c57b7cf';
const apiUrl = 'https://graph.mapillary.com/';
const baseTileUrl = 'https://tiles.mapillary.com/maps/vtp';
const mapFeatureTileUrl = `${baseTileUrl}/mly_map_feature_point/2/{z}/{x}/{y}?access_token=${accessToken}`;
const tileUrl = `${baseTileUrl}/mly1_public/2/{z}/{x}/{y}?access_token=${accessToken}`;
const trafficSignTileUrl = `${baseTileUrl}/mly_map_feature_traffic_sign/2/{z}/{x}/{y}?access_token=${accessToken}`;
const viewercss = 'mapillary-js/mapillary.min.css';
const viewerjs = 'mapillary-js/mapillary.min.js';
const viewercss = 'mapillary-js/mapillary.css';
const viewerjs = 'mapillary-js/mapillary.js';
const minZoom = 14;
const dispatch = d3_dispatch('change', 'loadedImages', 'loadedSigns', 'loadedMapFeatures', 'bearingChanged', 'nodeChanged');
const dispatch = d3_dispatch('change', 'loadedImages', 'loadedSigns', 'loadedMapFeatures', 'bearingChanged', 'imageChanged');
let _loadViewerPromise;
let _mlyActiveImage;
let _mlyCache;
let _mlyClicks;
let _mlyFallback = false;
let _mlyHighlightedDetection;
let _mlyShowFeatureDetections = false;
@@ -52,11 +32,8 @@ let _mlyShowSignDetections = false;
let _mlyViewer;
let _mlyViewerFilter = ['all'];
function abortRequest(controller) {
controller.abort();
}
// Load all data for the specified type from Mapillary vector tiles
function loadTiles(which, url, maxZoom, projection) {
const tiler = utilTiler().zoomExtent([minZoom, maxZoom]).skipNullIsland(true);
const tiles = tiler.getTiles(projection);
@@ -67,6 +44,7 @@ function loadTiles(which, url, maxZoom, projection) {
}
// Load all data for the specified type from one vector tile
function loadTile(which, url, tile) {
const cache = _mlyCache.requests;
const tileId = `${tile.id}-${which}`;
@@ -94,7 +72,7 @@ function loadTile(which, url, tile) {
if (which === 'images') {
dispatch.call('loadedImages');
} else if (which === 'map_features') {
} else if (which === 'signs') {
dispatch.call('loadedSigns');
} else if (which === 'points') {
dispatch.call('loadedMapFeatures');
@@ -107,8 +85,9 @@ function loadTile(which, url, tile) {
}
// Load the data from the vector tile into cache
function loadTileDataToCache(data, tile, which) {
const vectorTile = new vt.VectorTile(new Protobuf(data));
const vectorTile = new VectorTile(new Protobuf(data));
let features,
cache,
layer,
@@ -117,24 +96,23 @@ function loadTileDataToCache(data, tile, which) {
loc,
d;
if (vectorTile.layers.hasOwnProperty('mapillary-images')) {
if (vectorTile.layers.hasOwnProperty('image')) {
features = [];
cache = _mlyCache.images;
layer = vectorTile.layers['mapillary-images'];
layer = vectorTile.layers.image;
for (i = 0; i < layer.length; i++) {
feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);
loc = feature.geometry.coordinates;
d = {
loc: loc,
key: feature.properties.key,
ca: feature.properties.ca,
captured_at: feature.properties.captured_at,
captured_by: feature.properties.userkey,
pano: feature.properties.pano,
skey: feature.properties.skey,
ca: feature.properties.compass_angle,
id: feature.properties.id,
is_pano: feature.properties.is_pano,
sequence_id: feature.properties.sequence_id,
};
cache.forImageKey[d.key] = d;
cache.forImageId[d.id] = d;
features.push({
minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d
});
@@ -144,36 +122,61 @@ function loadTileDataToCache(data, tile, which) {
}
}
if (vectorTile.layers.hasOwnProperty('mapillary-sequences')) {
if (vectorTile.layers.hasOwnProperty('sequence')) {
features = [];
cache = _mlyCache.sequences;
layer = vectorTile.layers['mapillary-sequences'];
layer = vectorTile.layers.sequence;
for (i = 0; i < layer.length; i++) {
feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);
if (cache.lineString[feature.properties.key]) {
cache.lineString[feature.properties.key].push(feature);
if (cache.lineString[feature.properties.id]) {
cache.lineString[feature.properties.id].push(feature);
} else {
cache.lineString[feature.properties.key] = [feature];
cache.lineString[feature.properties.id] = [feature];
}
}
}
if (vectorTile.layers.hasOwnProperty('mapillary-map-features')) {
if (vectorTile.layers.hasOwnProperty('point')) {
features = [];
cache = _mlyCache[which];
layer = vectorTile.layers['mapillary-map-features'];
layer = vectorTile.layers.point;
for (i = 0; i < layer.length; i++) {
feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);
loc = feature.geometry.coordinates;
d = {
loc: loc,
key: feature.properties.key,
value: feature.properties.value,
detections: JSON.parse(feature.properties.detections),
id: feature.properties.id,
first_seen_at: feature.properties.first_seen_at,
last_seen_at: feature.properties.last_seen_at
last_seen_at: feature.properties.last_seen_at,
value: feature.properties.value
};
features.push({
minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d
});
}
if (cache.rtree) {
cache.rtree.load(features);
}
}
if (vectorTile.layers.hasOwnProperty('traffic_sign')) {
features = [];
cache = _mlyCache[which];
layer = vectorTile.layers.point;
for (i = 0; i < layer.length; i++) {
feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);
loc = feature.geometry.coordinates;
d = {
loc: loc,
id: feature.properties.id,
first_seen_at: feature.properties.first_seen_at,
last_seen_at: feature.properties.last_seen_at,
value: feature.properties.value
};
features.push({
minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d
@@ -186,44 +189,25 @@ function loadTileDataToCache(data, tile, which) {
}
function loadData(which, url) {
const cache = _mlyCache[which];
const options = {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
};
return fetch(url, options)
// Get data from the API
function loadData(url) {
return fetch(url)
.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');
.then(function(result) {
if (!result) {
return [];
}
data.features.forEach(function(feature) {
if (which === 'image_detections') {
const imageKey = feature.properties.image_key;
if (!cache.forImageKey[imageKey]) {
cache.forImageKey[imageKey] = [];
}
cache.forImageKey[imageKey].push({
key: feature.properties.key,
image_key: feature.properties.image_key,
value: feature.properties.value,
shape: feature.properties.shape
});
}
});
return result.data || [];
});
}
// partition viewport into higher zoom tiles
// Partition viewport into higher zoom tiles
function partitionViewport(projection) {
const z = geoScaleToZoom(projection.scale());
const z2 = (Math.ceil(z * 2) / 2) + 2.5; // round to next 0.5 and add 2.5
@@ -234,7 +218,7 @@ function partitionViewport(projection) {
}
// no more than `limit` results per partition.
// Return no more than `limit` results per partition.
function searchLimited(limit, projection, rtree) {
limit = limit || 5;
@@ -250,7 +234,7 @@ function searchLimited(limit, projection, rtree) {
export default {
// initialize Mapillary
// Initialize Mapillary
init: function() {
if (!_mlyCache) {
this.reset();
@@ -259,89 +243,92 @@ export default {
this.event = utilRebind(this, dispatch, 'on');
},
// reset cache and state
// Reset cache and state
reset: function() {
if (_mlyCache) {
Object.values(_mlyCache.requests.inflight).forEach(abortRequest);
Object.values(_mlyCache.requests.inflight).forEach(function(request) { request.abort(); });
}
_mlyCache = {
images: { rtree: new RBush(), forImageKey: {} },
image_detections: { forImageKey: {} },
map_features: { rtree: new RBush() },
images: { rtree: new RBush(), forImageId: {} },
image_detections: { forImageId: {} },
signs: { rtree: new RBush() },
points: { rtree: new RBush() },
sequences: { rtree: new RBush(), lineString: {} },
requests: { loaded: {}, inflight: {} }
};
_mlyActiveImage = null;
_mlyClicks = [];
},
// get visible images
// Get visible images
images: function(projection) {
const limit = 5;
return searchLimited(limit, projection, _mlyCache.images.rtree);
},
/**
* get visible traffic signs
*/
// Get visible traffic signs
signs: function(projection) {
const limit = 5;
return searchLimited(limit, projection, _mlyCache.map_features.rtree);
return searchLimited(limit, projection, _mlyCache.signs.rtree);
},
// get visible map (point) features
// Get visible map (point) features
mapFeatures: function(projection) {
const limit = 5;
return searchLimited(limit, projection, _mlyCache.points.rtree);
},
// get cached image by key
cachedImage: function(imageKey) {
return _mlyCache.images.forImageKey[imageKey];
// Get cached image by id
cachedImage: function(imageId) {
return _mlyCache.images.forImageId[imageId];
},
// get visible sequences
// Get visible sequences
sequences: function(projection) {
const viewport = projection.clipExtent();
const min = [viewport[0][0], viewport[1][1]];
const max = [viewport[1][0], viewport[0][1]];
const bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();
const sequenceKeys = {};
const sequenceIds = {};
let lineStrings = [];
// find sequences for images in viewport
_mlyCache.images.rtree.search(bbox)
.forEach(function(d) {
if (d.data.skey) {
sequenceKeys[d.data.skey] = true;
if (d.data.sequence_id) {
sequenceIds[d.data.sequence_id] = true;
}
});
Object.keys(sequenceKeys).forEach(function(sequenceKey) {
lineStrings = lineStrings.concat(_mlyCache.sequences.lineString[sequenceKey]);
Object.keys(sequenceIds).forEach(function(sequenceId) {
if (_mlyCache.sequences.lineString[sequenceId]) {
lineStrings = lineStrings.concat(_mlyCache.sequences.lineString[sequenceId]);
}
});
return lineStrings;
},
// Load images in the visible area
loadImages: function(projection) {
loadTiles('images', tileUrl, 14, projection);
},
// Load traffic signs in the visible area
loadSigns: function(projection) {
loadTiles('map_features', `${mapFeatureTileUrl}?layers=trafficsigns&per_page=1000&client_id=${clientId}`, 18, projection);
loadTiles('signs', trafficSignTileUrl, 14, projection);
},
// Load map (point) features in the visible area
loadMapFeatures: function(projection) {
loadTiles('points', `${mapFeatureTileUrl}?layers=points&per_page=1000&client_id=${clientId}`, 18, projection);
loadTiles('points', mapFeatureTileUrl, 14, projection);
},
// Return a promise that resolves when the image viewer (Mapillary JS) library has finished loading
ensureViewerLoaded: function(context) {
if (_loadViewerPromise) return _loadViewerPromise;
@@ -362,6 +349,7 @@ export default {
let loadedCount = 0;
function loaded() {
loadedCount += 1;
// wait until both files are loaded
if (loadedCount === 2) resolve();
}
@@ -406,19 +394,21 @@ export default {
},
// Load traffic sign image sprites
loadSignResources: function(context) {
context.ui().svgDefs.addSprites(['mapillary-sprite'], false /* don't override colors */ );
return this;
},
// Load map (point) feature image sprites
loadObjectResources: function(context) {
context.ui().svgDefs.addSprites(['mapillary-object-sprite'], false /* don't override colors */ );
return this;
},
// remove previous detections in viewer
// Remove previous detections in image viewer
resetTags: function() {
if (_mlyViewer && !_mlyFallback) {
_mlyViewer.getComponent('tag').removeAll();
@@ -426,7 +416,7 @@ export default {
},
// show map feature detections in viewer
// Show map feature detections in image viewer
showFeatureDetections: function(value) {
_mlyShowFeatureDetections = value;
if (!_mlyShowFeatureDetections && !_mlyShowSignDetections) {
@@ -435,7 +425,7 @@ export default {
},
// show traffic sign detections in viewer
// Show traffic sign detections in image viewer
showSignDetections: function(value) {
_mlyShowSignDetections = value;
if (!_mlyShowFeatureDetections && !_mlyShowSignDetections) {
@@ -444,7 +434,7 @@ export default {
},
// apply filter to viewer
// Apply filter to image viewer
filterViewer: function(context) {
const showsPano = context.photos().showsPanoramic();
const showsFlat = context.photos().showsFlat();
@@ -452,7 +442,7 @@ export default {
const toDate = context.photos().toDate();
const filter = ['all'];
if (!showsPano) filter.push(['==', 'pano', false]);
if (!showsPano) filter.push([ '!=', 'cameraType', 'spherical' ]);
if (!showsFlat && showsPano) filter.push(['==', 'pano', true]);
if (fromDate) {
filter.push(['>=', 'capturedAt', new Date(fromDate).getTime()]);
@@ -470,6 +460,7 @@ export default {
},
// Make the image viewer visible
showViewer: function(context) {
const wrap = context.container().select('.photoviewer')
.classed('hide', false);
@@ -492,6 +483,7 @@ export default {
},
// Hide the image viewer and resets map markers
hideViewer: function(context) {
_mlyActiveImage = null;
@@ -509,20 +501,20 @@ export default {
this.updateUrlImage(null);
dispatch.call('nodeChanged');
dispatch.call('imageChanged');
dispatch.call('loadedMapFeatures');
dispatch.call('loadedSigns');
return this.setStyles(context, null, true);
return this.setStyles(context, null);
},
// update the URL with current image key
updateUrlImage: function(imageKey) {
// Update the URL with current image id
updateUrlImage: function(imageId) {
if (!window.mocha) {
const hash = utilStringQs(window.location.hash);
if (imageKey) {
hash.photo = 'mapillary/' + imageKey;
if (imageId) {
hash.photo = 'mapillary/' + imageId;
} else {
delete hash.photo;
}
@@ -531,23 +523,23 @@ export default {
},
// highlight the detection in the viewer that is related to the clicked map feature
// Highlight the detection in the viewer that is related to the clicked map feature
highlightDetection: function(detection) {
if (detection) {
_mlyHighlightedDetection = detection.detection_key;
_mlyHighlightedDetection = detection.id;
}
return this;
},
// Initialize image viewer (Mapillar JS)
initViewer: function(context) {
const that = this;
if (!window.Mapillary) return;
if (!window.mapillary) return;
const opts = {
apiClient: clientId,
baseImageSize: 320,
accessToken: accessToken,
component: {
cover: false,
keyboard: false,
@@ -557,7 +549,7 @@ export default {
};
// Disable components requiring WebGL support
if (!Mapillary.isSupported() && Mapillary.isFallbackSupported()) {
if (!mapillary.isSupported() && mapillary.isFallbackSupported()) {
_mlyFallback = true;
opts.component = {
cover: false,
@@ -572,9 +564,10 @@ export default {
};
}
_mlyViewer = new Mapillary.Viewer(opts);
_mlyViewer.on('nodechanged', nodeChanged);
_mlyViewer.on('bearingchanged', bearingChanged);
_mlyViewer = new mapillary.Viewer(opts);
_mlyViewer.on('image', imageChanged);
_mlyViewer.on('bearing', bearingChanged);
if (_mlyViewerFilter) {
_mlyViewer.setFilter(_mlyViewerFilter);
}
@@ -584,149 +577,117 @@ export default {
if (_mlyViewer) _mlyViewer.resize();
});
// nodeChanged: called after the viewer has changed images and is ready.
//
// There is some logic here to batch up clicks into a _mlyClicks array
// because the user might click on a lot of markers quickly and nodechanged
// may be called out of order asynchronously.
//
// Clicks are added to the array in `selectedImage` and removed here.
//
function nodeChanged(node) {
// imageChanged: called after the viewer has changed images and is ready.
function imageChanged(node) {
that.resetTags();
const clicks = _mlyClicks;
const index = clicks.indexOf(node.key);
that.setActiveImage(node);
that.setStyles(context, null, true);
const image = node.image;
that.setActiveImage(image);
that.setStyles(context, null);
const loc = [image.originalLngLat.lng, image.originalLngLat.lat];
context.map().centerEase(loc);
that.updateUrlImage(image.id);
if (index > -1) { // `nodechanged` initiated from clicking on a marker..
clicks.splice(index, 1); // remove the click
} else { // `nodechanged` initiated from the Mapillary viewer controls..
const loc = node.computedLatLon ? [node.computedLatLon.lon, node.computedLatLon.lat] : [node.latLon.lon, node.latLon.lat];
context.map().centerEase(loc);
that.selectImage(context, node.key, true);
if (_mlyShowFeatureDetections || _mlyShowSignDetections) {
that.updateDetections(image.id, `${apiUrl}/${image.id}/detections?access_token=${accessToken}&fields=id,image,geometry,value`);
}
dispatch.call('nodeChanged');
dispatch.call('imageChanged');
}
// bearingChanged: called when the bearing changes in the image viewer.
function bearingChanged(e) {
dispatch.call('bearingChanged', undefined, e);
}
},
// Pass in the image key string as `imageKey`.
// This allows images to be selected from places that dont have access
// to the full image datum (like the street signs layer or the js viewer)
selectImage: function(context, imageKey, fromViewer) {
this.updateUrlImage(imageKey);
const d = _mlyCache.images.forImageKey[imageKey];
const viewer = context.container().select('.photoviewer');
if (!viewer.empty()) viewer.datum(d);
imageKey = (d && d.key) || imageKey;
if (!fromViewer && imageKey) {
_mlyClicks.push(imageKey);
}
if (_mlyShowFeatureDetections) {
this.updateDetections(imageKey, `${imageDetectionUrl}?layers=points&values=${mapFeatureValues}&image_keys=${imageKey}&client_id=${clientId}`);
}
if (_mlyShowSignDetections) {
this.updateDetections(imageKey, `${imageDetectionUrl}?layers=trafficsigns&image_keys=${imageKey}&client_id=${clientId}`);
}
if (_mlyViewer && imageKey) {
_mlyViewer.moveToKey(imageKey)
.catch(function(e) { console.error('mly3', e); }); // eslint-disable-line no-console
// Move to an image
selectImage: function(context, imageId) {
if (_mlyViewer && imageId) {
_mlyViewer.moveTo(imageId)
.catch(function(e) {
console.error('mly3', e); // eslint-disable-line no-console
});
}
return this;
},
// Return the currently displayed image
getActiveImage: function() {
return _mlyActiveImage;
},
setActiveImage: function(node) {
if (node) {
// Return a list of detection objects for the given id
getDetections: function(id) {
return loadData(`${apiUrl}/${id}/detections?access_token=${accessToken}&fields=id,value,image`);
},
// Set the currently visible image
setActiveImage: function(image) {
if (image) {
_mlyActiveImage = {
ca: node.originalCA,
key: node.key,
loc: [node.originalLatLon.lon, node.originalLatLon.lat],
pano: node.pano,
sequenceKey: node.sequenceKey
ca: image.originalCompassAngle,
id: image.id,
loc: [image.originalLngLat.lng, image.originalLngLat.lat],
is_pano: image.cameraType === 'spherical',
sequence_id: image.sequenceId
};
} else {
_mlyActiveImage = 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
setStyles: function(context, hovered, reset) {
if (reset) { // reset all layers
context.container().selectAll('.viewfield-group')
.classed('highlighted', false)
.classed('hovered', false);
context.container().selectAll('.sequence')
.classed('highlighted', false)
.classed('currentView', false);
}
const hoveredImageKey = hovered && hovered.key;
const hoveredSequenceKey = hovered && hovered.skey;
const selectedImageKey = _mlyActiveImage && _mlyActiveImage.key;
const selectedSequenceKey = _mlyActiveImage && _mlyActiveImage.sequenceKey;
// Update the currently highlighted sequence and selected bubble.
setStyles: function(context, hovered) {
const hoveredImageId = hovered && hovered.id;
const hoveredSequenceId = hovered && hovered.sequence_id;
const selectedSequenceId = _mlyActiveImage && _mlyActiveImage.sequence_id;
context.container().selectAll('.layer-mapillary .viewfield-group')
.classed('highlighted', function(d) { return d.skey === selectedSequenceKey; })
.classed('hovered', function(d) { return d.key === hoveredImageKey; });
.classed('highlighted', function(d) { return (d.sequence_id === selectedSequenceId) || (d.id === hoveredImageId); })
.classed('hovered', function(d) { return d.id === hoveredImageId; });
context.container().selectAll('.layer-mapillary .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() {
const d = this.parentNode.__data__;
if (d.pano && d.key !== selectedImageKey) {
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';
}
}
.classed('highlighted', function(d) { return d.properties.id === hoveredSequenceId; })
.classed('currentView', function(d) { return d.properties.id === selectedSequenceId; });
return this;
},
updateDetections: function(imageKey, url) {
// Get detections for the current image and shows them in the image viewer
updateDetections: function(imageId, url) {
if (!_mlyViewer || _mlyFallback) return;
if (!imageKey) return;
if (!_mlyCache.image_detections.forImageKey[imageKey]) {
loadData('image_detections', url)
.then(() => {
showDetections(_mlyCache.image_detections.forImageKey[imageKey] || []);
});
if (!imageId) return;
const cache = _mlyCache.image_detections;
if (cache.forImageId[imageId]) {
showDetections(_mlyCache.image_detections.forImageId[imageId]);
} else {
showDetections(_mlyCache.image_detections.forImageKey[imageKey]);
loadData(url)
.then(detections => {
detections.forEach(function(detection) {
if (!cache.forImageId[imageId]) {
cache.forImageId[imageId] = [];
}
cache.forImageId[imageId].push({
geometry: detection.geometry,
id: detection.id,
image_id: imageId,
value:detection.value
});
});
showDetections(_mlyCache.image_detections.forImageId[imageId] || []);
});
}
// Create a tag for each detection and shows it in the image viewer
function showDetections(detections) {
const tagComponent = _mlyViewer.getComponent('tag');
detections.forEach(function(data) {
@@ -737,6 +698,8 @@ export default {
});
}
// Create a Mapillary JS tag object
function makeTag(data) {
const valueParts = data.value.split('--');
if (!valueParts.length) return;
@@ -745,7 +708,7 @@ export default {
let text;
let color = 0xffffff;
if (_mlyHighlightedDetection === data.key) {
if (_mlyHighlightedDetection === data.id) {
color = 0xffff00;
text = valueParts[1];
if (text === 'flat' || text === 'discrete' || text === 'sign') {
@@ -756,45 +719,35 @@ export default {
_mlyHighlightedDetection = null;
}
if (data.shape.type === 'Polygon') {
const polygonGeometry = new Mapillary
.TagComponent
.PolygonGeometry(data.shape.coordinates[0]);
const geometry = base64.toByteArray(data.geometry);
const tile = new VectorTile(new Protobuf(geometry));
const layer = tile.layers['mpy-or'];
tag = new Mapillary.TagComponent.OutlineTag(
data.key,
polygonGeometry,
{
text: text,
textColor: color,
lineColor: color,
lineWidth: 2,
fillColor: color,
fillOpacity: 0.3,
}
);
const geometries = layer.feature(0).loadGeometry();
} else if (data.shape.type === 'Point') {
const pointGeometry = new Mapillary
.TagComponent
.PointGeometry(data.shape.coordinates[0]);
const polygon = geometries.map(ring =>
ring.map(point =>
[point.x / layer.extent, point.y / layer.extent]));
tag = new Mapillary.TagComponent.SpotTag(
data.key,
pointGeometry,
{
text: text,
color: color,
textColor: color
}
);
}
tag = new mapillary.OutlineTag(
data.id,
new mapillary.PolygonGeometry(polygon[0]),
{
text: text,
textColor: color,
lineColor: color,
lineWidth: 2,
fillColor: color,
fillOpacity: 0.3,
}
);
return tag;
}
},
// Return the current cache
cache: function() {
return _mlyCache;
}

View File

@@ -544,7 +544,7 @@ export default {
.classed('currentView', function(d) { return d.properties.key === selectedSequenceKey; });
// update viewfields if needed
context.container().selectAll('.viewfield-group .viewfield')
context.container().selectAll('.layer-openstreetcam .viewfield-group .viewfield')
.attr('d', viewfieldPath);
function viewfieldPath() {

View File

@@ -942,7 +942,7 @@ export default {
.classed('currentView', d => d.properties.key === selectedSequenceKey);
// update viewfields if needed
context.container().selectAll('.viewfield-group .viewfield')
context.container().selectAll('.layer-streetside-images .viewfield-group .viewfield')
.attr('d', viewfieldPath);
function viewfieldPath() {

View File

@@ -70,7 +70,7 @@ export function svgMapillaryImages(projection, context, dispatch) {
}
function click(d3_event, d) {
function click(d3_event, image) {
const service = getService();
if (!service) return;
@@ -78,18 +78,18 @@ export function svgMapillaryImages(projection, context, dispatch) {
.ensureViewerLoaded(context)
.then(function() {
service
.selectImage(context, d.key)
.selectImage(context, image.id)
.showViewer(context);
});
context.map().centerEase(d.loc);
context.map().centerEase(image.loc);
}
function mouseover(d3_event, d) {
function mouseover(d3_event, image) {
const service = getService();
if (service) service.setStyles(context, d);
if (service) service.setStyles(context, image);
}
@@ -116,7 +116,7 @@ export function svgMapillaryImages(projection, context, dispatch) {
if (!showsPano || !showsFlat) {
images = images.filter(function(image) {
if (image.pano) return showsPano;
if (image.is_pano) return showsPano;
return showsFlat;
});
}
@@ -142,8 +142,8 @@ export function svgMapillaryImages(projection, context, dispatch) {
if (!showsPano || !showsFlat) {
sequences = sequences.filter(function(sequence) {
if (sequence.properties.hasOwnProperty('pano')) {
if (sequence.properties.pano) return showsPano;
if (sequence.properties.hasOwnProperty('is_pano')) {
if (sequence.properties.is_pano) return showsPano;
return showsFlat;
}
return false;
@@ -151,12 +151,12 @@ export function svgMapillaryImages(projection, context, dispatch) {
}
if (fromDate) {
sequences = sequences.filter(function(sequence) {
return new Date(sequence.properties.captured_at).getTime() >= new Date(fromDate).getTime();
return new Date(sequence.properties.captured_at).getTime() >= new Date(fromDate).getTime().toString();
});
}
if (toDate) {
sequences = sequences.filter(function(sequence) {
return new Date(sequence.properties.captured_at).getTime() <= new Date(toDate).getTime();
return new Date(sequence.properties.captured_at).getTime() <= new Date(toDate).getTime().toString();
});
}
@@ -175,10 +175,11 @@ export function svgMapillaryImages(projection, context, dispatch) {
images = filterImages(images);
sequences = filterSequences(sequences, service);
service.filterViewer(context);
let traces = layer.selectAll('.sequences').selectAll('.sequence')
.data(sequences, function(d) { return d.properties.key; });
.data(sequences, function(d) { return d.properties.id; });
// exit
traces.exit()
@@ -193,7 +194,7 @@ export function svgMapillaryImages(projection, context, dispatch) {
const groups = layer.selectAll('.markers').selectAll('.viewfield-group')
.data(images, function(d) { return d.key; });
.data(images, function(d) { return d.id; });
// exit
groups.exit()
@@ -238,13 +239,12 @@ export function svgMapillaryImages(projection, context, dispatch) {
viewfields.enter() // viewfields may or may not be drawn...
.insert('path', 'circle') // but if they are, draw below the circles
.attr('class', 'viewfield')
.classed('pano', function() { return this.parentNode.__data__.pano; })
.classed('pano', function() { return this.parentNode.__data__.is_pano; })
.attr('transform', 'scale(1.5,1.5),translate(-8, -13)')
.attr('d', viewfieldPath);
function viewfieldPath() {
const d = this.parentNode.__data__;
if (d.pano) {
if (this.parentNode.__data__.is_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';

View File

@@ -61,30 +61,26 @@ export function svgMapillaryMapFeatures(projection, context, dispatch) {
context.map().centerEase(d.loc);
const selectedImageKey = service.getActiveImage() && service.getActiveImage().key;
let imageKey;
let highlightedDetection;
const selectedImageId = service.getActiveImage() && service.getActiveImage().id;
d.detections.forEach(function(detection) {
if (!imageKey || selectedImageKey === detection.image_key) {
imageKey = detection.image_key;
highlightedDetection = detection;
service.getDetections(d.id).then(detections => {
if (detections.length) {
const imageId = detections[0].image.id;
if (imageId === selectedImageId) {
service
.highlightDetection(detections[0])
.selectImage(context, imageId);
} else {
service.ensureViewerLoaded(context)
.then(function() {
service
.highlightDetection(detections[0])
.selectImage(context, imageId)
.showViewer(context);
});
}
}
});
if (imageKey === selectedImageKey) {
service
.highlightDetection(highlightedDetection)
.selectImage(context, imageKey);
} else {
service.ensureViewerLoaded(context)
.then(function() {
service
.highlightDetection(highlightedDetection)
.selectImage(context, imageKey)
.showViewer(context);
});
}
}
@@ -112,11 +108,10 @@ export function svgMapillaryMapFeatures(projection, context, dispatch) {
let data = (service ? service.mapFeatures(projection) : []);
data = filterData(data);
const selectedImageKey = service && service.getActiveImage() && service.getActiveImage().key;
const transform = svgPointTransform(projection);
const mapFeatures = layer.selectAll('.icon-map-feature')
.data(data, function(d) { return d.key; });
.data(data, function(d) { return d.id; });
// exit
mapFeatures.exit()
@@ -159,26 +154,7 @@ export function svgMapillaryMapFeatures(projection, context, dispatch) {
// update
mapFeatures
.merge(enter)
.attr('transform', transform)
.classed('currentView', function(d) {
return d.detections.some(function(detection) {
return detection.image_key === selectedImageKey;
});
})
.sort(function(a, b) {
const aSelected = a.detections.some(function(detection) {
return detection.image_key === selectedImageKey;
});
const bSelected = b.detections.some(function(detection) {
return detection.image_key === selectedImageKey;
});
if (aSelected === bSelected) {
return b.loc[1] - a.loc[1]; // sort Y
} else if (aSelected) {
return 1;
}
return -1;
});
.attr('transform', transform);
}

View File

@@ -23,15 +23,15 @@ export function svgMapillaryPosition(projection, context) {
function getService() {
if (services.mapillary && !_mapillary) {
_mapillary = services.mapillary;
_mapillary.event.on('nodeChanged', throttledRedraw);
_mapillary.event.on('imageChanged', throttledRedraw);
_mapillary.event.on('bearingChanged', function(e) {
viewerCompassAngle = e;
viewerCompassAngle = e.bearing;
if (context.map().isTransformed()) return;
layer.selectAll('.viewfield-group.currentView')
.filter(function(d) {
return d.pano;
return d.is_pano;
})
.attr('transform', transform);
});
@@ -55,7 +55,7 @@ export function svgMapillaryPosition(projection, context) {
function transform(d) {
let t = svgPointTransform(projection)(d);
if (d.pano && viewerCompassAngle !== null && isFinite(viewerCompassAngle)) {
if (d.is_pano && viewerCompassAngle !== null && isFinite(viewerCompassAngle)) {
t += ' rotate(' + Math.floor(viewerCompassAngle) + ',0,0)';
} else if (d.ca) {
t += ' rotate(' + Math.floor(d.ca) + ',0,0)';
@@ -69,10 +69,10 @@ export function svgMapillaryPosition(projection, context) {
const showViewfields = (z >= minViewfieldZoom);
const service = getService();
const node = service && service.getActiveImage();
const image = service && service.getActiveImage();
const groups = layer.selectAll('.markers').selectAll('.viewfield-group')
.data(node ? [node] : [], function(d) { return d.key; });
.data(image ? [image] : [], function(d) { return d.id; });
// exit
groups.exit()
@@ -112,18 +112,8 @@ export function svgMapillaryPosition(projection, context) {
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() {
const 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';
}
}
.attr('d', 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z');
}

View File

@@ -61,31 +61,26 @@ export function svgMapillarySigns(projection, context, dispatch) {
context.map().centerEase(d.loc);
const selectedImageKey = service.getActiveImage() && service.getActiveImage().key;
let imageKey;
let highlightedDetection;
const selectedImageId = service.getActiveImage() && service.getActiveImage().id;
d.detections.forEach(function(detection) {
if (!imageKey || selectedImageKey === detection.image_key) {
imageKey = detection.image_key;
highlightedDetection = detection;
service.getDetections(d.id).then(detections => {
if (detections.length) {
const imageId = detections[0].image.id;
if (imageId === selectedImageId) {
service
.highlightDetection(detections[0])
.selectImage(context, imageId);
} else {
service.ensureViewerLoaded(context)
.then(function() {
service
.highlightDetection(detections[0])
.selectImage(context, imageId)
.showViewer(context);
});
}
}
});
if (imageKey === selectedImageKey) {
service
.highlightDetection(highlightedDetection)
.selectImage(context, imageKey);
} else {
service.ensureViewerLoaded(context)
.then(function() {
service
.highlightDetection(highlightedDetection)
.selectImage(context, imageKey)
.showViewer(context);
});
}
}
@@ -115,11 +110,10 @@ export function svgMapillarySigns(projection, context, dispatch) {
let data = (service ? service.signs(projection) : []);
data = filterData(data);
const selectedImageKey = service.getActiveImage() && service.getActiveImage().key;
const transform = svgPointTransform(projection);
const signs = layer.selectAll('.icon-sign')
.data(data, function(d) { return d.key; });
.data(data, function(d) { return d.id; });
// exit
signs.exit()
@@ -149,26 +143,7 @@ export function svgMapillarySigns(projection, context, dispatch) {
// update
signs
.merge(enter)
.attr('transform', transform)
.classed('currentView', function(d) {
return d.detections.some(function(detection) {
return detection.image_key === selectedImageKey;
});
})
.sort(function(a, b) {
const aSelected = a.detections.some(function(detection) {
return detection.image_key === selectedImageKey;
});
const bSelected = b.detections.some(function(detection) {
return detection.image_key === selectedImageKey;
});
if (aSelected === bSelected) {
return b.loc[1] - a.loc[1]; // sort Y
} else if (aSelected) {
return 1;
}
return -1;
});
.attr('transform', transform);
}

View File

@@ -19,9 +19,9 @@ export function uiPhotoviewer(context) {
.append('button')
.attr('class', 'thumb-hide')
.on('click', function () {
if (services.streetside) { services.streetside.hideViewer(context); }
//if (services.streetside) { services.streetside.hideViewer(context); }
if (services.mapillary) { services.mapillary.hideViewer(context); }
if (services.openstreetcam) { services.openstreetcam.hideViewer(context); }
//if (services.openstreetcam) { services.openstreetcam.hideViewer(context); }
})
.append('div')
.call(svgIcon('#iD-icon-close'));

View File

@@ -52,6 +52,7 @@
"abortcontroller-polyfill": "^1.4.0",
"aes-js": "^3.1.2",
"alif-toolkit": "^1.2.9",
"base64-js": "1.5.1",
"core-js": "^3.6.5",
"diacritics": "1.3.0",
"fast-deep-equal": "~3.1.1",
@@ -94,7 +95,7 @@
"happen": "^0.3.1",
"js-yaml": "^4.0.0",
"json-stringify-pretty-compact": "^3.0.0",
"mapillary-js": "~3.1.0",
"mapillary-js": "4.0.0",
"mapillary_sprite_source": "^1.8.0",
"minimist": "^1.2.3",
"mocha": "^7.0.1",

View File

@@ -33,7 +33,8 @@ describe('iD.serviceMapillary', function() {
var cache = mapillary.cache();
expect(cache).to.have.property('images');
expect(cache).to.have.property('image_detections');
expect(cache).to.have.property('map_features');
expect(cache).to.have.property('points');
expect(cache).to.have.property('signs');
expect(cache).to.have.property('sequences');
mapillary.init();
@@ -91,26 +92,56 @@ describe('iD.serviceMapillary', function() {
describe('#signs', function() {
it('returns signs in the visible map area', function() {
var detections = [{
detection_key: '78vqha63gs1upg15s823qckcmn',
image_key: 'bwYs-uXLDvm_meo_EC5Nzw'
}];
var features = [
{ minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '0', loc: [10,0], detections: detections } },
{ minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '1', loc: [10,0], detections: detections } },
{ minX: 10, minY: 1, maxX: 10, maxY: 1, data: { key: '2', loc: [10,1], detections: detections } }
{ minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '0', loc: [10,0] } },
{ minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '1', loc: [10,0] } },
{ minX: 10, minY: 1, maxX: 10, maxY: 1, data: { key: '2', loc: [10,1] } }
];
mapillary.cache().map_features.rtree.load(features);
mapillary.cache().signs.rtree.load(features);
var res = mapillary.signs(context.projection);
expect(res).to.deep.eql([
{ key: '0', loc: [10,0], detections: detections },
{ key: '1', loc: [10,0], detections: detections }
{ key: '0', loc: [10,0] },
{ key: '1', loc: [10,0] }
]);
});
it('limits results no more than 5 stacked signs in one spot', function() {
var features = [
{ minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '0', loc: [10,0] } },
{ minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '1', loc: [10,0] } },
{ minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '2', loc: [10,0] } },
{ minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '3', loc: [10,0] } },
{ minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '4', loc: [10,0] } },
{ minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '5', loc: [10,0] } }
];
mapillary.cache().signs.rtree.load(features);
var res = mapillary.signs(context.projection);
expect(res).to.have.length.of.at.most(5);
});
});
describe('#mapFeatures', function() {
it('returns map features in the visible map area', function() {
var features = [
{ minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '0', loc: [10,0] } },
{ minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '1', loc: [10,0] } },
{ minX: 10, minY: 1, maxX: 10, maxY: 1, data: { key: '2', loc: [10,1] } }
];
mapillary.cache().points.rtree.load(features);
var res = mapillary.mapFeatures(context.projection);
expect(res).to.deep.eql([
{ key: '0', loc: [10,0] },
{ key: '1', loc: [10,0] }
]);
});
it('limits results no more than 5 stacked map features in one spot', function() {
var detections = [{
detection_key: '78vqha63gs1upg15s823qckcmn',
image_key: 'bwYs-uXLDvm_meo_EC5Nzw'
@@ -124,8 +155,8 @@ describe('iD.serviceMapillary', function() {
{ minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '5', loc: [10,0], detections: detections } }
];
mapillary.cache().map_features.rtree.load(features);
var res = mapillary.signs(context.projection);
mapillary.cache().points.rtree.load(features);
var res = mapillary.mapFeatures(context.projection);
expect(res).to.have.length.of.at.most(5);
});
});
@@ -134,9 +165,9 @@ describe('iD.serviceMapillary', function() {
describe('#sequences', function() {
it('returns sequence linestrings in the visible map area', function() {
var features = [
{ minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '0', loc: [10,0], ca: 90, skey: '-' } },
{ minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '1', loc: [10,0], ca: 90, skey: '-' } },
{ minX: 10, minY: 1, maxX: 10, maxY: 1, data: { key: '2', loc: [10,1], ca: 90, skey: '-' } }
{ minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '0', loc: [10,0], ca: 90, sequence_id: '-' } },
{ minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '1', loc: [10,0], ca: 90, sequence_id: '-' } },
{ minX: 10, minY: 1, maxX: 10, maxY: 1, data: { key: '2', loc: [10,1], ca: 90, sequence_id: '-' } }
];
mapillary.cache().images.rtree.load(features);
@@ -167,9 +198,9 @@ describe('iD.serviceMapillary', function() {
describe('#setActiveImage', function() {
it('gets and sets the selected image', function() {
var node = { key: 'baz', originalLatLon: [10,0] };
var node = { id: 'baz', originalLngLat: {lng: 10, lat: 0}};
mapillary.setActiveImage(node);
expect(mapillary.getActiveImage().key).to.eql(node.key);
expect(mapillary.getActiveImage().id).to.eql(node.id);
});
});