mirror of
https://github.com/FoggedLens/iD.git
synced 2026-02-13 09:12:52 +00:00
802 lines
25 KiB
JavaScript
802 lines
25 KiB
JavaScript
/* global Mapillary:false */
|
|
import { dispatch as d3_dispatch } from 'd3-dispatch';
|
|
import { select as d3_select } from 'd3-selection';
|
|
|
|
import Protobuf from 'pbf';
|
|
import RBush from 'rbush';
|
|
import vt 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 viewercss = 'mapillary-js/mapillary.min.css';
|
|
const viewerjs = 'mapillary-js/mapillary.min.js';
|
|
const minZoom = 14;
|
|
const dispatch = d3_dispatch('change', 'loadedImages', 'loadedSigns', 'loadedMapFeatures', 'bearingChanged', 'nodeChanged');
|
|
|
|
let _loadViewerPromise;
|
|
let _mlyActiveImage;
|
|
let _mlyCache;
|
|
let _mlyClicks;
|
|
let _mlyFallback = false;
|
|
let _mlyHighlightedDetection;
|
|
let _mlyShowFeatureDetections = false;
|
|
let _mlyShowSignDetections = false;
|
|
let _mlyViewer;
|
|
let _mlyViewerFilter = ['all'];
|
|
|
|
function abortRequest(controller) {
|
|
controller.abort();
|
|
}
|
|
|
|
|
|
function loadTiles(which, url, maxZoom, projection) {
|
|
const tiler = utilTiler().zoomExtent([minZoom, maxZoom]).skipNullIsland(true);
|
|
const tiles = tiler.getTiles(projection);
|
|
|
|
tiles.forEach(function(tile) {
|
|
loadTile(which, url, tile);
|
|
});
|
|
}
|
|
|
|
|
|
function loadTile(which, url, tile) {
|
|
const cache = _mlyCache.requests;
|
|
const tileId = `${tile.id}-${which}`;
|
|
if (cache.loaded[tileId] || cache.inflight[tileId]) return;
|
|
const controller = new AbortController();
|
|
cache.inflight[tileId] = controller;
|
|
const requestUrl = url.replace('{x}', tile.xyz[0])
|
|
.replace('{y}', tile.xyz[1])
|
|
.replace('{z}', tile.xyz[2]);
|
|
|
|
fetch(requestUrl, { signal: controller.signal })
|
|
.then(function(response) {
|
|
if (!response.ok) {
|
|
throw new Error(response.status + ' ' + response.statusText);
|
|
}
|
|
cache.loaded[tileId] = true;
|
|
delete cache.inflight[tileId];
|
|
return response.arrayBuffer();
|
|
})
|
|
.then(function(data) {
|
|
if (!data) {
|
|
throw new Error('No Data');
|
|
}
|
|
loadTileDataToCache(data, tile, which);
|
|
|
|
if (which === 'images') {
|
|
dispatch.call('loadedImages');
|
|
} else if (which === 'map_features') {
|
|
dispatch.call('loadedSigns');
|
|
} else if (which === 'points') {
|
|
dispatch.call('loadedMapFeatures');
|
|
}
|
|
})
|
|
.catch(function() {
|
|
cache.loaded[tileId] = true;
|
|
delete cache.inflight[tileId];
|
|
});
|
|
}
|
|
|
|
|
|
function loadTileDataToCache(data, tile, which) {
|
|
const vectorTile = new vt.VectorTile(new Protobuf(data));
|
|
let features,
|
|
cache,
|
|
layer,
|
|
i,
|
|
feature,
|
|
loc,
|
|
d;
|
|
|
|
if (vectorTile.layers.hasOwnProperty('mapillary-images')) {
|
|
features = [];
|
|
cache = _mlyCache.images;
|
|
layer = vectorTile.layers['mapillary-images'];
|
|
|
|
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,
|
|
};
|
|
cache.forImageKey[d.key] = d;
|
|
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('mapillary-sequences')) {
|
|
features = [];
|
|
cache = _mlyCache.sequences;
|
|
layer = vectorTile.layers['mapillary-sequences'];
|
|
|
|
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);
|
|
} else {
|
|
cache.lineString[feature.properties.key] = [feature];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (vectorTile.layers.hasOwnProperty('mapillary-map-features')) {
|
|
features = [];
|
|
cache = _mlyCache[which];
|
|
layer = vectorTile.layers['mapillary-map-features'];
|
|
|
|
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),
|
|
first_seen_at: feature.properties.first_seen_at,
|
|
last_seen_at: feature.properties.last_seen_at
|
|
};
|
|
features.push({
|
|
minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d
|
|
});
|
|
}
|
|
if (cache.rtree) {
|
|
cache.rtree.load(features);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function loadData(which, url) {
|
|
const cache = _mlyCache[which];
|
|
const options = {
|
|
method: 'GET',
|
|
headers: { 'Content-Type': 'application/json' }
|
|
};
|
|
|
|
return fetch(url, 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) {
|
|
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
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
|
|
// 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
|
|
const tiler = utilTiler().zoomExtent([z2, z2]);
|
|
|
|
return tiler.getTiles(projection)
|
|
.map(function(tile) { return tile.extent; });
|
|
}
|
|
|
|
|
|
// no more than `limit` results per partition.
|
|
function searchLimited(limit, projection, rtree) {
|
|
limit = limit || 5;
|
|
|
|
return partitionViewport(projection)
|
|
.reduce(function(result, extent) {
|
|
const found = rtree.search(extent.bbox())
|
|
.slice(0, limit)
|
|
.map(function(d) { return d.data; });
|
|
|
|
return (found.length ? result.concat(found) : result);
|
|
}, []);
|
|
}
|
|
|
|
|
|
export default {
|
|
// initialize Mapillary
|
|
init: function() {
|
|
if (!_mlyCache) {
|
|
this.reset();
|
|
}
|
|
|
|
this.event = utilRebind(this, dispatch, 'on');
|
|
},
|
|
|
|
// reset cache and state
|
|
reset: function() {
|
|
if (_mlyCache) {
|
|
Object.values(_mlyCache.requests.inflight).forEach(abortRequest);
|
|
}
|
|
|
|
_mlyCache = {
|
|
images: { rtree: new RBush(), forImageKey: {} },
|
|
image_detections: { forImageKey: {} },
|
|
map_features: { rtree: new RBush() },
|
|
points: { rtree: new RBush() },
|
|
sequences: { rtree: new RBush(), lineString: {} },
|
|
requests: { loaded: {}, inflight: {} }
|
|
};
|
|
|
|
_mlyActiveImage = null;
|
|
_mlyClicks = [];
|
|
},
|
|
|
|
// get visible images
|
|
images: function(projection) {
|
|
const limit = 5;
|
|
return searchLimited(limit, projection, _mlyCache.images.rtree);
|
|
},
|
|
|
|
/**
|
|
* get visible traffic signs
|
|
*/
|
|
signs: function(projection) {
|
|
const limit = 5;
|
|
return searchLimited(limit, projection, _mlyCache.map_features.rtree);
|
|
},
|
|
|
|
// 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 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 = {};
|
|
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;
|
|
}
|
|
});
|
|
|
|
Object.keys(sequenceKeys).forEach(function(sequenceKey) {
|
|
lineStrings = lineStrings.concat(_mlyCache.sequences.lineString[sequenceKey]);
|
|
});
|
|
|
|
return lineStrings;
|
|
},
|
|
|
|
|
|
loadImages: function(projection) {
|
|
loadTiles('images', tileUrl, 14, projection);
|
|
},
|
|
|
|
|
|
loadSigns: function(projection) {
|
|
loadTiles('map_features', `${mapFeatureTileUrl}?layers=trafficsigns&per_page=1000&client_id=${clientId}`, 18, projection);
|
|
},
|
|
|
|
|
|
loadMapFeatures: function(projection) {
|
|
loadTiles('points', `${mapFeatureTileUrl}?layers=points&per_page=1000&client_id=${clientId}`, 18, projection);
|
|
},
|
|
|
|
|
|
ensureViewerLoaded: function(context) {
|
|
if (_loadViewerPromise) return _loadViewerPromise;
|
|
|
|
// add mly-wrapper
|
|
const wrap = context.container().select('.photoviewer')
|
|
.selectAll('.mly-wrapper')
|
|
.data([0]);
|
|
|
|
wrap.enter()
|
|
.append('div')
|
|
.attr('id', 'ideditor-mly')
|
|
.attr('class', 'photo-wrapper mly-wrapper')
|
|
.classed('hide', true);
|
|
|
|
const that = this;
|
|
|
|
_loadViewerPromise = new Promise((resolve, reject) => {
|
|
let loadedCount = 0;
|
|
function loaded() {
|
|
loadedCount += 1;
|
|
// wait until both files are loaded
|
|
if (loadedCount === 2) resolve();
|
|
}
|
|
|
|
const head = d3_select('head');
|
|
|
|
// load mapillary-viewercss
|
|
head.selectAll('#ideditor-mapillary-viewercss')
|
|
.data([0])
|
|
.enter()
|
|
.append('link')
|
|
.attr('id', 'ideditor-mapillary-viewercss')
|
|
.attr('rel', 'stylesheet')
|
|
.attr('crossorigin', 'anonymous')
|
|
.attr('href', context.asset(viewercss))
|
|
.on('load.serviceMapillary', loaded)
|
|
.on('error.serviceMapillary', function() {
|
|
reject();
|
|
});
|
|
|
|
// load mapillary-viewerjs
|
|
head.selectAll('#ideditor-mapillary-viewerjs')
|
|
.data([0])
|
|
.enter()
|
|
.append('script')
|
|
.attr('id', 'ideditor-mapillary-viewerjs')
|
|
.attr('crossorigin', 'anonymous')
|
|
.attr('src', context.asset(viewerjs))
|
|
.on('load.serviceMapillary', loaded)
|
|
.on('error.serviceMapillary', function() {
|
|
reject();
|
|
});
|
|
})
|
|
.catch(function() {
|
|
_loadViewerPromise = null;
|
|
})
|
|
.then(function() {
|
|
that.initViewer(context);
|
|
});
|
|
|
|
return _loadViewerPromise;
|
|
},
|
|
|
|
|
|
loadSignResources: function(context) {
|
|
context.ui().svgDefs.addSprites(['mapillary-sprite'], false /* don't override colors */ );
|
|
return this;
|
|
},
|
|
|
|
|
|
loadObjectResources: function(context) {
|
|
context.ui().svgDefs.addSprites(['mapillary-object-sprite'], false /* don't override colors */ );
|
|
return this;
|
|
},
|
|
|
|
|
|
// remove previous detections in viewer
|
|
resetTags: function() {
|
|
if (_mlyViewer && !_mlyFallback) {
|
|
_mlyViewer.getComponent('tag').removeAll();
|
|
}
|
|
},
|
|
|
|
|
|
// show map feature detections in viewer
|
|
showFeatureDetections: function(value) {
|
|
_mlyShowFeatureDetections = value;
|
|
if (!_mlyShowFeatureDetections && !_mlyShowSignDetections) {
|
|
this.resetTags();
|
|
}
|
|
},
|
|
|
|
|
|
// show traffic sign detections in viewer
|
|
showSignDetections: function(value) {
|
|
_mlyShowSignDetections = value;
|
|
if (!_mlyShowFeatureDetections && !_mlyShowSignDetections) {
|
|
this.resetTags();
|
|
}
|
|
},
|
|
|
|
|
|
// apply filter to viewer
|
|
filterViewer: function(context) {
|
|
const showsPano = context.photos().showsPanoramic();
|
|
const showsFlat = context.photos().showsFlat();
|
|
const fromDate = context.photos().fromDate();
|
|
const toDate = context.photos().toDate();
|
|
const filter = ['all'];
|
|
|
|
if (!showsPano) filter.push(['==', 'pano', false]);
|
|
if (!showsFlat && showsPano) filter.push(['==', 'pano', true]);
|
|
if (fromDate) {
|
|
filter.push(['>=', 'capturedAt', new Date(fromDate).getTime()]);
|
|
}
|
|
if (toDate) {
|
|
filter.push(['>=', 'capturedAt', new Date(toDate).getTime()]);
|
|
}
|
|
|
|
if (_mlyViewer) {
|
|
_mlyViewer.setFilter(filter);
|
|
}
|
|
_mlyViewerFilter = filter;
|
|
|
|
return filter;
|
|
},
|
|
|
|
|
|
showViewer: function(context) {
|
|
const wrap = context.container().select('.photoviewer')
|
|
.classed('hide', false);
|
|
|
|
const isHidden = wrap.selectAll('.photo-wrapper.mly-wrapper.hide').size();
|
|
|
|
if (isHidden && _mlyViewer) {
|
|
wrap
|
|
.selectAll('.photo-wrapper:not(.mly-wrapper)')
|
|
.classed('hide', true);
|
|
|
|
wrap
|
|
.selectAll('.photo-wrapper.mly-wrapper')
|
|
.classed('hide', false);
|
|
|
|
_mlyViewer.resize();
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
|
|
hideViewer: function(context) {
|
|
_mlyActiveImage = null;
|
|
|
|
if (!_mlyFallback && _mlyViewer) {
|
|
_mlyViewer.getComponent('sequence').stop();
|
|
}
|
|
|
|
const viewer = context.container().select('.photoviewer');
|
|
if (!viewer.empty()) viewer.datum(null);
|
|
|
|
viewer
|
|
.classed('hide', true)
|
|
.selectAll('.photo-wrapper')
|
|
.classed('hide', true);
|
|
|
|
this.updateUrlImage(null);
|
|
|
|
dispatch.call('nodeChanged');
|
|
dispatch.call('loadedMapFeatures');
|
|
dispatch.call('loadedSigns');
|
|
|
|
return this.setStyles(context, null, true);
|
|
},
|
|
|
|
|
|
// update the URL with current image key
|
|
updateUrlImage: function(imageKey) {
|
|
if (!window.mocha) {
|
|
const hash = utilStringQs(window.location.hash);
|
|
if (imageKey) {
|
|
hash.photo = 'mapillary/' + imageKey;
|
|
} else {
|
|
delete hash.photo;
|
|
}
|
|
window.location.replace('#' + utilQsString(hash, true));
|
|
}
|
|
},
|
|
|
|
|
|
// highlight the detection in the viewer that is related to the clicked map feature
|
|
highlightDetection: function(detection) {
|
|
if (detection) {
|
|
_mlyHighlightedDetection = detection.detection_key;
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
|
|
initViewer: function(context) {
|
|
const that = this;
|
|
if (!window.Mapillary) return;
|
|
|
|
const opts = {
|
|
apiClient: clientId,
|
|
baseImageSize: 320,
|
|
component: {
|
|
cover: false,
|
|
keyboard: false,
|
|
tag: true
|
|
},
|
|
container: 'ideditor-mly',
|
|
};
|
|
|
|
// Disable components requiring WebGL support
|
|
if (!Mapillary.isSupported() && Mapillary.isFallbackSupported()) {
|
|
_mlyFallback = true;
|
|
opts.component = {
|
|
cover: false,
|
|
direction: false,
|
|
imagePlane: false,
|
|
keyboard: false,
|
|
mouse: false,
|
|
sequence: false,
|
|
tag: false,
|
|
image: true, // fallback
|
|
navigation: true // fallback
|
|
};
|
|
}
|
|
|
|
_mlyViewer = new Mapillary.Viewer(opts);
|
|
_mlyViewer.on('nodechanged', nodeChanged);
|
|
_mlyViewer.on('bearingchanged', bearingChanged);
|
|
if (_mlyViewerFilter) {
|
|
_mlyViewer.setFilter(_mlyViewerFilter);
|
|
}
|
|
|
|
// Register viewer resize handler
|
|
context.ui().photoviewer.on('resize.mapillary', function() {
|
|
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) {
|
|
that.resetTags();
|
|
const clicks = _mlyClicks;
|
|
const index = clicks.indexOf(node.key);
|
|
that.setActiveImage(node);
|
|
that.setStyles(context, null, true);
|
|
|
|
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);
|
|
}
|
|
dispatch.call('nodeChanged');
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
|
|
getActiveImage: function() {
|
|
return _mlyActiveImage;
|
|
},
|
|
|
|
|
|
setActiveImage: function(node) {
|
|
if (node) {
|
|
_mlyActiveImage = {
|
|
ca: node.originalCA,
|
|
key: node.key,
|
|
loc: [node.originalLatLon.lon, node.originalLatLon.lat],
|
|
pano: node.pano,
|
|
sequenceKey: node.sequenceKey
|
|
};
|
|
} 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;
|
|
|
|
context.container().selectAll('.layer-mapillary .viewfield-group')
|
|
.classed('highlighted', function(d) { return d.skey === selectedSequenceKey; })
|
|
.classed('hovered', function(d) { return d.key === hoveredImageKey; });
|
|
|
|
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';
|
|
}
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
|
|
updateDetections: function(imageKey, 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] || []);
|
|
});
|
|
} else {
|
|
showDetections(_mlyCache.image_detections.forImageKey[imageKey]);
|
|
}
|
|
|
|
function showDetections(detections) {
|
|
const tagComponent = _mlyViewer.getComponent('tag');
|
|
detections.forEach(function(data) {
|
|
const tag = makeTag(data);
|
|
if (tag) {
|
|
tagComponent.add([tag]);
|
|
}
|
|
});
|
|
}
|
|
|
|
function makeTag(data) {
|
|
const valueParts = data.value.split('--');
|
|
if (!valueParts.length) return;
|
|
|
|
let tag;
|
|
let text;
|
|
let color = 0xffffff;
|
|
|
|
if (_mlyHighlightedDetection === data.key) {
|
|
color = 0xffff00;
|
|
text = valueParts[1];
|
|
if (text === 'flat' || text === 'discrete' || text === 'sign') {
|
|
text = valueParts[2];
|
|
}
|
|
text = text.replace(/-/g, ' ');
|
|
text = text.charAt(0).toUpperCase() + text.slice(1);
|
|
_mlyHighlightedDetection = null;
|
|
}
|
|
|
|
if (data.shape.type === 'Polygon') {
|
|
const polygonGeometry = new Mapillary
|
|
.TagComponent
|
|
.PolygonGeometry(data.shape.coordinates[0]);
|
|
|
|
tag = new Mapillary.TagComponent.OutlineTag(
|
|
data.key,
|
|
polygonGeometry,
|
|
{
|
|
text: text,
|
|
textColor: color,
|
|
lineColor: color,
|
|
lineWidth: 2,
|
|
fillColor: color,
|
|
fillOpacity: 0.3,
|
|
}
|
|
);
|
|
|
|
} else if (data.shape.type === 'Point') {
|
|
const pointGeometry = new Mapillary
|
|
.TagComponent
|
|
.PointGeometry(data.shape.coordinates[0]);
|
|
|
|
tag = new Mapillary.TagComponent.SpotTag(
|
|
data.key,
|
|
pointGeometry,
|
|
{
|
|
text: text,
|
|
color: color,
|
|
textColor: color
|
|
}
|
|
);
|
|
}
|
|
|
|
return tag;
|
|
}
|
|
},
|
|
|
|
|
|
cache: function() {
|
|
return _mlyCache;
|
|
}
|
|
};
|