From e87b0b9432c286abd7e12c9d41d3c51a9283d910 Mon Sep 17 00:00:00 2001 From: Nikola Plesa Date: Fri, 31 Jul 2020 14:55:17 +0200 Subject: [PATCH] feat: updates for mapillary map features and traffic signs --- modules/services/mapillary.js | 262 ++++++++++++++++++-------- modules/svg/layers.js | 2 + modules/svg/mapillary_images.js | 17 +- modules/svg/mapillary_map_features.js | 6 +- modules/svg/mapillary_position.js | 173 +++++++++++++++++ modules/svg/mapillary_signs.js | 3 +- 6 files changed, 367 insertions(+), 96 deletions(-) create mode 100644 modules/svg/mapillary_position.js diff --git a/modules/services/mapillary.js b/modules/services/mapillary.js index 50ce60ede..2011d0469 100644 --- a/modules/services/mapillary.js +++ b/modules/services/mapillary.js @@ -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 + 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; } diff --git a/modules/svg/layers.js b/modules/svg/layers.js index 114ba2d80..07f246952 100644 --- a/modules/svg/layers.js +++ b/modules/svg/layers.js @@ -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) }, diff --git a/modules/svg/mapillary_images.js b/modules/svg/mapillary_images.js index 83f66bf76..07c1af1db 100644 --- a/modules/svg/mapillary_images.js +++ b/modules/svg/mapillary_images.js @@ -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'); diff --git a/modules/svg/mapillary_map_features.js b/modules/svg/mapillary_map_features.js index f9eabd7f2..d31300bfa 100644 --- a/modules/svg/mapillary_map_features.js +++ b/modules/svg/mapillary_map_features.js @@ -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); } diff --git a/modules/svg/mapillary_position.js b/modules/svg/mapillary_position.js new file mode 100644 index 000000000..a606cd3c5 --- /dev/null +++ b/modules/svg/mapillary_position.js @@ -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; +} diff --git a/modules/svg/mapillary_signs.js b/modules/svg/mapillary_signs.js index 127566738..a68556366 100644 --- a/modules/svg/mapillary_signs.js +++ b/modules/svg/mapillary_signs.js @@ -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); }