From 00b30e29f05ca0a39e73177670e798c5058b25d9 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 2 Nov 2017 22:04:47 -0400 Subject: [PATCH 01/12] WIP: Add OpenStreetCam support todo: Need to figure out API issue with bbox, then imageviewer --- css/{60_mapillary.css => 60_photos.css} | 40 +++ data/core.yaml | 5 + dist/locales/en.json | 7 + modules/renderer/background.js | 5 + modules/services/index.js | 4 +- modules/services/openstreetcam.js | 356 ++++++++++++++++++++++++ modules/svg/index.js | 1 + modules/svg/layers.js | 2 + modules/svg/openstreetcam_images.js | 189 +++++++++++++ modules/ui/map_data.js | 126 +++------ test/spec/svg/layers.js | 5 +- 11 files changed, 650 insertions(+), 90 deletions(-) rename css/{60_mapillary.css => 60_photos.css} (73%) create mode 100644 modules/services/openstreetcam.js create mode 100644 modules/svg/openstreetcam_images.js diff --git a/css/60_mapillary.css b/css/60_photos.css similarity index 73% rename from css/60_mapillary.css rename to css/60_photos.css index a3067ea7e..f1c81e420 100644 --- a/css/60_mapillary.css +++ b/css/60_photos.css @@ -67,6 +67,46 @@ } +/* OpenStreetCam Image Layer */ + +.layer-openstreetcam-images { + pointer-events: none; +} + +.layer-openstreetcam-images .viewfield-group { + pointer-events: visible; + cursor: pointer; +} + +.layer-openstreetcam-images .viewfield-group * { + stroke-width: 1; + stroke: #444; + fill: #ffc600; + z-index: 50; +} + +.layer-openstreetcam-images .viewfield-group:hover * { + stroke-width: 1; + stroke: #333; + fill: #ff9900; + z-index: 60; +} + +.layer-openstreetcam-images .viewfield-group.selected * { + stroke-width: 2; + stroke: #222; + fill: #ff5800; + z-index: 60; +} + +.layer-openstreetcam-images .viewfield-group:hover path.viewfield, +.layer-openstreetcam-images .viewfield-group.selected path.viewfield, +.layer-openstreetcam-images .viewfield-group path.viewfield { + stroke-width: 0; + fill-opacity: 0.6; +} + + /* Mapillary viewer */ #mly .domRenderer .TagSymbol { font-size: 10px; diff --git a/data/core.yaml b/data/core.yaml index a55aa2fff..7d41e40ae 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -546,6 +546,11 @@ en: title: "Traffic Sign Overlay (Mapillary)" mapillary: view_on_mapillary: "View this image on Mapillary" + openstreetcam_images: + tooltip: "Street-level photos from OpenStreetCam" + title: "Photo Overlay (OpenStreetCam)" + openstreetcam: + view_on_openstreetcam: "View this image on OpenStreetCam" help: title: "Help" key: H diff --git a/dist/locales/en.json b/dist/locales/en.json index 49cda42a7..4d4b92219 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -670,6 +670,13 @@ "mapillary": { "view_on_mapillary": "View this image on Mapillary" }, + "openstreetcam_images": { + "tooltip": "Street-level photos from OpenStreetCam", + "title": "Photo Overlay (OpenStreetCam)" + }, + "openstreetcam": { + "view_on_openstreetcam": "View this image on OpenStreetCam" + }, "help": { "title": "Help", "key": "H", diff --git a/modules/renderer/background.js b/modules/renderer/background.js index ba6946e3e..a6d72909b 100644 --- a/modules/renderer/background.js +++ b/modules/renderer/background.js @@ -104,6 +104,11 @@ export function rendererBackground(context) { imageryUsed.push('Mapillary Signs'); } + var openstreetcam_images = context.layers().layer('openstreetcam-images'); + if (openstreetcam_images && openstreetcam_images.enabled()) { + imageryUsed.push('OpenStreetCam Images'); + } + context.history().imageryUsed(imageryUsed); }; diff --git a/modules/services/index.js b/modules/services/index.js index f893714ab..d5ab2d365 100644 --- a/modules/services/index.js +++ b/modules/services/index.js @@ -1,13 +1,15 @@ import serviceMapillary from './mapillary'; import serviceNominatim from './nominatim'; +import serviceOpenstreetcam from './openstreetcam'; import serviceOsm from './osm'; import serviceTaginfo from './taginfo'; import serviceWikidata from './wikidata'; import serviceWikipedia from './wikipedia'; export var services = { - mapillary: serviceMapillary, geocoder: serviceNominatim, + mapillary: serviceMapillary, + openstreetcam: serviceOpenstreetcam, osm: serviceOsm, taginfo: serviceTaginfo, wikidata: serviceWikidata, diff --git a/modules/services/openstreetcam.js b/modules/services/openstreetcam.js new file mode 100644 index 000000000..eb32ade74 --- /dev/null +++ b/modules/services/openstreetcam.js @@ -0,0 +1,356 @@ +import _filter from 'lodash-es/filter'; +import _find from 'lodash-es/find'; +import _flatten from 'lodash-es/flatten'; +import _forEach from 'lodash-es/forEach'; +import _map from 'lodash-es/map'; +import _some from 'lodash-es/some'; + +import { range as d3_range } from 'd3-array'; +import { dispatch as d3_dispatch } from 'd3-dispatch'; +import { request as d3_request } from 'd3-request'; + +import { + select as d3_select, + selectAll as d3_selectAll +} from 'd3-selection'; + +import rbush from 'rbush'; + +import { d3geoTile as d3_geoTile } from '../lib/d3.geo.tile'; +import { geoExtent } from '../geo'; +import { utilQsString, utilRebind } from '../util'; + + +var apibase = 'http://openstreetcam.org', + maxResults = 1000, + tileZoom = 14, + dispatch = d3_dispatch('loadedImages'), + openstreetcamCache, + openstreetcamImage; + + +function abortRequest(i) { + i.abort(); +} + + +function nearNullIsland(x, y, z) { + if (z >= 7) { + var center = Math.pow(2, z - 1), + width = Math.pow(2, z - 6), + min = center - (width / 2), + max = center + (width / 2) - 1; + return x >= min && x <= max && y >= min && y <= max; + } + return false; +} + + +function maxPageAtZoom(z) { + if (z < 15) return 2; + if (z === 15) return 5; + if (z === 16) return 10; + if (z === 17) return 20; + if (z === 18) return 40; + if (z > 18) return 80; +} + + +function getTiles(projection) { + var s = projection.scale() * 2 * Math.PI, + z = Math.max(Math.log(s) / Math.log(2) - 8, 0), + ts = 256 * Math.pow(2, z - tileZoom), + origin = [ + s / 2 - projection.translate()[0], + s / 2 - projection.translate()[1]]; + + return d3_geoTile() + .scaleExtent([tileZoom, tileZoom]) + .scale(s) + .size(projection.clipExtent()[1]) + .translate(projection.translate())() + .map(function(tile) { + var x = tile[0] * ts - origin[0], + y = tile[1] * ts - origin[1]; + + return { + id: tile.toString(), + xyz: tile, + extent: geoExtent( + projection.invert([x, y + ts]), + projection.invert([x + ts, y]) + ) + }; + }); +} + + +function loadTiles(which, url, projection) { + var s = projection.scale() * 2 * Math.PI, + currZoom = Math.floor(Math.max(Math.log(s) / Math.log(2) - 8, 0)); + + var tiles = getTiles(projection).filter(function(t) { + return !nearNullIsland(t.xyz[0], t.xyz[1], t.xyz[2]); + }); + + _filter(which.inflight, function(v, k) { + var wanted = _find(tiles, function(tile) { return k === (tile.id + ',0'); }); + if (!wanted) delete which.inflight[k]; + return !wanted; + }).map(abortRequest); + + tiles.forEach(function(tile) { + loadNextTilePage(which, currZoom, url, tile); + }); +} + + +function loadNextTilePage(which, currZoom, url, tile) { + var cache = openstreetcamCache[which]; + var bbox = tile.extent.bbox(); + var maxPages = maxPageAtZoom(currZoom); + var nextPage = cache.nextPage[tile.id] || 1; + var params = utilQsString({ + ipp: maxResults, + page: nextPage, + // client_id: clientId, + bbTopLeft: [bbox.maxY, bbox.minX].join(','), + bbBottomRight: [bbox.minY, bbox.maxX].join(',') + }, true); + + if (nextPage > maxPages) return; + + var id = tile.id + ',' + String(nextPage); + if (cache.loaded[id] || cache.inflight[id]) return; + + cache.inflight[id] = d3_request(url) + .mimeType('application/json') + .post(params, function(err, data) { + cache.loaded[id] = true; + delete cache.inflight[id]; + if (err || !data.currentPageItems || !data.currentPageItems.length) return; + + var features = data.currentPageItems.map(function(item) { + var loc = [+item.lng, +item.lat], + d; + + if (which === 'images') { + d = { + loc: loc, + key: item.id, + ca: +item.heading, + captured_at: item.date_added, + }; + } + return { + minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d + }; + }); + + cache.rtree.load(features); + + if (which === 'images') { + dispatch.call('loadedImages'); + } + + if (data.currentPageItems.length === maxResults) { // more pages to load + cache.nextPage[tile.id] = nextPage + 1; + loadNextTilePage(which, currZoom, url, tile); + } else { + cache.nextPage[tile.id] = Infinity; // no more pages to load + } + }); +} + + +// partition viewport into `psize` x `psize` regions +function partitionViewport(psize, projection) { + var dimensions = projection.clipExtent()[1]; + psize = psize || 16; + var cols = d3_range(0, dimensions[0], psize), + rows = d3_range(0, dimensions[1], psize), + partitions = []; + + rows.forEach(function(y) { + cols.forEach(function(x) { + var min = [x, y + psize], + max = [x + psize, y]; + partitions.push( + geoExtent(projection.invert(min), projection.invert(max))); + }); + }); + + return partitions; +} + + +// no more than `limit` results per partition. +function searchLimited(psize, limit, projection, rtree) { + limit = limit || 3; + + var partitions = partitionViewport(psize, projection); + var results; + + results = _flatten(_map(partitions, function(extent) { + return rtree.search(extent.bbox()) + .slice(0, limit) + .map(function(d) { return d.data; }); + })); + return results; +} + + + +export default { + + init: function() { + if (!openstreetcamCache) { + this.reset(); + } + + this.event = utilRebind(this, dispatch, 'on'); + }, + + reset: function() { + var cache = openstreetcamCache; + + if (cache) { + if (cache.images && cache.images.inflight) { + _forEach(cache.images.inflight, abortRequest); + } + } + + openstreetcamCache = { + images: { inflight: {}, loaded: {}, nextPage: {}, rtree: rbush() } + }; + + openstreetcamImage = null; + }, + + + images: function(projection) { + var psize = 16, limit = 3; + return searchLimited(psize, limit, projection, openstreetcamCache.images.rtree); + }, + + loadImages: function(projection) { + var url = apibase + '/1.0/list/nearby-photos/'; + loadTiles('images', url, projection); + }, + + + loadViewer: function(context) { + // var that = this; + // var wrap = d3_select('#content').selectAll('.openstreetcam-wrap') + // .data([0]); + + // var enter = wrap.enter() + // .append('div') + // .attr('class', 'openstreetcam-wrap') + // .classed('al', true) // 'al'=left, 'ar'=right + // .classed('hidden', true); + + // enter + // .append('button') + // .attr('class', 'thumb-hide') + // .on('click', function () { that.hideViewer(); }) + // .append('div') + // .call(svgIcon('#icon-close')); + + // enter + // .append('div') + // .attr('id', 'mly') + // .attr('class', 'mly-wrapper') + // .classed('active', false); + }, + + + showViewer: function() { + // d3_select('#content') + // .selectAll('.openstreetcam-wrap') + // .classed('hidden', false) + // .selectAll('.mly-wrapper') + // .classed('active', true); + + return this; + }, + + + hideViewer: function() { + // d3_select('#content') + // .selectAll('.openstreetcam-wrap') + // .classed('hidden', true) + // .selectAll('.mly-wrapper') + // .classed('active', false); + + // d3_selectAll('.layer-openstreetcam-images .viewfield-group') + // .classed('selected', false); + + openstreetcamImage = null; + return this; + }, + + + updateViewer: function(imageKey, context) { + if (!imageKey) return; + + // if (!openstreetcamViewer) { + // this.initViewer(imageKey, context); + // } else { + // openstreetcamViewer.moveToKey(imageKey); + // } + + return this; + }, + + + selectedImage: function(imageKey) { + if (!arguments.length) return openstreetcamImage; + openstreetcamImage = imageKey; + + // d3_selectAll('.layer-openstreetcam-images .viewfield-group') + // .classed('selected', function(d) { + // return d.key === imageKey; + // }); + + // if (!imageKey) return this; + + + // function localeTimestamp(s) { + // if (!s) return null; + // var d = new Date(s); + // if (isNaN(d.getTime())) return null; + // return d.toLocaleString(undefined, { timeZone: 'UTC' }); + // } + + // var selected = d3_selectAll('.layer-openstreetcam-images .viewfield-group.selected'); + // if (selected.empty()) return this; + + // var datum = selected.datum(); + // var timestamp = localeTimestamp(datum.captured_at); + // var attribution = d3_select('.openstreetcam-js-dom .Attribution'); + // var capturedAt = attribution.selectAll('.captured-at'); + // if (capturedAt.empty()) { + // attribution + // .append('span') + // .text('|'); + // capturedAt = attribution + // .append('span') + // .attr('class', 'captured-at'); + // } + // capturedAt + // .text(timestamp); + + // this.updateDetections(); + + return this; + }, + + + cache: function(_) { + if (!arguments.length) return openstreetcamCache; + openstreetcamCache = _; + return this; + } + +}; diff --git a/modules/svg/index.js b/modules/svg/index.js index 9b48ba93d..8f54f5a5e 100644 --- a/modules/svg/index.js +++ b/modules/svg/index.js @@ -10,6 +10,7 @@ export { svgMapillaryImages } from './mapillary_images.js'; export { svgMapillarySigns } from './mapillary_signs.js'; export { svgMidpoints } from './midpoints.js'; export { svgOneWaySegments } from './one_way_segments.js'; +export { svgOpenstreetcamImages } from './openstreetcam_images.js'; export { svgOsm } from './osm.js'; export { svgPath } from './path.js'; export { svgPointTransform } from './point_transform.js'; diff --git a/modules/svg/layers.js b/modules/svg/layers.js index b0f590410..08f92db18 100644 --- a/modules/svg/layers.js +++ b/modules/svg/layers.js @@ -10,6 +10,7 @@ import { svgDebug } from './debug'; import { svgGpx } from './gpx'; import { svgMapillaryImages } from './mapillary_images'; import { svgMapillarySigns } from './mapillary_signs'; +import { svgOpenstreetcamImages } from './openstreetcam_images'; import { svgOsm } from './osm'; import { utilRebind } from '../util/rebind'; import { utilGetDimensions, utilSetDimensions } from '../util/dimensions'; @@ -23,6 +24,7 @@ export function svgLayers(projection, context) { { id: 'gpx', layer: svgGpx(projection, context, dispatch) }, { id: 'mapillary-images', layer: svgMapillaryImages(projection, context, dispatch) }, { id: 'mapillary-signs', layer: svgMapillarySigns(projection, context, dispatch) }, + { id: 'openstreetcam-images', layer: svgOpenstreetcamImages(projection, context, dispatch) }, { id: 'debug', layer: svgDebug(projection, context, dispatch) } ]; diff --git a/modules/svg/openstreetcam_images.js b/modules/svg/openstreetcam_images.js new file mode 100644 index 000000000..5087ee96d --- /dev/null +++ b/modules/svg/openstreetcam_images.js @@ -0,0 +1,189 @@ +import _throttle from 'lodash-es/throttle'; +import { select as d3_select } from 'd3-selection'; +import { svgPointTransform } from './point_transform'; +import { services } from '../services'; + + +export function svgOpenstreetcamImages(projection, context, dispatch) { + var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000), + minZoom = 12, + minViewfieldZoom = 17, + layer = d3_select(null), + _openstreetcam; + + + function init() { + if (svgOpenstreetcamImages.initialized) return; // run once + svgOpenstreetcamImages.enabled = false; + svgOpenstreetcamImages.initialized = true; + } + + + function getOpenstreetcam() { + if (services.openstreetcam && !_openstreetcam) { + _openstreetcam = services.openstreetcam; + _openstreetcam.event.on('loadedImages', throttledRedraw); + } else if (!services.openstreetcam && _openstreetcam) { + _openstreetcam = null; + } + + return _openstreetcam; + } + + + function showLayer() { + var openstreetcam = getOpenstreetcam(); + if (!openstreetcam) return; + + openstreetcam.loadViewer(context); + editOn(); + + layer + .style('opacity', 0) + .transition() + .duration(250) + .style('opacity', 1) + .on('end', function () { dispatch.call('change'); }); + } + + + function hideLayer() { + var openstreetcam = getOpenstreetcam(); + if (openstreetcam) { + openstreetcam.hideViewer(); + } + + throttledRedraw.cancel(); + + layer + .transition() + .duration(250) + .style('opacity', 0) + .on('end', editOff); + } + + + function editOn() { + layer.style('display', 'block'); + } + + + function editOff() { + layer.selectAll('.viewfield-group').remove(); + layer.style('display', 'none'); + } + + + function click(d) { + var openstreetcam = getOpenstreetcam(); + if (!openstreetcam) return; + + context.map().centerEase(d.loc); + + openstreetcam + .selectedImage(d.key, true) + .updateViewer(d.key, context) + .showViewer(); + } + + + function transform(d) { + var t = svgPointTransform(projection)(d); + if (d.ca) t += ' rotate(' + Math.floor(d.ca) + ',0,0)'; + return t; + } + + + function update() { + var openstreetcam = getOpenstreetcam(), + data = (openstreetcam ? openstreetcam.images(projection) : []), + imageKey = openstreetcam ? openstreetcam.selectedImage() : null; + + var markers = layer.selectAll('.viewfield-group') + .data(data, function(d) { return d.key; }); + + markers.exit() + .remove(); + + var enter = markers.enter() + .append('g') + .attr('class', 'viewfield-group') + .classed('selected', function(d) { return d.key === imageKey; }) + .on('click', click); + + markers = markers + .merge(enter) + .attr('transform', transform); + + + var viewfields = markers.selectAll('.viewfield') + .data(~~context.map().zoom() >= minViewfieldZoom ? [0] : []); + + viewfields.exit() + .remove(); + + viewfields.enter() + .append('path') + .attr('class', 'viewfield') + .attr('transform', 'scale(1.5,1.5),translate(-8, -13)') + .attr('d', 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z'); + + markers.selectAll('circle') + .data([0]) + .enter() + .append('circle') + .attr('dx', '0') + .attr('dy', '0') + .attr('r', '6'); + } + + + function drawImages(selection) { + var enabled = svgOpenstreetcamImages.enabled, + openstreetcam = getOpenstreetcam(); + + layer = selection.selectAll('.layer-openstreetcam-images') + .data(openstreetcam ? [0] : []); + + layer.exit() + .remove(); + + layer = layer.enter() + .append('g') + .attr('class', 'layer-openstreetcam-images') + .style('display', enabled ? 'block' : 'none') + .merge(layer); + + if (enabled) { + if (openstreetcam && ~~context.map().zoom() >= minZoom) { + editOn(); + update(); + openstreetcam.loadImages(projection); + } else { + editOff(); + } + } + } + + + drawImages.enabled = function(_) { + if (!arguments.length) return svgOpenstreetcamImages.enabled; + svgOpenstreetcamImages.enabled = _; + if (svgOpenstreetcamImages.enabled) { + showLayer(); + } else { + hideLayer(); + } + dispatch.call('change'); + return this; + }; + + + drawImages.supported = function() { + return !!getOpenstreetcam(); + }; + + + init(); + return drawImages; +} diff --git a/modules/ui/map_data.js b/modules/ui/map_data.js index 12e676d0e..49a238ba3 100644 --- a/modules/ui/map_data.js +++ b/modules/ui/map_data.js @@ -80,113 +80,65 @@ export function uiMapData(context) { } - function clickMapillaryImages() { - toggleLayer('mapillary-images'); - if (!showsLayer('mapillary-images')) { - setLayer('mapillary-signs', false); + function drawPhotoItems(selection) { + var photoKeys = ['mapillary-images', 'mapillary-signs', 'openstreetcam-images']; + var photoLayers = layers.all().filter(function(obj) { return photoKeys.indexOf(obj.id) !== -1; }); + var data = photoLayers.filter(function(obj) { return obj.layer.supported(); }); + + function layerSupported(d) { + return d.layer && d.layer.supported(); + } + function layerEnabled(d) { + return layerSupported(d) && d.layer.enabled(); } - } - - function clickMapillarySigns() { - toggleLayer('mapillary-signs'); - } - - - function drawMapillaryItems(selection) { - var mapillaryImages = layers.layer('mapillary-images'), - mapillarySigns = layers.layer('mapillary-signs'), - supportsMapillaryImages = mapillaryImages && mapillaryImages.supported(), - supportsMapillarySigns = mapillarySigns && mapillarySigns.supported(), - showsMapillaryImages = supportsMapillaryImages && mapillaryImages.enabled(), - showsMapillarySigns = supportsMapillarySigns && mapillarySigns.enabled(); - - var mapillaryList = selection - .selectAll('.layer-list-mapillary') + var ul = selection + .selectAll('.layer-list-photos') .data([0]); - mapillaryList = mapillaryList.enter() + ul = ul.enter() .append('ul') - .attr('class', 'layer-list layer-list-mapillary') - .merge(mapillaryList); + .attr('class', 'layer-list layer-list-photos') + .merge(ul); + var li = ul.selectAll('.list-item-photos') + .data(data); - var mapillaryImageLayerItem = mapillaryList - .selectAll('.list-item-mapillary-images') - .data(supportsMapillaryImages ? [0] : []); - - mapillaryImageLayerItem.exit() + li.exit() .remove(); - var enterImages = mapillaryImageLayerItem.enter() + var liEnter = li.enter() .append('li') - .attr('class', 'list-item-mapillary-images'); + .attr('class', function(d) { return 'list-item-photos list-item-' + d.id; }); - var labelImages = enterImages + var labelEnter = liEnter .append('label') - .call(tooltip() - .title(t('mapillary_images.tooltip')) - .placement('top')); + .each(function(d) { + d3_select(this) + .call(tooltip() + .title(t(d.id.replace('-', '_') + '.tooltip')) + .placement('top') + ); + }); - labelImages + labelEnter .append('input') .attr('type', 'checkbox') - .on('change', clickMapillaryImages); + .on('change', function(d) { toggleLayer(d.id); }); - labelImages + labelEnter .append('span') - .text(t('mapillary_images.title')); + .text(function(d) { return t(d.id.replace('-', '_') + '.title'); }); - var mapillarySignLayerItem = mapillaryList - .selectAll('.list-item-mapillary-signs') - .data(supportsMapillarySigns ? [0] : []); + // Update + li = li + .merge(liEnter); - mapillarySignLayerItem.exit() - .remove(); - - var enterSigns = mapillarySignLayerItem.enter() - .append('li') - .attr('class', 'list-item-mapillary-signs'); - - var labelSigns = enterSigns - .append('label') - .call(tooltip() - .title(t('mapillary_signs.tooltip')) - .placement('top')); - - labelSigns - .append('input') - .attr('type', 'checkbox') - .on('change', clickMapillarySigns); - - labelSigns - .append('span') - .text(t('mapillary_signs.title')); - - - // Updates - mapillaryImageLayerItem = mapillaryImageLayerItem - .merge(enterImages); - - mapillaryImageLayerItem - .classed('active', showsMapillaryImages) + li + .classed('active', layerEnabled) .selectAll('input') - .property('checked', showsMapillaryImages); - - - mapillarySignLayerItem = mapillarySignLayerItem - .merge(enterSigns); - - mapillarySignLayerItem - .classed('active', showsMapillarySigns) - .selectAll('input') - .property('disabled', !showsMapillaryImages) - .property('checked', showsMapillarySigns); - - mapillarySignLayerItem - .selectAll('label') - .classed('deemphasize', !showsMapillaryImages); + .property('checked', layerEnabled); } @@ -377,7 +329,7 @@ export function uiMapData(context) { function update() { dataLayerContainer .call(drawOsmItem) - .call(drawMapillaryItems) + .call(drawPhotoItems) .call(drawGpxItem); fillList diff --git a/test/spec/svg/layers.js b/test/spec/svg/layers.js index ec7ca46ed..e3cb29a89 100644 --- a/test/spec/svg/layers.js +++ b/test/spec/svg/layers.js @@ -26,12 +26,13 @@ describe('iD.svgLayers', function () { it('creates default data layers', function () { container.call(iD.svgLayers(projection, context)); var nodes = container.selectAll('svg .data-layer').nodes(); - expect(nodes.length).to.eql(5); + expect(nodes.length).to.eql(6); expect(d3.select(nodes[0]).classed('data-layer-osm')).to.be.true; expect(d3.select(nodes[1]).classed('data-layer-gpx')).to.be.true; expect(d3.select(nodes[2]).classed('data-layer-mapillary-images')).to.be.true; expect(d3.select(nodes[3]).classed('data-layer-mapillary-signs')).to.be.true; - expect(d3.select(nodes[4]).classed('data-layer-debug')).to.be.true; + expect(d3.select(nodes[4]).classed('data-layer-openstreetcam-images')).to.be.true; + expect(d3.select(nodes[5]).classed('data-layer-debug')).to.be.true; }); }); From 6dd71ebf917696f31a593dbdabe80457cc0ef5c1 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 3 Nov 2017 16:48:18 -0400 Subject: [PATCH 02/12] Fix loading of images from OpenStreetCam API, add viewer \o/ --- css/60_photos.css | 66 ++++++++------- modules/services/mapillary.js | 59 +++++++------- modules/services/openstreetcam.js | 120 ++++++++++------------------ modules/svg/openstreetcam_images.js | 4 +- modules/ui/init.js | 25 +++++- 5 files changed, 124 insertions(+), 150 deletions(-) diff --git a/css/60_photos.css b/css/60_photos.css index f1c81e420..c09f559fe 100644 --- a/css/60_photos.css +++ b/css/60_photos.css @@ -1,5 +1,36 @@ -/* Mapillary Image Layer */ +/* photo viewer div */ +#photoviewer { + position: absolute; + bottom: 30px; + width: 330px; + height: 250px; + padding: 5px; + background-color: #fff; +} +#photoviewer button.thumb-hide { + border-radius: 0; + padding: 5px; + position: absolute; + right: 0; + top: 0; + z-index: 500; +} + +.photo-wrapper, +.photo-wrapper img { + width: 100%; + height: 100%; +} + +/*#photoviewer.hide, +.photo-wrapper.hide { + visibility: hidden; + display: block; +} +*/ + +/* Mapillary Image Layer */ .layer-mapillary-images { pointer-events: none; } @@ -39,7 +70,6 @@ } /* Mapillary Sign Layer */ - .layer-mapillary-signs { pointer-events: none; } @@ -68,7 +98,6 @@ /* OpenStreetCam Image Layer */ - .layer-openstreetcam-images { pointer-events: none; } @@ -122,34 +151,3 @@ text-align: right; } -.mapillary-wrap { - position: absolute; - bottom: 30px; - width: 330px; - height: 250px; - padding: 5px; - background-color: #fff; -} - -.mapillary-wrap.hidden { - visibility: hidden; -} - -.mapillary-wrap button.thumb-hide { - border-radius: 0; - padding: 5px; - position: absolute; - right: 0; - top: 0; - z-index: 500; -} - -.mly-wrapper { - visibility: hidden; - width: 100%; - height: 100%; -} - -.mly-wrapper.active { - visibility: visible; -} diff --git a/modules/services/mapillary.js b/modules/services/mapillary.js index bc0c5d425..b9d3a5a61 100644 --- a/modules/services/mapillary.js +++ b/modules/services/mapillary.js @@ -24,7 +24,6 @@ import rbush from 'rbush'; import { d3geoTile as d3_geoTile } from '../lib/d3.geo.tile'; import { geoExtent } from '../geo'; -import { svgIcon } from '../svg'; import { utilDetect } from '../util/detect'; import { utilQsString, utilRebind } from '../util'; @@ -377,28 +376,14 @@ export default { loadViewer: function(context) { - var that = this; - var wrap = d3_select('#content').selectAll('.mapillary-wrap') - .data([0]); - - var enter = wrap.enter() - .append('div') - .attr('class', 'mapillary-wrap') - .classed('al', true) // 'al'=left, 'ar'=right - .classed('hidden', true); - - enter - .append('button') - .attr('class', 'thumb-hide') - .on('click', function () { that.hideViewer(); }) - .append('div') - .call(svgIcon('#icon-close')); - - enter + // add mly-wrapper for viewer-js + d3_select('#photoviewer').selectAll('.mly-wrapper') + .data([0]) + .enter() .append('div') .attr('id', 'mly') - .attr('class', 'mly-wrapper') - .classed('active', false); + .attr('class', 'photo-wrapper mly-wrapper') + .classed('hide', true); // load mapillary-viewercss d3_select('head').selectAll('#mapillary-viewercss') @@ -420,22 +405,32 @@ export default { showViewer: function() { - d3_select('#content') - .selectAll('.mapillary-wrap') - .classed('hidden', false) - .selectAll('.mly-wrapper') - .classed('active', true); + var wrap = d3_select('#photoviewer') + .classed('hide', false); + + var isHidden = wrap.selectAll('.photo-wrapper.mly-wrapper.hide').size(); + + if (isHidden) { + wrap + .selectAll('.photo-wrapper:not(.mly-wrapper)') + .classed('hide', true); + + wrap + .selectAll('.photo-wrapper.mly-wrapper') + .classed('hide', false); + + mapillaryViewer.resize(); + } return this; }, hideViewer: function() { - d3_select('#content') - .selectAll('.mapillary-wrap') - .classed('hidden', true) - .selectAll('.mly-wrapper') - .classed('active', false); + d3_select('#photoviewer') + .classed('hide', true) + .selectAll('.photo-wrapper') + .classed('hide', true); d3_selectAll('.layer-mapillary-images .viewfield-group, .layer-mapillary-signs .icon-sign') .classed('selected', false); @@ -514,7 +509,7 @@ export default { mapillaryClicks.push(imageKey); } - d3_selectAll('.layer-mapillary-images .viewfield-group') + d3_selectAll('.viewfield-group') .classed('selected', function(d) { return d.key === imageKey; }); diff --git a/modules/services/openstreetcam.js b/modules/services/openstreetcam.js index eb32ade74..7a16cf875 100644 --- a/modules/services/openstreetcam.js +++ b/modules/services/openstreetcam.js @@ -3,7 +3,6 @@ import _find from 'lodash-es/find'; import _flatten from 'lodash-es/flatten'; import _forEach from 'lodash-es/forEach'; import _map from 'lodash-es/map'; -import _some from 'lodash-es/some'; import { range as d3_range } from 'd3-array'; import { dispatch as d3_dispatch } from 'd3-dispatch'; @@ -125,6 +124,8 @@ function loadNextTilePage(which, currZoom, url, tile) { cache.inflight[id] = d3_request(url) .mimeType('application/json') + .header('Content-type', 'application/x-www-form-urlencoded') + .response(function(xhr) { return JSON.parse(xhr.responseText); }) .post(params, function(err, data) { cache.loaded[id] = true; delete cache.inflight[id]; @@ -140,6 +141,7 @@ function loadNextTilePage(which, currZoom, url, tile) { key: item.id, ca: +item.heading, captured_at: item.date_added, + imagePath: item.name }; } return { @@ -239,67 +241,57 @@ export default { }, - loadViewer: function(context) { - // var that = this; - // var wrap = d3_select('#content').selectAll('.openstreetcam-wrap') - // .data([0]); - - // var enter = wrap.enter() - // .append('div') - // .attr('class', 'openstreetcam-wrap') - // .classed('al', true) // 'al'=left, 'ar'=right - // .classed('hidden', true); - - // enter - // .append('button') - // .attr('class', 'thumb-hide') - // .on('click', function () { that.hideViewer(); }) - // .append('div') - // .call(svgIcon('#icon-close')); - - // enter - // .append('div') - // .attr('id', 'mly') - // .attr('class', 'mly-wrapper') - // .classed('active', false); + loadViewer: function() { + // add osc-wrapper + d3_select('#photoviewer').selectAll('.osc-wrapper') + .data([0]) + .enter() + .append('div') + .attr('class', 'photo-wrapper osc-wrapper') + .classed('hide', true) + .append('img'); }, showViewer: function() { - // d3_select('#content') - // .selectAll('.openstreetcam-wrap') - // .classed('hidden', false) - // .selectAll('.mly-wrapper') - // .classed('active', true); + var wrap = d3_select('#photoviewer') + .classed('hide', false); + + var isHidden = wrap.selectAll('.photo-wrapper.osc-wrapper.hide').size(); + + if (isHidden) { + wrap + .selectAll('.photo-wrapper:not(.osc-wrapper)') + .classed('hide', true); + + wrap + .selectAll('.photo-wrapper.osc-wrapper') + .classed('hide', false); + } return this; }, hideViewer: function() { - // d3_select('#content') - // .selectAll('.openstreetcam-wrap') - // .classed('hidden', true) - // .selectAll('.mly-wrapper') - // .classed('active', false); + d3_select('#photoviewer') + .classed('hide', true) + .selectAll('.photo-wrapper') + .classed('hide', true); - // d3_selectAll('.layer-openstreetcam-images .viewfield-group') - // .classed('selected', false); + d3_selectAll('.layer-openstreetcam-images .viewfield-group') + .classed('selected', false); openstreetcamImage = null; return this; }, - updateViewer: function(imageKey, context) { - if (!imageKey) return; - - // if (!openstreetcamViewer) { - // this.initViewer(imageKey, context); - // } else { - // openstreetcamViewer.moveToKey(imageKey); - // } - + updateViewer: function(imagePath) { + if (imagePath) { + d3_select('#photoviewer .osc-wrapper img') + .attr('src', apibase + '/' + imagePath); + } return this; }, @@ -308,40 +300,10 @@ export default { if (!arguments.length) return openstreetcamImage; openstreetcamImage = imageKey; - // d3_selectAll('.layer-openstreetcam-images .viewfield-group') - // .classed('selected', function(d) { - // return d.key === imageKey; - // }); - - // if (!imageKey) return this; - - - // function localeTimestamp(s) { - // if (!s) return null; - // var d = new Date(s); - // if (isNaN(d.getTime())) return null; - // return d.toLocaleString(undefined, { timeZone: 'UTC' }); - // } - - // var selected = d3_selectAll('.layer-openstreetcam-images .viewfield-group.selected'); - // if (selected.empty()) return this; - - // var datum = selected.datum(); - // var timestamp = localeTimestamp(datum.captured_at); - // var attribution = d3_select('.openstreetcam-js-dom .Attribution'); - // var capturedAt = attribution.selectAll('.captured-at'); - // if (capturedAt.empty()) { - // attribution - // .append('span') - // .text('|'); - // capturedAt = attribution - // .append('span') - // .attr('class', 'captured-at'); - // } - // capturedAt - // .text(timestamp); - - // this.updateDetections(); + d3_selectAll('.viewfield-group') + .classed('selected', function(d) { + return d.key === imageKey; + }); return this; }, diff --git a/modules/svg/openstreetcam_images.js b/modules/svg/openstreetcam_images.js index 5087ee96d..53b0f7c61 100644 --- a/modules/svg/openstreetcam_images.js +++ b/modules/svg/openstreetcam_images.js @@ -81,8 +81,8 @@ export function svgOpenstreetcamImages(projection, context, dispatch) { context.map().centerEase(d.loc); openstreetcam - .selectedImage(d.key, true) - .updateViewer(d.key, context) + .selectedImage(d.key) + .updateViewer(d.imagePath) .showViewer(); } diff --git a/modules/ui/init.js b/modules/ui/init.js index 8519c6f88..69eda0302 100644 --- a/modules/ui/init.js +++ b/modules/ui/init.js @@ -8,9 +8,9 @@ import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js'; import { t, textDirection } from '../util/locale'; import { tooltip } from '../util/tooltip'; -import { svgDefs, svgIcon } from '../svg/index'; -import { modeBrowse } from '../modes/index'; -import { behaviorHash } from '../behavior/index'; +import { svgDefs, svgIcon } from '../svg'; +import { modeBrowse } from '../modes'; +import { behaviorHash } from '../behavior'; import { utilGetDimensions } from '../util/dimensions'; import { uiAccount } from './account'; @@ -238,6 +238,25 @@ export function uiInit(context) { .call(uiContributors(context)); + var photoviewer = content + .append('div') + .attr('id', 'photoviewer') + .classed('al', true) // 'al'=left, 'ar'=right + .classed('hide', true); + + photoviewer + .append('button') + .attr('class', 'thumb-hide') + .on('click', function () { + d3_select('#photoviewer') + .classed('hide', true) + .select('div') + .classed('hide', true); + }) + .append('div') + .call(svgIcon('#icon-close')); + + window.onbeforeunload = function() { return context.save(); }; From dd75737fce046a6d8b346c4808944b875e4dd124 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 3 Nov 2017 20:32:04 -0400 Subject: [PATCH 03/12] Add loading spinner --- css/60_photos.css | 14 ++++++++------ modules/services/openstreetcam.js | 19 ++++++++++++++++--- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/css/60_photos.css b/css/60_photos.css index c09f559fe..7ce43b5ad 100644 --- a/css/60_photos.css +++ b/css/60_photos.css @@ -23,12 +23,6 @@ height: 100%; } -/*#photoviewer.hide, -.photo-wrapper.hide { - visibility: hidden; - display: block; -} -*/ /* Mapillary Image Layer */ .layer-mapillary-images { @@ -151,3 +145,11 @@ text-align: right; } + +/* OpenStreetCam viewer */ +.osc-wrapper { + background-color: #000; + background-image: url(img/loader-black.gif); + background-position: center; + background-repeat: no-repeat; +} diff --git a/modules/services/openstreetcam.js b/modules/services/openstreetcam.js index 7a16cf875..22a1db571 100644 --- a/modules/services/openstreetcam.js +++ b/modules/services/openstreetcam.js @@ -131,6 +131,13 @@ function loadNextTilePage(which, currZoom, url, tile) { delete cache.inflight[id]; if (err || !data.currentPageItems || !data.currentPageItems.length) return; + function localeDateString(s) { + if (!s) return null; + var d = new Date(s); + if (isNaN(d.getTime())) return null; + return d.toLocaleDateString(); + } + var features = data.currentPageItems.map(function(item) { var loc = [+item.lng, +item.lat], d; @@ -140,8 +147,9 @@ function loadNextTilePage(which, currZoom, url, tile) { loc: loc, key: item.id, ca: +item.heading, - captured_at: item.date_added, - imagePath: item.name + captured_at: localeDateString(item.shot_date || item.date_added), + captured_by: item.username, + imagePath: item.lth_name }; } return { @@ -288,8 +296,13 @@ export default { updateViewer: function(imagePath) { + var wrap = d3_select('#photoviewer .osc-wrapper'); + + wrap.selectAll('img') + .remove(); + if (imagePath) { - d3_select('#photoviewer .osc-wrapper img') + wrap.append('img') .attr('src', apibase + '/' + imagePath); } return this; From 51fa9280a4cd19b029f0fc4b6c2743ea3c94747c Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 3 Nov 2017 21:20:14 -0400 Subject: [PATCH 04/12] Add attribution line for openstreetcam --- css/60_photos.css | 17 ++++++++++ modules/services/openstreetcam.js | 52 ++++++++++++++++++++++++----- modules/svg/openstreetcam_images.js | 2 +- 3 files changed, 61 insertions(+), 10 deletions(-) diff --git a/css/60_photos.css b/css/60_photos.css index 7ce43b5ad..ad6116fcf 100644 --- a/css/60_photos.css +++ b/css/60_photos.css @@ -148,8 +148,25 @@ /* OpenStreetCam viewer */ .osc-wrapper { + position: relative; background-color: #000; background-image: url(img/loader-black.gif); background-position: center; background-repeat: no-repeat; } + +.osc-wrapper .osc-attribution { + width: 100%; + font-size: 10px; + text-align: right; + position: absolute; + bottom: 0; + right: 0; + padding: 4px 2px; + z-index: 10; +} + +.osc-attribution a, +.osc-attribution span { + padding: 4px 2px; +} diff --git a/modules/services/openstreetcam.js b/modules/services/openstreetcam.js index 22a1db571..593fc3be4 100644 --- a/modules/services/openstreetcam.js +++ b/modules/services/openstreetcam.js @@ -149,7 +149,9 @@ function loadNextTilePage(which, currZoom, url, tile) { ca: +item.heading, captured_at: localeDateString(item.shot_date || item.date_added), captured_by: item.username, - imagePath: item.lth_name + imagePath: item.lth_name, + sequence_id: item.sequence_id, + sequence_index: item.sequence_index }; } return { @@ -257,22 +259,23 @@ export default { .append('div') .attr('class', 'photo-wrapper osc-wrapper') .classed('hide', true) - .append('img'); + .append('div') + .attr('class', 'osc-attribution fillD'); }, showViewer: function() { - var wrap = d3_select('#photoviewer') + var viewer = d3_select('#photoviewer') .classed('hide', false); - var isHidden = wrap.selectAll('.photo-wrapper.osc-wrapper.hide').size(); + var isHidden = viewer.selectAll('.photo-wrapper.osc-wrapper.hide').size(); if (isHidden) { - wrap + viewer .selectAll('.photo-wrapper:not(.osc-wrapper)') .classed('hide', true); - wrap + viewer .selectAll('.photo-wrapper.osc-wrapper') .classed('hide', false); } @@ -295,15 +298,46 @@ export default { }, - updateViewer: function(imagePath) { + updateViewer: function(d) { var wrap = d3_select('#photoviewer .osc-wrapper'); wrap.selectAll('img') .remove(); - if (imagePath) { + if (d) { wrap.append('img') - .attr('src', apibase + '/' + imagePath); + .attr('src', apibase + '/' + d.imagePath); + + var attribution = wrap.selectAll('.osc-attribution').html(''); + + if (d.captured_by) { + attribution + .append('a') + .attr('class', 'captured_by') + .attr('href', apibase + '/user/' + d.captured_by) + .text('@' + d.captured_by); + + attribution + .append('span') + .text('|'); + } + + if (d.captured_at) { + attribution + .append('span') + .attr('class', 'captured_at') + .text(d.captured_at); + + attribution + .append('span') + .text('|'); + } + + attribution + .append('a') + .attr('class', 'image_link') + .attr('href', apibase + '/details/' + d.sequence_id + '/' + d.sequence_index) + .text('openstreetcam.org'); } return this; }, diff --git a/modules/svg/openstreetcam_images.js b/modules/svg/openstreetcam_images.js index 53b0f7c61..59f228ce1 100644 --- a/modules/svg/openstreetcam_images.js +++ b/modules/svg/openstreetcam_images.js @@ -82,7 +82,7 @@ export function svgOpenstreetcamImages(projection, context, dispatch) { openstreetcam .selectedImage(d.key) - .updateViewer(d.imagePath) + .updateViewer(d) .showViewer(); } From 4c69341d830290771eec32a27db8b180e0b21552 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sun, 5 Nov 2017 22:30:19 -0500 Subject: [PATCH 05/12] Distinguish between Mapillary and OpenStreetCam markers --- css/60_photos.css | 95 +++++++++++++++++------------------------------ 1 file changed, 35 insertions(+), 60 deletions(-) diff --git a/css/60_photos.css b/css/60_photos.css index ad6116fcf..2184940b2 100644 --- a/css/60_photos.css +++ b/css/60_photos.css @@ -24,44 +24,48 @@ } +.viewfield-group { + pointer-events: visible; + cursor: pointer; +} + +.viewfield-group * { + stroke-width: 1; + stroke: #444; + z-index: 50; +} + +.viewfield-group.selected * { + stroke-width: 2; + stroke: #222; + fill: #ff5800 !important; + z-index: 60; +} + +.viewfield-group:hover * { + stroke-width: 1; + stroke: #333; + fill: #ff9900 !important; + z-index: 70; +} + +.viewfield-group:hover path.viewfield, +.viewfield-group.selected path.viewfield, +.viewfield-group path.viewfield { + stroke-width: 0; + fill-opacity: 0.6; +} + + /* Mapillary Image Layer */ .layer-mapillary-images { pointer-events: none; } -.layer-mapillary-images .viewfield-group { - pointer-events: visible; - cursor: pointer; /* Opera */ - cursor: url(img/cursor-select-mapillary.png) 6 1, pointer; /* FF */ -} - .layer-mapillary-images .viewfield-group * { - stroke-width: 1; - stroke: #444; - fill: #ffc600; - z-index: 50; + fill: #55ff22; } -.layer-mapillary-images .viewfield-group:hover * { - stroke-width: 1; - stroke: #333; - fill: #ff9900; - z-index: 60; -} - -.layer-mapillary-images .viewfield-group.selected * { - stroke-width: 2; - stroke: #222; - fill: #ff5800; - z-index: 60; -} - -.layer-mapillary-images .viewfield-group:hover path.viewfield, -.layer-mapillary-images .viewfield-group.selected path.viewfield, -.layer-mapillary-images .viewfield-group path.viewfield { - stroke-width: 0; - fill-opacity: 0.6; -} /* Mapillary Sign Layer */ .layer-mapillary-signs { @@ -96,37 +100,8 @@ pointer-events: none; } -.layer-openstreetcam-images .viewfield-group { - pointer-events: visible; - cursor: pointer; -} - .layer-openstreetcam-images .viewfield-group * { - stroke-width: 1; - stroke: #444; - fill: #ffc600; - z-index: 50; -} - -.layer-openstreetcam-images .viewfield-group:hover * { - stroke-width: 1; - stroke: #333; - fill: #ff9900; - z-index: 60; -} - -.layer-openstreetcam-images .viewfield-group.selected * { - stroke-width: 2; - stroke: #222; - fill: #ff5800; - z-index: 60; -} - -.layer-openstreetcam-images .viewfield-group:hover path.viewfield, -.layer-openstreetcam-images .viewfield-group.selected path.viewfield, -.layer-openstreetcam-images .viewfield-group path.viewfield { - stroke-width: 0; - fill-opacity: 0.6; + fill: #77ddff; } From ccc81533706e98ef23dc81243702a439096595a5 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sun, 5 Nov 2017 22:46:53 -0500 Subject: [PATCH 06/12] Standardize on "@user | captured_at | sitelink" for attribution --- css/60_photos.css | 6 ++++++ modules/services/mapillary.js | 8 ++++---- modules/services/openstreetcam.js | 2 ++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/css/60_photos.css b/css/60_photos.css index 2184940b2..cb794cc33 100644 --- a/css/60_photos.css +++ b/css/60_photos.css @@ -142,6 +142,12 @@ } .osc-attribution a, +.osc-attribution a:visited, .osc-attribution span { padding: 4px 2px; + color: #fff; +} +.osc-attribution a:active, +.osc-attribution a:hover { + color: #77ddff; } diff --git a/modules/services/mapillary.js b/modules/services/mapillary.js index b9d3a5a61..7b266a511 100644 --- a/modules/services/mapillary.js +++ b/modules/services/mapillary.js @@ -539,12 +539,12 @@ export default { var attribution = d3_select('.mapillary-js-dom .Attribution'); var capturedAt = attribution.selectAll('.captured-at'); if (capturedAt.empty()) { - attribution - .append('span') - .text('|'); capturedAt = attribution - .append('span') + .insert('span', ':last-child') .attr('class', 'captured-at'); + attribution + .insert('span', ':last-child') + .text('|'); } capturedAt .text(timestamp); diff --git a/modules/services/openstreetcam.js b/modules/services/openstreetcam.js index 593fc3be4..494bfa3f4 100644 --- a/modules/services/openstreetcam.js +++ b/modules/services/openstreetcam.js @@ -314,6 +314,7 @@ export default { attribution .append('a') .attr('class', 'captured_by') + .attr('target', '_blank') .attr('href', apibase + '/user/' + d.captured_by) .text('@' + d.captured_by); @@ -336,6 +337,7 @@ export default { attribution .append('a') .attr('class', 'image_link') + .attr('target', '_blank') .attr('href', apibase + '/details/' + d.sequence_id + '/' + d.sequence_index) .text('openstreetcam.org'); } From eefbb68ddab6ac0479642b45282d3ae7be68daef Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 6 Nov 2017 10:41:17 -0500 Subject: [PATCH 07/12] Add viewer controls.. Rotation works and will cache val per sequence --- css/60_photos.css | 34 +++++++++++++++ modules/services/openstreetcam.js | 68 +++++++++++++++++++++++++---- modules/svg/openstreetcam_images.js | 5 ++- 3 files changed, 96 insertions(+), 11 deletions(-) diff --git a/css/60_photos.css b/css/60_photos.css index cb794cc33..637e1ae70 100644 --- a/css/60_photos.css +++ b/css/60_photos.css @@ -21,6 +21,7 @@ .photo-wrapper img { width: 100%; height: 100%; + overflow: hidden; } @@ -151,3 +152,36 @@ .osc-attribution a:hover { color: #77ddff; } + +.osc-controls-wrap { + text-align: center; + position: absolute; + top: 10px; + width: 100%; + z-index: 10; +} + +.osc-controls { + display: inline-block; + z-index: 10; +} + +.osc-controls button { + height: 18px; + width: 18px; + background: rgba(0,0,0,0.65); + color: #eee; + border-radius: 0; +} +.osc-controls button:first-of-type { + border-radius: 3px 0 0 3px; +} +.osc-controls button:last-of-type { + border-radius: 0 3px 3px 0; +} +.osc-controls button:hover, +.osc-controls button:active, +.osc-controls button:focus { + background: rgba(0,0,0,0.85); + color: #fff; +} diff --git a/modules/services/openstreetcam.js b/modules/services/openstreetcam.js index 494bfa3f4..0bc1f6d86 100644 --- a/modules/services/openstreetcam.js +++ b/modules/services/openstreetcam.js @@ -233,7 +233,8 @@ export default { } openstreetcamCache = { - images: { inflight: {}, loaded: {}, nextPage: {}, rtree: rbush() } + images: { inflight: {}, loaded: {}, nextPage: {}, rtree: rbush() }, + sequences: { rotation: {} } }; openstreetcamImage = null; @@ -245,6 +246,7 @@ export default { return searchLimited(psize, limit, projection, openstreetcamCache.images.rtree); }, + loadImages: function(projection) { var url = apibase + '/1.0/list/nearby-photos/'; loadTiles('images', url, projection); @@ -253,14 +255,58 @@ export default { loadViewer: function() { // add osc-wrapper - d3_select('#photoviewer').selectAll('.osc-wrapper') - .data([0]) - .enter() + var wrap = d3_select('#photoviewer').selectAll('.osc-wrapper') + .data([0]); + + var wrapEnter = wrap.enter() .append('div') .attr('class', 'photo-wrapper osc-wrapper') - .classed('hide', true) + .classed('hide', true); + + wrapEnter .append('div') .attr('class', 'osc-attribution fillD'); + + var controlsEnter = wrapEnter + .append('div') + .attr('class', 'osc-controls-wrap') + .append('div') + .attr('class', 'osc-controls'); + + controlsEnter + .append('button') + .text('◅'); + + controlsEnter + .append('button') + .on('click.rotate-cw', rotate(90)) + .text('↻'); + + controlsEnter + .append('button') + .on('click.rotate-ccw', rotate(-90)) + .text('↺'); + + controlsEnter + .append('button') + .text('▻'); + + + function rotate(deg) { + return function() { + if (!openstreetcamImage) return; + var seq = openstreetcamImage.sequence_id; + var r = openstreetcamCache.sequences.rotation[seq] || 0; + + r += deg; + openstreetcamCache.sequences.rotation[seq] = r; + + d3_select('#photoviewer .osc-wrapper .osc-image') + .transition() + .duration(100) + .style('transform', 'rotate(' + r + 'deg)'); + }; + } }, @@ -301,11 +347,15 @@ export default { updateViewer: function(d) { var wrap = d3_select('#photoviewer .osc-wrapper'); - wrap.selectAll('img') + wrap.selectAll('.osc-image') .remove(); if (d) { + var r = openstreetcamCache.sequences.rotation[d.sequence_id] || 0; + wrap.append('img') + .attr('class', 'osc-image') + .style('transform', 'rotate(' + r + 'deg)') .attr('src', apibase + '/' + d.imagePath); var attribution = wrap.selectAll('.osc-attribution').html(''); @@ -345,13 +395,13 @@ export default { }, - selectedImage: function(imageKey) { + selectedImage: function(d) { if (!arguments.length) return openstreetcamImage; - openstreetcamImage = imageKey; + openstreetcamImage = d; d3_selectAll('.viewfield-group') .classed('selected', function(d) { - return d.key === imageKey; + return d.key === openstreetcamImage.key; }); return this; diff --git a/modules/svg/openstreetcam_images.js b/modules/svg/openstreetcam_images.js index 59f228ce1..e17f073d2 100644 --- a/modules/svg/openstreetcam_images.js +++ b/modules/svg/openstreetcam_images.js @@ -81,7 +81,7 @@ export function svgOpenstreetcamImages(projection, context, dispatch) { context.map().centerEase(d.loc); openstreetcam - .selectedImage(d.key) + .selectedImage(d) .updateViewer(d) .showViewer(); } @@ -97,7 +97,8 @@ export function svgOpenstreetcamImages(projection, context, dispatch) { function update() { var openstreetcam = getOpenstreetcam(), data = (openstreetcam ? openstreetcam.images(projection) : []), - imageKey = openstreetcam ? openstreetcam.selectedImage() : null; + image = openstreetcam && openstreetcam.selectedImage(), + imageKey = image && image.key; var markers = layer.selectAll('.viewfield-group') .data(data, function(d) { return d.key; }); From e439ea406a43a199c8da98db16275954a3318a20 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 6 Nov 2017 15:48:44 -0500 Subject: [PATCH 08/12] Cache sequence info, draw sequence linestrings --- css/60_photos.css | 14 ++++++- modules/services/openstreetcam.js | 61 ++++++++++++++++++++++++----- modules/svg/openstreetcam_images.js | 57 ++++++++++++++++++++++----- 3 files changed, 113 insertions(+), 19 deletions(-) diff --git a/css/60_photos.css b/css/60_photos.css index 637e1ae70..a251a2658 100644 --- a/css/60_photos.css +++ b/css/60_photos.css @@ -24,7 +24,6 @@ overflow: hidden; } - .viewfield-group { pointer-events: visible; cursor: pointer; @@ -57,6 +56,11 @@ fill-opacity: 0.6; } +.sequence { + stroke-width: 2; + fill: none; +} + /* Mapillary Image Layer */ .layer-mapillary-images { @@ -67,6 +71,10 @@ fill: #55ff22; } +.layer-mapillary-images .sequence { + stroke: #55ff22; +} + /* Mapillary Sign Layer */ .layer-mapillary-signs { @@ -105,6 +113,10 @@ fill: #77ddff; } +.layer-openstreetcam-images .sequence { + stroke: #77ddff; +} + /* Mapillary viewer */ #mly .domRenderer .TagSymbol { diff --git a/modules/services/openstreetcam.js b/modules/services/openstreetcam.js index 0bc1f6d86..e12dcec6a 100644 --- a/modules/services/openstreetcam.js +++ b/modules/services/openstreetcam.js @@ -150,10 +150,19 @@ function loadNextTilePage(which, currZoom, url, tile) { captured_at: localeDateString(item.shot_date || item.date_added), captured_by: item.username, imagePath: item.lth_name, - sequence_id: item.sequence_id, - sequence_index: item.sequence_index + sequence_id: +item.sequence_id, + sequence_index: +item.sequence_index }; + + // cache sequence info + var seq = openstreetcamCache.sequences[d.sequence_id]; + if (!seq) { + seq = { rotation: 0, images: [] }; + openstreetcamCache.sequences[d.sequence_id] = seq; + } + seq.images[d.sequence_index] = d; } + return { minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d }; @@ -234,7 +243,7 @@ export default { openstreetcamCache = { images: { inflight: {}, loaded: {}, nextPage: {}, rtree: rbush() }, - sequences: { rotation: {} } + sequences: {} }; openstreetcamImage = null; @@ -247,6 +256,33 @@ export default { }, + sequences: function(projection) { + var viewport = projection.clipExtent(); + var min = [viewport[0][0], viewport[1][1]]; + var max = [viewport[1][0], viewport[0][1]]; + var bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox(); + var seq_ids = {}; + + // all sequences for images in viewport + openstreetcamCache.images.rtree.search(bbox) + .forEach(function(d) { seq_ids[d.data.sequence_id] = true; }); + + // make linestrings from those sequences + var lineStrings = []; + Object.keys(seq_ids).forEach(function(seq_id) { + var seq = openstreetcamCache.sequences[seq_id]; + var images = seq && seq.images; + if (images) { + lineStrings.push({ + type: 'LineString', + coordinates: images.map(function (d) { return d.loc; }).filter(Boolean) + }); + } + }); + return lineStrings; + }, + + loadImages: function(projection) { var url = apibase + '/1.0/list/nearby-photos/'; loadTiles('images', url, projection); @@ -275,7 +311,7 @@ export default { controlsEnter .append('button') - .text('◅'); + .text('◄'); controlsEnter .append('button') @@ -289,17 +325,23 @@ export default { controlsEnter .append('button') - .text('▻'); + .text('►'); function rotate(deg) { return function() { if (!openstreetcamImage) return; - var seq = openstreetcamImage.sequence_id; - var r = openstreetcamCache.sequences.rotation[seq] || 0; + var seq_id = openstreetcamImage.sequence_id; + var seq = openstreetcamCache.sequences[seq_id]; + if (!seq) { + seq = { rotation: 0, coords: [] }; + openstreetcamCache.sequences[seq_id] = seq; + } + + var r = seq.rotation || 0; r += deg; - openstreetcamCache.sequences.rotation[seq] = r; + seq.rotation = r; d3_select('#photoviewer .osc-wrapper .osc-image') .transition() @@ -351,7 +393,8 @@ export default { .remove(); if (d) { - var r = openstreetcamCache.sequences.rotation[d.sequence_id] || 0; + var seq = openstreetcamCache.sequences[d.sequence_id]; + var r = (seq && seq.rotation) || 0; wrap.append('img') .attr('class', 'osc-image') diff --git a/modules/svg/openstreetcam_images.js b/modules/svg/openstreetcam_images.js index e17f073d2..458bec47c 100644 --- a/modules/svg/openstreetcam_images.js +++ b/modules/svg/openstreetcam_images.js @@ -1,5 +1,11 @@ import _throttle from 'lodash-es/throttle'; + import { select as d3_select } from 'd3-selection'; +import { + geoIdentity as d3_geoIdentity, + geoPath as d3_geoPath +} from 'd3-geo'; + import { svgPointTransform } from './point_transform'; import { services } from '../services'; @@ -95,13 +101,36 @@ export function svgOpenstreetcamImages(projection, context, dispatch) { function update() { - var openstreetcam = getOpenstreetcam(), - data = (openstreetcam ? openstreetcam.images(projection) : []), - image = openstreetcam && openstreetcam.selectedImage(), - imageKey = image && image.key; + var highZoom = ~~context.map().zoom() >= minViewfieldZoom; + var openstreetcam = getOpenstreetcam(); + var sequences = (openstreetcam && highZoom ? openstreetcam.sequences(projection) : []); + var images = (openstreetcam ? openstreetcam.images(projection) : []); + var selectedImage = openstreetcam && openstreetcam.selectedImage(); + var imageKey = selectedImage && selectedImage.key; - var markers = layer.selectAll('.viewfield-group') - .data(data, function(d) { return d.key; }); + var clip = d3_geoIdentity().clipExtent(projection.clipExtent()).stream; + var project = projection.stream; + var makePath = d3_geoPath().projection({ stream: function(output) { + return project(clip(output)); + }}); + + var lineStrings = layer.selectAll('.sequences').selectAll('.sequence') + .data(sequences); + + lineStrings.exit() + .remove(); + + lineStrings = lineStrings.enter() + .append('path') + .attr('class', 'sequence') + .merge(lineStrings); + + lineStrings + .attr('d', makePath); + + + var markers = layer.selectAll('.markers').selectAll('.viewfield-group') + .data(images, function(d) { return d.key; }); markers.exit() .remove(); @@ -118,7 +147,7 @@ export function svgOpenstreetcamImages(projection, context, dispatch) { var viewfields = markers.selectAll('.viewfield') - .data(~~context.map().zoom() >= minViewfieldZoom ? [0] : []); + .data(highZoom ? [0] : []); viewfields.exit() .remove(); @@ -149,10 +178,20 @@ export function svgOpenstreetcamImages(projection, context, dispatch) { layer.exit() .remove(); - layer = layer.enter() + var layerEnter = layer.enter() .append('g') .attr('class', 'layer-openstreetcam-images') - .style('display', enabled ? 'block' : 'none') + .style('display', enabled ? 'block' : 'none'); + + layerEnter + .append('g') + .attr('class', 'sequences'); + + layerEnter + .append('g') + .attr('class', 'markers'); + + layer = layerEnter .merge(layer); if (enabled) { From 5f2b257ddc9344439fba6cf0ea7b6bd895cd0268 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 6 Nov 2017 15:51:48 -0500 Subject: [PATCH 09/12] Swap clockwise/counterclockwise rotation buttons, change glyphs --- modules/services/openstreetcam.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/services/openstreetcam.js b/modules/services/openstreetcam.js index e12dcec6a..bfcf2af25 100644 --- a/modules/services/openstreetcam.js +++ b/modules/services/openstreetcam.js @@ -315,13 +315,13 @@ export default { controlsEnter .append('button') - .on('click.rotate-cw', rotate(90)) - .text('↻'); + .on('click.rotate-ccw', rotate(-90)) + .text('⤿'); controlsEnter .append('button') - .on('click.rotate-ccw', rotate(-90)) - .text('↺'); + .on('click.rotate-cw', rotate(90)) + .text('⤾'); controlsEnter .append('button') From d44b7546562dd50a65dd58706fc2125e822b19d8 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 6 Nov 2017 16:06:25 -0500 Subject: [PATCH 10/12] Implement sequence step forward/backward --- modules/services/openstreetcam.js | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/modules/services/openstreetcam.js b/modules/services/openstreetcam.js index bfcf2af25..01320c78f 100644 --- a/modules/services/openstreetcam.js +++ b/modules/services/openstreetcam.js @@ -289,7 +289,9 @@ export default { }, - loadViewer: function() { + loadViewer: function(context) { + var that = this; + // add osc-wrapper var wrap = d3_select('#photoviewer').selectAll('.osc-wrapper') .data([0]); @@ -311,6 +313,7 @@ export default { controlsEnter .append('button') + .on('click.back', step(-1)) .text('◄'); controlsEnter @@ -325,6 +328,7 @@ export default { controlsEnter .append('button') + .on('click.forward', step(1)) .text('►'); @@ -333,11 +337,7 @@ export default { if (!openstreetcamImage) return; var seq_id = openstreetcamImage.sequence_id; var seq = openstreetcamCache.sequences[seq_id]; - - if (!seq) { - seq = { rotation: 0, coords: [] }; - openstreetcamCache.sequences[seq_id] = seq; - } + if (!seq) return; var r = seq.rotation || 0; r += deg; @@ -349,6 +349,25 @@ export default { .style('transform', 'rotate(' + r + 'deg)'); }; } + + function step(stepBy) { + return function() { + if (!openstreetcamImage) return; + var seq_id = openstreetcamImage.sequence_id; + var seq = openstreetcamCache.sequences[seq_id]; + if (!seq) return; + + var nextIndex = openstreetcamImage.sequence_index + stepBy; + var nextImage = seq.images[nextIndex]; + if (!nextImage) return; + + context.map().centerEase(nextImage.loc); + + that + .selectedImage(nextImage) + .updateViewer(nextImage); + }; + } }, From 095dc191645671f51e45f5fe4f59ded509300807 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 6 Nov 2017 21:32:03 -0500 Subject: [PATCH 11/12] Add sequence linestrings to mapillary photo layer too --- modules/services/mapillary.js | 46 ++++++++++++++++++++++++--- modules/svg/mapillary_images.js | 56 ++++++++++++++++++++++++++++----- 2 files changed, 89 insertions(+), 13 deletions(-) diff --git a/modules/services/mapillary.js b/modules/services/mapillary.js index 7b266a511..932911d8a 100644 --- a/modules/services/mapillary.js +++ b/modules/services/mapillary.js @@ -165,6 +165,15 @@ function loadNextTilePage(which, currZoom, url, tile) { captured_at: feature.properties.captured_at, pano: feature.properties.pano }; + + } else if (which === 'sequences') { + var sk = feature.properties.key; + cache.lineString[sk] = feature; // cache sequence_key -> linestring + feature.properties.coordinateProperties.image_keys.forEach(function(ik) { + cache.forImage[ik] = sk; // cache image_key -> sequence_key + }); + return false; // nothing to actually insert + } else if (which === 'objects') { d = { loc: loc, @@ -190,11 +199,11 @@ function loadNextTilePage(which, currZoom, url, tile) { return { minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d }; - }); + }).filter(Boolean); cache.rtree.load(features); - if (which === 'images') { + if (which === 'images' || which === 'sequences') { dispatch.call('loadedImages'); } else if (which === 'objects') { dispatch.call('loadedSigns'); @@ -303,11 +312,15 @@ export default { if (cache.objects && cache.objects.inflight) { _forEach(cache.objects.inflight, abortRequest); } + if (cache.sequences && cache.sequences.inflight) { + _forEach(cache.sequences.inflight, abortRequest); + } } mapillaryCache = { images: { inflight: {}, loaded: {}, nextPage: {}, nextURL: {}, rtree: rbush() }, - objects: { inflight: {}, loaded: {}, nextPage: {}, nextURL: {}, rtree: rbush() }, + objects: { inflight: {}, loaded: {}, nextPage: {}, nextURL: {}, rtree: rbush() }, + sequences: { inflight: {}, loaded: {}, nextPage: {}, nextURL: {}, rtree: rbush(), forImage: {}, lineString: {} }, detections: {} }; @@ -328,6 +341,29 @@ export default { }, + sequences: function(projection) { + var viewport = projection.clipExtent(); + var min = [viewport[0][0], viewport[1][1]]; + var max = [viewport[1][0], viewport[0][1]]; + var bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox(); + var sequenceKeys = {}; + + // all sequences for images in viewport + mapillaryCache.images.rtree.search(bbox) + .forEach(function(d) { + var sk = mapillaryCache.sequences.forImage[d.data.key]; + if (sk) { + sequenceKeys[sk] = true; + } + }); + + // Return linestrings for the sequences + return Object.keys(sequenceKeys).map(function(sk) { + return mapillaryCache.sequences.lineString[sk]; + }); + }, + + signsSupported: function() { var detected = utilDetect(); if (detected.ie) return false; @@ -354,8 +390,8 @@ export default { loadImages: function(projection) { - var url = apibase + 'images?'; - loadTiles('images', url, projection); + loadTiles('images', apibase + 'images?', projection); + loadTiles('sequences', apibase + 'sequences?', projection); }, diff --git a/modules/svg/mapillary_images.js b/modules/svg/mapillary_images.js index 9b537d616..3d2f21afa 100644 --- a/modules/svg/mapillary_images.js +++ b/modules/svg/mapillary_images.js @@ -1,5 +1,12 @@ import _throttle from 'lodash-es/throttle'; + +import { + geoIdentity as d3_geoIdentity, + geoPath as d3_geoPath +} from 'd3-geo'; + import { select as d3_select } from 'd3-selection'; + import { svgPointTransform } from './point_transform'; import { services } from '../services'; @@ -95,12 +102,35 @@ export function svgMapillaryImages(projection, context, dispatch) { function update() { - var mapillary = getMapillary(), - data = (mapillary ? mapillary.images(projection) : []), - imageKey = mapillary ? mapillary.selectedImage() : null; + var highZoom = ~~context.map().zoom() >= minViewfieldZoom; + var mapillary = getMapillary(); + var images = (mapillary ? mapillary.images(projection) : []); + var sequences = (mapillary && highZoom ? mapillary.sequences(projection) : []); + var imageKey = mapillary ? mapillary.selectedImage() : null; - var markers = layer.selectAll('.viewfield-group') - .data(data, function(d) { return d.key; }); + var clip = d3_geoIdentity().clipExtent(projection.clipExtent()).stream; + var project = projection.stream; + var makePath = d3_geoPath().projection({ stream: function(output) { + return project(clip(output)); + }}); + + var lineStrings = layer.selectAll('.sequences').selectAll('.sequence') + .data(sequences); + + lineStrings.exit() + .remove(); + + lineStrings = lineStrings.enter() + .append('path') + .attr('class', 'sequence') + .merge(lineStrings); + + lineStrings + .attr('d', makePath); + + + var markers = layer.selectAll('.markers').selectAll('.viewfield-group') + .data(images, function(d) { return d.key; }); markers.exit() .remove(); @@ -117,7 +147,7 @@ export function svgMapillaryImages(projection, context, dispatch) { var viewfields = markers.selectAll('.viewfield') - .data(~~context.map().zoom() >= minViewfieldZoom ? [0] : []); + .data(highZoom ? [0] : []); viewfields.exit() .remove(); @@ -148,10 +178,20 @@ export function svgMapillaryImages(projection, context, dispatch) { layer.exit() .remove(); - layer = layer.enter() + var layerEnter = layer.enter() .append('g') .attr('class', 'layer-mapillary-images') - .style('display', enabled ? 'block' : 'none') + .style('display', enabled ? 'block' : 'none'); + + layerEnter + .append('g') + .attr('class', 'sequences'); + + layerEnter + .append('g') + .attr('class', 'markers'); + + layer = layerEnter .merge(layer); if (enabled) { From 2947dcc2fb892203134437b96c1df16ce1fc4861 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 6 Nov 2017 23:11:00 -0500 Subject: [PATCH 12/12] Add tests for Openstreetcam service and Mapillary sequences --- modules/services/index.js | 1 + test/index.html | 1 + test/spec/services/mapillary.js | 39 ++++ test/spec/services/openstreetcam.js | 310 ++++++++++++++++++++++++++++ 4 files changed, 351 insertions(+) create mode 100644 test/spec/services/openstreetcam.js diff --git a/modules/services/index.js b/modules/services/index.js index d5ab2d365..aa5055b5c 100644 --- a/modules/services/index.js +++ b/modules/services/index.js @@ -19,6 +19,7 @@ export var services = { export { serviceMapillary, serviceNominatim, + serviceOpenstreetcam, serviceOsm, serviceTaginfo, serviceWikidata, diff --git a/test/index.html b/test/index.html index c391ca88e..18092851b 100644 --- a/test/index.html +++ b/test/index.html @@ -102,6 +102,7 @@ + diff --git a/test/spec/services/mapillary.js b/test/spec/services/mapillary.js index dd184db05..e70d896f1 100644 --- a/test/spec/services/mapillary.js +++ b/test/spec/services/mapillary.js @@ -54,6 +54,7 @@ describe('iD.serviceMapillary', function() { var cache = mapillary.cache(); expect(cache).to.have.property('images'); expect(cache).to.have.property('objects'); + expect(cache).to.have.property('sequences'); expect(cache).to.have.property('detections'); mapillary.init(); @@ -348,6 +349,44 @@ describe('iD.serviceMapillary', function() { }); }); + + describe('#sequences', function() { + it('returns sequence linestrings in the visible map area', function() { + var features = [ + { minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '0', loc: [10,0], ca: 90 } }, + { minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '1', loc: [10,0], ca: 90 } }, + { minX: 10, minY: 1, maxX: 10, maxY: 1, data: { key: '2', loc: [10,1], ca: 90 } } + ]; + + mapillary.cache().images.rtree.load(features); + + var gj = { + type: 'Feature', + geometry: { + type: 'LineString', + coordinates: [[10,0], [10,0], [10,1]], + properties: { + key: '-', + pano: false, + coordinateProperties: { + cas: [90, 90, 90], + image_keys: ['0', '1', '2'] + } + } + } + }; + + mapillary.cache().sequences.lineString['-'] = gj; + mapillary.cache().sequences.forImage['0'] = '-'; + mapillary.cache().sequences.forImage['1'] = '-'; + mapillary.cache().sequences.forImage['2'] = '-'; + + var res = mapillary.sequences(context.projection); + expect(res).to.deep.eql([gj]); + }); + }); + + describe('#signsSupported', function() { it('returns false for Internet Explorer', function() { ua = 'Trident/7.0; rv:11.0'; diff --git a/test/spec/services/openstreetcam.js b/test/spec/services/openstreetcam.js new file mode 100644 index 000000000..facdd55ac --- /dev/null +++ b/test/spec/services/openstreetcam.js @@ -0,0 +1,310 @@ +describe('iD.serviceOpenstreetcam', function() { + var dimensions = [64, 64], + ua = navigator.userAgent, + isPhantom = (navigator.userAgent.match(/PhantomJS/) !== null), + uaMock = function () { return ua; }, + context, server, openstreetcam, orig; + + before(function() { + iD.services.openstreetcam = iD.serviceOpenstreetcam; + }); + + after(function() { + delete iD.services.openstreetcam; + }); + + beforeEach(function() { + context = iD.Context().assetPath('../dist/'); + context.projection + .scale(667544.214430109) // z14 + .translate([-116508, 0]) // 10,0 + .clipExtent([[0,0], dimensions]); + + server = sinon.fakeServer.create(); + openstreetcam = iD.services.openstreetcam; + openstreetcam.reset(); + + /* eslint-disable no-global-assign */ + /* mock userAgent */ + if (isPhantom) { + orig = navigator; + navigator = Object.create(orig, { userAgent: { get: uaMock }}); + } else { + orig = navigator.__lookupGetter__('userAgent'); + navigator.__defineGetter__('userAgent', uaMock); + } + }); + + afterEach(function() { + server.restore(); + + /* restore userAgent */ + if (isPhantom) { + navigator = orig; + } else { + navigator.__defineGetter__('userAgent', orig); + } + /* eslint-enable no-global-assign */ + }); + + + describe('#init', function() { + it('Initializes cache one time', function() { + var cache = openstreetcam.cache(); + expect(cache).to.have.property('images'); + expect(cache).to.have.property('sequences'); + + openstreetcam.init(); + var cache2 = openstreetcam.cache(); + expect(cache).to.equal(cache2); + }); + }); + + describe('#reset', function() { + it('resets cache and image', function() { + openstreetcam.cache({foo: 'bar'}); + openstreetcam.selectedImage('baz'); + + openstreetcam.reset(); + expect(openstreetcam.cache()).to.not.have.property('foo'); + expect(openstreetcam.selectedImage()).to.be.null; + }); + }); + + describe('#loadImages', function() { + it('fires loadedImages when images are loaded', function() { + var spy = sinon.spy(); + openstreetcam.on('loadedImages', spy); + openstreetcam.loadImages(context.projection); + + var data = { + status: { apiCode: '600', httpCode: 200, httpMessage: 'Success' }, + currentPageItems:[{ + id: '1', + sequence_id: '100', + sequence_index: '1', + lat: '0', + lng: '10.001', + name: 'storage6\/files\/photo\/foo1.jpg', + lth_name: 'storage6\/files\/photo\/lth\/foo1.jpg', + th_name: 'storage6\/files\/photo\/th\/foo1.jpg', + shot_date: '2017-09-24 23:58:07', + heading: '90', + username: 'test' + }, { + id: '2', + sequence_id: '100', + sequence_index: '2', + lat: '0', + lng: '10.002', + name: 'storage6\/files\/photo\/foo2.jpg', + lth_name: 'storage6\/files\/photo\/lth\/foo2.jpg', + th_name: 'storage6\/files\/photo\/th\/foo2.jpg', + shot_date: '2017-09-24 23:58:07', + heading: '90', + username: 'test' + }, { + id: '3', + sequence_id: '100', + sequence_index: '3', + lat: '0', + lng: '10.003', + name: 'storage6\/files\/photo\/foo3.jpg', + lth_name: 'storage6\/files\/photo\/lth\/foo3.jpg', + th_name: 'storage6\/files\/photo\/th\/foo3.jpg', + shot_date: '2017-09-24 23:58:07', + heading: '90', + username: 'test' + }], + totalFilteredItems: ['3'] + }; + + server.respondWith('POST', /nearby-photos/, + [200, { 'Content-Type': 'application/json' }, JSON.stringify(data) ]); + server.respond(); + + expect(spy).to.have.been.calledOnce; + }); + + it('does not load images around null island', function() { + var spy = sinon.spy(); + context.projection.translate([0,0]); + openstreetcam.on('loadedImages', spy); + openstreetcam.loadImages(context.projection); + + var data = { + status: { apiCode: '600', httpCode: 200, httpMessage: 'Success' }, + currentPageItems:[{ + id: '1', + sequence_id: '100', + sequence_index: '1', + lat: '0', + lng: '0', + name: 'storage6\/files\/photo\/foo1.jpg', + lth_name: 'storage6\/files\/photo\/lth\/foo1.jpg', + th_name: 'storage6\/files\/photo\/th\/foo1.jpg', + shot_date: '2017-09-24 23:58:07', + heading: '90', + username: 'test' + }, { + id: '2', + sequence_id: '100', + sequence_index: '2', + lat: '0', + lng: '0', + name: 'storage6\/files\/photo\/foo2.jpg', + lth_name: 'storage6\/files\/photo\/lth\/foo2.jpg', + th_name: 'storage6\/files\/photo\/th\/foo2.jpg', + shot_date: '2017-09-24 23:58:07', + heading: '90', + username: 'test' + }, { + id: '3', + sequence_id: '100', + sequence_index: '3', + lat: '0', + lng: '0', + name: 'storage6\/files\/photo\/foo3.jpg', + lth_name: 'storage6\/files\/photo\/lth\/foo3.jpg', + th_name: 'storage6\/files\/photo\/th\/foo3.jpg', + shot_date: '2017-09-24 23:58:07', + heading: '90', + username: 'test' + }], + totalFilteredItems: ['3'] + }; + + server.respondWith('POST', /nearby-photos/, + [200, { 'Content-Type': 'application/json' }, JSON.stringify(data) ]); + server.respond(); + + expect(spy).to.have.been.not.called; + }); + + it.skip('loads multiple pages of image results', function() { + var spy = sinon.spy(); + openstreetcam.on('loadedImages', spy); + openstreetcam.loadImages(context.projection); + + var features0 = [], + features1 = [], + i; + + for (i = 0; i < 1000; i++) { + features0.push({ + id: String(i), + sequence_id: '100', + sequence_index: String(i), + lat: '10', + lng: '0', + name: 'storage6\/files\/photo\/foo' + String(i) +'.jpg', + lth_name: 'storage6\/files\/photo\/lth\/foo' + String(i) +'.jpg', + th_name: 'storage6\/files\/photo\/th\/foo' + String(i) +'.jpg', + shot_date: '2017-09-24 23:58:07', + heading: '90', + username: 'test' + }); + } + for (i = 0; i < 500; i++) { + features1.push({ + id: String(i), + sequence_id: '100', + sequence_index: String(1000 + i), + lat: '10', + lng: '0', + name: 'storage6\/files\/photo\/foo' + String(1000 + i) +'.jpg', + lth_name: 'storage6\/files\/photo\/lth\/foo' + String(1000 + i) +'.jpg', + th_name: 'storage6\/files\/photo\/th\/foo' + String(1000 + i) +'.jpg', + shot_date: '2017-09-24 23:58:07', + heading: '90', + username: 'test' + }); + } + + var response0 = { + status: { apiCode: '600', httpCode: 200, httpMessage: 'Success' }, + currentPageItems: [features0], + totalFilteredItems: ['1000'] + }, + response1 = { + status: { apiCode: '600', httpCode: 200, httpMessage: 'Success' }, + currentPageItems: [features1], + totalFilteredItems: ['500'] + }; + + server.respondWith('POST', /nearby-photos/, function (request) { + var response; + if (request.requestBody.match(/page=1/) !== null) { + response = JSON.stringify(response0); + } else if (request.requestBody.match(/page=2/) !== null) { + response = JSON.stringify(response1); + } + request.respond(200, {'Content-Type': 'application/json'}, response); + }); + server.respond(); + + expect(spy).to.have.been.calledTwice; + }); + }); + + + describe('#images', function() { + it('returns images in the visible map area', function() { + var features = [ + { minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '0', loc: [10,0], ca: 90, sequence_id: 100, sequence_index: 0 } }, + { minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '1', loc: [10,0], ca: 90, sequence_id: 100, sequence_index: 1 } }, + { minX: 10, minY: 1, maxX: 10, maxY: 1, data: { key: '2', loc: [10,1], ca: 90, sequence_id: 100, sequence_index: 2 } } + ]; + + openstreetcam.cache().images.rtree.load(features); + var res = openstreetcam.images(context.projection); + + expect(res).to.deep.eql([ + { key: '0', loc: [10,0], ca: 90, sequence_id: 100, sequence_index: 0 }, + { key: '1', loc: [10,0], ca: 90, sequence_id: 100, sequence_index: 1 } + ]); + }); + + it('limits results no more than 3 stacked images in one spot', function() { + var features = [ + { minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '0', loc: [10,0], ca: 90, sequence_id: 100, sequence_index: 0 } }, + { minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '1', loc: [10,0], ca: 90, sequence_id: 100, sequence_index: 1 } }, + { minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '2', loc: [10,0], ca: 90, sequence_id: 100, sequence_index: 2 } }, + { minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '3', loc: [10,0], ca: 90, sequence_id: 100, sequence_index: 3 } }, + { minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '4', loc: [10,0], ca: 90, sequence_id: 100, sequence_index: 4 } } + ]; + + openstreetcam.cache().images.rtree.load(features); + var res = openstreetcam.images(context.projection); + expect(res).to.have.length.of.at.most(3); + }); + }); + + + describe('#sequences', function() { + it('returns sequence linestrings in the visible map area', function() { + var features = [ + { minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '0', loc: [10,0], ca: 90, sequence_id: 100, sequence_index: 0 } }, + { minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '1', loc: [10,0], ca: 90, sequence_id: 100, sequence_index: 1 } }, + { minX: 10, minY: 1, maxX: 10, maxY: 1, data: { key: '2', loc: [10,1], ca: 90, sequence_id: 100, sequence_index: 2 } } + ]; + + openstreetcam.cache().images.rtree.load(features); + openstreetcam.cache().sequences['100'] = { rotation: 0, images: [ features[0].data, features[1].data, features[2].data ] }; + + var res = openstreetcam.sequences(context.projection); + expect(res).to.deep.eql([{ + type: 'LineString', + coordinates: [[10,0], [10,0], [10,1]] + }]); + }); + }); + + describe('#selectedImage', function() { + it('sets and gets selected image', function() { + openstreetcam.selectedImage('foo'); + expect(openstreetcam.selectedImage()).to.eql('foo'); + }); + }); + +});