Merge branch 'master' of github.com:systemed/iD

This commit is contained in:
Saman Bemel-Benrud
2013-01-04 19:28:27 -05:00
33 changed files with 739 additions and 352 deletions

View File

@@ -17,6 +17,7 @@ all: \
js/lib/d3.keybinding.js \
js/lib/d3.one.js \
js/lib/d3.size.js \
js/lib/d3.trigger.js \
js/lib/d3.typeahead.js \
js/lib/jxon.js \
js/lib/lodash.js \

View File

@@ -291,6 +291,25 @@ button.delete:hover {
button.save {
background-color: #6bc641;
width: 120px;
position: relative;
}
button.save.has-count {
padding-right:10px;
}
button.save.has-count .count {
display: block;
position: absolute;
font-size:small;
top: 0;
right: 0;
background: #fff;
height: 37px;
border-radius: 0 3px 3px 0;
padding: 0 10px;
line-height: 40px;
}
button.save:hover {
@@ -452,7 +471,7 @@ button.Browse .label {
position:relative;
}
.inspector-wrap a.permalink {
.inspector-inner.head a {
text-decoration:none;
margin-right: 10px;
display: inline-block
@@ -489,7 +508,7 @@ button.Browse .label {
.tag-row input {
width: 50%;
border-right: 0;
border-left: 0;
}
.tag-row input.key {
@@ -500,9 +519,8 @@ button.Browse .label {
border-top: 1px solid #ccc;
}
.tag-row input.value {
position: relative;
border-right: 1px solid #ccc;
.tag-row input.key {
border-left: 1px solid #ccc;
}
.input-wrap::after {
@@ -531,8 +549,17 @@ button.Browse .label {
border-top: 1px solid #ccc;
}
.tag-row-empty button {
display: none;
.inspector-inner .add-tag-row {
width: 100%;
padding-right: 70px;
}
.inspector-inner .add-tag {
width: 50%;
height: 30px;
font-size: 100%;
border: 1px solid #ccc;
border-top: 0;
}
/* Map Controls */
@@ -572,6 +599,30 @@ button.Browse .label {
top:210px;
}
.layerswitcher-control .adjustments {
padding:5px;
opacity:0.2;
}
.layerswitcher-control .adjustments:hover {
opacity:1;
}
.layerswitcher-control .adjustments .reset {
height:20px;
font-size:10px;
font-weight:normal;
padding:0 5px;
}
.layerswitcher-control .nudge {
height:20px;
width:20px;
font-size:10px;
margin-right:2px;
font-weight:normal;
}
.opacity-options-wrapper {
padding: 10px 10px 0 10px;
}
@@ -630,6 +681,10 @@ button.Browse .label {
margin: 4px;
}
.geolocate-control {
top:260px;
}
/* Map
------------------------------------------------------- */
@@ -709,8 +764,8 @@ div.typeahead {
box-shadow: 0 5px 10px 0 rgba(0,0,0,.2);
margin-top: -1px;
background: white;
max-height: 120px;
overflow: auto;
max-height: 180px;
overflow: hidden;
border: 1px solid #ccc;
}
@@ -722,6 +777,7 @@ div.typeahead a {
border-top:1px solid #ccc;
background-color: #fff;
padding:1px 4px;
white-space: nowrap;
}
div.typeahead a:hover,
@@ -801,6 +857,14 @@ div.typeahead a:first-child {
padding:5px 10px;
}
.changeset-list li span.count {
font-size:10px;
color:#555;
}
.changeset-list li span.count:before { content: '('; }
.changeset-list li span.count:after { content: ')'; }
.changeset-list li:first-child { border-top: 0;}
.commit-modal .changeset-comment {
@@ -823,6 +887,7 @@ div.typeahead a:first-child {
left:70px;
width:250px;
height:50px;
padding:10px;
background:#fff;
font-size: 20px;
font-weight: bold;

View File

@@ -10,30 +10,51 @@ g.point circle {
fill:#fff;
}
g.point.hover circle,
g.point.selected circle {
fill:#ffff00;
stroke-width:4;
stroke:#fff
g.point.hover circle.stroke,
g.point.selected circle.stroke {
fill:#333;
-webkit-transform:scale(1.2, 1.2);
-moz-transform:scale(1.2, 1.2);
transform:scale(1.2, 1.2);
}
/* interactive elements */
circle.vertex {
g.vertex circle.fill {
fill:white;
stroke:#333;
fill-opacity:1;
stroke-width:2;
stroke-opacity: 1;
}
circle.vertex.shared {
circle.stroke,
circle.fill {
-webkit-transition: -webkit-transform 50ms linear;
transition: transform 50ms linear;
-moz-transition: stroke 50ms linear;
-webkit-transform:scale(1, 1);
-moz-transform:scale(1, 1);
transform:scale(1, 1);
}
g.vertex circle.stroke {
fill:#333;
}
g.vertex.shared circle {
fill:#aff;
}
circle.vertex.hover {
g.vertex.hover circle.fill {
-webkit-transform:scale(2, 2);
-moz-transform:scale(2, 2);
transform:scale(2, 2);
}
circle.vertex.selected {
g.vertex.hover circle.stroke {
-webkit-transform:scale(1.8, 1.8);
-moz-transform:scale(1.8, 1.8);
transform:scale(1.8, 1.8);
}
g.vertex circle.selected {
fill: #ffff00;
}
@@ -60,12 +81,12 @@ path.casing {
path.casing.hover {
stroke:#FF0F0F !important;
opacity:0.8;
stroke-opacity:0.8;
}
path.casing.selected {
stroke:#E96666 !important;
opacity:1 !important;
stroke-opacity:1 !important;
stroke-width:10 !important;
}
@@ -134,7 +155,7 @@ path.stroke.highway-residential {
path.casing.highway-residential {
stroke:#E8E8E8;
stroke-width:10;
opacity:0.4;
stroke-opacity:0.4;
}
path.stroke.highway-unclassified,
@@ -179,12 +200,12 @@ path.stroke.highway-motorway, path.stroke.highway-motorway_link {
path.casing.highway-motorway, path.casing.highway-motorway_link {
stroke:#809BC0;
stroke-width:9;
opacity:0.4;
stroke-opacity:0.4;
}
path.stroke.highway-trunk, path.stroke.highway-trunk_link {
stroke-width:7;
opacity:0.4;
stroke-opacity:0.4;
stroke:#7FC97F;
}
path.casing.highway-trunk, path.casing.highway-trunk_link {
@@ -198,7 +219,7 @@ path.stroke.highway-primary, path.stroke.highway-primary_link {
}
path.casing.highway-primary, path.casing.highway-primary_link {
stroke:#FF6363;
opacity:0.4;
stroke-opacity:0.4;
stroke-width:12;
}
@@ -207,7 +228,7 @@ path.stroke.highway-secondary, path.stroke.highway-secondary_link {
stroke-width:4;
}
path.casing.highway-secondary, path.casing.highway-secondary_link {
opacity:0.4;
stroke-opacity:0.4;
stroke:#FDBF6F;
stroke-width:11;
}

View File

@@ -20,6 +20,7 @@
<script src='js/lib/d3.typeahead.js'></script>
<script src='js/lib/d3.geo.tile.js'></script>
<script src='js/lib/d3.size.js'></script>
<script src='js/lib/d3.trigger.js'></script>
<script src='js/lib/d3.keybinding.js'></script>
<script src='js/lib/d3-compat.js'></script>
<script src='js/lib/queue.js'></script>
@@ -39,6 +40,7 @@
<script src='js/id/ui/modal.js'></script>
<script src='js/id/ui/confirm.js'></script>
<script src='js/id/ui/commit.js'></script>
<script src='js/id/ui/success.js'></script>
<script src='js/id/ui/loading.js'></script>
<script src='js/id/ui/userpanel.js'></script>
<script src='js/id/ui/layerswitcher.js'></script>

View File

@@ -14,17 +14,19 @@ iD.Connection = function() {
}
function bboxFromAPI(box, tile, callback) {
loadFromURL(bboxUrl(box), function(err, parsed) {
function done(err, parsed) {
loadedTiles[tile.toString()] = true;
callback(err, parsed);
});
}
loadFromURL(bboxUrl(box), done);
}
function loadFromURL(url, callback) {
function done(dom) {
return callback(null, parse(dom));
}
inflight.push(d3.xml(url).get()
.on('load', function(dom) {
return callback(null, parse(dom));
}));
.on('load', done));
}
function getNodes(obj) {
@@ -67,9 +69,7 @@ iD.Connection = function() {
tags: getTags(obj)
};
for (var i = 0, l = obj.attributes.length; i < l; i++) {
var n = obj.attributes[i].nodeName;
var v = obj.attributes[i].nodeValue;
o[n] = v;
o[obj.attributes[i].nodeName] = obj.attributes[i].nodeValue;
}
if (o.lon && o.lat) {
o.loc = [parseFloat(o.lon), parseFloat(o.lat)];
@@ -129,13 +129,14 @@ iD.Connection = function() {
};
function userDetails(callback) {
oauth.xhr({ method: 'GET', path: '/api/0.6/user/details' }, function(err, user_details) {
function done(err, user_details) {
var u = user_details.getElementsByTagName('user')[0];
callback(connection.user({
display_name: u.attributes.display_name.nodeValue,
id: u.attributes.id.nodeValue
}).user());
});
}
oauth.xhr({ method: 'GET', path: '/api/0.6/user/details' }, done);
}
function tileAlreadyLoaded(c) { return !loadedTiles[c.toString()]; }
@@ -143,9 +144,10 @@ iD.Connection = function() {
function abortRequest(i) { i.abort(); }
function loadTile(e) {
bboxFromAPI(e.box, e.tile, function(err, g) {
function done(err, g) {
event.load(err, g);
});
}
bboxFromAPI(e.box, e.tile, done);
}
function loadTiles(projection) {
@@ -211,10 +213,11 @@ iD.Connection = function() {
};
connection.authenticate = function(callback) {
return oauth.authenticate(function(err, res) {
function done(err, res) {
event.auth();
if (callback) callback(err, res);
});
}
return oauth.authenticate(done);
};
connection.bboxFromAPI = bboxFromAPI;

View File

@@ -84,6 +84,28 @@ iD.Entity.prototype = {
key != 'odbl' &&
key.indexOf('tiger:') !== 0;
});
},
friendlyName: function() {
// Generate a string such as 'river' or 'Fred's House' for an entity.
if (!this.tags || !Object.keys(this.tags).length) { return ''; }
var mainkeys = ['highway','amenity','railway','waterway','natural'],
n = [];
if (this.tags.name) n.push(this.tags.name);
if (this.tags.ref) n.push(this.tags.ref);
if (!n.length) {
for (var k in this.tags) {
if (mainkeys.indexOf(k) !== -1) {
n.push(this.tags[k]);
break;
}
}
}
return n.length === 0 ? 'unknown' : n.join('; ');
}
};

View File

@@ -3,5 +3,9 @@ iD.Node = iD.Entity.extend({
extent: function() {
return [this.loc, this.loc];
},
geometry: function() {
return this._poi ? 'point' : 'vertex';
}
});

View File

@@ -36,5 +36,9 @@ iD.Way = iD.Entity.extend({
this.tags.area !== 'no' &&
!this.tags.highway &&
!this.tags.barrier);
},
geometry: function() {
return this.isArea() ? 'area' : 'line';
}
});

View File

@@ -107,10 +107,11 @@ window.iD = function(container) {
.append('div')
.attr('class', 'hello');
bar.append('button')
var save_button = bar.append('button')
.attr('class', 'save action wide')
.html("<span class='icon icon-pre-text save'></span><span class='label'>Save</span><small id='as-username'></small>")
.attr('title', 'Save changes to OpenStreetMap, making them visible to other users')
.property('disabled', true)
.call(bootstrap.tooltip()
.placement('bottom'))
.on('click', function() {
@@ -121,6 +122,17 @@ window.iD = function(container) {
l.remove();
history.reset();
map.flush().redraw();
var modal = iD.modal();
modal.select('.content')
.classed('success-modal', true)
.datum({
id: changeset_id,
comment: e.comment
})
.call(iD.success()
.on('cancel', function() {
modal.remove();
}));
});
}
var changes = history.changes();
@@ -146,6 +158,24 @@ window.iD = function(container) {
}
});
save_button.append('span')
.attr('class', 'count');
history.on('change.save-button', function() {
var changes = history.changes(),
num_changes = d3.sum(d3.values(changes).map(function(c) {
return c.length;
}));
save_button.property('disabled', num_changes === 0);
save_button
.classed('has-count', num_changes > 0);
save_button.select('span.count')
.text(num_changes);
});
bar.append('div')
.attr('class', 'messages');
@@ -160,6 +190,21 @@ window.iD = function(container) {
return d[0] + ' icon';
});
function geolocateSuccess(position) {
map.center([position.coords.longitude, position.coords.latitude]);
}
function geolocateError() { }
if (navigator.geolocation) {
container.append('div')
.attr('class', 'geolocate-control map-control')
.append('button')
.attr('class', 'narrow')
.text('G')
.on('click', function() {
navigator.geolocation.getCurrentPosition(geolocateSuccess, geolocateError);
});
}
var gc = container.append('div').attr('class', 'geocode-control map-control')
.call(iD.geocoder().map(map));

View File

@@ -13,7 +13,6 @@ iD.modes.AddLine = function() {
controller = mode.controller;
map.dblclickEnable(false)
.hoverEnable(false)
.hoverEnable(false)
.hint('Click on the map to start drawing an road, path, or route.');

View File

@@ -30,7 +30,7 @@ iD.modes.AddPoint = function() {
};
mode.exit = function() {
map.hoverEnable(true);
mode.map.hoverEnable(true);
mode.map.hint(false);
mode.map.surface.on('click.addpoint', null);
mode.map.keybinding().on('⎋.addpoint', null);

View File

@@ -66,14 +66,35 @@ iD.modes.Select = function (entity) {
mode.controller.exit();
});
surface.on('click.select', function () {
function click() {
var datum = d3.select(d3.event.target).datum();
if (datum instanceof iD.Entity) {
mode.controller.enter(iD.modes.Select(datum));
} else {
mode.controller.enter(iD.modes.Browse());
}
});
}
function dblclick() {
var datum = d3.select(d3.event.target).datum();
if (datum instanceof iD.Entity &&
(datum.geometry() === 'area' || datum.geometry() === 'line')) {
var choice = iD.util.geo.chooseIndex(datum,
d3.mouse(mode.map.surface.node()), mode.map),
node = iD.Node({ loc: choice.loc });
mode.history.perform(
iD.actions.AddNode(node),
iD.actions.AddWayNode(datum.id, node.id, choice.index),
'added a point to a road');
d3.event.preventDefault();
d3.event.stopPropagation();
}
}
surface.on('click.select', click)
.on('dblclick.browse', dblclick);
mode.map.keybinding().on('⌫.select', function(e) {
remove();

View File

@@ -66,6 +66,10 @@ iD.OAuth = function() {
var l = iD.loading('contacting openstreetmap...');
// it would make more sense to have this code within the callback
// to oauth.xhr below. however, it needs to be directly within a
// browser event handler in order to open a popup without it being
// blocked.
var w = 600, h = 550,
settings = [
['width', w], ['height', h],

View File

@@ -2,6 +2,7 @@ iD.Background = function() {
var tile = d3.geo.tile(),
projection,
cache = {},
offset = [0, 0],
transformProp = iD.util.prefixCSSProperty('Transform'),
source = d3.functor('');
@@ -27,6 +28,10 @@ iD.Background = function() {
return tiles;
}
function tileSize(d, z) {
return Math.ceil(256 * Math.pow(2, z - d[2])) / 256;
}
// derive the tiles onscreen, remove those offscreen and position tiles
// correctly for the currentstate of `projection`
function background() {
@@ -82,21 +87,29 @@ iD.Background = function() {
.on('error', error)
.on('load', load);
function tileSize(d) {
return Math.ceil(256 * Math.pow(2, z - d[2])) / 256;
}
image.style(transformProp, function(d) {
var _ts = 256 * Math.pow(2, z - d[2]);
var scale = tileSize(d);
var scale = tileSize(d, z);
return 'translate(' +
Math.round((d[0] * _ts) - tile_origin[0]) + 'px,' +
Math.round((d[1] * _ts) - tile_origin[1]) + 'px) scale(' + scale + ',' + scale + ')';
(Math.round((d[0] * _ts) - tile_origin[0]) + offset[0]) + 'px,' +
(Math.round((d[1] * _ts) - tile_origin[1]) + offset[1]) + 'px) scale(' + scale + ',' + scale + ')';
});
if (Object.keys(cache).length > 100) cache = {};
}
background.offset = function(_) {
if (!arguments.length) return offset;
offset = _;
return background;
};
background.nudge = function(_) {
offset[0] += _[0];
offset[1] += _[1];
return background;
};
background.projection = function(_) {
if (!arguments.length) return projection;
projection = _;

View File

@@ -45,3 +45,7 @@ iD.BackgroundSource.Tiger2012 = iD.BackgroundSource.template(
iD.BackgroundSource.OSM = iD.BackgroundSource.template(
'http://{t}.tile.openstreetmap.org/{z}/{x}/{y}.png',
['a', 'b', 'c'], [0, 18]);
iD.BackgroundSource.MapBox = iD.BackgroundSource.template(
'http://{t}.tiles.mapbox.com/v3/openstreetmap.map-4wvf9l0l/{z}/{x}/{y}.jpg70',
['a', 'b', 'c'], [0, 16]);

View File

@@ -84,9 +84,9 @@ iD.Map = function() {
filter = d3.functor(true);
} else {
var only = {};
difference.forEach(function (id) {
difference.forEach(function buildDifference(id) {
only[id] = graph.fetch(id);
graph.parentWays(id).forEach(function (parent) {
graph.parentWays(id).forEach(function buildOnly(parent) {
only[parent.id] = graph.fetch(parent.id);
});
});
@@ -94,7 +94,6 @@ iD.Map = function() {
filter = function(d) { return d.accuracy ? d.way in only : d.id in only; };
}
if (all.length > 10000) return editOff();
else editOn();
@@ -112,9 +111,10 @@ iD.Map = function() {
}
}
var parentStructure = graph.parentStructure(ways);
var wayAccuracyHandles = ways.reduce(function(mem, w) {
return mem.concat(accuracyHandles(w));
}, []);
var wayAccuracyHandles = [];
for (i = 0; i < ways.length; i++) {
accuracyHandles(ways[i], wayAccuracyHandles);
}
drawVertices(vertices, parentStructure, filter);
drawAccuracyHandles(wayAccuracyHandles, filter);
drawCasings(lines, filter);
@@ -123,8 +123,8 @@ iD.Map = function() {
drawPoints(points, filter);
}
function accuracyHandles(way) {
var handles = [];
// updates handles by reference
function accuracyHandles(way, handles) {
for (var i = 0; i < way.nodes.length - 1; i++) {
if (iD.util.geo.dist(way.nodes[i].loc, way.nodes[i + 1].loc) > 0.0001) {
handles.push({
@@ -135,32 +135,36 @@ iD.Map = function() {
});
}
}
return handles;
}
function pointTransform(entity) {
return 'translate(' + iD.util.geo.roundCoords(projection(entity.loc)) + ')';
}
function drawVertices(vertices, parentStructure, filter) {
function shared(d) { return parentStructure[d.id] > 1; }
var circles = g.hit.selectAll('circle.vertex')
var circles = g.hit.selectAll('g.vertex')
.filter(filter)
.data(vertices, key);
circles.exit().remove();
circles.enter().insert('circle', ':first-child')
var cg = circles.enter()
.insert('g', ':first-child')
.attr('class', 'node vertex');
circles.attr('transform', function(entity) {
var p = projection(entity.loc);
return 'translate(' + [~~p[0], ~~p[1]] +
')';
})
cg.append('circle')
.attr('class', 'stroke')
.attr('r', 6);
cg.append('circle')
.attr('class', 'fill')
.attr('r', 4);
circles.attr('transform', pointTransform)
.classed('shared', shared)
.classed('hover', classHover);
circles.transition().duration(50).attr('r', function(d) {
return d.id === hover ? 8: 4;
});
}
function drawAccuracyHandles(waynodes, filter) {
@@ -170,10 +174,7 @@ iD.Map = function() {
handles.exit().remove();
handles.enter().append('circle')
.attr({ r: 3, 'class': 'accuracy-handle' });
handles.attr('transform', function(entity) {
var p = projection(entity.loc);
return 'translate(' + [~~p[0], ~~p[1]] + ')';
});
handles.attr('transform', pointTransform);
}
function editOff() {
@@ -209,20 +210,30 @@ iD.Map = function() {
}
function drawPoints(points, filter) {
var groups = g.hit.selectAll('g.point')
.filter(filter)
.data(points, key);
groups.exit().remove();
var group = groups.enter().append('g')
.attr('class', 'node point');
group.append('circle')
.attr({ r: 10, cx: 8, cy: 8 });
.attr('class', 'stroke')
.attr({ r: 10 });
group.append('circle')
.attr('class', 'fill')
.attr({ r: 10 });
group.append('image')
.attr({ width: 16, height: 16 });
groups.attr('transform', function(d) {
var pt = projection(d.loc);
return 'translate(' + [~~pt[0], ~~pt[1]] + ') translate(-8, -8)';
});
.attr({ width: 16, height: 16 })
.attr('transform', 'translate(-8, -8)');
groups.attr('transform', pointTransform);
groups.classed('hover', classHover);
groups.select('image').attr('xlink:href', iD.Style.pointImage);
}
@@ -299,11 +310,11 @@ iD.Map = function() {
if (fast) {
if (!translateStart) translateStart = d3.event.translate.slice();
var a = d3.event.translate,
b = translateStart;
tilegroup.style(transformProp,
'translate(' + ~~(a[0] - b[0]) + 'px,' + ~~(a[1] - b[1]) + 'px)');
surface.style(transformProp,
'translate(' + ~~(a[0] - b[0]) + 'px,' + ~~(a[1] - b[1]) + 'px)');
b = translateStart,
translate = 'translate(' + ~~(a[0] - b[0]) + 'px,' +
~~(a[1] - b[1]) + 'px)';
tilegroup.style(transformProp, translate);
surface.style(transformProp, translate);
} else {
redraw();
translateStart = null;

View File

@@ -5,7 +5,7 @@ iD.taginfo = function() {
taginfo.keys = function(parameters, callback) {
d3.json(endpoint + 'db/keys?' +
iD.util.qsString(_.extend({
rp: 20,
rp: 6,
sortname: 'count_all',
sortorder: 'desc',
page: 1

View File

@@ -14,51 +14,77 @@ iD.commit = function() {
.attr('class', 'changeset-comment')
.attr('placeholder', 'Brief Description of your contributions');
var buttonwrap = commit.append('div')
.attr('class', 'buttons');
var buttonwrap = commit.append('div')
.attr('class', 'buttons');
var savebutton = buttonwrap.append('button')
.attr('class', 'action wide')
.on('click.save', function() {
event.save({
comment: d3.select('textarea.changeset-comment').node().value
});
});
savebutton.append('span').attr('class','icon save icon-pre-text');
savebutton.append('span').attr('class','label').text('Save');
var cancelbutton = buttonwrap.append('button')
.attr('class', 'cancel wide')
.on('click.cancel', function() {
event.cancel();
});
cancelbutton.append('span').attr('class','icon close icon-pre-text');
cancelbutton.append('span').attr('class','label').text('Cancel');
var savebutton = buttonwrap.append('button')
.attr('class', 'action wide')
.on('click.save', function() {
event.save({
comment: d3.select('textarea.changeset-comment').node().value
});
});
savebutton.append('span').attr('class','icon save icon-pre-text');
savebutton.append('span').attr('class','label').text('Save');
var cancelbutton = buttonwrap.append('button')
.attr('class', 'cancel wide')
.on('click.cancel', function() {
event.cancel();
});
cancelbutton.append('span').attr('class','icon close icon-pre-text');
cancelbutton.append('span').attr('class','label').text('Cancel');
function changesLength(d) { return changes[d].length; }
var section = body.selectAll('div.commit-section')
.data(['modified', 'deleted', 'created'].filter(function(d) {
return changes[d].length;
}))
.data(['modified', 'deleted', 'created'].filter(changesLength))
.enter()
.append('div').attr('class', 'commit-section modal-section fillL2');
section.append('h3').text(String)
.append('small')
.attr('class', 'count')
.text(function(d) { return changes[d].length; });
.text(changesLength);
function zipSame(d) {
var c = [], n = -1;
for (var i = 0; i < d.length; i++) {
var desc = {
name: d[i].friendlyName(),
type: d[i].type,
count: 1,
tagText: iD.util.tagText(d[i])
};
if (c[n] &&
c[n].name == desc.name &&
c[n].tagText == desc.tagText) {
c[n].count++;
} else {
c[++n] = desc;
}
}
return c;
}
var li = section.append('ul')
.attr('class','changeset-list')
.selectAll('li')
.data(function(d) { return changes[d]; })
.data(function(d) { return zipSame(changes[d]); })
.enter()
.append('li');
li.append('strong').text(function(d) { return d.type + ' '; });
li.append('strong').text(function(d) {
return (d.count > 1) ? d.type + 's ' : d.type + ' ';
});
li.append('span')
.text(function(d) {
return iD.util.friendlyName(d);
})
.attr('title', iD.util.tagText);
.text(function(d) { return d.name; })
.attr('title', function(d) { return d.tagText; });
li.filter(function(d) { return d.count > 1; })
.append('span')
.attr('class', 'count')
.text(function(d) { return d.count; });
}
return d3.rebind(commit, event, 'on');

View File

@@ -2,220 +2,237 @@ iD.Inspector = function() {
var event = d3.dispatch('changeTags', 'changeWayDirection',
'update', 'remove', 'close', 'splitWay'),
taginfo = iD.taginfo(),
inspectorwrap;
tagList;
function inspector(selection) {
var entity = selection.datum();
selection.html("").append('button')
.attr('class', 'narrow close')
.html("<span class='icon close'></span>")
.on('click', function() {
event.close(entity);
});
selection.append('div')
.attr('class', 'head inspector-inner')
.call(drawHead);
var inspectorbody = selection.append('div')
.attr('class', 'inspector-body');
var inspectorwrap = inspectorbody.append('div')
.attr('class', 'inspector-inner tag-wrap fillL2');
inspectorwrap.append('h4')
.text('Edit tags');
tagList = inspectorwrap.append('ul');
inspectorwrap.append('div').attr('class', 'add-tag-row').append('button')
.attr('class', 'add-tag')
.text('+ Add New Tag')
.on('click', function() {
addTag();
tagList.selectAll('li:last-child input.key').node().focus();
});
var formsel = drawTags(entity.tags);
inspectorbody.append('div')
.attr('class', 'inspector-buttons')
.call(drawButtons);
var inHeight = inspectorbody.node().offsetHeight;
inspectorbody.style('display', 'none')
.style('margin-top', (-inHeight) + 'px');
var inspectortoggle = selection.append('button')
.attr('class', 'inspector-toggle action')
.on('click', function() {
inspectortoggle.style('display', 'none');
inspectorbody
.style('display', 'block')
.transition()
.style('margin-top', '0px');
});
formsel.selectAll('input').node().focus();
inspectortoggle.append('span')
.text('Details')
.attr('class','label');
}
function drawHead(selection) {
var entity = selection.datum();
function drawhead(selection) {
function osmLink(d) {
return 'http://www.openstreetmap.org/browse/' + d.type + '/' + d.osmId();
}
function emitChangeDirection(d) { event.changeWayDirection(d); }
function emitSplitWay(d) { event.splitWay(d); }
selection.html('');
var h2 = selection.append('h2');
h2.append('span').attr('class', function(d) {
var icons = { way: 'line', node: 'point' };
return 'icon big icon-pre-text big-' + icons[d.type];
});
h2.append('span').text(iD.util.friendlyName(selection.datum()));
h2.append('span')
.attr('class', 'icon big icon-pre-text big-' + entity.geometry());
h2.append('span')
.text(entity.friendlyName());
selection.append('a')
.attr('class', 'permalink')
.attr('href', osmLink)
.attr('href', 'http://www.openstreetmap.org/browse/' + entity.type + '/' + entity.osmId())
.text('View on OSM');
if (selection.datum().type === 'way') {
if (entity.type === 'way') {
selection.append('a')
.attr('class', 'permalink')
.attr('href', '#')
.text('Reverse Direction')
.on('click', emitChangeDirection);
.on('click', function() { event.changeWayDirection(entity); });
}
if (selection.datum().type === 'node' && !selection.datum()._poi) {
if (entity.geometry() === 'vertex') {
selection.append('a')
.attr('class', 'permalink')
.attr('href', '#')
.text('Split Way')
.on('click', emitSplitWay);
.on('click', function() { event.splitWay(entity); });
}
}
function inspector(selection) {
selection.each(function(entity) {
function drawButtons(selection) {
selection.append('button')
.attr('class', 'apply wide action')
.html("<span class='icon icon-pre-text apply'></span><span class='label'>Apply</span>")
.on('click', apply);
function draw(tags) {
selection.append('button')
.attr('class', 'delete wide action')
.html("<span class='icon icon-pre-text delete'></span><span class='label'>Delete</span>")
.on('click', function(entity) { event.remove(entity); });
}
function emptyTag(d) {
return d.key === '';
}
function drawTags(tags) {
tags = d3.entries(tags);
function pushMore() {
if (d3.event.keyCode === 9) {
draw(inspector.tags());
}
}
if (!tags.length) {
tags = [{key: '', value: ''}];
}
function bindTypeahead() {
var row = d3.select(this),
key = row.selectAll('.key'),
value = row.selectAll('.value');
var li = tagList.selectAll('li')
.data(tags, function(d) { return d.key; });
key.call(d3.typeahead()
.data(function(_, callback) {
taginfo.keys({query: key.property('value')}, function(err, data) {
callback(data.data.map(function (d) {
return {value: d.key};
}));
});
}));
li.exit().remove();
value.call(d3.typeahead()
.data(function(_, callback) {
taginfo.values({key: key.property('value'), query: value.property('value')}, function(err, data) {
callback(data.data.map(function (d) {
return {value: d.value, title: d.description};
}));
});
}));
}
var row = li.enter().append('li')
.attr('class', 'tag-row');
tags = d3.entries(tags);
tags.push({ key: '', value: ''});
var inputs = row.append('div')
.attr('class', 'input-wrap');
var li = inspectorwrap.selectAll('li')
.data(tags, function(d) { return d.key; });
inputs.append('input')
.property('type', 'text')
.attr('class', 'key')
.property('value', function(d) { return d.key; })
.on('change', function(d) { d.key = this.value; });
li.exit().remove();
inputs.append('input')
.property('type', 'text')
.attr('class', 'value')
.property('value', function(d) { return d.value; })
.on('change', function(d) { d.value = this.value; })
.on('keydown.push-more', pushMore);
var row = li.enter().append('li').attr('class','tag-row');
var inputs = row.append('div').attr('class','input-wrap');
inputs.each(bindTypeahead);
li.classed('tag-row-empty', emptyTag);
var removeBtn = row.append('button')
.attr('tabindex', -1)
.attr('class','remove minor')
.on('click', removeTag);
inputs.append('input')
.property('type', 'text')
.attr('class', 'key')
.property('value', function(d) { return d.key; });
removeBtn.append('span')
.attr('class', 'icon remove');
inputs.append('input')
.property('type', 'text')
.attr('class', 'value')
.property('value', function(d) { return d.value; })
.on('keydown.push-more', pushMore);
inputs.each(bindTypeahead);
var removeBtn = row.append('button')
.attr('tabindex', -1)
.attr('class','remove minor')
.on('click', removeTag);
removeBtn.append('span').attr('class', 'icon remove');
var helpBtn = row.append('button')
.attr('tabindex', -1)
.attr('class', 'tag-help minor')
.append('a')
.attr('tabindex', -1)
.attr('target', '_blank')
.on('click', function(d) {
taginfo.docs(d, function(err, docs) {
var en = _.find(docs, function(d) {
return d.lang == 'en';
});
if (en) {
var types = [];
if (en.on_area) types.push('area');
if (en.on_node) types.push('point');
if (en.on_way) types.push('line');
en.types = types;
var mod = iD.modal();
mod.select('.content')
.datum(en)
.call(iD.tagReference);
}
});
d3.event.preventDefault();
})
.attr('href', function(d) {
return 'http://taginfo.openstreetmap.org/keys/' + d.key;
var helpBtn = row.append('button')
.attr('tabindex', -1)
.attr('class', 'tag-help minor')
.append('a')
.attr('tabindex', -1)
.attr('target', '_blank')
.on('click', function(d) {
taginfo.docs(d, function(err, docs) {
var en = _.find(docs, function(d) {
return d.lang == 'en';
});
helpBtn.append('span').attr('class', 'icon inspect');
return li;
}
function removeTag(d) {
var tags = inspector.tags();
delete tags[d.key];
draw(tags);
}
function apply(entity) {
event.changeTags(entity, inspector.tags());
event.close(entity);
}
function drawbuttons(selection) {
selection.append('button')
.attr('class', 'apply wide action')
.html("<span class='icon icon-pre-text apply'></span><span class='label'>Apply</span>")
.on('click', apply);
selection.append('button')
.attr('class', 'delete wide action')
.html("<span class='icon icon-pre-text delete'></span><span class='label'>Delete</span>")
.on('click', function(entity) { event.remove(entity); });
}
selection.html("").append('button')
.attr('class', 'narrow close')
.html("<span class='icon close'></span>")
.on('click', function() {
event.close(entity);
if (en) {
var types = [];
if (en.on_area) types.push('area');
if (en.on_node) types.push('point');
if (en.on_way) types.push('line');
en.types = types;
var mod = iD.modal();
mod.select('.content')
.datum(en)
.call(iD.tagReference);
}
});
d3.event.preventDefault();
})
.attr('href', function(d) {
return 'http://taginfo.openstreetmap.org/keys/' + d.key;
});
selection.append('div')
.attr('class', 'head inspector-inner')
.call(drawhead);
helpBtn.append('span')
.attr('class', 'icon inspect');
var inspectorbody = selection.append('div')
.attr('class', 'inspector-body');
return li;
}
inspectorwrap = inspectorbody
.append('ul').attr('class', 'inspector-inner tag-wrap fillL2');
function pushMore() {
if (d3.event.keyCode === 9 && tagList.selectAll('li:last-child input.value').node() === this) {
addTag();
}
}
inspectorwrap.append('h4').text('Edit tags');
function bindTypeahead() {
var row = d3.select(this),
key = row.selectAll('.key'),
value = row.selectAll('.value');
var formsel = draw(entity.tags);
inspectorbody.append('div')
.attr('class', 'inspector-buttons').call(drawbuttons);
var inHeight = inspectorbody.node().offsetHeight;
inspectorbody.style('display', 'none')
.style('margin-top', (-inHeight) + 'px');
var inspectortoggle = selection.append('button')
.attr('class', 'inspector-toggle action')
.on('click', function() {
inspectortoggle.style('display', 'none');
inspectorbody
.style('display', 'block')
.transition()
.style('margin-top', '0px');
key.call(d3.typeahead()
.data(function(_, callback) {
taginfo.keys({query: key.property('value')}, function(err, data) {
callback(data.data.map(function (d) {
return {value: d.key};
}));
});
}));
formsel.select('input').node().focus();
value.call(d3.typeahead()
.data(function(_, callback) {
taginfo.values({key: key.property('value'), query: value.property('value')}, function(err, data) {
callback(data.data.map(function (d) {
return {value: d.value, title: d.description};
}));
});
}));
}
inspectortoggle.append('span')
.text('Details')
.attr('class','label');
});
function addTag() {
var tags = inspector.tags();
tags[''] = '';
drawTags(tags);
}
function removeTag(d) {
var tags = inspector.tags();
delete tags[d.key];
drawTags(tags);
}
function apply(entity) {
event.changeTags(entity, inspector.tags());
event.close(entity);
}
inspector.tags = function () {
var tags = {};
inspectorwrap.selectAll('li').each(function() {
tagList.selectAll('li').each(function() {
var row = d3.select(this),
key = row.selectAll('.key').property('value'),
value = row.selectAll('.value').property('value');

View File

@@ -12,6 +12,10 @@ iD.layerswitcher = function(map) {
name: 'OSM',
source: iD.BackgroundSource.OSM,
description: 'The default OpenStreetMap layer.'
}, {
name: 'MapBox',
source: iD.BackgroundSource.MapBox,
description: 'Satellite and Aerial Imagery'
}, {
name: 'Custom',
source: iD.BackgroundSource.Custom,
@@ -121,6 +125,36 @@ iD.layerswitcher = function(map) {
.insert('span')
.attr('class','icon toggle');
var adjustments = content
.append('div')
.attr('class', 'adjustments');
var directions = [
['←', [-1, 0]],
['↑', [0, -1]],
['→', [1, 0]],
['↓', [0, 1]]];
function nudge(d) {
map.background.nudge(d[1]);
map.redraw();
}
adjustments.selectAll('button')
.data(directions).enter()
.append('button')
.attr('class', 'nudge')
.text(function(d) { return d[0]; })
.on('click', nudge);
adjustments.append('button')
.text('reset')
.attr('class', 'reset')
.on('click', function() {
map.background.offset([0, 0]);
map.redraw();
});
selection.call(clickoutside);
selectLayer(map.background.source());

View File

@@ -3,7 +3,7 @@ iD.notice = function(selection) {
notice = {};
notice.message = function(_) {
selection.attr('class','inner');
selection.attr('class', 'notice inner');
if (!arguments.length) return _;
if (!message && _) {
selection

43
js/id/ui/success.js Normal file
View File

@@ -0,0 +1,43 @@
iD.success = function() {
var event = d3.dispatch('cancel', 'save');
function success(selection) {
var changeset = selection.datum(),
header = selection.append('div').attr('class', 'header modal-section'),
body = selection.append('div').attr('class', 'body');
var section = body.append('div').attr('class','modal-section');
header.append('h2').text('You Just Edited OpenStreetMap!');
header.append('p').text('You just improved the world\'s best free map');
var m = '';
if (changeset.comment) {
m = '"' + changeset.comment.substring(0, 20) + '" ';
}
var message = 'Edited OpenStreetMap! ' + m +
'http://osm.org/browse/changeset/' + changeset.id;
section.append('a')
.attr('href', function(d) {
return 'https://twitter.com/intent/tweet?source=webclient&text=' +
encodeURIComponent(message);
})
.text('Tweet: ' + message);
var buttonwrap = section.append('div')
.attr('class', 'buttons');
var okbutton = buttonwrap.append('button')
.attr('class', 'action wide')
.on('click.save', function() {
event.cancel();
});
okbutton.append('span').attr('class','icon apply icon-pre-text');
okbutton.append('span').attr('class','label').text('OK');
}
return d3.rebind(success, event, 'on');
};

View File

@@ -6,28 +6,6 @@ iD.util.trueObj = function(arr) {
return o;
};
iD.util.friendlyName = function(entity) {
// Generate a string such as 'river' or 'Fred's House' for an entity.
if (!entity.tags || !Object.keys(entity.tags).length) { return ''; }
var mainkeys = ['highway','amenity','railway','waterway','natural'],
n = [];
if (entity.tags.name) n.push(entity.tags.name);
if (entity.tags.ref) n.push(entity.tags.ref);
if (!n.length) {
for (var k in entity.tags) {
if (mainkeys.indexOf(k) !== -1) {
n.push(entity.tags[k]);
break;
}
}
}
return n.length === 0 ? 'unknown' : n.join('; ');
};
iD.util.codeWindow = function(content) {
top.win = window.open('','contentWindow',
'width=350,height=350,menubar=0' +

7
js/lib/d3.trigger.js Normal file
View File

@@ -0,0 +1,7 @@
d3.selection.prototype.trigger = function (type) {
this.each(function() {
var evt = document.createEvent('HTMLEvents');
evt.initEvent(type, true, true);
this.dispatchEvent(evt);
});
};

View File

@@ -14,35 +14,47 @@ d3.typeahead = function() {
top: rect.bottom + 'px'
});
selection
.on('keyup.typeahead', update);
.on('keyup.typeahead', key);
hidden = false;
}
function hide() {
window.setTimeout(function() {
container.remove();
idx = 0;
hidden = true;
}, 500);
container.remove();
idx = 0;
hidden = true;
}
function slowHide() {
window.setTimeout(hide, 150);
}
selection
.on('focus.typeahead', setup)
.on('blur.typeahead', hide);
.on('blur.typeahead', slowHide);
function update() {
if (hidden) setup();
if (d3.event.keyCode === 40) idx++;
if (d3.event.keyCode === 38) idx--;
if (d3.event.keyCode === 13) {
selection.property('value', container.select('a.selected').datum().value);
hide();
}
function key() {
if (d3.event.keyCode === 40) {
idx++;
return highlight();
} else if (d3.event.keyCode === 38) {
idx--;
return highlight();
} else if (d3.event.keyCode === 13) {
select(container.select('a.selected').datum());
hide();
} else {
update();
}
}
function highlight() {
container
.selectAll('a')
.classed('selected', function(d, i) { return i == idx; });
}
function update() {
if (hidden) setup();
data(selection, function(data) {
container.style('display', function() {
@@ -57,11 +69,21 @@ d3.typeahead = function() {
.append('a')
.text(function(d) { return d.value; })
.attr('title', function(d) { return d.title; })
.on('click', function(d) { selection.property('value', d.value); });
.on('click', select);
options.exit().remove();
options
.classed('selected', function(d, i) { return i == idx; });
});
}
function select(d) {
selection
.property('value', d.value)
.trigger('change');
}
};
typeahead.data = function(_) {

View File

@@ -22,6 +22,7 @@
<script src='../js/lib/d3.geo.tile.js'></script>
<script src='../js/lib/d3.keybinding.js'></script>
<script src='../js/lib/d3.size.js'></script>
<script src='../js/lib/d3.trigger.js'></script>
<script src='../js/lib/d3.typeahead.js'></script>
<script src='../js/lib/d3.one.js'></script>
<script src='../js/lib/ohauth.js'></script>

View File

@@ -130,4 +130,18 @@ describe('iD.Entity', function () {
expect(iD.Entity({tags: {'tiger:source': 'blah', 'tiger:foo': 'bar'}}).hasInterestingTags()).to.equal(false);
});
});
describe("#friendlyName", function () {
it("returns the name", function () {
expect(iD.Entity({ tags: { name: 'hi' }}).friendlyName()).to.equal('hi');
});
it("returns a highway tag value", function () {
expect(iD.Entity({ tags: { highway: 'Route 5' }}).friendlyName()).to.equal('Route 5');
});
it("prefers the name to a highway tag value", function () {
expect(iD.Entity({ tags: { name: 'hi', highway: 'Route 5' }}).friendlyName()).to.equal('hi');
});
});
});

View File

@@ -36,4 +36,14 @@ describe('iD.Node', function () {
expect(iD.Node({loc: [0, 0]}).intersects([[100, 90], [180, -90]])).to.equal(false);
});
});
describe("#geometry", function () {
it("returns 'vertex' if the node is not a point", function () {
expect(iD.Node().geometry()).to.equal('vertex');
});
it("returns 'point' if the node is a point", function () {
expect(iD.Node({_poi: true}).geometry()).to.equal('point');
});
});
});

View File

@@ -110,4 +110,14 @@ describe('iD.Way', function() {
expect(iD.Way({tags: { highway: 'residential' }, nodes: ['n1', 'n1']}).isArea()).to.equal(false);
});
});
describe("#geometry", function() {
it("returns 'line' when the way is not an area", function () {
expect(iD.Way().geometry()).to.equal('line');
});
it("returns 'area' when the way is an area", function () {
expect(iD.Way({tags: { area: 'yes' }}).geometry()).to.equal('area');
});
});
});

View File

@@ -8,6 +8,8 @@ describe("iD.modes.AddPoint", function () {
controller = iD.Controller(map, history);
container.call(map);
container.append('div')
.attr('class', 'inspector-wrap');
mode = iD.modes.AddPoint();
controller.enter(mode);

View File

@@ -26,7 +26,7 @@ describe("iD.taginfo", function() {
server.respond();
expect(query(server.requests[0].url)).to.eql(
{query: "amen", page: "1", rp: "20", sortname: "count_all", sortorder: "desc"});
{query: "amen", page: "1", rp: "6", sortname: "count_all", sortorder: "desc"});
expect(callback).to.have.been.calledWith(null,
{"data":[{"count_all":5190337,"key":"amenity"}]});
});

View File

@@ -1,15 +1,20 @@
describe("iD.Inspector", function () {
var inspector, element,
tags = {highway: 'residential'},
entity = iD.Entity({type: 'node', id: "n12345", tags: tags});
entity;
beforeEach(function () {
function render() {
inspector = iD.Inspector();
element = d3.select('body')
.append('div')
.attr('id', 'inspector-wrap')
.datum(entity)
.call(inspector);
}
beforeEach(function () {
entity = iD.Entity({type: 'node', id: "n12345", tags: tags});
render();
});
afterEach(function () {
@@ -22,9 +27,9 @@ describe("iD.Inspector", function () {
});
it("returns updated tags when input values have changed", function () {
element.selectAll(".tag-row-empty input.key").property('value', 'k');
element.selectAll(".tag-row-empty input.value").property('value', 'v');
expect(inspector.tags()).to.eql({highway: 'residential', k: 'v'});
element.selectAll("input.key").property('value', 'k');
element.selectAll("input.value").property('value', 'v');
expect(inspector.tags()).to.eql({k: 'v'});
});
});
@@ -33,17 +38,22 @@ describe("iD.Inspector", function () {
expect(element.selectAll("input[value=residential]")).not.to.be.empty;
});
it("creates one trailing pair of empty input elements", function () {
it("creates a pair of empty input elements if the entity has no tags", function () {
element.remove();
entity = entity.update({tags: {}});
render();
expect(element.selectAll("input.value").property('value')).to.be.empty;
expect(element.selectAll("input.key").property('value')).to.be.empty;
});
it("adds tags when clicking the add button", function () {
element.selectAll("button.add-tag").trigger('click');
expect(element.selectAll("input")[0][2].value).to.be.empty;
expect(element.selectAll("input")[0][3].value).to.be.empty;
});
it("sets the 'tag-row-empty' class on the placeholder row", function () {
expect(element.selectAll(".tag-row:last-child").classed('tag-row-empty')).to.be.true;
});
it("removes tags when clicking the remove button", function () {
happen.click(element.selectAll("button.remove").node());
element.selectAll("button.remove").trigger('click');
expect(inspector.tags()).to.eql({});
});
@@ -51,7 +61,7 @@ describe("iD.Inspector", function () {
var spy = sinon.spy();
inspector.on('close', spy);
happen.click(element.select('.close').node());
element.select('.close').trigger('click');
expect(spy).to.have.been.calledWith(entity);
});
@@ -60,7 +70,7 @@ describe("iD.Inspector", function () {
var spy = sinon.spy();
inspector.on('changeTags', spy);
happen.click(element.select('.apply').node());
element.select('.apply').trigger('click');
expect(spy).to.have.been.calledWith(entity, tags);
});
@@ -69,7 +79,7 @@ describe("iD.Inspector", function () {
var spy = sinon.spy();
inspector.on('remove', spy);
happen.click(element.select('.delete').node());
element.select('.delete').trigger('click');
expect(spy).to.have.been.calledWith(entity);
});

View File

@@ -24,12 +24,6 @@ describe('Util', function() {
expect(iD.util.qsString({})).to.eql('');
});
it('#friendlyName', function() {
expect(iD.util.friendlyName({ tags: { name: 'hi' }})).to.equal('hi');
expect(iD.util.friendlyName({ tags: { highway: 'Route 5' }})).to.equal('Route 5');
expect(iD.util.friendlyName({ tags: { name: 'hi', highway: 'Route 5' }})).to.equal('hi');
});
describe('geo', function() {
describe('#roundCoords', function() {
expect(iD.util.geo.roundCoords([0.1, 1])).to.eql([0, 1]);