From d106dee0c619dd43c9646025e6a6d00e961cbcea Mon Sep 17 00:00:00 2001 From: Nikola Plesa Date: Thu, 23 Jul 2020 13:28:19 +0200 Subject: [PATCH 01/22] 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 02/22] 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 03/22] 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; }); From 22bc5121a4ff659f83e8a36fbb3bd8b7e9236eeb Mon Sep 17 00:00:00 2001 From: Nikola Plesa Date: Thu, 23 Jul 2020 13:28:19 +0200 Subject: [PATCH 04/22] 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 202c932abe6ee477210dfbea188019f09494e64e Mon Sep 17 00:00:00 2001 From: Nikola Plesa Date: Thu, 23 Jul 2020 14:00:24 +0200 Subject: [PATCH 05/22] 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 23bce4261847ba9e2f61c629828828db6556acad Mon Sep 17 00:00:00 2001 From: Nikola Plesa Date: Fri, 24 Jul 2020 17:13:18 +0200 Subject: [PATCH 06/22] 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; }); From e324a00e544b434758fed73f4385a74b331046b3 Mon Sep 17 00:00:00 2001 From: Nikola Plesa Date: Mon, 31 Aug 2020 11:25:06 +0200 Subject: [PATCH 07/22] fix: fix for filter input values being reset on data loading --- modules/ui/sections/photo_overlays.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/modules/ui/sections/photo_overlays.js b/modules/ui/sections/photo_overlays.js index 802a22a3e..89faafa8c 100644 --- a/modules/ui/sections/photo_overlays.js +++ b/modules/ui/sections/photo_overlays.js @@ -215,6 +215,9 @@ export function uiSectionPhotoOverlays(context) { .attr('type', 'date') .attr('class', 'list-item-input') .attr('placeholder', 'dd/mm/yyyy') + .property('value', function(d) { + return context.photos().dateFilterValue(d); + }) .on('change', function(d) { var value = d3_select(this).property('value'); context.photos().setDateFilter(d, value); @@ -222,11 +225,7 @@ export function uiSectionPhotoOverlays(context) { li .merge(liEnter) - .classed('active', filterEnabled) - .selectAll('input') - .property('value', function(d) { - return context.photos().dateFilterValue(d); - }); + .classed('active', filterEnabled); } function drawUsernameFilter(selection) { @@ -273,6 +272,7 @@ export function uiSectionPhotoOverlays(context) { .append('input') .attr('type', 'text') .attr('class', 'list-item-input') + .property('value', context.photos().username()) .on('change', function() { var value = d3_select(this).property('value'); context.photos().setUsernameFilter(value); @@ -280,9 +280,7 @@ export function uiSectionPhotoOverlays(context) { li .merge(liEnter) - .classed('active', filterEnabled) - .selectAll('input') - .property('value', context.photos().username()); + .classed('active', filterEnabled); } function toggleLayer(which) { From d4736ef481b6bc233d6e72b2964ac3911d5c7afb Mon Sep 17 00:00:00 2001 From: Quincy Morgan <2046746+quincylvania@users.noreply.github.com> Date: Mon, 12 Oct 2020 16:18:46 -0400 Subject: [PATCH 08/22] Update date field styling --- css/80_app.css | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/css/80_app.css b/css/80_app.css index c82617273..d4b88b033 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -105,7 +105,8 @@ input[type=search]:focus, input[type=number]:focus, input[type=url]:focus, input[type=tel]:focus, -input[type=email]:focus { +input[type=email]:focus, +input[type=date]:focus { outline-color: transparent; outline-style: none; } @@ -192,7 +193,8 @@ input[type=search], input[type=number], input[type=url], input[type=tel], -input[type=email] { +input[type=email], +input[type=date] { /* need this since line-height interpretation may vary by font or browser */ height: 2.585em; } From c1746ae09f71f71ff92031f373f2484ae9710ca5 Mon Sep 17 00:00:00 2001 From: Nikola Plesa Date: Tue, 13 Oct 2020 10:42:28 +0200 Subject: [PATCH 09/22] Persist photo layer filters in the URL --- modules/renderer/photos.js | 31 +++++++++++++++++++++++++-- modules/ui/sections/photo_overlays.js | 4 ++-- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/modules/renderer/photos.js b/modules/renderer/photos.js index b93bab667..ca0b5fb0d 100644 --- a/modules/renderer/photos.js +++ b/modules/renderer/photos.js @@ -49,17 +49,35 @@ export function rendererPhotos(context) { return val === _dateFilters[0] ? _fromDate : _toDate; }; - photos.setDateFilter = function(type, val) { + photos.setDateFilter = function(type, val, updateUrl) { if (type === _dateFilters[0]) _fromDate = val; if (type === _dateFilters[1]) _toDate = val; dispatch.call('change', this); + if (updateUrl) { + setUrlFilterValue(type, val); + } }; - photos.setUsernameFilter = function(val) { + photos.setUsernameFilter = function(val, updateUrl) { _username = val; dispatch.call('change', this); + if (updateUrl) { + setUrlFilterValue('username', val); + } }; + function setUrlFilterValue(type, val) { + if (!window.mocha) { + var hash = utilStringQs(window.location.hash); + if (val) { + hash[type] = val; + } else { + delete hash[type]; + } + window.location.replace('#' + utilQsString(hash, true)); + } + } + function showsLayer(id) { var layer = context.layers().layer(id); return layer && layer.supported() && layer.enabled(); @@ -124,6 +142,15 @@ export function rendererPhotos(context) { if (layer) layer.enabled(true); }); } + if (hash.fromDate) { + this.setDateFilter('fromDate', hash.fromDate, false); + } + if (hash.toDate) { + this.setDateFilter('toDate', hash.toDate, false); + } + if (hash.username) { + this.setUsernameFilter(hash.username, false); + } context.layers().on('change.rendererPhotos', updateStorage); }; diff --git a/modules/ui/sections/photo_overlays.js b/modules/ui/sections/photo_overlays.js index 89faafa8c..deba09358 100644 --- a/modules/ui/sections/photo_overlays.js +++ b/modules/ui/sections/photo_overlays.js @@ -220,7 +220,7 @@ export function uiSectionPhotoOverlays(context) { }) .on('change', function(d) { var value = d3_select(this).property('value'); - context.photos().setDateFilter(d, value); + context.photos().setDateFilter(d, value, true); }); li @@ -275,7 +275,7 @@ export function uiSectionPhotoOverlays(context) { .property('value', context.photos().username()) .on('change', function() { var value = d3_select(this).property('value'); - context.photos().setUsernameFilter(value); + context.photos().setUsernameFilter(value, true); }); li From 058a6b087da31208210e72bd2eaf29132b48ebad Mon Sep 17 00:00:00 2001 From: Quincy Morgan <2046746+quincylvania@users.noreply.github.com> Date: Thu, 15 Oct 2020 13:58:57 -0400 Subject: [PATCH 10/22] Fix change event for d3 6 --- modules/ui/sections/photo_overlays.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/ui/sections/photo_overlays.js b/modules/ui/sections/photo_overlays.js index 2986d6083..ec93a4834 100644 --- a/modules/ui/sections/photo_overlays.js +++ b/modules/ui/sections/photo_overlays.js @@ -169,7 +169,7 @@ export function uiSectionPhotoOverlays(context) { var data = context.photos().dateFilters(); function filterEnabled(d) { - return context.photos().dateFilterValue(d); + return context.photos().dateFilterValue(d); } var ul = selection @@ -218,7 +218,7 @@ export function uiSectionPhotoOverlays(context) { .property('value', function(d) { return context.photos().dateFilterValue(d); }) - .on('change', function(d) { + .on('change', function(d3_event, d) { var value = d3_select(this).property('value'); context.photos().setDateFilter(d, value, true); }); @@ -230,7 +230,7 @@ export function uiSectionPhotoOverlays(context) { function drawUsernameFilter(selection) { function filterEnabled() { - return context.photos().username(); + return context.photos().username(); } var ul = selection .selectAll('.layer-list-username-filter') @@ -277,7 +277,7 @@ export function uiSectionPhotoOverlays(context) { var value = d3_select(this).property('value'); context.photos().setUsernameFilter(value, true); }); - + li .merge(liEnter) .classed('active', filterEnabled); From b2b97c55a6db3e7c946ff6b1e111a4e927ea6779 Mon Sep 17 00:00:00 2001 From: Quincy Morgan <2046746+quincylvania@users.noreply.github.com> Date: Thu, 15 Oct 2020 16:46:07 -0400 Subject: [PATCH 11/22] Don't show username filter for streetside --- modules/renderer/photos.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/renderer/photos.js b/modules/renderer/photos.js index daefdf2e3..366e2a6b4 100644 --- a/modules/renderer/photos.js +++ b/modules/renderer/photos.js @@ -94,7 +94,7 @@ export function rendererPhotos(context) { }; photos.shouldFilterByUsername = function() { - return showsLayer('mapillary') || showsLayer('openstreetcam') || showsLayer('streetside'); + return showsLayer('mapillary') || showsLayer('openstreetcam'); }; photos.showsPhotoType = function(val) { From 17d87359565233da460e54ad5a9472646dcf7a37 Mon Sep 17 00:00:00 2001 From: Quincy Morgan <2046746+quincylvania@users.noreply.github.com> Date: Thu, 15 Oct 2020 16:46:53 -0400 Subject: [PATCH 12/22] Read photo filters from hash before reading enabled overlays/photos --- modules/renderer/photos.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/modules/renderer/photos.js b/modules/renderer/photos.js index 366e2a6b4..4076a75b4 100644 --- a/modules/renderer/photos.js +++ b/modules/renderer/photos.js @@ -136,15 +136,6 @@ export function rendererPhotos(context) { photos.init = function() { var hash = utilStringQs(window.location.hash); - if (hash.photo_overlay) { - // support enabling photo layers by default via a URL parameter, e.g. `photo_overlay=openstreetcam;mapillary;streetside` - - var hashOverlayIDs = hash.photo_overlay.replace(/;/g, ',').split(','); - hashOverlayIDs.forEach(function(id) { - var layer = _layerIDs.indexOf(id) !== -1 && context.layers().layer(id); - if (layer && !layer.enabled()) layer.enabled(true); - }); - } if (hash.fromDate) { this.setDateFilter('fromDate', hash.fromDate, false); } @@ -154,6 +145,15 @@ export function rendererPhotos(context) { if (hash.username) { this.setUsernameFilter(hash.username, false); } + if (hash.photo_overlay) { + // support enabling photo layers by default via a URL parameter, e.g. `photo_overlay=openstreetcam;mapillary;streetside` + + var hashOverlayIDs = hash.photo_overlay.replace(/;/g, ',').split(','); + hashOverlayIDs.forEach(function(id) { + var layer = _layerIDs.indexOf(id) !== -1 && context.layers().layer(id); + if (layer && !layer.enabled()) layer.enabled(true); + }); + } if (hash.photo) { // support opening a photo via a URL parameter, e.g. `photo=mapillary-fztgSDtLpa08ohPZFZjeRQ` From a4e2b8043cbbea304e0c2b883d760098201e5cb7 Mon Sep 17 00:00:00 2001 From: Quincy Morgan <2046746+quincylvania@users.noreply.github.com> Date: Thu, 15 Oct 2020 16:48:45 -0400 Subject: [PATCH 13/22] Validate filter date values from hash/input Compensate if from date is set after to date --- modules/renderer/photos.js | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/modules/renderer/photos.js b/modules/renderer/photos.js index 4076a75b4..1c1d97569 100644 --- a/modules/renderer/photos.js +++ b/modules/renderer/photos.js @@ -51,8 +51,25 @@ export function rendererPhotos(context) { }; photos.setDateFilter = function(type, val, updateUrl) { - if (type === _dateFilters[0]) _fromDate = val; - if (type === _dateFilters[1]) _toDate = val; + // validate the date + var date = val && new Date(val); + if (date && !isNaN(date)) { + val = date.toISOString().substr(0, 10); + } else { + val = null; + } + if (type === _dateFilters[0]) { + _fromDate = val; + if (_fromDate && _toDate && new Date(_toDate) < new Date(_fromDate)) { + _toDate = _fromDate; + } + } + if (type === _dateFilters[1]) { + _toDate = val; + if (_fromDate && _toDate && new Date(_toDate) < new Date(_fromDate)) { + _fromDate = _toDate; + } + } dispatch.call('change', this); if (updateUrl) { setUrlFilterValue(type, val); @@ -118,7 +135,7 @@ export function rendererPhotos(context) { photos.toDate = function() { return _toDate; }; - + photos.togglePhotoType = function(val) { var index = _shownPhotoTypes.indexOf(val); if (index !== -1) { From 1c0892281d808c6590fca739411e2da27070d7b7 Mon Sep 17 00:00:00 2001 From: Quincy Morgan <2046746+quincylvania@users.noreply.github.com> Date: Thu, 15 Oct 2020 16:50:03 -0400 Subject: [PATCH 14/22] Use HTML translated string endpoints for photo option labels --- modules/ui/sections/photo_overlays.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/ui/sections/photo_overlays.js b/modules/ui/sections/photo_overlays.js index ec93a4834..a0f6dda12 100644 --- a/modules/ui/sections/photo_overlays.js +++ b/modules/ui/sections/photo_overlays.js @@ -153,7 +153,7 @@ export function uiSectionPhotoOverlays(context) { labelEnter .append('span') .html(function(d) { - return t('photo_overlays.photo_type.' + d + '.title'); + return t.html('photo_overlays.photo_type.' + d + '.title'); }); @@ -199,15 +199,15 @@ export function uiSectionPhotoOverlays(context) { .each(function(d) { d3_select(this) .call(uiTooltip() - .title(t('photo_overlays.date_filter.' + d + '.tooltip')) + .title(t.html('photo_overlays.date_filter.' + d + '.tooltip')) .placement('top') ); }); labelEnter .append('span') - .text(function(d) { - return t('photo_overlays.date_filter.' + d + '.title'); + .html(function(d) { + return t.html('photo_overlays.date_filter.' + d + '.title'); }); labelEnter @@ -259,14 +259,14 @@ export function uiSectionPhotoOverlays(context) { .each(function() { d3_select(this) .call(uiTooltip() - .title(t('photo_overlays.username_filter.tooltip')) + .title(t.html('photo_overlays.username_filter.tooltip')) .placement('top') ); }); labelEnter .append('span') - .text(t('photo_overlays.username_filter.title')); + .html(t.html('photo_overlays.username_filter.title')); labelEnter .append('input') From 730e16c1cf41d374a57a8ab30e0153e2f4c41dca Mon Sep 17 00:00:00 2001 From: Quincy Morgan <2046746+quincylvania@users.noreply.github.com> Date: Thu, 15 Oct 2020 16:52:03 -0400 Subject: [PATCH 15/22] Use localized placeholder for expected date filter format --- data/core.yaml | 2 ++ dist/locales/en.json | 3 ++- modules/ui/sections/photo_overlays.js | 7 ++++--- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/data/core.yaml b/data/core.yaml index d0313ecc9..c97715be5 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -2312,6 +2312,8 @@ en: west: "W" coordinate: "{coordinate}{direction}" coordinate_pair: "{latitude}, {longitude}" + # translate the letters but leave the format the same + year_month_day: "YYYY-MM-DD" wikidata: identifier: "Identifier" label: "Label" diff --git a/dist/locales/en.json b/dist/locales/en.json index 2cadfbedb..5d839f3a9 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -2858,7 +2858,8 @@ "east": "E", "west": "W", "coordinate": "{coordinate}{direction}", - "coordinate_pair": "{latitude}, {longitude}" + "coordinate_pair": "{latitude}, {longitude}", + "year_month_day": "YYYY-MM-DD" }, "wikidata": { "identifier": "Identifier", diff --git a/modules/ui/sections/photo_overlays.js b/modules/ui/sections/photo_overlays.js index a0f6dda12..cd68bff1e 100644 --- a/modules/ui/sections/photo_overlays.js +++ b/modules/ui/sections/photo_overlays.js @@ -5,6 +5,7 @@ import { import { t } from '../../core/localizer'; import { uiTooltip } from '../tooltip'; import { uiSection } from '../section'; +import { utilGetSetValue } from '../../util'; export function uiSectionPhotoOverlays(context) { @@ -214,9 +215,9 @@ export function uiSectionPhotoOverlays(context) { .append('input') .attr('type', 'date') .attr('class', 'list-item-input') - .attr('placeholder', 'dd/mm/yyyy') - .property('value', function(d) { - return context.photos().dateFilterValue(d); + .attr('placeholder', t('units.year_month_day')) + .each(function(d) { + utilGetSetValue(d3_select(this), context.photos().dateFilterValue(d) || ''); }) .on('change', function(d3_event, d) { var value = d3_select(this).property('value'); From 88b6d4a82e660fd6dc390e94ce59c05f5d4d6b82 Mon Sep 17 00:00:00 2001 From: Quincy Morgan <2046746+quincylvania@users.noreply.github.com> Date: Thu, 15 Oct 2020 16:52:46 -0400 Subject: [PATCH 16/22] Reload date filter values after input change --- modules/ui/sections/photo_overlays.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/modules/ui/sections/photo_overlays.js b/modules/ui/sections/photo_overlays.js index cd68bff1e..6df04b70a 100644 --- a/modules/ui/sections/photo_overlays.js +++ b/modules/ui/sections/photo_overlays.js @@ -220,11 +220,16 @@ export function uiSectionPhotoOverlays(context) { utilGetSetValue(d3_select(this), context.photos().dateFilterValue(d) || ''); }) .on('change', function(d3_event, d) { - var value = d3_select(this).property('value'); + var value = utilGetSetValue(d3_select(this)).trim(); context.photos().setDateFilter(d, value, true); + // reload the displayed dates + li.selectAll('input') + .each(function(d) { + utilGetSetValue(d3_select(this), context.photos().dateFilterValue(d) || ''); + }); }); - li + li = li .merge(liEnter) .classed('active', filterEnabled); } From 893629da4f38a4ea9811b606aa934129ee914882 Mon Sep 17 00:00:00 2001 From: Quincy Morgan <2046746+quincylvania@users.noreply.github.com> Date: Mon, 19 Oct 2020 15:43:25 -0400 Subject: [PATCH 17/22] Combine `fromDate` and `toDate` parameters into single `photo_dates` parameter Rename `username` parameter to `photo_username` Add API documentation of photo filter parameters --- API.md | 4 ++++ modules/renderer/photos.js | 30 ++++++++++++++++++------------ 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/API.md b/API.md index 9a6f38b83..4e4f994fe 100644 --- a/API.md +++ b/API.md @@ -43,6 +43,10 @@ of iD (e.g. `https://ideditor-release.netlify.app`), the following parameters ar * __`photo_overlay`__ - The street-level photo overlay layers to enable.
_Example:_ `photo_overlay=streetside,mapillary,openstreetcam`
_Available values:_ `streetside` (Microsoft Bing), `mapillary`, `mapillary-signs`, `mapillary-map-features`, `openstreetcam` +* __`photo_dates`__ - The range of capture dates by which to filter street-level photos. Dates are given in YYYY-MM-DD format and separated by `_`. One-sided ranges are supported.
+ _Example:_ `photo_dates=2019-01-01_2020-12-31`, `photo_dates=2019-01-01_`, `photo_dates=_2020-12-31`
+* __`photo_username`__ - The Mapillary or OpenStreetCam username by which to filter street-level photos.
+ _Example:_ `photo_user=quincylvania`
* __`photo`__ - The service and ID of the street-level photo to show.
_Example:_ `photo=streetside/718514589`
_Available prefixes:_ `streetside/`, `mapillary/`, `openstreetcam/` diff --git a/modules/renderer/photos.js b/modules/renderer/photos.js index 1c1d97569..bc4d984a1 100644 --- a/modules/renderer/photos.js +++ b/modules/renderer/photos.js @@ -72,7 +72,11 @@ export function rendererPhotos(context) { } dispatch.call('change', this); if (updateUrl) { - setUrlFilterValue(type, val); + var rangeString; + if (_fromDate || _toDate) { + rangeString = (_fromDate || '') + '_' + (_toDate || ''); + } + setUrlFilterValue('photo_dates', rangeString); } }; @@ -80,17 +84,19 @@ export function rendererPhotos(context) { _username = val; dispatch.call('change', this); if (updateUrl) { - setUrlFilterValue('username', val); + setUrlFilterValue('photo_username', val); } }; - function setUrlFilterValue(type, val) { + function setUrlFilterValue(property, val) { if (!window.mocha) { var hash = utilStringQs(window.location.hash); if (val) { - hash[type] = val; + if (hash[property] === val) return; + hash[property] = val; } else { - delete hash[type]; + if (!(property in hash)) return; + delete hash[property]; } window.location.replace('#' + utilQsString(hash, true)); } @@ -153,14 +159,14 @@ export function rendererPhotos(context) { photos.init = function() { var hash = utilStringQs(window.location.hash); - if (hash.fromDate) { - this.setDateFilter('fromDate', hash.fromDate, false); + if (hash.photo_dates) { + // expect format like `photo_dates=2019-01-01_2020-12-31`, but allow a few different separators + var parts = /^(.*)[–\/_+:](.*)$/g.exec(hash.photo_dates.trim()); + this.setDateFilter('fromDate', parts && parts.length >= 2 && parts[1], false); + this.setDateFilter('toDate', parts && parts.length >= 3 && parts[2], false); } - if (hash.toDate) { - this.setDateFilter('toDate', hash.toDate, false); - } - if (hash.username) { - this.setUsernameFilter(hash.username, false); + if (hash.photo_username) { + this.setUsernameFilter(hash.photo_username, false); } if (hash.photo_overlay) { // support enabling photo layers by default via a URL parameter, e.g. `photo_overlay=openstreetcam;mapillary;streetside` From c6fd22e420acaea1626bca2779b444dd008d7a0b Mon Sep 17 00:00:00 2001 From: Quincy Morgan <2046746+quincylvania@users.noreply.github.com> Date: Mon, 19 Oct 2020 15:49:59 -0400 Subject: [PATCH 18/22] Disable auto features on photo filter inputs --- modules/ui/sections/photo_overlays.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/ui/sections/photo_overlays.js b/modules/ui/sections/photo_overlays.js index 6df04b70a..9ea1a8739 100644 --- a/modules/ui/sections/photo_overlays.js +++ b/modules/ui/sections/photo_overlays.js @@ -5,7 +5,7 @@ import { import { t } from '../../core/localizer'; import { uiTooltip } from '../tooltip'; import { uiSection } from '../section'; -import { utilGetSetValue } from '../../util'; +import { utilGetSetValue, utilNoAuto } from '../../util'; export function uiSectionPhotoOverlays(context) { @@ -216,6 +216,7 @@ export function uiSectionPhotoOverlays(context) { .attr('type', 'date') .attr('class', 'list-item-input') .attr('placeholder', t('units.year_month_day')) + .call(utilNoAuto) .each(function(d) { utilGetSetValue(d3_select(this), context.photos().dateFilterValue(d) || ''); }) @@ -278,6 +279,7 @@ export function uiSectionPhotoOverlays(context) { .append('input') .attr('type', 'text') .attr('class', 'list-item-input') + .call(utilNoAuto) .property('value', context.photos().username()) .on('change', function() { var value = d3_select(this).property('value'); From f5d53caaf7d8e8203ca7d35d2bf75f42ac7d3a8a Mon Sep 17 00:00:00 2001 From: Quincy Morgan <2046746+quincylvania@users.noreply.github.com> Date: Mon, 19 Oct 2020 15:51:46 -0400 Subject: [PATCH 19/22] Don't allow date separators that could be used in some date formats --- modules/renderer/photos.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/renderer/photos.js b/modules/renderer/photos.js index bc4d984a1..865797e5e 100644 --- a/modules/renderer/photos.js +++ b/modules/renderer/photos.js @@ -160,8 +160,8 @@ export function rendererPhotos(context) { photos.init = function() { var hash = utilStringQs(window.location.hash); if (hash.photo_dates) { - // expect format like `photo_dates=2019-01-01_2020-12-31`, but allow a few different separators - var parts = /^(.*)[–\/_+:](.*)$/g.exec(hash.photo_dates.trim()); + // expect format like `photo_dates=2019-01-01_2020-12-31`, but allow a couple different separators + var parts = /^(.*)[–_](.*)$/g.exec(hash.photo_dates.trim()); this.setDateFilter('fromDate', parts && parts.length >= 2 && parts[1], false); this.setDateFilter('toDate', parts && parts.length >= 3 && parts[2], false); } From 081db7c7a6e8d778c27dfa0c988576106249dcd0 Mon Sep 17 00:00:00 2001 From: Quincy Morgan <2046746+quincylvania@users.noreply.github.com> Date: Mon, 19 Oct 2020 16:27:07 -0400 Subject: [PATCH 20/22] Support filtering by multiple photo usernames --- API.md | 4 ++-- modules/renderer/photos.js | 23 +++++++++++++++++------ modules/services/mapillary.js | 4 ++-- modules/svg/mapillary_images.js | 12 ++++++------ modules/svg/openstreetcam_images.js | 14 +++++++------- modules/svg/streetside.js | 12 ++++++------ modules/ui/sections/photo_overlays.js | 11 +++++++++-- 7 files changed, 49 insertions(+), 31 deletions(-) diff --git a/API.md b/API.md index 4e4f994fe..de5a9a21c 100644 --- a/API.md +++ b/API.md @@ -45,8 +45,8 @@ of iD (e.g. `https://ideditor-release.netlify.app`), the following parameters ar _Available values:_ `streetside` (Microsoft Bing), `mapillary`, `mapillary-signs`, `mapillary-map-features`, `openstreetcam` * __`photo_dates`__ - The range of capture dates by which to filter street-level photos. Dates are given in YYYY-MM-DD format and separated by `_`. One-sided ranges are supported.
_Example:_ `photo_dates=2019-01-01_2020-12-31`, `photo_dates=2019-01-01_`, `photo_dates=_2020-12-31`
-* __`photo_username`__ - The Mapillary or OpenStreetCam username by which to filter street-level photos.
- _Example:_ `photo_user=quincylvania`
+* __`photo_username`__ - The Mapillary or OpenStreetCam username by which to filter street-level photos. Multiple comma-separated usernames are supported.
+ _Example:_ `photo_user=quincylvania`, `photo_user=quincylvania,chrisbeddow`
* __`photo`__ - The service and ID of the street-level photo to show.
_Example:_ `photo=streetside/718514589`
_Available prefixes:_ `streetside/`, `mapillary/`, `openstreetcam/` diff --git a/modules/renderer/photos.js b/modules/renderer/photos.js index 865797e5e..d8267a048 100644 --- a/modules/renderer/photos.js +++ b/modules/renderer/photos.js @@ -13,7 +13,7 @@ export function rendererPhotos(context) { var _dateFilters = ['fromDate', 'toDate']; var _fromDate; var _toDate; - var _username; + var _usernames; function photos() {} @@ -81,10 +81,21 @@ export function rendererPhotos(context) { }; photos.setUsernameFilter = function(val, updateUrl) { - _username = val; + if (val && typeof val === 'string') val = val.replace(/;/g, ',').split(','); + if (val) { + val = val.map(d => d.trim()).filter(Boolean); + if (!val.length) { + val = null; + } + } + _usernames = val; dispatch.call('change', this); if (updateUrl) { - setUrlFilterValue('photo_username', val); + var hashString; + if (_usernames) { + hashString = _usernames.join(','); + } + setUrlFilterValue('photo_username', hashString); } }; @@ -117,7 +128,7 @@ export function rendererPhotos(context) { }; photos.shouldFilterByUsername = function() { - return showsLayer('mapillary') || showsLayer('openstreetcam'); + return showsLayer('mapillary') || showsLayer('openstreetcam') || showsLayer('streetside'); }; photos.showsPhotoType = function(val) { @@ -153,8 +164,8 @@ export function rendererPhotos(context) { return photos; }; - photos.username = function() { - return _username; + photos.usernames = function() { + return _usernames; }; photos.init = function() { diff --git a/modules/services/mapillary.js b/modules/services/mapillary.js index ee44d441f..60e969f5a 100644 --- a/modules/services/mapillary.js +++ b/modules/services/mapillary.js @@ -492,12 +492,12 @@ export default { var showsFlat = context.photos().showsFlat(); var fromDate = context.photos().fromDate(); var toDate = context.photos().toDate(); - var username = context.photos().username(); + var usernames = context.photos().usernames(); var filter = ['all']; if (!showsPano) filter.push(['==', 'pano', false]); if (!showsFlat && showsPano) filter.push(['==', 'pano', true]); - if (username) filter.push(['==', 'username', username]); + if (usernames && usernames.length) filter.push(['==', 'username', usernames[0]]); if (fromDate) { var fromTimestamp = new Date(fromDate).getTime(); filter.push(['>=', 'capturedAt', fromTimestamp]); diff --git a/modules/svg/mapillary_images.js b/modules/svg/mapillary_images.js index 88b148e20..515bdda12 100644 --- a/modules/svg/mapillary_images.js +++ b/modules/svg/mapillary_images.js @@ -116,7 +116,7 @@ export function svgMapillaryImages(projection, context, dispatch) { var showsFlat = context.photos().showsFlat(); var fromDate = context.photos().fromDate(); var toDate = context.photos().toDate(); - var username = context.photos().username(); + var usernames = context.photos().usernames(); if (!showsPano || !showsFlat) { images = images.filter(function(image) { @@ -136,9 +136,9 @@ export function svgMapillaryImages(projection, context, dispatch) { return new Date(image.captured_at).getTime() <= toTimestamp; }); } - if (username) { + if (usernames) { images = images.filter(function(image) { - return image.captured_by === username; + return usernames.indexOf(image.captured_by) !== -1; }); } return images; @@ -149,7 +149,7 @@ export function svgMapillaryImages(projection, context, dispatch) { var showsFlat = context.photos().showsFlat(); var fromDate = context.photos().fromDate(); var toDate = context.photos().toDate(); - var username = context.photos().username(); + var usernames = context.photos().usernames(); if (!showsPano || !showsFlat) { sequences = sequences.filter(function(sequence) { @@ -184,9 +184,9 @@ export function svgMapillaryImages(projection, context, dispatch) { return new Date(sequence.properties.captured_at).getTime() <= toTimestamp; }); } - if (username) { + if (usernames) { sequences = sequences.filter(function(sequence) { - return sequence.properties.username === username; + return usernames.indexOf(sequence.properties.username) !== -1; }); } diff --git a/modules/svg/openstreetcam_images.js b/modules/svg/openstreetcam_images.js index aac65acdd..786ac4a79 100644 --- a/modules/svg/openstreetcam_images.js +++ b/modules/svg/openstreetcam_images.js @@ -110,7 +110,7 @@ export function svgOpenstreetcamImages(projection, context, dispatch) { function filterImages(images) { var fromDate = context.photos().fromDate(); var toDate = context.photos().toDate(); - var username = context.photos().username(); + var usernames = context.photos().usernames(); if (fromDate) { var fromTimestamp = new Date(fromDate).getTime(); @@ -124,9 +124,9 @@ export function svgOpenstreetcamImages(projection, context, dispatch) { return new Date(item.captured_at).getTime() <= toTimestamp; }); } - if (username) { + if (usernames) { images = images.filter(function(item) { - return item.captured_by === username; + return usernames.indexOf(item.captured_by) !== -1; }); } @@ -136,7 +136,7 @@ export function svgOpenstreetcamImages(projection, context, dispatch) { function filterSequences(sequences) { var fromDate = context.photos().fromDate(); var toDate = context.photos().toDate(); - var username = context.photos().username(); + var usernames = context.photos().usernames(); if (fromDate) { var fromTimestamp = new Date(fromDate).getTime(); @@ -150,9 +150,9 @@ export function svgOpenstreetcamImages(projection, context, dispatch) { return new Date(image.properties.captured_at).getTime() <= toTimestamp; }); } - if (username) { + if (usernames) { sequences = sequences.filter(function(image) { - return image.properties.captured_by === username; + return usernames.indexOf(image.properties.captured_by) !== -1; }); } @@ -177,7 +177,7 @@ export function svgOpenstreetcamImages(projection, context, dispatch) { sequences = filterSequences(sequences); 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 e591824b7..ab1bc4a15 100644 --- a/modules/svg/streetside.js +++ b/modules/svg/streetside.js @@ -162,7 +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(); + var usernames = context.photos().usernames(); if (fromDate) { var fromTimestamp = new Date(fromDate).getTime(); @@ -176,9 +176,9 @@ export function svgStreetside(projection, context, dispatch) { return new Date(bubble.captured_at).getTime() <= toTimestamp; }); } - if (username) { + if (usernames) { bubbles = bubbles.filter(function(bubble) { - return bubble.captured_by === username; + return usernames.indexOf(bubble.captured_by) !== -1; }); } @@ -188,7 +188,7 @@ export function svgStreetside(projection, context, dispatch) { function filterSequences(sequences) { var fromDate = context.photos().fromDate(); var toDate = context.photos().toDate(); - var username = context.photos().username(); + var usernames = context.photos().usernames(); if (fromDate) { var fromTimestamp = new Date(fromDate).getTime(); @@ -202,9 +202,9 @@ export function svgStreetside(projection, context, dispatch) { return new Date(sequences.properties.captured_at).getTime() <= toTimestamp; }); } - if (username) { + if (usernames) { sequences = sequences.filter(function(sequences) { - return sequences.properties.captured_by === username; + return usernames.indexOf(sequences.properties.captured_by) !== -1; }); } diff --git a/modules/ui/sections/photo_overlays.js b/modules/ui/sections/photo_overlays.js index 9ea1a8739..7d661c5b1 100644 --- a/modules/ui/sections/photo_overlays.js +++ b/modules/ui/sections/photo_overlays.js @@ -237,7 +237,7 @@ export function uiSectionPhotoOverlays(context) { function drawUsernameFilter(selection) { function filterEnabled() { - return context.photos().username(); + return context.photos().usernames(); } var ul = selection .selectAll('.layer-list-username-filter') @@ -280,15 +280,22 @@ export function uiSectionPhotoOverlays(context) { .attr('type', 'text') .attr('class', 'list-item-input') .call(utilNoAuto) - .property('value', context.photos().username()) + .property('value', usernameValue) .on('change', function() { var value = d3_select(this).property('value'); context.photos().setUsernameFilter(value, true); + d3_select(this).property('value', usernameValue); }); li .merge(liEnter) .classed('active', filterEnabled); + + function usernameValue() { + var usernames = context.photos().usernames(); + if (usernames) return usernames.join('; '); + return usernames; + } } function toggleLayer(which) { From f4903d6986c9cdd6f26daa3466abaf9949956c19 Mon Sep 17 00:00:00 2001 From: Quincy Morgan <2046746+quincylvania@users.noreply.github.com> Date: Mon, 19 Oct 2020 16:34:40 -0400 Subject: [PATCH 21/22] Make sure photo filter controls are always shown in the same order --- modules/ui/sections/photo_overlays.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/ui/sections/photo_overlays.js b/modules/ui/sections/photo_overlays.js index 7d661c5b1..38075ef6f 100644 --- a/modules/ui/sections/photo_overlays.js +++ b/modules/ui/sections/photo_overlays.js @@ -112,7 +112,7 @@ export function uiSectionPhotoOverlays(context) { var ul = selection .selectAll('.layer-list-photo-types') - .data(context.photos().shouldFilterByPhotoType() ? [0] : []); + .data([0]); ul.exit() .remove(); @@ -123,7 +123,7 @@ export function uiSectionPhotoOverlays(context) { .merge(ul); var li = ul.selectAll('.list-item-photo-types') - .data(data); + .data(context.photos().shouldFilterByPhotoType() ? data : []); li.exit() .remove(); @@ -175,7 +175,7 @@ export function uiSectionPhotoOverlays(context) { var ul = selection .selectAll('.layer-list-date-filter') - .data(context.photos().shouldFilterByDate() ? [0] : []); + .data([0]); ul.exit() .remove(); @@ -186,7 +186,7 @@ export function uiSectionPhotoOverlays(context) { .merge(ul); var li = ul.selectAll('.list-item-date-filter') - .data(data); + .data(context.photos().shouldFilterByDate() ? data : []); li.exit() .remove(); @@ -241,7 +241,7 @@ export function uiSectionPhotoOverlays(context) { } var ul = selection .selectAll('.layer-list-username-filter') - .data(context.photos().shouldFilterByUsername() ? [0] : []); + .data([0]); ul.exit() .remove(); @@ -252,7 +252,7 @@ export function uiSectionPhotoOverlays(context) { .merge(ul); var li = ul.selectAll('.list-item-username-filter') - .data(['username-filter']); + .data(context.photos().shouldFilterByUsername() ? ['username-filter'] : []); li.exit() .remove(); From 515a7c28ac8897f75d3785225092bccd90b3c01d Mon Sep 17 00:00:00 2001 From: Quincy Morgan <2046746+quincylvania@users.noreply.github.com> Date: Mon, 19 Oct 2020 16:43:51 -0400 Subject: [PATCH 22/22] Adjust photo filter input styling --- css/80_app.css | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/css/80_app.css b/css/80_app.css index d4b88b033..7f51f5fc2 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -183,11 +183,6 @@ input[type=date] { text-overflow: ellipsis; overflow: auto; } -input.list-item-input { - height: 20px; - padding: 0px 4px; - width: 160px; -} input[type=text], input[type=search], input[type=number], @@ -3112,6 +3107,13 @@ div.full-screen > button:focus { flex-grow: 1; } +.layer-list input.list-item-input { + height: 2.2em; + padding: 0px 4px; + width: 50%; + min-width: 160px; +} + .map-data-pane .layer-list button, .background-pane .layer-list button { border-left: 1px solid #ccc;