From d106dee0c619dd43c9646025e6a6d00e961cbcea Mon Sep 17 00:00:00 2001 From: Nikola Plesa Date: Thu, 23 Jul 2020 13:28:19 +0200 Subject: [PATCH 1/3] feat: date and username filtering for photo overlay layers --- css/80_app.css | 8 +- data/core.yaml | 10 +++ dist/locales/en.json | 14 +++ modules/renderer/photos.js | 43 +++++++++ modules/services/mapillary.js | 32 +++++++ modules/svg/mapillary_images.js | 43 +++++++++ modules/svg/openstreetcam_images.js | 28 ++++++ modules/svg/streetside.js | 22 +++++ modules/ui/sections/photo_overlays.js | 125 +++++++++++++++++++++++++- test/spec/services/mapillary.js | 15 ++++ 10 files changed, 337 insertions(+), 3 deletions(-) diff --git a/css/80_app.css b/css/80_app.css index d499eb905..0a1098ab0 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -168,7 +168,8 @@ input[type=search], input[type=number], input[type=url], input[type=tel], -input[type=email] { +input[type=email], +input[type=date] { background-color: #fff; color: #333; border: 1px solid #ccc; @@ -178,6 +179,11 @@ input[type=email] { text-overflow: ellipsis; overflow: hidden; } +input.list-item-input { + height: 20px; + padding: 0px 4px; + width: 160px; +} .ideditor[dir='rtl'] textarea, .ideditor[dir='rtl'] input[type=text], .ideditor[dir='rtl'] input[type=search], diff --git a/data/core.yaml b/data/core.yaml index 80d36ba07..c98e1fc21 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -740,6 +740,16 @@ en: panoramic: title: "Panoramic Photos" tooltip: "360° photos" + date_filter: + fromDate: + title: "From" + tooltip: "Show photos taken after this date" + toDate: + title: "To" + tooltip: "Show photos taken before this date" + username_filter: + title: "Username" + tooltip: "Show only photos by this user" feature: points: description: Points diff --git a/dist/locales/en.json b/dist/locales/en.json index 5f051d1da..a6b531df6 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -928,6 +928,20 @@ "title": "Panoramic Photos", "tooltip": "360° photos" } + }, + "date_filter": { + "fromDate": { + "title": "From", + "tooltip": "Show photos taken after this date" + }, + "toDate": { + "title": "To", + "tooltip": "Show photos taken before this date" + } + }, + "username_filter": { + "title": "Username", + "tooltip": "Show only photos by this user" } }, "feature": { diff --git a/modules/renderer/photos.js b/modules/renderer/photos.js index 62eb89ce9..5bd223d72 100644 --- a/modules/renderer/photos.js +++ b/modules/renderer/photos.js @@ -9,6 +9,10 @@ export function rendererPhotos(context) { var _layerIDs = ['streetside', 'mapillary', 'mapillary-map-features', 'mapillary-signs', 'openstreetcam']; var _allPhotoTypes = ['flat', 'panoramic']; var _shownPhotoTypes = _allPhotoTypes.slice(); // shallow copy + var _dateFilters = ['fromDate', 'toDate']; + var _fromDate; + var _toDate; + var _username; function photos() {} @@ -37,16 +41,43 @@ export function rendererPhotos(context) { return _allPhotoTypes; }; + photos.dateFilters = function() { + return _dateFilters; + }; + + photos.dateFilterValue = function(val) { + return val === _dateFilters[0] ? _fromDate : _toDate; + }; + + photos.setDateFilter = function(type, val) { + if (type === _dateFilters[0]) _fromDate = val; + if (type === _dateFilters[1]) _toDate = val; + dispatch.call('change', this); + }; + + photos.setUsernameFilter = function(val) { + _username = val; + dispatch.call('change', this); + }; + function showsLayer(id) { var layer = context.layers().layer(id); return layer && layer.supported() && layer.enabled(); } + photos.shouldFilterByDate = function() { + return showsLayer('mapillary') || showsLayer('openstreetcam') || showsLayer('streetside'); + }; + photos.shouldFilterByPhotoType = function() { return showsLayer('mapillary') || (showsLayer('streetside') && showsLayer('openstreetcam')); }; + photos.shouldFilterByUsername = function() { + return showsLayer('mapillary') || showsLayer('openstreetcam'); + }; + photos.showsPhotoType = function(val) { if (!photos.shouldFilterByPhotoType()) return true; @@ -61,6 +92,14 @@ export function rendererPhotos(context) { return photos.showsPhotoType('panoramic'); }; + photos.fromDate = function() { + return _fromDate; + }; + + photos.toDate = function() { + return _toDate; + }; + photos.togglePhotoType = function(val) { var index = _shownPhotoTypes.indexOf(val); if (index !== -1) { @@ -72,6 +111,10 @@ export function rendererPhotos(context) { return photos; }; + photos.username = function() { + return _username; + }; + photos.init = function() { var hash = utilStringQs(window.location.hash); if (hash.photo_overlay) { diff --git a/modules/services/mapillary.js b/modules/services/mapillary.js index 50ce60ede..4a917e8c4 100644 --- a/modules/services/mapillary.js +++ b/modules/services/mapillary.js @@ -46,6 +46,7 @@ var _mlyCache; var _mlyClicks; var _mlySelectedImageKey; var _mlyViewer; +var _mlyViewerFilter = ['all']; function abortRequest(controller) { @@ -417,6 +418,34 @@ export default { }); }, + filterViewer: function(context) { + var showsPano = context.photos().showsPanoramic(); + var showsFlat = context.photos().showsFlat(); + var fromDate = context.photos().fromDate(); + var toDate = context.photos().toDate(); + var username = context.photos().username(); + var filter = ['all']; + + if (!showsPano) filter.push(['==', 'pano', false]); + if (!showsFlat && showsPano) filter.push(['==', 'pano', true]); + if (username) filter.push(['==', 'username', username]); + if (fromDate) { + var fromTimestamp = new Date(fromDate).getTime(); + filter.push(['>=', 'capturedAt', fromTimestamp]); + } + if (toDate) { + var toTimestamp = new Date(toDate).getTime(); + filter.push(['>=', 'capturedAt', toTimestamp]); + } + + if (_mlyViewer) { + _mlyViewer.setFilter(filter); + } + _mlyViewerFilter = filter; + + return filter; + }, + showViewer: function(context) { var wrap = context.container().select('.photoviewer') @@ -512,6 +541,9 @@ export default { _mlyViewer.on('bearingchanged', bearingChanged); _mlyViewer.moveToKey(imageKey) .catch(function(e) { console.error('mly3', e); }); // eslint-disable-line no-console + if (_mlyViewerFilter) { + _mlyViewer.setFilter(_mlyViewerFilter); + } } // nodeChanged: called after the viewer has changed images and is ready. diff --git a/modules/svg/mapillary_images.js b/modules/svg/mapillary_images.js index 83f66bf76..cce1702bb 100644 --- a/modules/svg/mapillary_images.js +++ b/modules/svg/mapillary_images.js @@ -124,18 +124,43 @@ export function svgMapillaryImages(projection, context, dispatch) { function filterImages(images) { var showsPano = context.photos().showsPanoramic(); var showsFlat = context.photos().showsFlat(); + var fromDate = context.photos().fromDate(); + var toDate = context.photos().toDate(); + var username = context.photos().username(); + if (!showsPano || !showsFlat) { images = images.filter(function(image) { if (image.pano) return showsPano; return showsFlat; }); } + if (fromDate) { + var fromTimestamp = new Date(fromDate).getTime(); + images = images.filter(function(image) { + return new Date(image.captured_at).getTime() >= fromTimestamp; + }); + } + if (toDate) { + var toTimestamp = new Date(toDate).getTime(); + images = images.filter(function(image) { + return new Date(image.captured_at).getTime() <= toTimestamp; + }); + } + if (username) { + images = images.filter(function(image) { + return image.captured_by === username; + }); + } return images; } function filterSequences(sequences, service) { var showsPano = context.photos().showsPanoramic(); var showsFlat = context.photos().showsFlat(); + var fromDate = context.photos().fromDate(); + var toDate = context.photos().toDate(); + var username = context.photos().username(); + if (!showsPano || !showsFlat) { sequences = sequences.filter(function(sequence) { if (sequence.properties.hasOwnProperty('pano')) { @@ -157,6 +182,23 @@ export function svgMapillaryImages(projection, context, dispatch) { } }); } + if (fromDate) { + var fromTimestamp = new Date(fromDate).getTime(); + sequences = sequences.filter(function(sequence) { + return new Date(sequence.captured_at).getTime() >= fromTimestamp; + }); + } + if (toDate) { + var toTimestamp = new Date(toDate).getTime(); + sequences = sequences.filter(function(sequence) { + return new Date(sequence.captured_at).getTime() <= toTimestamp; + }); + } + if (username) { + sequences = sequences.filter(function(sequence) { + return sequence.captured_by === username; + }); + } return sequences; } @@ -173,6 +215,7 @@ export function svgMapillaryImages(projection, context, dispatch) { images = filterImages(images); sequences = filterSequences(sequences, service); + service.filterViewer(context); 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 1db4d6e22..61cbdfec1 100644 --- a/modules/svg/openstreetcam_images.js +++ b/modules/svg/openstreetcam_images.js @@ -105,6 +105,32 @@ export function svgOpenstreetcamImages(projection, context, dispatch) { context.photos().on('change.openstreetcam_images', update); + function filterImages(images) { + var fromDate = context.photos().fromDate(); + var toDate = context.photos().toDate(); + var username = context.photos().username(); + + if (fromDate) { + var fromTimestamp = new Date(fromDate).getTime(); + images = images.filter(function(image) { + return new Date(image.captured_at).getTime() >= fromTimestamp; + }); + } + if (toDate) { + var toTimestamp = new Date(toDate).getTime(); + images = images.filter(function(image) { + return new Date(image.captured_at).getTime() <= toTimestamp; + }); + } + if (username) { + images = images.filter(function(image) { + return image.captured_by === username; + }); + } + + return images; + } + function update() { var viewer = context.container().select('.photoviewer'); var selected = viewer.empty() ? undefined : viewer.datum(); @@ -121,6 +147,8 @@ export function svgOpenstreetcamImages(projection, context, dispatch) { sequences = (service ? service.sequences(projection) : []); images = (service && showMarkers ? service.images(projection) : []); } + + images = filterImages(images); 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 5d489e9aa..7f803822b 100644 --- a/modules/svg/streetside.js +++ b/modules/svg/streetside.js @@ -159,6 +159,26 @@ export function svgStreetside(projection, context, dispatch) { context.photos().on('change.streetside', update); + function filterBubbles(bubbles) { + var fromDate = context.photos().fromDate(); + var toDate = context.photos().toDate(); + + if (fromDate) { + var fromTimestamp = new Date(fromDate).getTime(); + bubbles = bubbles.filter(function(bubble) { + return new Date(bubble.captured_at).getTime() >= fromTimestamp; + }); + } + if (toDate) { + var toTimestamp = new Date(toDate).getTime(); + bubbles = bubbles.filter(function(bubble) { + return new Date(bubble.captured_at).getTime() <= toTimestamp; + }); + } + + return bubbles; + } + /** * update(). */ @@ -178,6 +198,8 @@ export function svgStreetside(projection, context, dispatch) { bubbles = (service && showMarkers ? service.bubbles(projection) : []); } + bubbles = filterBubbles(bubbles); + var traces = layer.selectAll('.sequences').selectAll('.sequence') .data(sequences, function(d) { return d.properties.key; }); diff --git a/modules/ui/sections/photo_overlays.js b/modules/ui/sections/photo_overlays.js index b3c83f2b2..802a22a3e 100644 --- a/modules/ui/sections/photo_overlays.js +++ b/modules/ui/sections/photo_overlays.js @@ -24,7 +24,9 @@ export function uiSectionPhotoOverlays(context) { .attr('class', 'photo-overlay-container') .merge(container) .call(drawPhotoItems) - .call(drawPhotoTypeItems); + .call(drawPhotoTypeItems) + .call(drawDateFilter) + .call(drawUsernameFilter); } function drawPhotoItems(selection) { @@ -92,7 +94,6 @@ export function uiSectionPhotoOverlays(context) { return t(id.replace(/-/g, '_') + '.title'); }); - // Update li .merge(liEnter) @@ -164,6 +165,126 @@ export function uiSectionPhotoOverlays(context) { .property('checked', typeEnabled); } + function drawDateFilter(selection) { + var data = context.photos().dateFilters(); + + function filterEnabled(d) { + return context.photos().dateFilterValue(d); + } + + var ul = selection + .selectAll('.layer-list-date-filter') + .data(context.photos().shouldFilterByDate() ? [0] : []); + + ul.exit() + .remove(); + + ul = ul.enter() + .append('ul') + .attr('class', 'layer-list layer-list-date-filter') + .merge(ul); + + var li = ul.selectAll('.list-item-date-filter') + .data(data); + + li.exit() + .remove(); + + var liEnter = li.enter() + .append('li') + .attr('class', 'list-item-date-filter'); + + var labelEnter = liEnter + .append('label') + .each(function(d) { + d3_select(this) + .call(uiTooltip() + .title(t('photo_overlays.date_filter.' + d + '.tooltip')) + .placement('top') + ); + }); + + labelEnter + .append('span') + .text(function(d) { + return t('photo_overlays.date_filter.' + d + '.title'); + }); + + labelEnter + .append('input') + .attr('type', 'date') + .attr('class', 'list-item-input') + .attr('placeholder', 'dd/mm/yyyy') + .on('change', function(d) { + var value = d3_select(this).property('value'); + context.photos().setDateFilter(d, value); + }); + + li + .merge(liEnter) + .classed('active', filterEnabled) + .selectAll('input') + .property('value', function(d) { + return context.photos().dateFilterValue(d); + }); + } + + function drawUsernameFilter(selection) { + function filterEnabled() { + return context.photos().username(); + } + var ul = selection + .selectAll('.layer-list-username-filter') + .data(context.photos().shouldFilterByUsername() ? [0] : []); + + ul.exit() + .remove(); + + ul = ul.enter() + .append('ul') + .attr('class', 'layer-list layer-list-username-filter') + .merge(ul); + + var li = ul.selectAll('.list-item-username-filter') + .data(['username-filter']); + + li.exit() + .remove(); + + var liEnter = li.enter() + .append('li') + .attr('class', 'list-item-username-filter'); + + var labelEnter = liEnter + .append('label') + .each(function() { + d3_select(this) + .call(uiTooltip() + .title(t('photo_overlays.username_filter.tooltip')) + .placement('top') + ); + }); + + labelEnter + .append('span') + .text(t('photo_overlays.username_filter.title')); + + labelEnter + .append('input') + .attr('type', 'text') + .attr('class', 'list-item-input') + .on('change', function() { + var value = d3_select(this).property('value'); + context.photos().setUsernameFilter(value); + }); + + li + .merge(liEnter) + .classed('active', filterEnabled) + .selectAll('input') + .property('value', context.photos().username()); + } + function toggleLayer(which) { setLayer(which, !showsLayer(which)); } diff --git a/test/spec/services/mapillary.js b/test/spec/services/mapillary.js index e7faf5d28..d32df854b 100644 --- a/test/spec/services/mapillary.js +++ b/test/spec/services/mapillary.js @@ -368,4 +368,19 @@ describe('iD.serviceMapillary', function() { }); }); + describe('#filterViewer', function() { + it('filters images by username', function() { + context.photos().setUsernameFilter('mapillary'); + var filter = mapillary.filterViewer(context); + expect(filter.length).to.be.equal(2); + }); + + it('filters images by dates', function() { + context.photos().setDateFilter('fromDate', '2020-01-01'); + context.photos().setDateFilter('toDate', '2021-01-01'); + var filter = mapillary.filterViewer(context); + expect(filter.length).to.be.equal(3); + }); + }); + }); From d7aa6f920bd938193f1e974ea8c173f0f13a8d8d Mon Sep 17 00:00:00 2001 From: Nikola Plesa Date: Thu, 23 Jul 2020 14:00:24 +0200 Subject: [PATCH 2/3] fix: mapillary sequence filtering --- modules/svg/mapillary_images.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/modules/svg/mapillary_images.js b/modules/svg/mapillary_images.js index cce1702bb..5301396f0 100644 --- a/modules/svg/mapillary_images.js +++ b/modules/svg/mapillary_images.js @@ -185,20 +185,21 @@ export function svgMapillaryImages(projection, context, dispatch) { if (fromDate) { var fromTimestamp = new Date(fromDate).getTime(); sequences = sequences.filter(function(sequence) { - return new Date(sequence.captured_at).getTime() >= fromTimestamp; + return new Date(sequence.properties.captured_at).getTime() >= fromTimestamp; }); } if (toDate) { var toTimestamp = new Date(toDate).getTime(); sequences = sequences.filter(function(sequence) { - return new Date(sequence.captured_at).getTime() <= toTimestamp; + return new Date(sequence.properties.captured_at).getTime() <= toTimestamp; }); } if (username) { sequences = sequences.filter(function(sequence) { - return sequence.captured_by === username; + return sequence.properties.username === username; }); } + return sequences; } From 8a5002aa15b0ffb4b9536ada58e2e20d7202cd78 Mon Sep 17 00:00:00 2001 From: Nikola Plesa Date: Fri, 24 Jul 2020 17:13:18 +0200 Subject: [PATCH 3/3] feat: filter sequences from openstreetcam and streetside --- modules/renderer/photos.js | 2 +- modules/services/openstreetcam.js | 7 ++++- modules/services/streetside.js | 6 ++++- modules/svg/openstreetcam_images.js | 42 +++++++++++++++++++++++------ modules/svg/streetside.js | 36 +++++++++++++++++++++++-- 5 files changed, 80 insertions(+), 13 deletions(-) diff --git a/modules/renderer/photos.js b/modules/renderer/photos.js index 5bd223d72..b93bab667 100644 --- a/modules/renderer/photos.js +++ b/modules/renderer/photos.js @@ -75,7 +75,7 @@ export function rendererPhotos(context) { }; photos.shouldFilterByUsername = function() { - return showsLayer('mapillary') || showsLayer('openstreetcam'); + return showsLayer('mapillary') || showsLayer('openstreetcam') || showsLayer('streetside'); }; photos.showsPhotoType = function(val) { diff --git a/modules/services/openstreetcam.js b/modules/services/openstreetcam.js index b516ce912..f37a31fba 100644 --- a/modules/services/openstreetcam.js +++ b/modules/services/openstreetcam.js @@ -217,11 +217,16 @@ export default { .forEach(function(sequenceKey) { var seq = _oscCache.sequences[sequenceKey]; var images = seq && seq.images; + if (images) { lineStrings.push({ type: 'LineString', coordinates: images.map(function (d) { return d.loc; }).filter(Boolean), - properties: { key: sequenceKey } + properties: { + captured_at: images[0] ? images[0].captured_at: null, + captured_by: images[0] ? images[0].captured_by: null, + key: sequenceKey + } }); } }); diff --git a/modules/services/streetside.js b/modules/services/streetside.js index 04ee1d41a..f7cb53ae2 100644 --- a/modules/services/streetside.js +++ b/modules/services/streetside.js @@ -177,7 +177,11 @@ function connectSequences() { // create a GeoJSON LineString sequence.geojson = { type: 'LineString', - properties: { key: sequence.key }, + properties: { + captured_at: sequence.bubbles[0] ? sequence.bubbles[0].captured_at : null, + captured_by: sequence.bubbles[0] ? sequence.bubbles[0].captured_by : null, + key: sequence.key + }, coordinates: sequence.bubbles.map(d => d.loc) }; diff --git a/modules/svg/openstreetcam_images.js b/modules/svg/openstreetcam_images.js index 61cbdfec1..bbd2fcf15 100644 --- a/modules/svg/openstreetcam_images.js +++ b/modules/svg/openstreetcam_images.js @@ -112,25 +112,51 @@ export function svgOpenstreetcamImages(projection, context, dispatch) { if (fromDate) { var fromTimestamp = new Date(fromDate).getTime(); - images = images.filter(function(image) { - return new Date(image.captured_at).getTime() >= fromTimestamp; + images = images.filter(function(item) { + return new Date(item.captured_at).getTime() >= fromTimestamp; }); } if (toDate) { var toTimestamp = new Date(toDate).getTime(); - images = images.filter(function(image) { - return new Date(image.captured_at).getTime() <= toTimestamp; + images = images.filter(function(item) { + return new Date(item.captured_at).getTime() <= toTimestamp; }); } if (username) { - images = images.filter(function(image) { - return image.captured_by === username; + images = images.filter(function(item) { + return item.captured_by === username; }); } return images; } + function filterSequences(sequences) { + var fromDate = context.photos().fromDate(); + var toDate = context.photos().toDate(); + var username = context.photos().username(); + + if (fromDate) { + var fromTimestamp = new Date(fromDate).getTime(); + sequences = sequences.filter(function(image) { + return new Date(image.properties.captured_at).getTime() >= fromTimestamp; + }); + } + if (toDate) { + var toTimestamp = new Date(toDate).getTime(); + sequences = sequences.filter(function(image) { + return new Date(image.properties.captured_at).getTime() <= toTimestamp; + }); + } + if (username) { + sequences = sequences.filter(function(image) { + return image.properties.captured_by === username; + }); + } + + return sequences; + } + function update() { var viewer = context.container().select('.photoviewer'); var selected = viewer.empty() ? undefined : viewer.datum(); @@ -146,10 +172,10 @@ export function svgOpenstreetcamImages(projection, context, dispatch) { if (context.photos().showsFlat()) { sequences = (service ? service.sequences(projection) : []); images = (service && showMarkers ? service.images(projection) : []); + sequences = filterSequences(sequences); + images = filterImages(images); } - images = filterImages(images); - 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 7f803822b..49213812d 100644 --- a/modules/svg/streetside.js +++ b/modules/svg/streetside.js @@ -162,6 +162,7 @@ export function svgStreetside(projection, context, dispatch) { function filterBubbles(bubbles) { var fromDate = context.photos().fromDate(); var toDate = context.photos().toDate(); + var username = context.photos().username(); if (fromDate) { var fromTimestamp = new Date(fromDate).getTime(); @@ -175,10 +176,41 @@ export function svgStreetside(projection, context, dispatch) { return new Date(bubble.captured_at).getTime() <= toTimestamp; }); } + if (username) { + bubbles = bubbles.filter(function(bubble) { + return bubble.captured_by === username; + }); + } return bubbles; } + function filterSequences(sequences) { + var fromDate = context.photos().fromDate(); + var toDate = context.photos().toDate(); + var username = context.photos().username(); + + if (fromDate) { + var fromTimestamp = new Date(fromDate).getTime(); + sequences = sequences.filter(function(sequences) { + return new Date(sequences.properties.captured_at).getTime() >= fromTimestamp; + }); + } + if (toDate) { + var toTimestamp = new Date(toDate).getTime(); + sequences = sequences.filter(function(sequences) { + return new Date(sequences.properties.captured_at).getTime() <= toTimestamp; + }); + } + if (username) { + sequences = sequences.filter(function(sequences) { + return sequences.properties.captured_by === username; + }); + } + + return sequences; + } + /** * update(). */ @@ -196,10 +228,10 @@ export function svgStreetside(projection, context, dispatch) { if (context.photos().showsPanoramic()) { sequences = (service ? service.sequences(projection) : []); bubbles = (service && showMarkers ? service.bubbles(projection) : []); + sequences = filterSequences(sequences); + bubbles = filterBubbles(bubbles); } - bubbles = filterBubbles(bubbles); - var traces = layer.selectAll('.sequences').selectAll('.sequence') .data(sequences, function(d) { return d.properties.key; });