diff --git a/API.md b/API.md
index 9a6f38b83..de5a9a21c 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. 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/css/80_app.css b/css/80_app.css
index 15512dd96..7f51f5fc2 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;
}
@@ -172,7 +173,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;
@@ -186,7 +188,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;
}
@@ -3104,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;
diff --git a/data/core.yaml b/data/core.yaml
index 1373fb6d1..c97715be5 100644
--- a/data/core.yaml
+++ b/data/core.yaml
@@ -813,6 +813,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
@@ -2302,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 cc576b64c..9292bf4ef 100644
--- a/dist/locales/en.json
+++ b/dist/locales/en.json
@@ -1042,6 +1042,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": {
@@ -2844,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/renderer/photos.js b/modules/renderer/photos.js
index 7130c386b..d8267a048 100644
--- a/modules/renderer/photos.js
+++ b/modules/renderer/photos.js
@@ -10,6 +10,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 _usernames;
function photos() {}
@@ -38,16 +42,95 @@ 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, updateUrl) {
+ // 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) {
+ var rangeString;
+ if (_fromDate || _toDate) {
+ rangeString = (_fromDate || '') + '_' + (_toDate || '');
+ }
+ setUrlFilterValue('photo_dates', rangeString);
+ }
+ };
+
+ photos.setUsernameFilter = function(val, updateUrl) {
+ 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) {
+ var hashString;
+ if (_usernames) {
+ hashString = _usernames.join(',');
+ }
+ setUrlFilterValue('photo_username', hashString);
+ }
+ };
+
+ function setUrlFilterValue(property, val) {
+ if (!window.mocha) {
+ var hash = utilStringQs(window.location.hash);
+ if (val) {
+ if (hash[property] === val) return;
+ hash[property] = val;
+ } else {
+ if (!(property in hash)) return;
+ delete hash[property];
+ }
+ window.location.replace('#' + utilQsString(hash, true));
+ }
+ }
+
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') || showsLayer('streetside');
+ };
+
photos.showsPhotoType = function(val) {
if (!photos.shouldFilterByPhotoType()) return true;
@@ -62,6 +145,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) {
@@ -73,8 +164,21 @@ export function rendererPhotos(context) {
return photos;
};
+ photos.usernames = function() {
+ return _usernames;
+ };
+
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 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);
+ }
+ 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`
diff --git a/modules/services/mapillary.js b/modules/services/mapillary.js
index f98136025..60e969f5a 100644
--- a/modules/services/mapillary.js
+++ b/modules/services/mapillary.js
@@ -46,6 +46,7 @@ var _mlyClicks;
var _mlyActiveImage;
var _mlySelectedImageKey;
var _mlyViewer;
+var _mlyViewerFilter = ['all'];
var _loadViewerPromise;
var _mlyHighlightedDetection;
var _mlyShowFeatureDetections = false;
@@ -486,6 +487,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 usernames = context.photos().usernames();
+ var filter = ['all'];
+
+ if (!showsPano) filter.push(['==', 'pano', false]);
+ if (!showsFlat && showsPano) filter.push(['==', 'pano', true]);
+ if (usernames && usernames.length) filter.push(['==', 'username', usernames[0]]);
+ 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')
@@ -590,6 +619,9 @@ export default {
_mlyViewer = new Mapillary.Viewer('ideditor-mly', clientId, null, opts);
_mlyViewer.on('nodechanged', nodeChanged);
_mlyViewer.on('bearingchanged', bearingChanged);
+ if (_mlyViewerFilter) {
+ _mlyViewer.setFilter(_mlyViewerFilter);
+ }
// Register viewer resize handler
context.ui().photoviewer.on('resize.mapillary', function() {
diff --git a/modules/services/openstreetcam.js b/modules/services/openstreetcam.js
index 357801017..1755182c2 100644
--- a/modules/services/openstreetcam.js
+++ b/modules/services/openstreetcam.js
@@ -218,11 +218,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 30af19ffa..b3c8004c8 100644
--- a/modules/services/streetside.js
+++ b/modules/services/streetside.js
@@ -186,7 +186,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/mapillary_images.js b/modules/svg/mapillary_images.js
index a4bd8f686..515bdda12 100644
--- a/modules/svg/mapillary_images.js
+++ b/modules/svg/mapillary_images.js
@@ -114,18 +114,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 usernames = context.photos().usernames();
+
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 (usernames) {
+ images = images.filter(function(image) {
+ return usernames.indexOf(image.captured_by) !== -1;
+ });
+ }
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 usernames = context.photos().usernames();
+
if (!showsPano || !showsFlat) {
sequences = sequences.filter(function(sequence) {
if (sequence.properties.hasOwnProperty('pano')) {
@@ -147,6 +172,24 @@ export function svgMapillaryImages(projection, context, dispatch) {
}
});
}
+ if (fromDate) {
+ var fromTimestamp = new Date(fromDate).getTime();
+ sequences = sequences.filter(function(sequence) {
+ 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.properties.captured_at).getTime() <= toTimestamp;
+ });
+ }
+ if (usernames) {
+ sequences = sequences.filter(function(sequence) {
+ return usernames.indexOf(sequence.properties.username) !== -1;
+ });
+ }
+
return sequences;
}
@@ -162,6 +205,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 5c405b94a..786ac4a79 100644
--- a/modules/svg/openstreetcam_images.js
+++ b/modules/svg/openstreetcam_images.js
@@ -107,6 +107,58 @@ 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 usernames = context.photos().usernames();
+
+ if (fromDate) {
+ var fromTimestamp = new Date(fromDate).getTime();
+ 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(item) {
+ return new Date(item.captured_at).getTime() <= toTimestamp;
+ });
+ }
+ if (usernames) {
+ images = images.filter(function(item) {
+ return usernames.indexOf(item.captured_by) !== -1;
+ });
+ }
+
+ return images;
+ }
+
+ function filterSequences(sequences) {
+ var fromDate = context.photos().fromDate();
+ var toDate = context.photos().toDate();
+ var usernames = context.photos().usernames();
+
+ 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 (usernames) {
+ sequences = sequences.filter(function(image) {
+ return usernames.indexOf(image.properties.captured_by) !== -1;
+ });
+ }
+
+ return sequences;
+ }
+
function update() {
var viewer = context.container().select('.photoviewer');
var selected = viewer.empty() ? undefined : viewer.datum();
@@ -122,6 +174,8 @@ 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);
}
var traces = layer.selectAll('.sequences').selectAll('.sequence')
diff --git a/modules/svg/streetside.js b/modules/svg/streetside.js
index 50e54be39..ab1bc4a15 100644
--- a/modules/svg/streetside.js
+++ b/modules/svg/streetside.js
@@ -159,6 +159,58 @@ 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();
+ var usernames = context.photos().usernames();
+
+ 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;
+ });
+ }
+ if (usernames) {
+ bubbles = bubbles.filter(function(bubble) {
+ return usernames.indexOf(bubble.captured_by) !== -1;
+ });
+ }
+
+ return bubbles;
+ }
+
+ function filterSequences(sequences) {
+ var fromDate = context.photos().fromDate();
+ var toDate = context.photos().toDate();
+ var usernames = context.photos().usernames();
+
+ 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 (usernames) {
+ sequences = sequences.filter(function(sequences) {
+ return usernames.indexOf(sequences.properties.captured_by) !== -1;
+ });
+ }
+
+ return sequences;
+ }
+
/**
* update().
*/
@@ -176,6 +228,8 @@ 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);
}
var traces = layer.selectAll('.sequences').selectAll('.sequence')
diff --git a/modules/ui/sections/photo_overlays.js b/modules/ui/sections/photo_overlays.js
index bc5f58101..38075ef6f 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, utilNoAuto } from '../../util';
export function uiSectionPhotoOverlays(context) {
@@ -24,7 +25,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 +95,6 @@ export function uiSectionPhotoOverlays(context) {
return t.html(id.replace(/-/g, '_') + '.title');
});
-
// Update
li
.merge(liEnter)
@@ -110,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();
@@ -121,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();
@@ -152,7 +154,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');
});
@@ -164,6 +166,138 @@ 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([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(context.photos().shouldFilterByDate() ? 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.html('photo_overlays.date_filter.' + d + '.tooltip'))
+ .placement('top')
+ );
+ });
+
+ labelEnter
+ .append('span')
+ .html(function(d) {
+ return t.html('photo_overlays.date_filter.' + d + '.title');
+ });
+
+ labelEnter
+ .append('input')
+ .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) || '');
+ })
+ .on('change', function(d3_event, d) {
+ 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
+ .merge(liEnter)
+ .classed('active', filterEnabled);
+ }
+
+ function drawUsernameFilter(selection) {
+ function filterEnabled() {
+ return context.photos().usernames();
+ }
+ var ul = selection
+ .selectAll('.layer-list-username-filter')
+ .data([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(context.photos().shouldFilterByUsername() ? ['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.html('photo_overlays.username_filter.tooltip'))
+ .placement('top')
+ );
+ });
+
+ labelEnter
+ .append('span')
+ .html(t.html('photo_overlays.username_filter.title'));
+
+ labelEnter
+ .append('input')
+ .attr('type', 'text')
+ .attr('class', 'list-item-input')
+ .call(utilNoAuto)
+ .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) {
setLayer(which, !showsLayer(which));
}
diff --git a/test/spec/services/mapillary.js b/test/spec/services/mapillary.js
index 9b3eb2de2..357c119d7 100644
--- a/test/spec/services/mapillary.js
+++ b/test/spec/services/mapillary.js
@@ -403,4 +403,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);
+ });
+ });
+
});
diff --git a/test/spec/services/openstreetcam.js b/test/spec/services/openstreetcam.js
index 606a29ba4..a00ab3a40 100644
--- a/test/spec/services/openstreetcam.js
+++ b/test/spec/services/openstreetcam.js
@@ -253,7 +253,11 @@ describe('iD.serviceOpenstreetcam', function() {
expect(res).to.deep.eql([{
type: 'LineString',
coordinates: [[10,0], [10,0], [10,1]],
- properties: { key: '100' }
+ properties: {
+ captured_at: undefined,
+ captured_by: undefined,
+ key: '100'
+ }
}]);
});
});