From c65a4d038efb0b5588ecc07d2fa308af76d16647 Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Thu, 14 Mar 2019 14:24:21 -0400 Subject: [PATCH] Add photo overlay filter options for hiding flat and pano photos (close #5433) --- data/core.yaml | 14 ++++-- dist/locales/en.json | 18 ++++++-- modules/core/context.js | 8 +++- modules/renderer/index.js | 1 + modules/renderer/photos.js | 57 +++++++++++++++++++++++ modules/services/mapillary.js | 5 ++ modules/svg/mapillary_images.js | 43 +++++++++++++++++ modules/svg/openstreetcam_images.js | 11 ++++- modules/svg/streetside.js | 11 ++++- modules/ui/map_data.js | 71 +++++++++++++++++++++++++++-- 10 files changed, 225 insertions(+), 14 deletions(-) create mode 100644 modules/renderer/photos.js diff --git a/data/core.yaml b/data/core.yaml index 387270a8e..4f0f78fcf 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -527,13 +527,21 @@ en: tooltip: "Drag and drop a data file onto the page, or click the button to setup" title: Custom Map Data zoom: Zoom to data - photo_overlays: Photo Overlays fill_area: Fill Areas map_features: Map Features - traffic_signs: - title: Traffic Signs autohidden: "These features have been automatically hidden because too many would be shown on the screen. You can zoom in to edit them." osmhidden: "These features have been automatically hidden because the OpenStreetMap layer is hidden." + photo_overlays: + title: Photo Overlays + traffic_signs: + title: Traffic Signs + photo_type: + flat: + title: "Flat Photos" + tooltip: "Traditional photos" + panoramic: + title: "Panoramic Photos" + tooltip: "360° photos" feature: points: description: Points diff --git a/dist/locales/en.json b/dist/locales/en.json index 6285a7ffd..3cd56139c 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -644,14 +644,26 @@ "zoom": "Zoom to data" } }, - "photo_overlays": "Photo Overlays", "fill_area": "Fill Areas", "map_features": "Map Features", + "autohidden": "These features have been automatically hidden because too many would be shown on the screen. You can zoom in to edit them.", + "osmhidden": "These features have been automatically hidden because the OpenStreetMap layer is hidden." + }, + "photo_overlays": { + "title": "Photo Overlays", "traffic_signs": { "title": "Traffic Signs" }, - "autohidden": "These features have been automatically hidden because too many would be shown on the screen. You can zoom in to edit them.", - "osmhidden": "These features have been automatically hidden because the OpenStreetMap layer is hidden." + "photo_type": { + "flat": { + "title": "Flat Photos", + "tooltip": "Traditional photos" + }, + "panoramic": { + "title": "Panoramic Photos", + "tooltip": "360° photos" + } + } }, "feature": { "points": { diff --git a/modules/core/context.js b/modules/core/context.js index b20b2d1dc..5e3c696f1 100644 --- a/modules/core/context.js +++ b/modules/core/context.js @@ -18,7 +18,7 @@ import { dataLocales, dataEn } from '../../data'; import { geoRawMercator } from '../geo/raw_mercator'; import { modeSelect } from '../modes/select'; import { presetIndex } from '../presets'; -import { rendererBackground, rendererFeatures, rendererMap } from '../renderer'; +import { rendererBackground, rendererFeatures, rendererMap, rendererPhotos } from '../renderer'; import { services } from '../services'; import { uiInit } from '../ui/init'; import { utilDetect } from '../util/detect'; @@ -309,6 +309,11 @@ export function coreContext() { }; + /* Photos */ + var photos; + context.photos = function() { return photos; }; + + /* Presets */ var presets; context.presets = function() { return presets; }; @@ -501,6 +506,7 @@ export function coreContext() { connection = services.osm; background = rendererBackground(context); features = rendererFeatures(context); + photos = rendererPhotos(context); presets = presetIndex(context); if (services.maprules && utilStringQs(window.location.hash).maprules) { diff --git a/modules/renderer/index.js b/modules/renderer/index.js index ba7af8acb..4a4d7d36e 100644 --- a/modules/renderer/index.js +++ b/modules/renderer/index.js @@ -2,4 +2,5 @@ export { rendererBackgroundSource } from './background_source'; export { rendererBackground } from './background'; export { rendererFeatures } from './features'; export { rendererMap } from './map'; +export { rendererPhotos } from './photos'; export { rendererTileLayer } from './tile_layer'; diff --git a/modules/renderer/photos.js b/modules/renderer/photos.js new file mode 100644 index 000000000..ffb52d809 --- /dev/null +++ b/modules/renderer/photos.js @@ -0,0 +1,57 @@ +import _clone from 'lodash-es/clone'; + +import { dispatch as d3_dispatch } from 'd3-dispatch'; + +import { utilRebind } from '../util/rebind'; + + +export function rendererPhotos(context) { + + var dispatch = d3_dispatch('change'); + + var _allPhotoTypes = ['flat', 'panoramic']; + var _shownPhotoTypes = _clone(_allPhotoTypes); + + function photos() {} + + photos.allPhotoTypes = function() { + return _allPhotoTypes; + }; + + function showsLayer(id) { + var layer = context.layers().layer(id); + return layer && layer.supported() && layer.enabled(); + } + + photos.shouldFilterByPhotoType = function() { + return showsLayer('mapillary-images') || + (showsLayer('streetside') && showsLayer('openstreetcam-images')); + }; + + photos.showsPhotoType = function(val) { + if (!photos.shouldFilterByPhotoType()) return true; + + return _shownPhotoTypes.indexOf(val) !== -1; + }; + + photos.showsFlat = function() { + return photos.showsPhotoType('flat'); + }; + + photos.showsPanoramic = function() { + return photos.showsPhotoType('panoramic'); + }; + + photos.togglePhotoType = function(val) { + var index = _shownPhotoTypes.indexOf(val); + if (index !== -1) { + _shownPhotoTypes.splice(index, 1); + } else { + _shownPhotoTypes.push(val); + } + dispatch.call('change', this); + return photos; + }; + + return utilRebind(photos, dispatch, 'on'); +} diff --git a/modules/services/mapillary.js b/modules/services/mapillary.js index 3ebefbd3d..ef90b39d0 100644 --- a/modules/services/mapillary.js +++ b/modules/services/mapillary.js @@ -289,6 +289,11 @@ export default { }, + cachedImage: function(imageKey) { + return _mlyCache.images.forImageKey[imageKey]; + }, + + sequences: function(projection) { var viewport = projection.clipExtent(); var min = [viewport[0][0], viewport[1][1]]; diff --git a/modules/svg/mapillary_images.js b/modules/svg/mapillary_images.js index 96b56a5f6..e6458c29f 100644 --- a/modules/svg/mapillary_images.js +++ b/modules/svg/mapillary_images.js @@ -120,6 +120,46 @@ export function svgMapillaryImages(projection, context, dispatch) { return t; } + context.photos().on('change.mapillary_images', update); + + function filterImages(images) { + var showsPano = context.photos().showsPanoramic(); + var showsFlat = context.photos().showsFlat(); + if (!showsPano || !showsFlat) { + images = images.filter(function(image) { + if (image.pano) return showsPano; + return showsFlat; + }); + } + return images; + } + + function filterSequences(sequences, service) { + var showsPano = context.photos().showsPanoramic(); + var showsFlat = context.photos().showsFlat(); + if (!showsPano || !showsFlat) { + sequences = sequences.filter(function(sequence) { + if (sequence.properties.hasOwnProperty('pano')) { + if (sequence.properties.pano) return showsPano; + return showsFlat; + } else { + // if the sequence doesn't specify pano or not, search its images + var cProps = sequence.properties.coordinateProperties; + if (cProps && cProps.image_keys && cProps.image_keys.length > 0) { + for (var index in cProps.image_keys) { + var imageKey = cProps.image_keys[index]; + var image = service.cachedImage(imageKey); + if (image && image.hasOwnProperty('pano')) { + if (image.pano) return showsPano; + return showsFlat; + } + } + } + } + }); + } + return sequences; + } function update() { var viewer = d3_select('#photoviewer'); @@ -133,6 +173,9 @@ export function svgMapillaryImages(projection, context, dispatch) { var sequences = (service ? service.sequences(projection) : []); var images = (service && showMarkers ? service.images(projection) : []); + images = filterImages(images); + sequences = filterSequences(sequences, service); + var traces = layer.selectAll('.sequences').selectAll('.sequence') .data(sequences, function(d) { return d.properties.key; }); diff --git a/modules/svg/openstreetcam_images.js b/modules/svg/openstreetcam_images.js index a94bc5174..ccc7f55d1 100644 --- a/modules/svg/openstreetcam_images.js +++ b/modules/svg/openstreetcam_images.js @@ -104,6 +104,8 @@ export function svgOpenstreetcamImages(projection, context, dispatch) { } + context.photos().on('change.openstreetcam_images', update); + function update() { var viewer = d3_select('#photoviewer'); var selected = viewer.empty() ? undefined : viewer.datum(); @@ -113,8 +115,13 @@ export function svgOpenstreetcamImages(projection, context, dispatch) { var showViewfields = (z >= minViewfieldZoom); var service = getService(); - var sequences = (service ? service.sequences(projection) : []); - var images = (service && showMarkers ? service.images(projection) : []); + var sequences = []; + var images = []; + + if (context.photos().showsFlat()) { + sequences = (service ? service.sequences(projection) : []); + images = (service && showMarkers ? service.images(projection) : []); + } var traces = layer.selectAll('.sequences').selectAll('.sequence') .data(sequences, function(d) { return d.properties.key; }); diff --git a/modules/svg/streetside.js b/modules/svg/streetside.js index 9d5b43e06..47b0f1dac 100644 --- a/modules/svg/streetside.js +++ b/modules/svg/streetside.js @@ -158,6 +158,8 @@ export function svgStreetside(projection, context, dispatch) { } + context.photos().on('change.streetside', update); + /** * update(). */ @@ -169,8 +171,13 @@ export function svgStreetside(projection, context, dispatch) { var showViewfields = (z >= minViewfieldZoom); var service = getService(); - var sequences = (service ? service.sequences(projection) : []); - var bubbles = (service && showMarkers ? service.bubbles(projection) : []); + var sequences = []; + var bubbles = []; + + if (context.photos().showsPanoramic()) { + sequences = (service ? service.sequences(projection) : []); + bubbles = (service && showMarkers ? service.bubbles(projection) : []); + } var traces = layer.selectAll('.sequences').selectAll('.sequence') .data(sequences, function(d) { return d.properties.key; }); diff --git a/modules/ui/map_data.js b/modules/ui/map_data.js index f0e4498c5..9e7e26d8a 100644 --- a/modules/ui/map_data.js +++ b/modules/ui/map_data.js @@ -178,7 +178,7 @@ export function uiMapData(context) { var id = d.id; if (id === 'mapillary-images') id = 'mapillary'; if (id === 'openstreetcam-images') id = 'openstreetcam'; - if (id === 'mapillary-signs') id = 'map_data.traffic_signs'; + if (id === 'mapillary-signs') id = 'photo_overlays.traffic_signs'; return t(id.replace('-', '_') + '.title'); }); @@ -191,6 +191,70 @@ export function uiMapData(context) { .property('checked', layerEnabled); } + function drawPhotoTypeItems(selection) { + var data = context.photos().allPhotoTypes(); + + function typeEnabled(d) { + return context.photos().showsPhotoType(d); + } + + var ul = selection + .selectAll('.layer-list-photo-types') + .data(context.photos().shouldFilterByPhotoType() ? [0] : []); + + ul.exit() + .remove(); + + ul = ul.enter() + .append('ul') + .attr('class', 'layer-list layer-list-photo-types') + .merge(ul); + + var li = ul.selectAll('.list-item-photo-types') + .data(data); + + li.exit() + .remove(); + + var liEnter = li.enter() + .append('li') + .attr('class', function(d) { + return 'list-item-photo-types list-item-' + d; + }); + + var labelEnter = liEnter + .append('label') + .each(function(d) { + d3_select(this) + .call(tooltip() + .title(t('photo_overlays.photo_type.' + d + '.tooltip')) + .placement('top') + ); + }); + + labelEnter + .append('input') + .attr('type', 'checkbox') + .on('change', function(d) { + context.photos().togglePhotoType(d); + update(); + }); + + labelEnter + .append('span') + .text(function(d) { + return t('photo_overlays.photo_type.' + d + '.title'); + }); + + + // Update + li + .merge(liEnter) + .classed('active', typeEnabled) + .selectAll('input') + .property('checked', typeEnabled); + } + function drawOsmItems(selection) { var osmKeys = ['osm', 'notes']; @@ -605,7 +669,8 @@ export function uiMapData(context) { function updatePhotoOverlays() { _photoOverlayContainer - .call(drawPhotoItems); + .call(drawPhotoItems) + .call(drawPhotoTypeItems); } function updateDataLayers() { @@ -759,7 +824,7 @@ export function uiMapData(context) { .append('div') .attr('class', 'map-data-photo-overlays') .call(uiDisclosure(context, 'photo_overlays', false) - .title(t('map_data.photo_overlays')) + .title(t('photo_overlays.title')) .content(renderPhotoOverlays) );