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;
});
});