diff --git a/API.md b/API.md
index b88592cae..e0981f8e0 100644
--- a/API.md
+++ b/API.md
@@ -137,7 +137,7 @@ iD can use external presets exclusively or along with the default OpenStreetMap
var iD = iD()
.presets(customPresets)
- .taginfo(iD.taginfo())
+ .taginfo(iD.services.taginfo())
.imagery(iD.data.imagery);
```
@@ -152,7 +152,7 @@ Just like Presets, Imagery can be configured using the `iD().imagery` accessor.
var iD = iD()
.presets(customPresets)
- .taginfo(iD.taginfo())
+ .taginfo(iD.services.taginfo())
.imagery(customImagery);
```
@@ -168,7 +168,7 @@ The Imagery object should follow the structure defined by [editor-imagery-index]
var iD = iD()
.presets(customPresets)
- .taginfo(iD.taginfo().endpoint('url'))
+ .taginfo(iD.services.taginfo().endpoint('url'))
.imagery(customImagery);
```
diff --git a/Makefile b/Makefile
index 4ad6861eb..65a4f96a5 100644
--- a/Makefile
+++ b/Makefile
@@ -63,6 +63,7 @@ dist/iD.js: \
js/lib/marked.js \
js/id/start.js \
js/id/id.js \
+ js/id/services.js \
js/id/services/*.js \
js/id/util.js \
js/id/util/*.js \
diff --git a/css/app.css b/css/app.css
index a83bb2847..243b1c43c 100644
--- a/css/app.css
+++ b/css/app.css
@@ -295,6 +295,8 @@ ul li { list-style: none;}
.fl { float: left;}
.fr { float: right;}
+.al { left: 0; }
+.ar { right: 0; }
div.hide,
form.hide,
@@ -655,7 +657,6 @@ button.save.has-count .count::before {
.mapillary-image {
position: absolute;
- right: 0;
bottom: 30px;
width: 330px;
height: 250px;
@@ -669,7 +670,6 @@ button.save.has-count .count::before {
height: auto;
background-color: rgba(0,0,0,.5);
bottom: 0;
- right: 0;
padding: 5px 10px;
}
@@ -1797,6 +1797,10 @@ div.full-screen > button:hover {
color: #7092FF;
}
+.layer-list:empty {
+ display: none;
+}
+
.layer-list > li:first-child {
border-radius: 3px 3px 0 0;
}
diff --git a/css/map.css b/css/map.css
index 624aa97a2..88fcb6003 100644
--- a/css/map.css
+++ b/css/map.css
@@ -1514,43 +1514,84 @@ text.gpx {
fill: #FF26D4;
}
-/* Mapillary Layer */
+/* Mapillary Image Layer */
-.layer-mapillary {
+.layer-mapillary-images {
pointer-events: none;
}
-.layer-mapillary g {
+.layer-mapillary-images .viewfield-group {
pointer-events: visible;
cursor: pointer; /* Opera */
cursor: url(img/cursor-select-mapillary.png) 6 1, pointer; /* FF */
}
-.layer-mapillary g * {
+.layer-mapillary-images .viewfield-group * {
stroke-width: 1;
stroke: #444;
fill: #ffc600;
+ z-index: 50;
}
-.layer-mapillary g:hover * {
+.layer-mapillary-images .viewfield-group:hover * {
stroke-width: 1;
stroke: #333;
fill: #ff9900;
+ z-index: 60;
}
-.layer-mapillary g.selected * {
+.layer-mapillary-images .viewfield-group.selected * {
stroke-width: 2;
stroke: #222;
fill: #ff5800;
+ z-index: 60;
}
-.layer-mapillary g:hover path.viewfield,
-.layer-mapillary g.selected path.viewfield,
-.layer-mapillary g path.viewfield {
+.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 body {
+ min-width: 20px;
+ height: 28px;
+ width: 28px;
+ border: 2px solid transparent;
+ pointer-events: visible;
+ cursor: pointer; /* Opera */
+ cursor: url(img/cursor-select-mapillary.png) 6 1, pointer; /* FF */
+ z-index: 70;
+}
+
+.layer-mapillary-signs .icon-sign:hover body {
+ border: 2px solid rgba(255,198,0,0.8);
+ z-index: 80;
+ }
+
+.layer-mapillary-signs .icon-sign.selected body {
+ border: 2px solid rgba(255,0,0,0.8);
+ z-index: 80;
+ }
+
+.layer-mapillary-signs .icon-sign .t {
+ font-size: 28px;
+ z-index: 70;
+ position: absolute;
+}
+
+.layer-mapillary-signs .icon-sign:hover .t,
+.layer-mapillary-signs .icon-sign.selected .t {
+ z-index: 80;
+}
+
/* Modes */
.mode-draw-line .vertex.active,
diff --git a/data/core.yaml b/data/core.yaml
index 0bed7af65..2a6864488 100644
--- a/data/core.yaml
+++ b/data/core.yaml
@@ -415,9 +415,13 @@ en:
drag_drop: "Drag and drop a .gpx file on the page, or click the button to the right to browse"
zoom: "Zoom to GPX track"
browse: "Browse for a .gpx file"
- mapillary:
+ mapillary_images:
tooltip: "Street-level photos from Mapillary"
title: "Photo Overlay (Mapillary)"
+ mapillary_signs:
+ tooltip: "Traffic signs from Mapillary"
+ title: "Traffic Sign Overlay (Mapillary)"
+ mapillary:
view_on_mapillary: "View this image on Mapillary"
help:
title: "Help"
diff --git a/dist/locales/en.json b/dist/locales/en.json
index ad4ddff19..35e4b7329 100644
--- a/dist/locales/en.json
+++ b/dist/locales/en.json
@@ -501,9 +501,15 @@
"zoom": "Zoom to GPX track",
"browse": "Browse for a .gpx file"
},
- "mapillary": {
+ "mapillary_images": {
"tooltip": "Street-level photos from Mapillary",
- "title": "Photo Overlay (Mapillary)",
+ "title": "Photo Overlay (Mapillary)"
+ },
+ "mapillary_signs": {
+ "tooltip": "Traffic signs from Mapillary",
+ "title": "Traffic Sign Overlay (Mapillary)"
+ },
+ "mapillary": {
"view_on_mapillary": "View this image on Mapillary"
},
"help": {
diff --git a/dist/traffico b/dist/traffico
new file mode 160000
index 000000000..2ff4ac82f
--- /dev/null
+++ b/dist/traffico
@@ -0,0 +1 @@
+Subproject commit 2ff4ac82f199f5e793e45e2326c24228db0d7726
diff --git a/index.html b/index.html
index 9bb30b26b..798ce1acc 100644
--- a/index.html
+++ b/index.html
@@ -6,6 +6,7 @@
+
@@ -39,7 +40,9 @@
-
+
+
+
@@ -57,7 +60,8 @@
-
+
+
@@ -249,7 +253,7 @@
id = iD()
.presets(iD.data.presets)
.imagery(iD.data.imagery)
- .taginfo(iD.taginfo())
+ .taginfo(iD.services.taginfo())
.assetPath('dist/');
d3.select('#id-container')
diff --git a/js/id/core/tree.js b/js/id/core/tree.js
index 6bc40c33d..05bbc2230 100644
--- a/js/id/core/tree.js
+++ b/js/id/core/tree.js
@@ -2,17 +2,8 @@ iD.Tree = function(head) {
var rtree = rbush(),
rectangles = {};
- function extentRectangle(extent) {
- return [
- extent[0][0],
- extent[0][1],
- extent[1][0],
- extent[1][1]
- ];
- }
-
function entityRectangle(entity) {
- var rect = extentRectangle(entity.extent(head));
+ var rect = entity.extent(head).rectangle();
rect.id = entity.id;
rectangles[entity.id] = rect;
return rect;
@@ -90,7 +81,7 @@ iD.Tree = function(head) {
rtree.load(_.map(insertions, entityRectangle));
}
- return rtree.search(extentRectangle(extent)).map(function(rect) {
+ return rtree.search(extent.rectangle()).map(function(rect) {
return head.entity(rect.id);
});
};
diff --git a/js/id/geo/extent.js b/js/id/geo/extent.js
index 912bcf08a..685c22282 100644
--- a/js/id/geo/extent.js
+++ b/js/id/geo/extent.js
@@ -45,6 +45,10 @@ _.extend(iD.geo.Extent.prototype, {
(this[0][1] + this[1][1]) / 2];
},
+ rectangle: function() {
+ return [this[0][0], this[0][1], this[1][0], this[1][1]];
+ },
+
polygon: function() {
return [
[this[0][0], this[0][1]],
@@ -100,7 +104,7 @@ _.extend(iD.geo.Extent.prototype, {
},
toParam: function() {
- return [this[0][0], this[0][1], this[1][0], this[1][1]].join(',');
+ return this.rectangle().join(',');
}
});
diff --git a/js/id/id.js b/js/id/id.js
index 0ff24a6f6..b0d3dc83d 100644
--- a/js/id/id.js
+++ b/js/id/id.js
@@ -2,11 +2,12 @@ window.iD = function () {
window.locale.en = iD.data.en;
window.locale.current('en');
- var context = {},
- storage;
+ var dispatch = d3.dispatch('enter', 'exit'),
+ context = {};
// https://github.com/openstreetmap/iD/issues/772
// http://mathiasbynens.be/notes/localstorage-pattern#comment-9
+ var storage;
try { storage = localStorage; } catch (e) {} // eslint-disable-line no-empty
storage = storage || (function() {
var s = {};
@@ -30,34 +31,7 @@ window.iD = function () {
}
};
- /* Accessor for setting minimum zoom for editing features. */
-
- var minEditableZoom = 16;
- context.minEditableZoom = function(_) {
- if (!arguments.length) return minEditableZoom;
- minEditableZoom = _;
- connection.tileZoom(_);
- return context;
- };
-
- var history = iD.History(context),
- dispatch = d3.dispatch('enter', 'exit'),
- mode,
- container,
- ui = iD.ui(context),
- connection = iD.Connection(),
- locale = iD.detect().locale,
- localePath;
-
- if (locale && iD.data.locales.indexOf(locale) === -1) {
- locale = locale.split('-')[0];
- }
-
- context.preauth = function(options) {
- connection.switch(options);
- return context;
- };
-
+ var locale, localePath;
context.locale = function(loc, path) {
locale = loc;
localePath = path;
@@ -83,16 +57,24 @@ window.iD = function () {
}
};
+
/* Straight accessors. Avoid using these if you can. */
+ var ui, connection, history;
context.ui = function() { return ui; };
context.connection = function() { return connection; };
context.history = function() { return history; };
+
/* Connection */
function entitiesLoaded(err, result) {
if (!err) history.merge(result.data, result.extent);
}
+ context.preauth = function(options) {
+ connection.switch(options);
+ return context;
+ };
+
context.loadTiles = function(projection, dimensions, callback) {
function done(err, result) {
entitiesLoaded(err, result);
@@ -133,13 +115,17 @@ window.iD = function () {
});
};
+ var minEditableZoom = 16;
+ context.minEditableZoom = function(_) {
+ if (!arguments.length) return minEditableZoom;
+ minEditableZoom = _;
+ connection.tileZoom(_);
+ return context;
+ };
+
+
/* History */
- context.graph = history.graph;
- context.changes = history.changes;
- context.intersects = history.intersects;
-
var inIntro = false;
-
context.inIntro = function(_) {
if (!arguments.length) return inIntro;
inIntro = _;
@@ -157,45 +143,34 @@ window.iD = function () {
connection.flush();
features.reset();
history.reset();
+ _.each(iD.services, function(service) {
+ var reset = service().reset;
+ if (reset) reset(context);
+ });
return context;
};
- // Debounce save, since it's a synchronous localStorage write,
- // and history changes can happen frequently (e.g. when dragging).
- context.debouncedSave = _.debounce(context.save, 350);
- function withDebouncedSave(fn) {
- return function() {
- var result = fn.apply(history, arguments);
- context.debouncedSave();
- return result;
- };
- }
-
- context.perform = withDebouncedSave(history.perform);
- context.replace = withDebouncedSave(history.replace);
- context.pop = withDebouncedSave(history.pop);
- context.overwrite = withDebouncedSave(history.overwrite);
- context.undo = withDebouncedSave(history.undo);
- context.redo = withDebouncedSave(history.redo);
/* Graph */
context.hasEntity = function(id) {
return history.graph().hasEntity(id);
};
-
context.entity = function(id) {
return history.graph().entity(id);
};
-
context.childNodes = function(way) {
return history.graph().childNodes(way);
};
-
context.geometry = function(id) {
return context.entity(id).geometry(history.graph());
};
+
/* Modes */
+ var mode;
+ context.mode = function() {
+ return mode;
+ };
context.enter = function(newMode) {
if (mode) {
mode.exit();
@@ -207,10 +182,6 @@ window.iD = function () {
dispatch.enter(mode);
};
- context.mode = function() {
- return mode;
- };
-
context.selectedIDs = function() {
if (mode && mode.selectedIDs) {
return mode.selectedIDs();
@@ -219,15 +190,16 @@ window.iD = function () {
}
};
+
/* Behaviors */
context.install = function(behavior) {
context.surface().call(behavior);
};
-
context.uninstall = function(behavior) {
context.surface().call(behavior.off);
};
+
/* Copy/Paste */
var copyIDs = [], copyGraph;
context.copyGraph = function() { return copyGraph; };
@@ -238,15 +210,14 @@ window.iD = function () {
return context;
};
- /* Projection */
- context.projection = iD.geo.RawMercator();
/* Background */
- var background = iD.Background(context);
+ var background;
context.background = function() { return background; };
+
/* Features */
- var features = iD.Features(context);
+ var features;
context.features = function() { return features; };
context.hasHiddenConnections = function(id) {
var graph = history.graph(),
@@ -254,20 +225,13 @@ window.iD = function () {
return features.hasHiddenConnections(entity, graph);
};
+
/* Map */
- var map = iD.Map(context);
+ var map;
context.map = function() { return map; };
context.layers = function() { return map.layers; };
context.surface = function() { return map.surface; };
context.editable = function() { return map.editable(); };
- context.mouse = map.mouse;
- context.extent = map.extent;
- context.pan = map.pan;
- context.zoomIn = map.zoomIn;
- context.zoomOut = map.zoomOut;
- context.zoomInFurther = map.zoomInFurther;
- context.zoomOutFurther = map.zoomOutFurther;
- context.redrawEnable = map.redrawEnable;
context.surfaceRect = function() {
// Work around a bug in Firefox.
@@ -276,9 +240,9 @@ window.iD = function () {
return context.surface().node().parentNode.getBoundingClientRect();
};
- /* Presets */
- var presets = iD.presets();
+ /* Presets */
+ var presets;
context.presets = function(_) {
if (!arguments.length) return presets;
presets.load(_);
@@ -286,17 +250,28 @@ window.iD = function () {
return context;
};
+
+ /* Imagery */
context.imagery = function(_) {
background.load(_);
return context;
};
+
+ /* Container */
+ var container, embed;
context.container = function(_) {
if (!arguments.length) return container;
container = _;
container.classed('id-container', true);
return context;
};
+ context.embed = function(_) {
+ if (!arguments.length) return embed;
+ embed = _;
+ return context;
+ };
+
/* Taginfo */
var taginfo;
@@ -306,13 +281,8 @@ window.iD = function () {
return context;
};
- var embed = false;
- context.embed = function(_) {
- if (!arguments.length) return embed;
- embed = _;
- return context;
- };
+ /* Assets */
var assetPath = '';
context.assetPath = function(_) {
if (!arguments.length) return assetPath;
@@ -332,9 +302,63 @@ window.iD = function () {
return assetMap[asset] || assetPath + asset;
};
+
+ /* Init */
+
+ context.projection = iD.geo.RawMercator();
+
+ locale = iD.detect().locale;
+ if (locale && iD.data.locales.indexOf(locale) === -1) {
+ locale = locale.split('-')[0];
+ }
+
+ history = iD.History(context);
+ context.graph = history.graph;
+ context.changes = history.changes;
+ context.intersects = history.intersects;
+
+ // Debounce save, since it's a synchronous localStorage write,
+ // and history changes can happen frequently (e.g. when dragging).
+ context.debouncedSave = _.debounce(context.save, 350);
+ function withDebouncedSave(fn) {
+ return function() {
+ var result = fn.apply(history, arguments);
+ context.debouncedSave();
+ return result;
+ };
+ }
+
+ context.perform = withDebouncedSave(history.perform);
+ context.replace = withDebouncedSave(history.replace);
+ context.pop = withDebouncedSave(history.pop);
+ context.overwrite = withDebouncedSave(history.overwrite);
+ context.undo = withDebouncedSave(history.undo);
+ context.redo = withDebouncedSave(history.redo);
+
+ ui = iD.ui(context);
+
+ connection = iD.Connection();
+
+ background = iD.Background(context);
+
+ features = iD.Features(context);
+
+ map = iD.Map(context);
+ context.mouse = map.mouse;
+ context.extent = map.extent;
+ context.pan = map.pan;
+ context.zoomIn = map.zoomIn;
+ context.zoomOut = map.zoomOut;
+ context.zoomInFurther = map.zoomInFurther;
+ context.zoomOutFurther = map.zoomOutFurther;
+ context.redrawEnable = map.redrawEnable;
+
+ presets = iD.presets();
+
return d3.rebind(context, dispatch, 'on');
};
+
iD.version = '1.8.5';
(function() {
diff --git a/js/id/renderer/background.js b/js/id/renderer/background.js
index ce478e220..60f3e3449 100644
--- a/js/id/renderer/background.js
+++ b/js/id/renderer/background.js
@@ -4,7 +4,8 @@ iD.Background = function(context) {
.projection(context.projection),
gpxLayer = iD.GpxLayer(context, dispatch)
.projection(context.projection),
- mapillaryLayer = iD.MapillaryLayer(context),
+ mapillaryImageLayer,
+ mapillarySignLayer,
overlayLayers = [];
var backgroundSources;
@@ -85,13 +86,43 @@ iD.Background = function(context) {
gpx.call(gpxLayer);
- var mapillary = selection.selectAll('.layer-mapillary')
- .data([0]);
- mapillary.enter().insert('div')
- .attr('class', 'layer-layer layer-mapillary');
+ var mapillary = iD.services.mapillary,
+ supportsMapillaryImages = !!mapillary,
+ supportsMapillarySigns = !!mapillary && mapillary().signsSupported();
- mapillary.call(mapillaryLayer);
+ var mapillaryImages = selection.selectAll('.layer-mapillary-images')
+ .data(supportsMapillaryImages ? [0] : []);
+
+ mapillaryImages.enter().insert('div')
+ .attr('class', 'layer-layer layer-mapillary-images');
+
+ if (supportsMapillaryImages) {
+ if (!mapillaryImageLayer) { mapillaryImageLayer = iD.MapillaryImageLayer(context); }
+ mapillaryImages.call(mapillaryImageLayer);
+ } else {
+ mapillaryImageLayer = null;
+ }
+
+ mapillaryImages.exit()
+ .remove();
+
+
+ var mapillarySigns = selection.selectAll('.layer-mapillary-signs')
+ .data(supportsMapillarySigns ? [0] : []);
+
+ mapillarySigns.enter().insert('div')
+ .attr('class', 'layer-layer layer-mapillary-signs');
+
+ if (supportsMapillarySigns) {
+ if (!mapillarySignLayer) { mapillarySignLayer = iD.MapillarySignLayer(context); }
+ mapillarySigns.call(mapillarySignLayer);
+ } else {
+ mapillarySignLayer = null;
+ }
+
+ mapillarySigns.exit()
+ .remove();
}
background.sources = function(extent) {
@@ -103,7 +134,8 @@ iD.Background = function(context) {
background.dimensions = function(_) {
baseLayer.dimensions(_);
gpxLayer.dimensions(_);
- mapillaryLayer.dimensions(_);
+ if (mapillaryImageLayer) mapillaryImageLayer.dimensions(_);
+ if (mapillarySignLayer) mapillarySignLayer.dimensions(_);
overlayLayers.forEach(function(layer) {
layer.dimensions(_);
@@ -172,12 +204,23 @@ iD.Background = function(context) {
dispatch.change();
};
- background.showsMapillaryLayer = function() {
- return mapillaryLayer.enable();
+ background.showsMapillaryImageLayer = function() {
+ return mapillaryImageLayer && mapillaryImageLayer.enable();
};
- background.toggleMapillaryLayer = function() {
- mapillaryLayer.enable(!mapillaryLayer.enable());
+ background.showsMapillarySignLayer = function() {
+ return mapillarySignLayer && mapillarySignLayer.enable();
+ };
+
+ background.toggleMapillaryImageLayer = function() {
+ if (!mapillaryImageLayer) return;
+ mapillaryImageLayer.enable(!mapillaryImageLayer.enable());
+ dispatch.change();
+ };
+
+ background.toggleMapillarySignLayer = function() {
+ if (!mapillarySignLayer) return;
+ mapillarySignLayer.enable(!mapillarySignLayer.enable());
dispatch.change();
};
diff --git a/js/id/renderer/map.js b/js/id/renderer/map.js
index 94023f33e..a8d70826d 100644
--- a/js/id/renderer/map.js
+++ b/js/id/renderer/map.js
@@ -37,6 +37,10 @@ iD.Map = function(context) {
supersurface = selection.append('div')
.attr('id', 'supersurface');
+ // Mapillary streetsigns require supersurface transform to have
+ // a value in order to do correct foreignObject positioning in Chrome
+ iD.util.setTransform(supersurface, 0, 0);
+
// Need a wrapper div because Opera can't cope with an absolutely positioned
// SVG element: http://bl.ocks.org/jfirebaugh/6fbfbd922552bf776c16
var dataLayer = supersurface.append('div')
diff --git a/js/id/renderer/mapillary_image_layer.js b/js/id/renderer/mapillary_image_layer.js
new file mode 100644
index 000000000..544ef011b
--- /dev/null
+++ b/js/id/renderer/mapillary_image_layer.js
@@ -0,0 +1,182 @@
+iD.MapillaryImageLayer = function(context) {
+ var debouncedRedraw = _.debounce(function () { context.pan([0,0]); }, 1000),
+ enabled = false,
+ minZoom = 12,
+ layer = d3.select(null),
+ _mapillary;
+
+ function getMapillary() {
+ if (iD.services.mapillary && !_mapillary) {
+ _mapillary = iD.services.mapillary().on('loadedImages', debouncedRedraw);
+ } else if (!iD.services.mapillary && _mapillary) {
+ _mapillary = null;
+ }
+ return _mapillary;
+ }
+
+ function showThumbnail(image) {
+ var mapillary = getMapillary();
+ if (!mapillary) return;
+
+ var thumb = mapillary.selectedThumbnail(),
+ posX = context.projection(image.loc)[0],
+ width = layer.dimensions()[0],
+ position = (posX < width / 2) ? 'right' : 'left';
+
+ if (thumb) {
+ d3.selectAll('.layer-mapillary-images .viewfield-group, .layer-mapillary-signs .icon-sign')
+ .classed('selected', function(d) { return d.key === thumb.key; });
+ }
+
+ mapillary.showThumbnail(image.key, position);
+ }
+
+ function hideThumbnail() {
+ d3.selectAll('.layer-mapillary-images .viewfield-group, .layer-mapillary-signs .icon-sign')
+ .classed('selected', false);
+
+ var mapillary = getMapillary();
+ if (mapillary) {
+ mapillary.hideThumbnail();
+ }
+ }
+
+ function showLayer() {
+ editOn();
+ layer
+ .style('opacity', 0)
+ .transition()
+ .duration(500)
+ .style('opacity', 1)
+ .each('end', debouncedRedraw);
+ }
+
+ function hideLayer() {
+ debouncedRedraw.cancel();
+ hideThumbnail();
+ layer
+ .transition()
+ .duration(500)
+ .style('opacity', 0)
+ .each('end', editOff);
+ }
+
+ function editOn() {
+ layer.style('display', 'block');
+ }
+
+ function editOff() {
+ layer.selectAll('.viewfield-group').remove();
+ layer.style('display', 'none');
+ }
+
+ function transform(d) {
+ var t = iD.svg.PointTransform(context.projection)(d);
+ if (d.ca) t += ' rotate(' + Math.floor(d.ca) + ',0,0)';
+ return t;
+ }
+
+ function drawMarkers() {
+ var mapillary = getMapillary(),
+ data = (mapillary ? mapillary.images(context.projection, layer.dimensions()) : []);
+
+ var markers = layer.selectAll('.viewfield-group')
+ .data(data, function(d) { return d.key; });
+
+ // Enter
+ var enter = markers.enter()
+ .append('g')
+ .attr('class', 'viewfield-group');
+
+ 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');
+
+ enter.append('circle')
+ .attr('dx', '0')
+ .attr('dy', '0')
+ .attr('r', '6');
+
+ // Exit
+ markers.exit()
+ .remove();
+
+ // Update
+ markers
+ .attr('transform', transform);
+ }
+
+ function render(selection) {
+ var mapillary = getMapillary();
+
+ layer = selection.selectAll('svg')
+ .data(mapillary ? [0] : []);
+
+ layer.enter()
+ .append('svg')
+ .style('display', enabled ? 'block' : 'none')
+ .dimensions(context.map().dimensions())
+ .on('click', function() { // deselect/select
+ var mapillary = getMapillary();
+ if (!mapillary) return;
+ var d = d3.event.target.__data__,
+ thumb = mapillary.selectedThumbnail();
+ if (thumb && thumb.key === d.key) {
+ hideThumbnail();
+ } else {
+ mapillary.selectedThumbnail(d);
+ context.map().centerEase(d.loc);
+ showThumbnail(d);
+ }
+ })
+ .on('mouseover', function() {
+ var mapillary = getMapillary();
+ if (!mapillary) return;
+ showThumbnail(d3.event.target.__data__);
+ })
+ .on('mouseout', function() {
+ var mapillary = getMapillary();
+ if (!mapillary) return;
+ var thumb = mapillary.selectedThumbnail();
+ if (thumb) {
+ showThumbnail(thumb);
+ } else {
+ hideThumbnail();
+ }
+ });
+
+ layer.exit()
+ .remove();
+
+ if (enabled) {
+ if (mapillary && ~~context.map().zoom() >= minZoom) {
+ editOn();
+ drawMarkers();
+ mapillary.loadImages(context.projection, layer.dimensions());
+ } else {
+ editOff();
+ }
+ }
+ }
+
+ render.enable = function(_) {
+ if (!arguments.length) return enabled;
+ enabled = _;
+ if (enabled) {
+ showLayer();
+ } else {
+ hideLayer();
+ }
+ return render;
+ };
+
+ render.dimensions = function(_) {
+ if (layer.empty()) return null;
+ if (!arguments.length) return layer.dimensions();
+ layer.dimensions(_);
+ return render;
+ };
+
+ return render;
+};
diff --git a/js/id/renderer/mapillary_layer.js b/js/id/renderer/mapillary_layer.js
deleted file mode 100644
index 05b6e4ed7..000000000
--- a/js/id/renderer/mapillary_layer.js
+++ /dev/null
@@ -1,159 +0,0 @@
-iD.MapillaryLayer = function (context) {
- var enable = false,
- currentImage,
- svg, div, request;
-
- function show(image) {
- svg.selectAll('g')
- .classed('selected', function(d) {
- return currentImage && d.key === currentImage.key;
- });
-
- div.classed('hidden', false)
- .classed('temp', image !== currentImage);
-
- div.selectAll('img')
- .attr('src', 'https://d1cuyjsrcm0gby.cloudfront.net/' + image.key + '/thumb-320.jpg');
-
- div.selectAll('a')
- .attr('href', 'https://www.mapillary.com/map/im/' + image.key);
- }
-
- function hide() {
- currentImage = undefined;
-
- svg.selectAll('g')
- .classed('selected', false);
-
- div.classed('hidden', true);
- }
-
- function transform(image) {
- var t = 'translate(' + context.projection(image.loc) + ')';
- if (image.ca) t += 'rotate(' + image.ca + ',0,0)';
- return t;
- }
-
- function render(selection) {
- svg = selection.selectAll('svg')
- .data([0]);
-
- svg.enter().append('svg')
- .on('click', function() {
- var image = d3.event.target.__data__;
- if (currentImage === image) {
- hide();
- } else {
- currentImage = image;
- show(image);
- }
- })
- .on('mouseover', function() {
- show(d3.event.target.__data__);
- })
- .on('mouseout', function() {
- if (currentImage) {
- show(currentImage);
- } else {
- hide();
- }
- });
-
- svg.style('display', enable ? 'block' : 'none');
-
- div = context.container().selectAll('.mapillary-image')
- .data([0]);
-
- var enter = div.enter().append('div')
- .attr('class', 'mapillary-image');
-
- enter.append('button')
- .on('click', hide)
- .append('div')
- .call(iD.svg.Icon('#icon-close'));
-
- enter.append('img');
-
- enter
- .append('a')
- .attr('class', 'link')
- .attr('target', '_blank')
- .call(iD.svg.Icon('#icon-out-link', 'inline'))
- .append('span')
- .text(t('mapillary.view_on_mapillary'));
-
- if (!enable) {
- hide();
-
- svg.selectAll('g')
- .remove();
-
- return;
- }
-
- // Update existing images while waiting for new ones to load.
- svg.selectAll('g')
- .attr('transform', transform);
-
- var extent = context.map().extent();
-
- if (request)
- request.abort();
-
- request = d3.json('https://a.mapillary.com/v2/search/s/geojson?client_id=NzNRM2otQkR2SHJzaXJmNmdQWVQ0dzoxNjQ3MDY4ZTUxY2QzNGI2&min_lat=' +
- extent[0][1] + '&max_lat=' + extent[1][1] + '&min_lon=' +
- extent[0][0] + '&max_lon=' + extent[1][0] + '&max_results=100&geojson=true',
- function (error, data) {
- if (error) return;
-
- var images = [];
-
- for (var i = 0; i < data.features.length; i++) {
- var sequence = data.features[i];
- for (var j = 0; j < sequence.geometry.coordinates.length; j++) {
- images.push({
- key: sequence.properties.keys[j],
- ca: sequence.properties.cas[j],
- loc: sequence.geometry.coordinates[j]
- });
- if (images.length >= 1000) break;
- }
- }
-
- var g = svg.selectAll('g')
- .data(images, function(d) { return d.key; });
-
- var enter = g.enter().append('g')
- .attr('class', 'image');
-
- 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');
-
- enter.append('circle')
- .attr('dx', '0')
- .attr('dy', '0')
- .attr('r', '6');
-
- g.attr('transform', transform);
-
- g.exit()
- .remove();
- });
- }
-
- render.enable = function(_) {
- if (!arguments.length) return enable;
- enable = _;
- return render;
- };
-
- render.dimensions = function(_) {
- if (!arguments.length) return svg.dimensions();
- svg.dimensions(_);
- return render;
- };
-
- return render;
-};
diff --git a/js/id/renderer/mapillary_sign_layer.js b/js/id/renderer/mapillary_sign_layer.js
new file mode 100644
index 000000000..88dea7292
--- /dev/null
+++ b/js/id/renderer/mapillary_sign_layer.js
@@ -0,0 +1,164 @@
+iD.MapillarySignLayer = function(context) {
+ var debouncedRedraw = _.debounce(function () { context.pan([0,0]); }, 1000),
+ enabled = false,
+ minZoom = 12,
+ layer = d3.select(null),
+ _mapillary;
+
+ function getMapillary() {
+ if (iD.services.mapillary && !_mapillary) {
+ _mapillary = iD.services.mapillary().on('loadedSigns', debouncedRedraw);
+ } else if (!iD.services.mapillary && _mapillary) {
+ _mapillary = null;
+ }
+ return _mapillary;
+ }
+
+ function showThumbnail(image) {
+ var mapillary = getMapillary();
+ if (!mapillary) return;
+
+ var thumb = mapillary.selectedThumbnail(),
+ posX = context.projection(image.loc)[0],
+ width = layer.dimensions()[0],
+ position = (posX < width / 2) ? 'right' : 'left';
+
+ if (thumb) {
+ d3.selectAll('.layer-mapillary-images .viewfield-group, .layer-mapillary-signs .icon-sign')
+ .classed('selected', function(d) { return d.key === thumb.key; });
+ }
+
+ mapillary.showThumbnail(image.key, position);
+ }
+
+ function hideThumbnail() {
+ d3.selectAll('.layer-mapillary-images .viewfield-group, .layer-mapillary-signs .icon-sign')
+ .classed('selected', false);
+
+ var mapillary = getMapillary();
+ if (mapillary) {
+ mapillary.hideThumbnail();
+ }
+ }
+
+ function showLayer() {
+ editOn();
+ debouncedRedraw();
+ }
+
+ function hideLayer() {
+ debouncedRedraw.cancel();
+ hideThumbnail();
+ editOff();
+ }
+
+ function editOn() {
+ layer.style('display', 'block');
+ }
+
+ function editOff() {
+ layer.selectAll('.icon-sign').remove();
+ layer.style('display', 'none');
+ }
+
+ function drawSigns() {
+ var mapillary = getMapillary(),
+ data = (mapillary ? mapillary.signs(context.projection, layer.dimensions()) : []);
+
+ var signs = layer.select('.mapillary-sign-offset')
+ .selectAll('.icon-sign')
+ .data(data, function(d) { return d.key; });
+
+ // Enter
+ var enter = signs.enter()
+ .append('foreignObject')
+ .attr('class', 'icon-sign')
+ .attr('width', '32px') // for Firefox
+ .attr('height', '32px'); // for Firefox
+
+ enter
+ .append('xhtml:body')
+ .html(mapillary.signHTML);
+
+ enter
+ .on('click', function(d) { // deselect/select
+ var mapillary = getMapillary();
+ if (!mapillary) return;
+ var thumb = mapillary.selectedThumbnail();
+ if (thumb && thumb.key === d.key) {
+ hideThumbnail();
+ } else {
+ mapillary.selectedThumbnail(d);
+ context.map().centerEase(d.loc);
+ showThumbnail(d);
+ }
+ })
+ .on('mouseover', showThumbnail)
+ .on('mouseout', function() {
+ var mapillary = getMapillary();
+ if (!mapillary) return;
+ var thumb = mapillary.selectedThumbnail();
+ if (thumb) {
+ showThumbnail(thumb);
+ } else {
+ hideThumbnail();
+ }
+ });
+
+ // Exit
+ signs.exit()
+ .remove();
+
+ // Update
+ signs
+ .attr('transform', iD.svg.PointTransform(context.projection));
+ }
+
+ function render(selection) {
+ var mapillary = getMapillary();
+
+ layer = selection.selectAll('svg')
+ .data(mapillary ? [0] : []);
+
+ layer.enter()
+ .append('svg')
+ .style('display', enabled ? 'block' : 'none')
+ .dimensions(context.map().dimensions())
+ .append('g')
+ .attr('class', 'mapillary-sign-offset')
+ .attr('transform', 'translate(-16, -16)'); // center signs on loc
+
+ layer.exit()
+ .remove();
+
+ if (enabled) {
+ if (mapillary && ~~context.map().zoom() >= minZoom) {
+ editOn();
+ drawSigns();
+ mapillary.loadSigns(context, context.projection, layer.dimensions());
+ } else {
+ editOff();
+ }
+ }
+ }
+
+ render.enable = function(_) {
+ if (!arguments.length) return enabled;
+ enabled = _;
+ if (enabled) {
+ showLayer();
+ } else {
+ hideLayer();
+ }
+ return render;
+ };
+
+ render.dimensions = function(_) {
+ if (layer.empty()) return null;
+ if (!arguments.length) return layer.dimensions();
+ layer.dimensions(_);
+ return render;
+ };
+
+ return render;
+};
diff --git a/js/id/services.js b/js/id/services.js
new file mode 100644
index 000000000..c11dc788f
--- /dev/null
+++ b/js/id/services.js
@@ -0,0 +1 @@
+iD.services = {};
diff --git a/js/id/services/mapillary.js b/js/id/services/mapillary.js
new file mode 100644
index 000000000..8a98e9dbb
--- /dev/null
+++ b/js/id/services/mapillary.js
@@ -0,0 +1,274 @@
+iD.services.mapillary = function() {
+ var mapillary = {},
+ dispatch = d3.dispatch('loadedImages', 'loadedSigns'),
+ apibase = 'https://a.mapillary.com/v2/',
+ urlImage = 'https://www.mapillary.com/map/im/',
+ urlThumb = 'https://d1cuyjsrcm0gby.cloudfront.net/',
+ clientId = 'NzNRM2otQkR2SHJzaXJmNmdQWVQ0dzo1ZWYyMmYwNjdmNDdlNmVi',
+ maxResults = 1000,
+ tileZoom = 14;
+
+
+ function loadSignDefs(context) {
+ if (!iD.services.mapillary.sign_defs) {
+ iD.services.mapillary.sign_defs = {};
+ _.each(['au', 'br', 'ca', 'de', 'us'], function(region) {
+ d3.json(context.assetPath() + 'traffico/string-maps/' + region + '-map.json', function(err, data) {
+ if (err) return;
+ if (region === 'de') region = 'eu';
+ iD.services.mapillary.sign_defs[region] = data;
+ });
+ });
+ }
+ }
+
+ function abortRequest(i) {
+ i.abort();
+ }
+
+ function getTiles(projection, dimensions) {
+ 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.geo.tile()
+ .scaleExtent([tileZoom, tileZoom])
+ .scale(s)
+ .size(dimensions)
+ .translate(projection.translate())()
+ .map(function(tile) {
+ var x = tile[0] * ts - origin[0],
+ y = tile[1] * ts - origin[1];
+
+ return {
+ id: tile.toString(),
+ extent: iD.geo.Extent(
+ projection.invert([x, y + ts]),
+ projection.invert([x + ts, y]))
+ };
+ });
+ }
+
+
+ function loadTiles(which, url, projection, dimensions) {
+ var tiles = getTiles(projection, dimensions);
+
+ _.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) {
+ loadTilePage(which, url, tile, 0);
+ });
+ }
+
+ function loadTilePage(which, url, tile, page) {
+ var cache = iD.services.mapillary.cache[which],
+ id = tile.id + ',' + String(page),
+ rect = tile.extent.rectangle();
+
+ if (cache.loaded[id] || cache.inflight[id]) return;
+
+ cache.inflight[id] = d3.json(url +
+ iD.util.qsString({
+ geojson: 'true',
+ limit: maxResults,
+ page: page,
+ client_id: clientId,
+ min_lon: rect[0],
+ min_lat: rect[1],
+ max_lon: rect[2],
+ max_lat: rect[3]
+ }), function(err, data) {
+ cache.loaded[id] = true;
+ delete cache.inflight[id];
+ if (err || !data.features || !data.features.length) return;
+
+ var features = [],
+ feature, loc, d;
+
+ for (var i = 0; i < data.features.length; i++) {
+ feature = data.features[i];
+ loc = feature.geometry.coordinates;
+ d = { key: feature.properties.key, loc: loc };
+ if (which === 'images') d.ca = feature.properties.ca;
+ if (which === 'signs') d.signs = feature.properties.rects;
+
+ features.push([loc[0], loc[1], loc[0], loc[1], d]);
+ }
+
+ cache.rtree.load(features);
+
+ if (which === 'images') dispatch.loadedImages();
+ if (which === 'signs') dispatch.loadedSigns();
+
+ if (data.features.length === maxResults) {
+ loadTilePage(which, url, tile, ++page);
+ }
+ }
+ );
+ }
+
+ mapillary.loadImages = function(projection, dimensions) {
+ var url = apibase + 'search/im/geojson?';
+ loadTiles('images', url, projection, dimensions);
+ };
+
+ mapillary.loadSigns = function(context, projection, dimensions) {
+ var url = apibase + 'search/im/geojson/or?';
+ loadSignDefs(context);
+ loadTiles('signs', url, projection, dimensions);
+ };
+
+
+ // partition viewport into `psize` x `psize` regions
+ function partitionViewport(psize, projection, dimensions) {
+ 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(
+ iD.geo.Extent(projection.invert(min), projection.invert(max)));
+ });
+ });
+
+ return partitions;
+ }
+
+ // no more than `limit` results per partition.
+ function searchLimited(psize, limit, projection, dimensions, rtree) {
+ limit = limit || 3;
+
+ var partitions = partitionViewport(psize, projection, dimensions);
+ return _.flatten(_.compact(_.map(partitions, function(extent) {
+ return rtree.search(extent.rectangle())
+ .slice(0, limit)
+ .map(function(d) { return d[4]; });
+ })));
+ }
+
+ mapillary.images = function(projection, dimensions) {
+ var psize = 16, limit = 3;
+ return searchLimited(psize, limit, projection, dimensions, iD.services.mapillary.cache.images.rtree);
+ };
+
+ mapillary.signs = function(projection, dimensions) {
+ var psize = 32, limit = 3;
+ return searchLimited(psize, limit, projection, dimensions, iD.services.mapillary.cache.signs.rtree);
+ };
+
+ mapillary.signsSupported = function() {
+ var detected = iD.detect();
+ return (!(detected.ie || detected.browser.toLowerCase() === 'safari'));
+ };
+
+ mapillary.signHTML = function(d) {
+ if (!iD.services.mapillary.sign_defs) return;
+
+ var detectionPackage = d.signs[0].package,
+ type = d.signs[0].type,
+ country = detectionPackage.split('_')[1];
+ return iD.services.mapillary.sign_defs[country][type];
+ };
+
+ mapillary.showThumbnail = function(imageKey, position) {
+ if (!imageKey) return;
+
+ var positionClass = {
+ 'ar': (position !== 'left'),
+ 'al': (position === 'left')
+ };
+
+ var thumbnail = d3.select('#content').selectAll('.mapillary-image')
+ .data([0]);
+
+ // Enter
+ var enter = thumbnail.enter().append('div')
+ .attr('class', 'mapillary-image ar');
+
+ enter.append('button')
+ .on('click', function () {
+ mapillary.hideThumbnail();
+ })
+ .append('div')
+ .call(iD.svg.Icon('#icon-close'));
+
+ enter.append('img');
+
+ enter.append('a')
+ .attr('class', 'link ar')
+ .attr('target', '_blank')
+ .call(iD.svg.Icon('#icon-out-link', 'inline'))
+ .append('span')
+ .text(t('mapillary.view_on_mapillary'));
+
+ // Update
+ thumbnail.selectAll('img')
+ .attr('src', urlThumb + imageKey + '/thumb-320.jpg');
+
+ var link = thumbnail.selectAll('a')
+ .attr('href', urlImage + imageKey);
+
+ if (position) {
+ thumbnail.classed(positionClass);
+ link.classed(positionClass);
+ }
+
+ thumbnail
+ .transition()
+ .duration(200)
+ .style('opacity', 1);
+ };
+
+ mapillary.hideThumbnail = function() {
+ if (iD.services.mapillary) {
+ iD.services.mapillary.thumb = null;
+ }
+ d3.select('#content').selectAll('.mapillary-image')
+ .transition()
+ .duration(200)
+ .style('opacity', 0)
+ .remove();
+ };
+
+ mapillary.selectedThumbnail = function(d) {
+ if (!iD.services.mapillary) return null;
+ if (!arguments.length) return iD.services.mapillary.thumb;
+ iD.services.mapillary.thumb = d;
+ };
+
+ mapillary.reset = function() {
+ var cache = iD.services.mapillary.cache;
+
+ if (cache) {
+ _.forEach(cache.images.inflight, abortRequest);
+ _.forEach(cache.signs.inflight, abortRequest);
+ }
+
+ iD.services.mapillary.cache = {
+ images: { inflight: {}, loaded: {}, rtree: rbush() },
+ signs: { inflight: {}, loaded: {}, rtree: rbush() }
+ };
+
+ iD.services.mapillary.thumb = null;
+
+ return mapillary;
+ };
+
+
+ if (!iD.services.mapillary.cache) {
+ mapillary.reset();
+ }
+
+ return d3.rebind(mapillary, dispatch, 'on');
+};
diff --git a/js/id/services/countrycode.js b/js/id/services/nominatim.js
similarity index 59%
rename from js/id/services/countrycode.js
rename to js/id/services/nominatim.js
index c324cf64e..e57921331 100644
--- a/js/id/services/countrycode.js
+++ b/js/id/services/nominatim.js
@@ -1,15 +1,11 @@
-iD.countryCode = function() {
- var countryCode = {},
+iD.services.nominatim = function() {
+ var nominatim = {},
endpoint = 'https://nominatim.openstreetmap.org/reverse?';
- if (!iD.countryCode.cache) {
- iD.countryCode.cache = rbush();
- }
- var cache = iD.countryCode.cache;
-
- countryCode.search = function(location, callback) {
- var countryCodes = cache.search([location[0], location[1], location[0], location[1]]);
+ nominatim.countryCode = function(location, callback) {
+ var cache = iD.services.nominatim.cache,
+ countryCodes = cache.search([location[0], location[1], location[0], location[1]]);
if (countryCodes.length > 0)
return callback(null, countryCodes[0][4]);
@@ -28,11 +24,21 @@ iD.countryCode = function() {
var extent = iD.geo.Extent(location).padByMeters(1000);
- cache.insert([extent[0][0], extent[0][1], extent[1][0], extent[1][1], result.address.country_code]);
+ cache.insert(extent.rectangle().concat(result.address.country_code));
callback(null, result.address.country_code);
});
};
- return countryCode;
+ nominatim.reset = function() {
+ iD.services.nominatim.cache = rbush();
+ return nominatim;
+ };
+
+
+ if (!iD.services.nominatim.cache) {
+ nominatim.reset();
+ }
+
+ return nominatim;
};
diff --git a/js/id/services/taginfo.js b/js/id/services/taginfo.js
index ade77ada7..b71661d25 100644
--- a/js/id/services/taginfo.js
+++ b/js/id/services/taginfo.js
@@ -1,4 +1,4 @@
-iD.taginfo = function() {
+iD.services.taginfo = function() {
var taginfo = {},
endpoint = 'https://taginfo.openstreetmap.org/api/4/',
tag_sorts = {
@@ -14,11 +14,6 @@ iD.taginfo = function() {
line: 'ways'
};
- if (!iD.taginfo.cache) {
- iD.taginfo.cache = {};
- }
-
- var cache = iD.taginfo.cache;
function sets(parameters, n, o) {
if (parameters.geometry && o[parameters.geometry]) {
@@ -68,6 +63,8 @@ iD.taginfo = function() {
var debounced = _.debounce(d3.json, 100, true);
function request(url, debounce, callback) {
+ var cache = iD.services.taginfo.cache;
+
if (cache[url]) {
callback(null, cache[url]);
} else if (debounce) {
@@ -132,5 +129,15 @@ iD.taginfo = function() {
return taginfo;
};
+ taginfo.reset = function() {
+ iD.services.taginfo.cache = {};
+ return taginfo;
+ };
+
+
+ if (!iD.services.taginfo.cache) {
+ taginfo.reset();
+ }
+
return taginfo;
};
diff --git a/js/id/services/wikipedia.js b/js/id/services/wikipedia.js
index eb121f57a..a33143a07 100644
--- a/js/id/services/wikipedia.js
+++ b/js/id/services/wikipedia.js
@@ -1,4 +1,4 @@
-iD.wikipedia = function() {
+iD.services.wikipedia = function() {
var wiki = {},
endpoint = 'https://en.wikipedia.org/w/api.php?';
diff --git a/js/id/ui/map_data.js b/js/id/ui/map_data.js
index 903860137..0298910e4 100644
--- a/js/id/ui/map_data.js
+++ b/js/id/ui/map_data.js
@@ -5,6 +5,7 @@ iD.ui.MapData = function(context) {
fillDefault = context.storage('area-fill') || 'partial',
fillSelected = fillDefault;
+
function map_data(selection) {
function showsFeature(d) {
@@ -42,16 +43,168 @@ iD.ui.MapData = function(context) {
update();
}
- function clickMapillary() {
- context.background().toggleMapillaryLayer();
+ function clickMapillaryImages() {
+ context.background().toggleMapillaryImageLayer();
update();
}
+ function clickMapillarySigns() {
+ context.background().toggleMapillarySignLayer();
+ update();
+ }
+
+
+ function drawMapillaryItems(selection) {
+ var mapillary = iD.services.mapillary,
+ supportsMapillaryImages = !!mapillary,
+ supportsMapillarySigns = !!mapillary && mapillary().signsSupported();
+
+ var mapillaryList = selection
+ .selectAll('.mapillary-list')
+ .data([0]);
+
+ // Enter
+ mapillaryList
+ .enter()
+ .append('ul')
+ .attr('class', 'layer-list mapillary-list');
+
+ var mapillaryImageLayerItem = mapillaryList
+ .selectAll('.item-mapillary-images')
+ .data(supportsMapillaryImages ? [0] : []);
+
+ var enterImages = mapillaryImageLayerItem.enter()
+ .append('li')
+ .attr('class', 'item-mapillary-images');
+
+ var labelImages = enterImages.append('label')
+ .call(bootstrap.tooltip()
+ .title(t('mapillary_images.tooltip'))
+ .placement('top'));
+
+ labelImages.append('input')
+ .attr('type', 'checkbox')
+ .on('change', clickMapillaryImages);
+
+ labelImages.append('span')
+ .text(t('mapillary_images.title'));
+
+
+ var mapillarySignLayerItem = mapillaryList
+ .selectAll('.item-mapillary-signs')
+ .data(supportsMapillarySigns ? [0] : []);
+
+ var enterSigns = mapillarySignLayerItem.enter()
+ .append('li')
+ .attr('class', 'item-mapillary-signs');
+
+ var labelSigns = enterSigns.append('label')
+ .call(bootstrap.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'));
+
+ // Update
+ var showsMapillaryImages = supportsMapillaryImages && context.background().showsMapillaryImageLayer(),
+ showsMapillarySigns = supportsMapillarySigns && context.background().showsMapillarySignLayer();
+
+ mapillaryImageLayerItem
+ .classed('active', showsMapillaryImages)
+ .selectAll('input')
+ .property('checked', showsMapillaryImages);
+
+ mapillarySignLayerItem
+ .classed('active', showsMapillarySigns)
+ .selectAll('input')
+ .property('checked', showsMapillarySigns);
+
+ // Exit
+ mapillaryImageLayerItem.exit()
+ .remove();
+ mapillarySignLayerItem.exit()
+ .remove();
+ }
+
+
+ function drawGpxItem(selection) {
+ var supportsGpx = iD.detect().filedrop,
+ gpxLayerItem = selection
+ .selectAll('.layer-gpx')
+ .data(supportsGpx ? [0] : []);
+
+ // Enter
+ var enter = gpxLayerItem.enter()
+ .append('ul')
+ .attr('class', 'layer-list layer-gpx')
+ .append('li')
+ .classed('layer-toggle-gpx', true);
+
+ enter.append('button')
+ .attr('class', 'layer-extent')
+ .call(bootstrap.tooltip()
+ .title(t('gpx.zoom'))
+ .placement('left'))
+ .on('click', function() {
+ d3.event.preventDefault();
+ d3.event.stopPropagation();
+ context.background().zoomToGpxLayer();
+ })
+ .call(iD.svg.Icon('#icon-search'));
+
+ enter.append('button')
+ .attr('class', 'layer-browse')
+ .call(bootstrap.tooltip()
+ .title(t('gpx.browse'))
+ .placement('left'))
+ .on('click', function() {
+ d3.select(document.createElement('input'))
+ .attr('type', 'file')
+ .on('change', function() {
+ context.background().gpxLayerFiles(d3.event.target.files);
+ })
+ .node().click();
+ })
+ .call(iD.svg.Icon('#icon-geolocate'));
+
+ var labelGpx = enter.append('label')
+ .call(bootstrap.tooltip()
+ .title(t('gpx.drag_drop'))
+ .placement('top'));
+
+ labelGpx.append('input')
+ .attr('type', 'checkbox')
+ .on('change', clickGpx);
+
+ labelGpx.append('span')
+ .text(t('gpx.local_layer'));
+
+ // Update
+ var hasGpx = supportsGpx && context.background().hasGpxLayer(),
+ showsGpx = supportsGpx && context.background().showsGpxLayer();
+
+ gpxLayerItem
+ .classed('active', showsGpx)
+ .selectAll('input')
+ .property('disabled', !hasGpx)
+ .property('checked', showsGpx);
+
+ // Exit
+ gpxLayerItem.exit()
+ .remove();
+ }
+
+
function drawList(selection, data, type, name, change, active) {
var items = selection.selectAll('li')
.data(data);
- //enter
+ // Enter
var enter = items.enter()
.append('li')
.attr('class', 'layer')
@@ -79,7 +232,7 @@ iD.ui.MapData = function(context) {
label.append('span')
.text(function(d) { return t(name + '.' + d + '.description'); });
- //update
+ // Update
items
.classed('active', active)
.selectAll('input')
@@ -88,32 +241,24 @@ iD.ui.MapData = function(context) {
return (name === 'feature' && autoHiddenFeature(d));
});
- //exit
+ // Exit
items.exit()
.remove();
}
+
function update() {
- featureList.call(drawList, features, 'checkbox', 'feature', clickFeature, showsFeature);
+ dataLayerContainer.call(drawMapillaryItems);
+ dataLayerContainer.call(drawGpxItem);
+
fillList.call(drawList, fills, 'radio', 'area_fill', setFill, showsFill);
- var hasGpx = context.background().hasGpxLayer(),
- showsGpx = context.background().showsGpxLayer(),
- showsMapillary = context.background().showsMapillaryLayer();
-
- gpxLayerItem
- .classed('active', showsGpx)
- .selectAll('input')
- .property('disabled', !hasGpx)
- .property('checked', showsGpx);
-
- mapillaryLayerItem
- .classed('active', showsMapillary)
- .selectAll('input')
- .property('checked', showsMapillary);
+ featureList.call(drawList, features, 'checkbox', 'feature', clickFeature, showsFeature);
}
- function hidePanel() { setVisible(false); }
+ function hidePanel() {
+ setVisible(false);
+ }
function togglePanel() {
if (d3.event) d3.event.preventDefault();
@@ -136,6 +281,7 @@ iD.ui.MapData = function(context) {
shown = show;
if (show) {
+ update();
selection.on('mousedown.map_data-inside', function() {
return d3.event.stopPropagation();
});
@@ -184,79 +330,15 @@ iD.ui.MapData = function(context) {
.classed('expanded', true)
.on('click', function() {
var exp = d3.select(this).classed('expanded');
- layerContainer.style('display', exp ? 'none' : 'block');
+ dataLayerContainer.style('display', exp ? 'none' : 'block');
d3.select(this).classed('expanded', !exp);
d3.event.preventDefault();
});
- var layerContainer = content.append('div')
- .attr('class', 'filters')
+ var dataLayerContainer = content.append('div')
+ .attr('class', 'data-data-layers')
.style('display', 'block');
- // mapillary
- var mapillaryLayerItem = layerContainer.append('ul')
- .attr('class', 'layer-list')
- .append('li');
-
- var label = mapillaryLayerItem.append('label')
- .call(bootstrap.tooltip()
- .title(t('mapillary.tooltip'))
- .placement('top'));
-
- label.append('input')
- .attr('type', 'checkbox')
- .on('change', clickMapillary);
-
- label.append('span')
- .text(t('mapillary.title'));
-
- // gpx
- var gpxLayerItem = layerContainer.append('ul')
- .style('display', iD.detect().filedrop ? 'block' : 'none')
- .attr('class', 'layer-list')
- .append('li')
- .classed('layer-toggle-gpx', true);
-
- gpxLayerItem.append('button')
- .attr('class', 'layer-extent')
- .call(bootstrap.tooltip()
- .title(t('gpx.zoom'))
- .placement('left'))
- .on('click', function() {
- d3.event.preventDefault();
- d3.event.stopPropagation();
- context.background().zoomToGpxLayer();
- })
- .call(iD.svg.Icon('#icon-search'));
-
- gpxLayerItem.append('button')
- .attr('class', 'layer-browse')
- .call(bootstrap.tooltip()
- .title(t('gpx.browse'))
- .placement('left'))
- .on('click', function() {
- d3.select(document.createElement('input'))
- .attr('type', 'file')
- .on('change', function() {
- context.background().gpxLayerFiles(d3.event.target.files);
- })
- .node().click();
- })
- .call(iD.svg.Icon('#icon-geolocate'));
-
- label = gpxLayerItem.append('label')
- .call(bootstrap.tooltip()
- .title(t('gpx.drag_drop'))
- .placement('top'));
-
- label.append('input')
- .attr('type', 'checkbox')
- .property('disabled', true)
- .on('change', clickGpx);
-
- label.append('span')
- .text(t('gpx.local_layer'));
-
// area fills
content.append('a')
@@ -272,11 +354,11 @@ iD.ui.MapData = function(context) {
});
var fillContainer = content.append('div')
- .attr('class', 'filters')
+ .attr('class', 'data-area-fills')
.style('display', 'none');
var fillList = fillContainer.append('ul')
- .attr('class', 'layer-list');
+ .attr('class', 'layer-list layer-fill-list');
// feature filters
@@ -293,11 +375,11 @@ iD.ui.MapData = function(context) {
});
var featureContainer = content.append('div')
- .attr('class', 'filters')
+ .attr('class', 'data-feature-filters')
.style('display', 'none');
var featureList = featureContainer.append('ul')
- .attr('class', 'layer-list');
+ .attr('class', 'layer-list layer-feature-list');
context.features()
diff --git a/js/id/ui/preset/address.js b/js/id/ui/preset/address.js
index d19fe9213..823b0e7f4 100644
--- a/js/id/ui/preset/address.js
+++ b/js/id/ui/preset/address.js
@@ -109,7 +109,7 @@ iD.ui.preset.address = function(field, context) {
var center = entity.extent(context.graph()).center(),
addressFormat;
- iD.countryCode().search(center, function (err, countryCode) {
+ iD.services.nominatim().countryCode(center, function (err, countryCode) {
addressFormat = _.find(iD.data.addressFormats, function (a) {
return a && a.countryCodes && _.contains(a.countryCodes, countryCode);
}) || _.first(iD.data.addressFormats);
diff --git a/js/id/ui/preset/localized.js b/js/id/ui/preset/localized.js
index 35e6307c7..18d27ff70 100644
--- a/js/id/ui/preset/localized.js
+++ b/js/id/ui/preset/localized.js
@@ -1,6 +1,6 @@
iD.ui.preset.localized = function(field, context) {
var dispatch = d3.dispatch('change', 'input'),
- wikipedia = iD.wikipedia(),
+ wikipedia = iD.services.wikipedia(),
input, localizedInputs, wikiTitles,
entity;
diff --git a/js/id/ui/preset/wikipedia.js b/js/id/ui/preset/wikipedia.js
index 4b069cc2b..5ac04cef3 100644
--- a/js/id/ui/preset/wikipedia.js
+++ b/js/id/ui/preset/wikipedia.js
@@ -1,6 +1,6 @@
iD.ui.preset.wikipedia = function(field, context) {
var dispatch = d3.dispatch('change'),
- wikipedia = iD.wikipedia(),
+ wikipedia = iD.services.wikipedia(),
link, entity, lang, title;
function i(selection) {
diff --git a/js/lib/d3.dimensions.js b/js/lib/d3.dimensions.js
index 7f9ef0521..eef257401 100644
--- a/js/lib/d3.dimensions.js
+++ b/js/lib/d3.dimensions.js
@@ -1,8 +1,17 @@
d3.selection.prototype.dimensions = function (dimensions) {
if (!arguments.length) {
var node = this.node();
- return [node.offsetWidth,
- node.offsetHeight];
+ if (!node) return;
+
+ var prop = this.property('__dimensions__');
+ if (!prop) {
+ var cr = node.getBoundingClientRect();
+ prop = [cr.width, cr.height];
+ this.property('__dimensions__', prop);
+ }
+ return prop;
}
+
+ this.property('__dimensions__', [dimensions[0], dimensions[1]]);
return this.attr({width: dimensions[0], height: dimensions[1]});
};
diff --git a/js/lib/rbush.js b/js/lib/rbush.js
index 19b841a8a..f6975dfe8 100644
--- a/js/lib/rbush.js
+++ b/js/lib/rbush.js
@@ -1,10 +1,11 @@
/*
- (c) 2013, Vladimir Agafonkin
+ (c) 2015, Vladimir Agafonkin
RBush, a JavaScript library for high-performance 2D spatial indexing of points and rectangles.
https://github.com/mourner/rbush
*/
-(function () { 'use strict';
+(function () {
+'use strict';
function rbush(maxEntries, format) {
@@ -57,6 +58,33 @@ rbush.prototype = {
return result;
},
+ collides: function (bbox) {
+
+ var node = this.data,
+ toBBox = this.toBBox;
+
+ if (!intersects(bbox, node.bbox)) return false;
+
+ var nodesToSearch = [],
+ i, len, child, childBBox;
+
+ while (node) {
+ for (i = 0, len = node.children.length; i < len; i++) {
+
+ child = node.children[i];
+ childBBox = node.leaf ? toBBox(child) : child.bbox;
+
+ if (intersects(bbox, childBBox)) {
+ if (node.leaf || contains(bbox, childBBox)) return true;
+ nodesToSearch.push(child);
+ }
+ }
+ node = nodesToSearch.pop();
+ }
+
+ return false;
+ },
+
load: function (data) {
if (!(data && data.length)) return this;
@@ -180,13 +208,14 @@ rbush.prototype = {
return result;
},
- _build: function (items, left, right, level, height) {
+ _build: function (items, left, right, height) {
var N = right - left + 1,
M = this._maxEntries,
node;
if (N <= M) {
+ // reached leaf level; return leaf
node = {
children: items.slice(left, right + 1),
height: 1,
@@ -197,7 +226,7 @@ rbush.prototype = {
return node;
}
- if (!level) {
+ if (!height) {
// target height of the bulk-loaded tree
height = Math.ceil(Math.log(N) / Math.log(M));
@@ -205,31 +234,33 @@ rbush.prototype = {
M = Math.ceil(N / Math.pow(M, height - 1));
}
- // TODO eliminate recursion?
-
node = {
children: [],
height: height,
- bbox: null
+ bbox: null,
+ leaf: false
};
+ // split the items into M mostly square tiles
+
var N2 = Math.ceil(N / M),
N1 = N2 * Math.ceil(Math.sqrt(M)),
- i, j, right2, childNode;
+ i, j, right2, right3;
+
+ multiSelect(items, left, right, N1, this.compareMinX);
- // split the items into M mostly square tiles
for (i = left; i <= right; i += N1) {
- if (i + N1 <= right) partitionSort(items, i, right, i + N1, this.compareMinX);
right2 = Math.min(i + N1 - 1, right);
+ multiSelect(items, i, right2, N2, this.compareMinY);
+
for (j = i; j <= right2; j += N2) {
- if (j + N2 <= right2) partitionSort(items, j, right2, j + N2, this.compareMinY);
+ right3 = Math.min(j + N2 - 1, right2);
// pack each entry recursively
- childNode = this._build(items, j, Math.min(j + N2 - 1, right2), level + 1, height - 1);
- node.children.push(childNode);
+ node.children.push(this._build(items, j, right3, height - 1));
}
}
@@ -309,9 +340,13 @@ rbush.prototype = {
this._chooseSplitAxis(node, m, M);
+ var splitIndex = this._chooseSplitIndex(node, m, M);
+
var newNode = {
- children: node.children.splice(this._chooseSplitIndex(node, m, M)),
- height: node.height
+ children: node.children.splice(splitIndex, node.children.length - splitIndex),
+ height: node.height,
+ bbox: null,
+ leaf: false
};
if (node.leaf) newNode.leaf = true;
@@ -327,7 +362,9 @@ rbush.prototype = {
// split root node
this.data = {
children: [node, newNode],
- height: node.height + 1
+ height: node.height + 1,
+ bbox: null,
+ leaf: false
};
calcBBox(this.data, this.toBBox);
},
@@ -442,6 +479,7 @@ rbush.prototype = {
}
};
+
// calculate node's bbox from bboxes of its children
function calcBBox(node, toBBox) {
node.bbox = distBBox(node, 0, node.children.length, toBBox);
@@ -459,7 +497,6 @@ function distBBox(node, k, p, toBBox) {
return bbox;
}
-
function empty() { return [Infinity, Infinity, -Infinity, -Infinity]; }
function extend(a, b) {
@@ -481,7 +518,7 @@ function enlargedArea(a, b) {
(Math.max(b[3], a[3]) - Math.min(b[1], a[1]));
}
-function intersectionArea (a, b) {
+function intersectionArea(a, b) {
var minX = Math.max(a[0], b[0]),
minY = Math.max(a[1], b[1]),
maxX = Math.min(a[2], b[2]),
@@ -498,44 +535,74 @@ function contains(a, b) {
b[3] <= a[3];
}
-function intersects (a, b) {
+function intersects(a, b) {
return b[0] <= a[2] &&
b[1] <= a[3] &&
b[2] >= a[0] &&
b[3] >= a[1];
}
+// sort an array so that items come in groups of n unsorted items, with groups sorted between each other;
+// combines selection algorithm with binary divide & conquer approach
-function partitionSort(arr, left, right, k, compare) {
- var pivot;
+function multiSelect(arr, left, right, n, compare) {
+ var stack = [left, right],
+ mid;
- while (true) {
- pivot = Math.floor((left + right) / 2);
- pivot = partition(arr, left, right, pivot, compare);
+ while (stack.length) {
+ right = stack.pop();
+ left = stack.pop();
- if (k === pivot) break;
- else if (k < pivot) right = pivot - 1;
- else left = pivot + 1;
+ if (right - left <= n) continue;
+
+ mid = left + Math.ceil((right - left) / n / 2) * n;
+ select(arr, left, right, mid, compare);
+
+ stack.push(left, mid, mid, right);
}
-
- partition(arr, left, right, k, compare);
}
-function partition(arr, left, right, pivot, compare) {
- var k = left,
- value = arr[pivot];
+// Floyd-Rivest selection algorithm:
+// sort an array between left and right (inclusive) so that the smallest k elements come first (unordered)
+function select(arr, left, right, k, compare) {
+ var n, i, z, s, sd, newLeft, newRight, t, j;
- swap(arr, pivot, right);
-
- for (var i = left; i < right; i++) {
- if (compare(arr[i], value) < 0) {
- swap(arr, k, i);
- k++;
+ while (right > left) {
+ if (right - left > 600) {
+ n = right - left + 1;
+ i = k - left + 1;
+ z = Math.log(n);
+ s = 0.5 * Math.exp(2 * z / 3);
+ sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (i - n / 2 < 0 ? -1 : 1);
+ newLeft = Math.max(left, Math.floor(k - i * s / n + sd));
+ newRight = Math.min(right, Math.floor(k + (n - i) * s / n + sd));
+ select(arr, newLeft, newRight, k, compare);
}
- }
- swap(arr, right, k);
- return k;
+ t = arr[k];
+ i = left;
+ j = right;
+
+ swap(arr, left, k);
+ if (compare(arr[right], t) > 0) swap(arr, left, right);
+
+ while (i < j) {
+ swap(arr, i, j);
+ i++;
+ j--;
+ while (compare(arr[i], t) < 0) i++;
+ while (compare(arr[j], t) > 0) j--;
+ }
+
+ if (compare(arr[left], t) === 0) swap(arr, left, j);
+ else {
+ j++;
+ swap(arr, j, right);
+ }
+
+ if (j <= k) left = j + 1;
+ if (k <= j) right = j - 1;
+ }
}
function swap(arr, i, j) {
@@ -546,9 +613,9 @@ function swap(arr, i, j) {
// export as AMD/CommonJS module or global variable
-if (typeof define === 'function' && define.amd) define(function() { return rbush; });
+if (typeof define === 'function' && define.amd) define('rbush', function () { return rbush; });
else if (typeof module !== 'undefined') module.exports = rbush;
else if (typeof self !== 'undefined') self.rbush = rbush;
else window.rbush = rbush;
-})();
\ No newline at end of file
+})();
diff --git a/test/index.html b/test/index.html
index 1de031b1b..c8b018623 100644
--- a/test/index.html
+++ b/test/index.html
@@ -38,7 +38,9 @@
-
+
+
+
@@ -56,7 +58,8 @@
-
+
+
@@ -261,6 +264,7 @@
+
@@ -300,9 +304,9 @@
-
-
-
+
+
+
diff --git a/test/index_packaged.html b/test/index_packaged.html
index 5af0b5d99..bb2b50d29 100644
--- a/test/index_packaged.html
+++ b/test/index_packaged.html
@@ -59,6 +59,7 @@
+
@@ -98,9 +99,9 @@
-
-
-
+
+
+
diff --git a/test/spec/geo/extent.js b/test/spec/geo/extent.js
index cd2e84cb0..b58dbefec 100644
--- a/test/spec/geo/extent.js
+++ b/test/spec/geo/extent.js
@@ -57,7 +57,20 @@ describe("iD.geo.Extent", function () {
describe("#center", function () {
it("returns the center point", function () {
- expect(iD.geo.Extent([0, 0], [5, 10]).center()).to.eql([2.5, 5]);
+ expect(iD.geo.Extent([0, 0], [5, 10]).center()).to.eql([2.5, 5]);
+ });
+ });
+
+ describe("#rectangle", function () {
+ it("returns the extent as a rectangle", function () {
+ expect(iD.geo.Extent([0, 0], [5, 10]).rectangle()).to.eql([0, 0, 5, 10]);
+ });
+ });
+
+ describe("#polygon", function () {
+ it("returns the extent as a polygon", function () {
+ expect(iD.geo.Extent([0, 0], [5, 10]).polygon())
+ .to.eql([[0, 0], [0, 10], [5, 10], [5, 0], [0, 0]]);
});
});
diff --git a/test/spec/services/mapillary.js b/test/spec/services/mapillary.js
new file mode 100644
index 000000000..cea1274ed
--- /dev/null
+++ b/test/spec/services/mapillary.js
@@ -0,0 +1,336 @@
+describe('iD.services.mapillary', function() {
+ var dimensions = [64, 64],
+ context, server, mapillary;
+
+ beforeEach(function() {
+ context = iD();
+ context.projection.scale(667544.214430109); // z14
+
+ server = sinon.fakeServer.create();
+ mapillary = iD.services.mapillary();
+ mapillary.reset();
+ });
+
+ afterEach(function() {
+ server.restore();
+ });
+
+
+ describe('Mapillary service', function() {
+ it('Initializes cache one time', function() {
+ var cache = iD.services.mapillary.cache;
+ expect(cache).to.have.property('images');
+ expect(cache).to.have.property('signs');
+
+ var mapillary2 = iD.services.mapillary();
+ var cache2 = iD.services.mapillary.cache;
+ expect(cache).to.equal(cache2);
+ });
+ });
+
+ describe('#loadImages', function() {
+ it('fires loadedImages when images are loaded', function() {
+ var spy = sinon.spy();
+ mapillary.on('loadedImages', spy);
+ mapillary.loadImages(context.projection, dimensions);
+
+ var match = /search\/im\/geojson/,
+ features = [{
+ type: 'Feature',
+ geometry: { type: 'Point', coordinates: [0,0] },
+ properties: { ca: 90, key: '0' }
+ }],
+ response = { type: 'FeatureCollection', features: features };
+
+ server.respondWith('GET', match,
+ [200, { 'Content-Type': 'application/json' }, JSON.stringify(response) ]);
+ server.respond();
+
+ expect(spy).to.have.been.calledOnce;
+ });
+
+ it('loads multiple pages of image results', function() {
+ var spy = sinon.spy();
+ mapillary.on('loadedImages', spy);
+ mapillary.loadImages(context.projection, dimensions);
+
+ var features0 = [],
+ features1 = [],
+ i;
+
+ for (i = 0; i < 1000; i++) {
+ features0.push({
+ type: 'Feature',
+ geometry: { type: 'Point', coordinates: [0,0] },
+ properties: { ca: 90, key: String(i) }
+ });
+ }
+ for (i = 0; i < 500; i++) {
+ features1.push({
+ type: 'Feature',
+ geometry: { type: 'Point', coordinates: [0,0] },
+ properties: { ca: 90, key: String(1000 + i) }
+ });
+ }
+
+ var match0 = /page=0/,
+ response0 = { type: 'FeatureCollection', features: features0 },
+ match1 = /page=1/,
+ response1 = { type: 'FeatureCollection', features: features1 }
+
+ server.respondWith('GET', match0,
+ [200, { 'Content-Type': 'application/json' }, JSON.stringify(response0) ]);
+ server.respondWith('GET', match1,
+ [200, { 'Content-Type': 'application/json' }, JSON.stringify(response1) ]);
+ server.respond();
+
+ expect(spy).to.have.been.calledTwice;
+ });
+ });
+
+ describe('#loadSigns', function() {
+ it('loads sign_defs', function() {
+ mapillary.loadSigns(context, context.projection, dimensions);
+
+ var base = 'regulatory--maximum-speed-limit-65--',
+ match = /traffico\/string-maps\/(\w+)-map.json/;
+
+ server.respondWith('GET', match, function (xhr, id) {
+ xhr.respond(200, { 'Content-Type': 'application/json' },
+ '{ "' + base + id + '": true }');
+ });
+ server.respond();
+
+ var sign_defs = iD.services.mapillary.sign_defs;
+
+ expect(sign_defs).to.have.property('au')
+ .that.is.an('object')
+ .that.deep.equals({'regulatory--maximum-speed-limit-65--au': true});
+ expect(sign_defs).to.have.property('br')
+ .that.is.an('object')
+ .that.deep.equals({'regulatory--maximum-speed-limit-65--br': true});
+ expect(sign_defs).to.have.property('ca')
+ .that.is.an('object')
+ .that.deep.equals({'regulatory--maximum-speed-limit-65--ca': true});
+ expect(sign_defs).to.have.property('eu')
+ .that.is.an('object')
+ .that.deep.equals({'regulatory--maximum-speed-limit-65--de': true});
+ expect(sign_defs).to.have.property('us')
+ .that.is.an('object')
+ .that.deep.equals({'regulatory--maximum-speed-limit-65--us': true});
+ });
+
+ it('fires loadedSigns when signs are loaded', function() {
+ var spy = sinon.spy();
+ mapillary.on('loadedSigns', spy);
+ mapillary.loadSigns(context, context.projection, dimensions);
+
+ var match = /search\/im\/geojson\/or/,
+ rects = [{
+ 'package': 'trafficsign_us_3.0',
+ rect: [ 0.805, 0.463, 0.833, 0.502 ],
+ length: 4,
+ score: '1.27',
+ type: 'regulatory--maximum-speed-limit-65--us'
+ }],
+ features = [{
+ type: 'Feature',
+ geometry: { type: 'Point', coordinates: [0,0] },
+ properties: { rects: rects, key: '0' }
+ }],
+ response = { type: 'FeatureCollection', features: features };
+
+ server.respondWith('GET', match,
+ [200, { 'Content-Type': 'application/json' }, JSON.stringify(response) ]);
+ server.respond();
+
+ expect(spy).to.have.been.calledOnce;
+ });
+
+ it('loads multiple pages of signs results', function() {
+ var spy = sinon.spy();
+ mapillary.on('loadedSigns', spy);
+ mapillary.loadSigns(context, context.projection, dimensions);
+
+ var rects = [{
+ 'package': 'trafficsign_us_3.0',
+ rect: [ 0.805, 0.463, 0.833, 0.502 ],
+ length: 4,
+ score: '1.27',
+ type: 'regulatory--maximum-speed-limit-65--us'
+ }],
+ features0 = [],
+ features1 = [],
+ i;
+
+ for (i = 0; i < 1000; i++) {
+ features0.push({
+ type: 'Feature',
+ geometry: { type: 'Point', coordinates: [0,0] },
+ properties: { rects: rects, key: String(i) }
+ });
+ }
+ for (i = 0; i < 500; i++) {
+ features1.push({
+ type: 'Feature',
+ geometry: { type: 'Point', coordinates: [0,0] },
+ properties: { rects: rects, key: String(1000 + i) }
+ });
+ }
+
+ var match0 = /page=0/,
+ response0 = { type: 'FeatureCollection', features: features0 },
+ match1 = /page=1/,
+ response1 = { type: 'FeatureCollection', features: features1 }
+
+ server.respondWith('GET', match0,
+ [200, { 'Content-Type': 'application/json' }, JSON.stringify(response0) ]);
+ server.respondWith('GET', match1,
+ [200, { 'Content-Type': 'application/json' }, JSON.stringify(response1) ]);
+ server.respond();
+
+ expect(spy).to.have.been.calledTwice;
+ });
+ });
+
+
+ describe('#images', function() {
+ it('returns images in the visible map area', function() {
+ var features = [
+ [0, 0, 0, 0, { key: '0', loc: [0,0], ca: 90 }],
+ [0, 0, 0, 0, { key: '1', loc: [0,0], ca: 90 }],
+ [0, 1, 0, 1, { key: '2', loc: [0,1], ca: 90 }]
+ ];
+
+ iD.services.mapillary.cache.images.rtree.load(features);
+ var res = mapillary.images(context.projection, dimensions);
+
+ expect(res).to.deep.eql([
+ { key: '0', loc: [0,0], ca: 90 },
+ { key: '1', loc: [0,0], ca: 90 }
+ ]);
+ });
+
+ it('limits results no more than 3 stacked images in one spot', function() {
+ var features = [
+ [0, 0, 0, 0, { key: '0', loc: [0,0], ca: 90 }],
+ [0, 0, 0, 0, { key: '1', loc: [0,0], ca: 90 }],
+ [0, 0, 0, 0, { key: '2', loc: [0,0], ca: 90 }],
+ [0, 0, 0, 0, { key: '3', loc: [0,0], ca: 90 }],
+ [0, 0, 0, 0, { key: '4', loc: [0,0], ca: 90 }]
+ ];
+
+ iD.services.mapillary.cache.images.rtree.load(features);
+ var res = mapillary.images(context.projection, dimensions);
+ expect(res).to.have.length.of.at.most(3);
+ });
+ });
+
+ describe('#signs', function() {
+ it('returns signs in the visible map area', function() {
+ var signs = [{
+ 'package': 'trafficsign_us_3.0',
+ rect: [ 0.805, 0.463, 0.833, 0.502 ],
+ length: 4,
+ score: '1.27',
+ type: 'regulatory--maximum-speed-limit-65--us'
+ }],
+ features = [
+ [0, 0, 0, 0, { key: '0', loc: [0,0], signs: signs }],
+ [0, 0, 0, 0, { key: '1', loc: [0,0], signs: signs }],
+ [0, 1, 0, 1, { key: '2', loc: [0,1], signs: signs }]
+ ];
+
+ iD.services.mapillary.cache.signs.rtree.load(features);
+ var res = mapillary.signs(context.projection, dimensions);
+
+ expect(res).to.deep.eql([
+ { key: '0', loc: [0,0], signs: signs },
+ { key: '1', loc: [0,0], signs: signs }
+ ]);
+ });
+
+ it('limits results no more than 3 stacked signs in one spot', function() {
+ var signs = [{
+ 'package': 'trafficsign_us_3.0',
+ rect: [ 0.805, 0.463, 0.833, 0.502 ],
+ length: 4,
+ score: '1.27',
+ type: 'regulatory--maximum-speed-limit-65--us'
+ }],
+ features = [
+ [0, 0, 0, 0, { key: '0', loc: [0,0], signs: signs }],
+ [0, 0, 0, 0, { key: '1', loc: [0,0], signs: signs }],
+ [0, 0, 0, 0, { key: '2', loc: [0,0], signs: signs }],
+ [0, 0, 0, 0, { key: '3', loc: [0,0], signs: signs }],
+ [0, 0, 0, 0, { key: '4', loc: [0,0], signs: signs }]
+ ];
+
+ iD.services.mapillary.cache.signs.rtree.load(features);
+ var res = mapillary.signs(context.projection, dimensions);
+ expect(res).to.have.length.of.at.most(3);
+ });
+ });
+
+ describe('#signsSupported', function() {
+ it('returns false for Internet Explorer', function() {
+ var detect = iD.detect;
+ iD.detect = function() { return { ie: true, browser: 'Internet Explorer' }; };
+ expect(mapillary.signsSupported()).to.be.false;
+ iD.detect = detect;
+ });
+
+ it('returns false for Safari', function() {
+ var detect = iD.detect;
+ iD.detect = function() { return { ie: false, browser: 'Safari' }; };
+ expect(mapillary.signsSupported()).to.be.false;
+ iD.detect = detect;
+ });
+ });
+
+ describe('#signHTML', function() {
+ it('returns sign HTML', function() {
+ iD.services.mapillary.sign_defs = {
+ us: {'regulatory--maximum-speed-limit-65--us': '65'}
+ };
+
+ var signdata = {
+ key: '0',
+ loc: [0,0],
+ signs: [{
+ 'package': 'trafficsign_us_3.0',
+ rect: [ 0.805, 0.463, 0.833, 0.502 ],
+ length: 4,
+ score: '1.27',
+ type: 'regulatory--maximum-speed-limit-65--us'
+ }]
+ };
+
+ expect(mapillary.signHTML(signdata)).to.eql('65')
+ });
+ });
+
+ describe('#selectedThumbnail', function() {
+ it('sets thumbnail image', function() {
+ mapillary.selectedThumbnail('foo');
+ expect(iD.services.mapillary.thumb).to.eql('foo');
+ });
+
+ it('gets thumbnail image', function() {
+ iD.services.mapillary.thumb = 'bar';
+ expect(mapillary.selectedThumbnail()).to.eql('bar');
+ });
+ });
+
+ describe('#reset', function() {
+ it('resets cache and thumbnail image', function() {
+ iD.services.mapillary.cache.foo = 'bar';
+ iD.services.mapillary.thumb = 'bar';
+
+ mapillary.reset();
+ expect(iD.services.mapillary.cache).to.not.have.property('foo');
+ expect(iD.services.mapillary.thumb).to.be.null;
+ });
+ });
+
+});
diff --git a/test/spec/countrycode.js b/test/spec/services/nominatim.js
similarity index 81%
rename from test/spec/countrycode.js
rename to test/spec/services/nominatim.js
index 1b519d3f4..e77dbd725 100644
--- a/test/spec/countrycode.js
+++ b/test/spec/services/nominatim.js
@@ -1,10 +1,10 @@
-describe("iD.countryCode", function() {
- var server, countryCode;
+describe("iD.services.nominatim", function() {
+ var server, nominatim;
beforeEach(function() {
server = sinon.fakeServer.create();
- iD.countryCode.cache = null;
- countryCode = iD.countryCode();
+ nominatim = iD.services.nominatim();
+ nominatim.reset();
});
afterEach(function() {
@@ -15,10 +15,10 @@ describe("iD.countryCode", function() {
return iD.util.stringQs(url.substring(url.indexOf('?') + 1));
}
- describe("#search", function() {
- it("calls the given callback with the results of the search query", function() {
+ describe("#countryCode", function() {
+ it("calls the given callback with the results of the country code query", function() {
var callback = sinon.spy();
- countryCode.search([16, 48], callback);
+ nominatim.countryCode([16, 48], callback);
server.respondWith("GET", "https://nominatim.openstreetmap.org/reverse?addressdetails=1&format=json&lat=48&lon=16",
[200, { "Content-Type": "application/json" },
@@ -29,9 +29,9 @@ describe("iD.countryCode", function() {
{format: "json", addressdetails: "1", lat: "48", lon: "16"});
expect(callback).to.have.been.calledWith(null, "at");
});
- it("should not cache the first search result", function() {
+ it("should not cache the first country code result", function() {
var callback = sinon.spy();
- countryCode.search([16, 48], callback);
+ nominatim.countryCode([16, 48], callback);
server.respondWith("GET", "https://nominatim.openstreetmap.org/reverse?addressdetails=1&format=json&lat=48&lon=16",
[200, { "Content-Type": "application/json" },
@@ -45,7 +45,7 @@ describe("iD.countryCode", function() {
server.restore();
server = sinon.fakeServer.create();
- countryCode.search([17, 49], callback);
+ nominatim.countryCode([17, 49], callback);
server.respondWith("GET", "https://nominatim.openstreetmap.org/reverse?addressdetails=1&format=json&lat=49&lon=17",
[200, { "Content-Type": "application/json" },
@@ -56,9 +56,9 @@ describe("iD.countryCode", function() {
{format: "json", addressdetails: "1", lat: "49", lon: "17"});
expect(callback).to.have.been.calledWith(null, "cz");
});
- it("should cache the first search result", function() {
+ it("should cache the first country code result", function() {
var callback = sinon.spy();
- countryCode.search([16, 48], callback);
+ nominatim.countryCode([16, 48], callback);
server.respondWith("GET", "https://nominatim.openstreetmap.org/reverse?addressdetails=1&format=json&lat=48&lon=16",
[200, { "Content-Type": "application/json" },
@@ -72,7 +72,7 @@ describe("iD.countryCode", function() {
server.restore();
server = sinon.fakeServer.create();
- countryCode.search([16.01, 48.01], callback);
+ nominatim.countryCode([16.01, 48.01], callback);
server.respondWith("GET", "https://nominatim.openstreetmap.org/reverse?addressdetails=1&format=json&lat=48.01&lon=16.01",
[200, { "Content-Type": "application/json" },
@@ -83,7 +83,7 @@ describe("iD.countryCode", function() {
});
it("calls the given callback with an error", function() {
var callback = sinon.spy();
- countryCode.search([1000, 1000], callback);
+ nominatim.countryCode([1000, 1000], callback);
server.respondWith("GET", "https://nominatim.openstreetmap.org/reverse?addressdetails=1&format=json&lat=1000&lon=1000",
[200, { "Content-Type": "application/json" },
diff --git a/test/spec/taginfo.js b/test/spec/services/taginfo.js
similarity index 98%
rename from test/spec/taginfo.js
rename to test/spec/services/taginfo.js
index 7d91d2f5b..df66510c6 100644
--- a/test/spec/taginfo.js
+++ b/test/spec/services/taginfo.js
@@ -1,9 +1,9 @@
-describe("iD.taginfo", function() {
+describe("iD.services.taginfo", function() {
var server, taginfo;
beforeEach(function() {
server = sinon.fakeServer.create();
- taginfo = iD.taginfo();
+ taginfo = iD.services.taginfo();
});
afterEach(function() {