diff --git a/css/60_mapillary.css b/css/60_mapillary.css deleted file mode 100644 index a3067ea7e..000000000 --- a/css/60_mapillary.css +++ /dev/null @@ -1,115 +0,0 @@ -/* 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; -} - -.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 { - pointer-events: none; -} - -.layer-mapillary-signs .icon-sign .icon-sign-body { - min-width: 20px; - height: 24px; - width: 24px; - outline: 2px solid transparent; - pointer-events: visible; - cursor: pointer; /* Opera */ - cursor: url(img/cursor-select-mapillary.png) 6 1, pointer; /* FF */ - z-index: 70; - overflow: visible; -} - -.layer-mapillary-signs .icon-sign:hover .icon-sign-body { - outline: 2px solid rgba(255,198,0,0.8); - z-index: 80; -} - -.layer-mapillary-signs .icon-sign.selected .icon-sign-body { - outline: 2px solid rgba(255,0,0,0.8); - z-index: 80; -} - - -/* Mapillary viewer */ -#mly .domRenderer .TagSymbol { - font-size: 10px; - background-color: rgba(0, 0, 0, 0.4); - padding: 0 4px; - border-radius: 4px; - top: -25px; -} - -#mly .domRenderer .Attribution { - width: 100%; - font-size: 10px; - 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/css/60_photos.css b/css/60_photos.css new file mode 100644 index 000000000..a251a2658 --- /dev/null +++ b/css/60_photos.css @@ -0,0 +1,199 @@ +/* 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%; + overflow: hidden; +} + +.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; +} + +.sequence { + stroke-width: 2; + fill: none; +} + + +/* Mapillary Image Layer */ +.layer-mapillary-images { + pointer-events: none; +} + +.layer-mapillary-images .viewfield-group * { + fill: #55ff22; +} + +.layer-mapillary-images .sequence { + stroke: #55ff22; +} + + +/* Mapillary Sign Layer */ +.layer-mapillary-signs { + pointer-events: none; +} + +.layer-mapillary-signs .icon-sign .icon-sign-body { + min-width: 20px; + height: 24px; + width: 24px; + outline: 2px solid transparent; + pointer-events: visible; + cursor: pointer; /* Opera */ + cursor: url(img/cursor-select-mapillary.png) 6 1, pointer; /* FF */ + z-index: 70; + overflow: visible; +} + +.layer-mapillary-signs .icon-sign:hover .icon-sign-body { + outline: 2px solid rgba(255,198,0,0.8); + z-index: 80; +} + +.layer-mapillary-signs .icon-sign.selected .icon-sign-body { + outline: 2px solid rgba(255,0,0,0.8); + z-index: 80; +} + + +/* OpenStreetCam Image Layer */ +.layer-openstreetcam-images { + pointer-events: none; +} + +.layer-openstreetcam-images .viewfield-group * { + fill: #77ddff; +} + +.layer-openstreetcam-images .sequence { + stroke: #77ddff; +} + + +/* Mapillary viewer */ +#mly .domRenderer .TagSymbol { + font-size: 10px; + background-color: rgba(0, 0, 0, 0.4); + padding: 0 4px; + border-radius: 4px; + top: -25px; +} + +#mly .domRenderer .Attribution { + width: 100%; + font-size: 10px; + text-align: right; +} + + +/* 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 a:visited, +.osc-attribution span { + padding: 4px 2px; + color: #fff; +} +.osc-attribution a:active, +.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/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 1536a1780..449ca6fae 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..aa5055b5c 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, @@ -17,6 +19,7 @@ export var services = { export { serviceMapillary, serviceNominatim, + serviceOpenstreetcam, serviceOsm, serviceTaginfo, serviceWikidata, diff --git a/modules/services/mapillary.js b/modules/services/mapillary.js index bc0c5d425..932911d8a 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'; @@ -166,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, @@ -191,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'); @@ -304,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: {} }; @@ -329,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; @@ -355,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); }, @@ -377,28 +412,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 +441,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 +545,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; }); @@ -544,12 +575,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 new file mode 100644 index 000000000..01320c78f --- /dev/null +++ b/modules/services/openstreetcam.js @@ -0,0 +1,479 @@ +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 { 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') + .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]; + 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; + + if (which === 'images') { + d = { + loc: loc, + key: item.id, + ca: +item.heading, + 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 + }; + + // 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 + }; + }); + + 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() }, + sequences: {} + }; + + openstreetcamImage = null; + }, + + + images: function(projection) { + var psize = 16, limit = 3; + return searchLimited(psize, limit, projection, openstreetcamCache.images.rtree); + }, + + + 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); + }, + + + loadViewer: function(context) { + var that = this; + + // add osc-wrapper + 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); + + 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') + .on('click.back', step(-1)) + .text('◄'); + + controlsEnter + .append('button') + .on('click.rotate-ccw', rotate(-90)) + .text('⤿'); + + controlsEnter + .append('button') + .on('click.rotate-cw', rotate(90)) + .text('⤾'); + + controlsEnter + .append('button') + .on('click.forward', step(1)) + .text('►'); + + + function rotate(deg) { + return function() { + if (!openstreetcamImage) return; + var seq_id = openstreetcamImage.sequence_id; + var seq = openstreetcamCache.sequences[seq_id]; + if (!seq) return; + + var r = seq.rotation || 0; + r += deg; + seq.rotation = r; + + d3_select('#photoviewer .osc-wrapper .osc-image') + .transition() + .duration(100) + .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); + }; + } + }, + + + showViewer: function() { + var viewer = d3_select('#photoviewer') + .classed('hide', false); + + var isHidden = viewer.selectAll('.photo-wrapper.osc-wrapper.hide').size(); + + if (isHidden) { + viewer + .selectAll('.photo-wrapper:not(.osc-wrapper)') + .classed('hide', true); + + viewer + .selectAll('.photo-wrapper.osc-wrapper') + .classed('hide', false); + } + + return this; + }, + + + hideViewer: function() { + d3_select('#photoviewer') + .classed('hide', true) + .selectAll('.photo-wrapper') + .classed('hide', true); + + d3_selectAll('.layer-openstreetcam-images .viewfield-group') + .classed('selected', false); + + openstreetcamImage = null; + return this; + }, + + + updateViewer: function(d) { + var wrap = d3_select('#photoviewer .osc-wrapper'); + + wrap.selectAll('.osc-image') + .remove(); + + if (d) { + var seq = openstreetcamCache.sequences[d.sequence_id]; + var r = (seq && seq.rotation) || 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(''); + + if (d.captured_by) { + attribution + .append('a') + .attr('class', 'captured_by') + .attr('target', '_blank') + .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('target', '_blank') + .attr('href', apibase + '/details/' + d.sequence_id + '/' + d.sequence_index) + .text('openstreetcam.org'); + } + return this; + }, + + + selectedImage: function(d) { + if (!arguments.length) return openstreetcamImage; + openstreetcamImage = d; + + d3_selectAll('.viewfield-group') + .classed('selected', function(d) { + return d.key === openstreetcamImage.key; + }); + + 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/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) { diff --git a/modules/svg/openstreetcam_images.js b/modules/svg/openstreetcam_images.js new file mode 100644 index 000000000..458bec47c --- /dev/null +++ b/modules/svg/openstreetcam_images.js @@ -0,0 +1,229 @@ +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'; + + +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) + .updateViewer(d) + .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 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 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(); + + 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(highZoom ? [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(); + + var layerEnter = layer.enter() + .append('g') + .attr('class', 'layer-openstreetcam-images') + .style('display', enabled ? 'block' : 'none'); + + layerEnter + .append('g') + .attr('class', 'sequences'); + + layerEnter + .append('g') + .attr('class', 'markers'); + + layer = layerEnter + .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/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(); }; 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/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'); + }); + }); + +}); 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; }); });