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' + } }]); }); });