mirror of
https://github.com/FoggedLens/iD.git
synced 2026-03-19 09:33:32 +00:00
6005 lines
201 KiB
JavaScript
6005 lines
201 KiB
JavaScript
(function (global, factory) {
|
|
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
|
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
|
(factory((global.iD = global.iD || {}, global.iD.ui = global.iD.ui || {})));
|
|
}(this, function (exports) { 'use strict';
|
|
|
|
function Account(context) {
|
|
var connection = context.connection();
|
|
|
|
function update(selection) {
|
|
if (!connection.authenticated()) {
|
|
selection.selectAll('#userLink, #logoutLink')
|
|
.classed('hide', true);
|
|
return;
|
|
}
|
|
|
|
connection.userDetails(function(err, details) {
|
|
var userLink = selection.select('#userLink'),
|
|
logoutLink = selection.select('#logoutLink');
|
|
|
|
userLink.html('');
|
|
logoutLink.html('');
|
|
|
|
if (err) return;
|
|
|
|
selection.selectAll('#userLink, #logoutLink')
|
|
.classed('hide', false);
|
|
|
|
// Link
|
|
userLink.append('a')
|
|
.attr('href', connection.userURL(details.display_name))
|
|
.attr('target', '_blank');
|
|
|
|
// Add thumbnail or dont
|
|
if (details.image_url) {
|
|
userLink.append('img')
|
|
.attr('class', 'icon pre-text user-icon')
|
|
.attr('src', details.image_url);
|
|
} else {
|
|
userLink
|
|
.call(iD.svg.Icon('#icon-avatar', 'pre-text light'));
|
|
}
|
|
|
|
// Add user name
|
|
userLink.append('span')
|
|
.attr('class', 'label')
|
|
.text(details.display_name);
|
|
|
|
logoutLink.append('a')
|
|
.attr('class', 'logout')
|
|
.attr('href', '#')
|
|
.text(t('logout'))
|
|
.on('click.logout', function() {
|
|
d3.event.preventDefault();
|
|
connection.logout();
|
|
});
|
|
});
|
|
}
|
|
|
|
return function(selection) {
|
|
selection.append('li')
|
|
.attr('id', 'logoutLink')
|
|
.classed('hide', true);
|
|
|
|
selection.append('li')
|
|
.attr('id', 'userLink')
|
|
.classed('hide', true);
|
|
|
|
connection.on('auth.account', function() { update(selection); });
|
|
update(selection);
|
|
};
|
|
}
|
|
|
|
function Attribution(context) {
|
|
var selection;
|
|
|
|
function attribution(data, klass) {
|
|
var div = selection.selectAll('.' + klass)
|
|
.data([0]);
|
|
|
|
div.enter()
|
|
.append('div')
|
|
.attr('class', klass);
|
|
|
|
var background = div.selectAll('.attribution')
|
|
.data(data, function(d) { return d.name(); });
|
|
|
|
background.enter()
|
|
.append('span')
|
|
.attr('class', 'attribution')
|
|
.each(function(d) {
|
|
if (d.terms_html) {
|
|
d3.select(this)
|
|
.html(d.terms_html);
|
|
return;
|
|
}
|
|
|
|
var source = d.terms_text || d.id || d.name();
|
|
|
|
if (d.logo) {
|
|
source = '<img class="source-image" src="' + context.imagePath(d.logo) + '">';
|
|
}
|
|
|
|
if (d.terms_url) {
|
|
d3.select(this)
|
|
.append('a')
|
|
.attr('href', d.terms_url)
|
|
.attr('target', '_blank')
|
|
.html(source);
|
|
} else {
|
|
d3.select(this)
|
|
.text(source);
|
|
}
|
|
});
|
|
|
|
background.exit()
|
|
.remove();
|
|
|
|
var copyright = background.selectAll('.copyright-notice')
|
|
.data(function(d) {
|
|
var notice = d.copyrightNotices(context.map().zoom(), context.map().extent());
|
|
return notice ? [notice] : [];
|
|
});
|
|
|
|
copyright.enter()
|
|
.append('span')
|
|
.attr('class', 'copyright-notice');
|
|
|
|
copyright.text(String);
|
|
|
|
copyright.exit()
|
|
.remove();
|
|
}
|
|
|
|
function update() {
|
|
attribution([context.background().baseLayerSource()], 'base-layer-attribution');
|
|
attribution(context.background().overlayLayerSources().filter(function (s) {
|
|
return s.validZoom(context.map().zoom());
|
|
}), 'overlay-layer-attribution');
|
|
}
|
|
|
|
return function(select) {
|
|
selection = select;
|
|
|
|
context.background()
|
|
.on('change.attribution', update);
|
|
|
|
context.map()
|
|
.on('move.attribution', _.throttle(update, 400, {leading: false}));
|
|
|
|
update();
|
|
};
|
|
}
|
|
|
|
// Translate a MacOS key command into the appropriate Windows/Linux equivalent.
|
|
// For example, ⌘Z -> Ctrl+Z
|
|
function cmd(code) {
|
|
if (iD.detect().os === 'mac') {
|
|
return code;
|
|
}
|
|
|
|
if (iD.detect().os === 'win') {
|
|
if (code === '⌘⇧Z') return 'Ctrl+Y';
|
|
}
|
|
|
|
var result = '',
|
|
replacements = {
|
|
'⌘': 'Ctrl',
|
|
'⇧': 'Shift',
|
|
'⌥': 'Alt',
|
|
'⌫': 'Backspace',
|
|
'⌦': 'Delete'
|
|
};
|
|
|
|
for (var i = 0; i < code.length; i++) {
|
|
if (code[i] in replacements) {
|
|
result += replacements[code[i]] + '+';
|
|
} else {
|
|
result += code[i];
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function MapInMap(context) {
|
|
var key = '/';
|
|
|
|
function map_in_map(selection) {
|
|
var backgroundLayer = iD.TileLayer(context),
|
|
overlayLayers = {},
|
|
projection = iD.geo.RawMercator(),
|
|
gpxLayer = iD.svg.Gpx(projection, context).showLabels(false),
|
|
debugLayer = iD.svg.Debug(projection, context),
|
|
zoom = d3.behavior.zoom()
|
|
.scaleExtent([ztok(0.5), ztok(24)])
|
|
.on('zoom', zoomPan),
|
|
transformed = false,
|
|
panning = false,
|
|
hidden = true,
|
|
zDiff = 6, // by default, minimap renders at (main zoom - 6)
|
|
tStart, tLast, tCurr, kLast, kCurr, tiles, viewport, timeoutId;
|
|
|
|
function ztok(z) { return 256 * Math.pow(2, z); }
|
|
function ktoz(k) { return Math.log(k) / Math.LN2 - 8; }
|
|
|
|
|
|
function startMouse() {
|
|
context.surface().on('mouseup.map-in-map-outside', endMouse);
|
|
context.container().on('mouseup.map-in-map-outside', endMouse);
|
|
|
|
tStart = tLast = tCurr = projection.translate();
|
|
panning = true;
|
|
}
|
|
|
|
|
|
function zoomPan() {
|
|
var e = d3.event.sourceEvent,
|
|
t = d3.event.translate,
|
|
k = d3.event.scale,
|
|
zMain = ktoz(context.projection.scale() * 2 * Math.PI),
|
|
zMini = ktoz(k);
|
|
|
|
// restrict minimap zoom to < (main zoom - 3)
|
|
if (zMini > zMain - 3) {
|
|
zMini = zMain - 3;
|
|
zoom.scale(kCurr).translate(tCurr); // restore last good values
|
|
return;
|
|
}
|
|
|
|
tCurr = t;
|
|
kCurr = k;
|
|
zDiff = zMain - zMini;
|
|
|
|
var scale = kCurr / kLast,
|
|
tX = (tCurr[0] / scale - tLast[0]) * scale,
|
|
tY = (tCurr[1] / scale - tLast[1]) * scale;
|
|
|
|
iD.util.setTransform(tiles, tX, tY, scale);
|
|
iD.util.setTransform(viewport, 0, 0, scale);
|
|
transformed = true;
|
|
|
|
queueRedraw();
|
|
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
|
|
|
|
function endMouse() {
|
|
context.surface().on('mouseup.map-in-map-outside', null);
|
|
context.container().on('mouseup.map-in-map-outside', null);
|
|
|
|
updateProjection();
|
|
panning = false;
|
|
|
|
if (tCurr[0] !== tStart[0] && tCurr[1] !== tStart[1]) {
|
|
var dMini = wrap.dimensions(),
|
|
cMini = [ dMini[0] / 2, dMini[1] / 2 ];
|
|
|
|
context.map().center(projection.invert(cMini));
|
|
}
|
|
}
|
|
|
|
|
|
function updateProjection() {
|
|
var loc = context.map().center(),
|
|
dMini = wrap.dimensions(),
|
|
cMini = [ dMini[0] / 2, dMini[1] / 2 ],
|
|
tMain = context.projection.translate(),
|
|
kMain = context.projection.scale(),
|
|
zMain = ktoz(kMain * 2 * Math.PI),
|
|
zMini = Math.max(zMain - zDiff, 0.5),
|
|
kMini = ztok(zMini);
|
|
|
|
projection
|
|
.translate(tMain)
|
|
.scale(kMini / (2 * Math.PI));
|
|
|
|
var s = projection(loc),
|
|
mouse = panning ? [ tCurr[0] - tStart[0], tCurr[1] - tStart[1] ] : [0, 0],
|
|
tMini = [
|
|
cMini[0] - s[0] + tMain[0] + mouse[0],
|
|
cMini[1] - s[1] + tMain[1] + mouse[1]
|
|
];
|
|
|
|
projection
|
|
.translate(tMini)
|
|
.clipExtent([[0, 0], dMini]);
|
|
|
|
zoom
|
|
.center(cMini)
|
|
.translate(tMini)
|
|
.scale(kMini);
|
|
|
|
tLast = tCurr = tMini;
|
|
kLast = kCurr = kMini;
|
|
|
|
if (transformed) {
|
|
iD.util.setTransform(tiles, 0, 0);
|
|
iD.util.setTransform(viewport, 0, 0);
|
|
transformed = false;
|
|
}
|
|
}
|
|
|
|
|
|
function redraw() {
|
|
if (hidden) return;
|
|
|
|
updateProjection();
|
|
|
|
var dMini = wrap.dimensions(),
|
|
zMini = ktoz(projection.scale() * 2 * Math.PI);
|
|
|
|
// setup tile container
|
|
tiles = wrap
|
|
.selectAll('.map-in-map-tiles')
|
|
.data([0]);
|
|
|
|
tiles
|
|
.enter()
|
|
.append('div')
|
|
.attr('class', 'map-in-map-tiles');
|
|
|
|
// redraw background
|
|
backgroundLayer
|
|
.source(context.background().baseLayerSource())
|
|
.projection(projection)
|
|
.dimensions(dMini);
|
|
|
|
var background = tiles
|
|
.selectAll('.map-in-map-background')
|
|
.data([0]);
|
|
|
|
background.enter()
|
|
.append('div')
|
|
.attr('class', 'map-in-map-background');
|
|
|
|
background
|
|
.call(backgroundLayer);
|
|
|
|
|
|
// redraw overlay
|
|
var overlaySources = context.background().overlayLayerSources();
|
|
var activeOverlayLayers = [];
|
|
for (var i = 0; i < overlaySources.length; i++) {
|
|
if (overlaySources[i].validZoom(zMini)) {
|
|
if (!overlayLayers[i]) overlayLayers[i] = iD.TileLayer(context);
|
|
activeOverlayLayers.push(overlayLayers[i]
|
|
.source(overlaySources[i])
|
|
.projection(projection)
|
|
.dimensions(dMini));
|
|
}
|
|
}
|
|
|
|
var overlay = tiles
|
|
.selectAll('.map-in-map-overlay')
|
|
.data([0]);
|
|
|
|
overlay.enter()
|
|
.append('div')
|
|
.attr('class', 'map-in-map-overlay');
|
|
|
|
var overlays = overlay
|
|
.selectAll('div')
|
|
.data(activeOverlayLayers, function(d) { return d.source().name(); });
|
|
|
|
overlays.enter().append('div');
|
|
overlays.each(function(layer) {
|
|
d3.select(this).call(layer);
|
|
});
|
|
|
|
overlays.exit()
|
|
.remove();
|
|
|
|
|
|
var dataLayers = tiles
|
|
.selectAll('.map-in-map-data')
|
|
.data([0]);
|
|
|
|
dataLayers.enter()
|
|
.append('svg')
|
|
.attr('class', 'map-in-map-data');
|
|
|
|
dataLayers.exit()
|
|
.remove();
|
|
|
|
dataLayers
|
|
.call(gpxLayer)
|
|
.call(debugLayer);
|
|
|
|
|
|
// redraw viewport bounding box
|
|
if (!panning) {
|
|
var getPath = d3.geo.path().projection(projection),
|
|
bbox = { type: 'Polygon', coordinates: [context.map().extent().polygon()] };
|
|
|
|
viewport = wrap.selectAll('.map-in-map-viewport')
|
|
.data([0]);
|
|
|
|
viewport.enter()
|
|
.append('svg')
|
|
.attr('class', 'map-in-map-viewport');
|
|
|
|
var path = viewport.selectAll('.map-in-map-bbox')
|
|
.data([bbox]);
|
|
|
|
path.enter()
|
|
.append('path')
|
|
.attr('class', 'map-in-map-bbox');
|
|
|
|
path
|
|
.attr('d', getPath)
|
|
.classed('thick', function(d) { return getPath.area(d) < 30; });
|
|
}
|
|
}
|
|
|
|
|
|
function queueRedraw() {
|
|
clearTimeout(timeoutId);
|
|
timeoutId = setTimeout(function() { redraw(); }, 300);
|
|
}
|
|
|
|
|
|
function toggle() {
|
|
if (d3.event) d3.event.preventDefault();
|
|
|
|
hidden = !hidden;
|
|
|
|
var label = d3.select('.minimap-toggle');
|
|
label.classed('active', !hidden)
|
|
.select('input').property('checked', !hidden);
|
|
|
|
if (hidden) {
|
|
wrap
|
|
.style('display', 'block')
|
|
.style('opacity', 1)
|
|
.transition()
|
|
.duration(200)
|
|
.style('opacity', 0)
|
|
.each('end', function() {
|
|
d3.select(this).style('display', 'none');
|
|
});
|
|
} else {
|
|
wrap
|
|
.style('display', 'block')
|
|
.style('opacity', 0)
|
|
.transition()
|
|
.duration(200)
|
|
.style('opacity', 1);
|
|
|
|
redraw();
|
|
}
|
|
}
|
|
|
|
MapInMap.toggle = toggle;
|
|
|
|
var wrap = selection.selectAll('.map-in-map')
|
|
.data([0]);
|
|
|
|
wrap.enter()
|
|
.append('div')
|
|
.attr('class', 'map-in-map')
|
|
.style('display', (hidden ? 'none' : 'block'))
|
|
.on('mousedown.map-in-map', startMouse)
|
|
.on('mouseup.map-in-map', endMouse)
|
|
.call(zoom)
|
|
.on('dblclick.zoom', null);
|
|
|
|
context.map()
|
|
.on('drawn.map-in-map', function(drawn) {
|
|
if (drawn.full === true) redraw();
|
|
});
|
|
|
|
redraw();
|
|
|
|
var keybinding = d3.keybinding('map-in-map')
|
|
.on(key, toggle);
|
|
|
|
d3.select(document)
|
|
.call(keybinding);
|
|
}
|
|
|
|
return map_in_map;
|
|
}
|
|
|
|
function Background(context) {
|
|
var key = 'B',
|
|
opacities = [1, 0.75, 0.5, 0.25],
|
|
directions = [
|
|
['right', [0.5, 0]],
|
|
['top', [0, -0.5]],
|
|
['left', [-0.5, 0]],
|
|
['bottom', [0, 0.5]]],
|
|
opacityDefault = (context.storage('background-opacity') !== null) ?
|
|
(+context.storage('background-opacity')) : 1.0,
|
|
customTemplate = context.storage('background-custom-template') || '',
|
|
previous;
|
|
|
|
// Can be 0 from <1.3.0 use or due to issue #1923.
|
|
if (opacityDefault === 0) opacityDefault = 1.0;
|
|
|
|
|
|
function background(selection) {
|
|
|
|
function sortSources(a, b) {
|
|
return a.best() && !b.best() ? -1
|
|
: b.best() && !a.best() ? 1
|
|
: d3.descending(a.area(), b.area()) || d3.ascending(a.name(), b.name()) || 0;
|
|
}
|
|
|
|
function setOpacity(d) {
|
|
var bg = context.container().selectAll('.layer-background')
|
|
.transition()
|
|
.style('opacity', d)
|
|
.attr('data-opacity', d);
|
|
|
|
if (!iD.detect().opera) {
|
|
iD.util.setTransform(bg, 0, 0);
|
|
}
|
|
|
|
opacityList.selectAll('li')
|
|
.classed('active', function(_) { return _ === d; });
|
|
|
|
context.storage('background-opacity', d);
|
|
}
|
|
|
|
function setTooltips(selection) {
|
|
selection.each(function(d) {
|
|
var item = d3.select(this);
|
|
if (d === previous) {
|
|
item.call(bootstrap.tooltip()
|
|
.html(true)
|
|
.title(function() {
|
|
var tip = '<div>' + t('background.switch') + '</div>';
|
|
return iD.ui.tooltipHtml(tip, iD.ui.cmd('⌘B'));
|
|
})
|
|
.placement('top')
|
|
);
|
|
} else if (d.description) {
|
|
item.call(bootstrap.tooltip()
|
|
.title(d.description)
|
|
.placement('top')
|
|
);
|
|
} else {
|
|
item.call(bootstrap.tooltip().destroy);
|
|
}
|
|
});
|
|
}
|
|
|
|
function selectLayer() {
|
|
function active(d) {
|
|
return context.background().showsLayer(d);
|
|
}
|
|
|
|
content.selectAll('.layer, .custom_layer')
|
|
.classed('active', active)
|
|
.classed('switch', function(d) { return d === previous; })
|
|
.call(setTooltips)
|
|
.selectAll('input')
|
|
.property('checked', active);
|
|
}
|
|
|
|
function clickSetSource(d) {
|
|
previous = context.background().baseLayerSource();
|
|
d3.event.preventDefault();
|
|
context.background().baseLayerSource(d);
|
|
selectLayer();
|
|
document.activeElement.blur();
|
|
}
|
|
|
|
function editCustom() {
|
|
d3.event.preventDefault();
|
|
var template = window.prompt(t('background.custom_prompt'), customTemplate);
|
|
if (!template ||
|
|
template.indexOf('google.com') !== -1 ||
|
|
template.indexOf('googleapis.com') !== -1 ||
|
|
template.indexOf('google.ru') !== -1) {
|
|
selectLayer();
|
|
return;
|
|
}
|
|
setCustom(template);
|
|
}
|
|
|
|
function setCustom(template) {
|
|
context.background().baseLayerSource(iD.BackgroundSource.Custom(template));
|
|
selectLayer();
|
|
context.storage('background-custom-template', template);
|
|
}
|
|
|
|
function clickSetOverlay(d) {
|
|
d3.event.preventDefault();
|
|
context.background().toggleOverlayLayer(d);
|
|
selectLayer();
|
|
document.activeElement.blur();
|
|
}
|
|
|
|
function drawList(layerList, type, change, filter) {
|
|
var sources = context.background()
|
|
.sources(context.map().extent())
|
|
.filter(filter);
|
|
|
|
var layerLinks = layerList.selectAll('li.layer')
|
|
.data(sources, function(d) { return d.name(); });
|
|
|
|
var enter = layerLinks.enter()
|
|
.insert('li', '.custom_layer')
|
|
.attr('class', 'layer')
|
|
.classed('best', function(d) { return d.best(); });
|
|
|
|
enter.filter(function(d) { return d.best(); })
|
|
.append('div')
|
|
.attr('class', 'best')
|
|
.call(bootstrap.tooltip()
|
|
.title(t('background.best_imagery'))
|
|
.placement('left'))
|
|
.append('span')
|
|
.html('★');
|
|
|
|
var label = enter.append('label');
|
|
|
|
label.append('input')
|
|
.attr('type', type)
|
|
.attr('name', 'layers')
|
|
.on('change', change);
|
|
|
|
label.append('span')
|
|
.text(function(d) { return d.name(); });
|
|
|
|
|
|
layerLinks.exit()
|
|
.remove();
|
|
|
|
layerList.selectAll('li.layer')
|
|
.sort(sortSources)
|
|
.style('display', layerList.selectAll('li.layer').data().length > 0 ? 'block' : 'none');
|
|
}
|
|
|
|
function update() {
|
|
backgroundList.call(drawList, 'radio', clickSetSource, function(d) { return !d.overlay; });
|
|
overlayList.call(drawList, 'checkbox', clickSetOverlay, function(d) { return d.overlay; });
|
|
|
|
selectLayer();
|
|
|
|
var source = context.background().baseLayerSource();
|
|
if (source.id === 'custom') {
|
|
customTemplate = source.template;
|
|
}
|
|
|
|
updateOffsetVal();
|
|
}
|
|
|
|
function updateOffsetVal() {
|
|
var meters = iD.geo.offsetToMeters(context.background().offset()),
|
|
x = +meters[0].toFixed(2),
|
|
y = +meters[1].toFixed(2);
|
|
|
|
d3.selectAll('.nudge-inner-rect')
|
|
.select('input')
|
|
.classed('error', false)
|
|
.property('value', x + ', ' + y);
|
|
|
|
d3.selectAll('.nudge-reset')
|
|
.classed('disabled', function() {
|
|
return (x === 0 && y === 0);
|
|
});
|
|
}
|
|
|
|
function resetOffset() {
|
|
context.background().offset([0, 0]);
|
|
updateOffsetVal();
|
|
}
|
|
|
|
function nudge(d) {
|
|
context.background().nudge(d, context.map().zoom());
|
|
updateOffsetVal();
|
|
}
|
|
|
|
function buttonOffset(d) {
|
|
var timeout = window.setTimeout(function() {
|
|
interval = window.setInterval(nudge.bind(null, d), 100);
|
|
}, 500),
|
|
interval;
|
|
|
|
d3.select(window).on('mouseup', function() {
|
|
window.clearInterval(interval);
|
|
window.clearTimeout(timeout);
|
|
d3.select(window).on('mouseup', null);
|
|
});
|
|
|
|
nudge(d);
|
|
}
|
|
|
|
function inputOffset() {
|
|
var input = d3.select(this);
|
|
var d = input.node().value;
|
|
|
|
if (d === '') return resetOffset();
|
|
|
|
d = d.replace(/;/g, ',').split(',').map(function(n) {
|
|
// if n is NaN, it will always get mapped to false.
|
|
return !isNaN(n) && n;
|
|
});
|
|
|
|
if (d.length !== 2 || !d[0] || !d[1]) {
|
|
input.classed('error', true);
|
|
return;
|
|
}
|
|
|
|
context.background().offset(iD.geo.metersToOffset(d));
|
|
updateOffsetVal();
|
|
}
|
|
|
|
function dragOffset() {
|
|
var origin = [d3.event.clientX, d3.event.clientY];
|
|
|
|
context.container()
|
|
.append('div')
|
|
.attr('class', 'nudge-surface');
|
|
|
|
d3.select(window)
|
|
.on('mousemove.offset', function() {
|
|
var latest = [d3.event.clientX, d3.event.clientY];
|
|
var d = [
|
|
-(origin[0] - latest[0]) / 4,
|
|
-(origin[1] - latest[1]) / 4
|
|
];
|
|
|
|
origin = latest;
|
|
nudge(d);
|
|
})
|
|
.on('mouseup.offset', function() {
|
|
d3.selectAll('.nudge-surface')
|
|
.remove();
|
|
|
|
d3.select(window)
|
|
.on('mousemove.offset', null)
|
|
.on('mouseup.offset', null);
|
|
});
|
|
|
|
d3.event.preventDefault();
|
|
}
|
|
|
|
function hide() {
|
|
setVisible(false);
|
|
}
|
|
|
|
function toggle() {
|
|
if (d3.event) d3.event.preventDefault();
|
|
tooltip.hide(button);
|
|
setVisible(!button.classed('active'));
|
|
}
|
|
|
|
function quickSwitch() {
|
|
if (previous) {
|
|
clickSetSource(previous);
|
|
}
|
|
}
|
|
|
|
function setVisible(show) {
|
|
if (show !== shown) {
|
|
button.classed('active', show);
|
|
shown = show;
|
|
|
|
if (show) {
|
|
selection.on('mousedown.background-inside', function() {
|
|
return d3.event.stopPropagation();
|
|
});
|
|
content.style('display', 'block')
|
|
.style('right', '-300px')
|
|
.transition()
|
|
.duration(200)
|
|
.style('right', '0px');
|
|
} else {
|
|
content.style('display', 'block')
|
|
.style('right', '0px')
|
|
.transition()
|
|
.duration(200)
|
|
.style('right', '-300px')
|
|
.each('end', function() {
|
|
d3.select(this).style('display', 'none');
|
|
});
|
|
selection.on('mousedown.background-inside', null);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
var content = selection.append('div')
|
|
.attr('class', 'fillL map-overlay col3 content hide'),
|
|
tooltip = bootstrap.tooltip()
|
|
.placement('left')
|
|
.html(true)
|
|
.title(iD.ui.tooltipHtml(t('background.description'), key)),
|
|
button = selection.append('button')
|
|
.attr('tabindex', -1)
|
|
.on('click', toggle)
|
|
.call(iD.svg.Icon('#icon-layers', 'light'))
|
|
.call(tooltip),
|
|
shown = false;
|
|
|
|
|
|
/* opacity switcher */
|
|
|
|
var opa = content.append('div')
|
|
.attr('class', 'opacity-options-wrapper');
|
|
|
|
opa.append('h4')
|
|
.text(t('background.title'));
|
|
|
|
var opacityList = opa.append('ul')
|
|
.attr('class', 'opacity-options');
|
|
|
|
opacityList.selectAll('div.opacity')
|
|
.data(opacities)
|
|
.enter()
|
|
.append('li')
|
|
.attr('data-original-title', function(d) {
|
|
return t('background.percent_brightness', { opacity: (d * 100) });
|
|
})
|
|
.on('click.set-opacity', setOpacity)
|
|
.html('<div class="select-box"></div>')
|
|
.call(bootstrap.tooltip()
|
|
.placement('left'))
|
|
.append('div')
|
|
.attr('class', 'opacity')
|
|
.style('opacity', function(d) { return 1.25 - d; });
|
|
|
|
|
|
/* background switcher */
|
|
|
|
var backgroundList = content.append('ul')
|
|
.attr('class', 'layer-list');
|
|
|
|
var custom = backgroundList.append('li')
|
|
.attr('class', 'custom_layer')
|
|
.datum(iD.BackgroundSource.Custom());
|
|
|
|
custom.append('button')
|
|
.attr('class', 'layer-browse')
|
|
.call(bootstrap.tooltip()
|
|
.title(t('background.custom_button'))
|
|
.placement('left'))
|
|
.on('click', editCustom)
|
|
.call(iD.svg.Icon('#icon-search'));
|
|
|
|
var label = custom.append('label');
|
|
|
|
label.append('input')
|
|
.attr('type', 'radio')
|
|
.attr('name', 'layers')
|
|
.on('change', function () {
|
|
if (customTemplate) {
|
|
setCustom(customTemplate);
|
|
} else {
|
|
editCustom();
|
|
}
|
|
});
|
|
|
|
label.append('span')
|
|
.text(t('background.custom'));
|
|
|
|
content.append('div')
|
|
.attr('class', 'imagery-faq')
|
|
.append('a')
|
|
.attr('target', '_blank')
|
|
.attr('tabindex', -1)
|
|
.call(iD.svg.Icon('#icon-out-link', 'inline'))
|
|
.attr('href', 'https://github.com/openstreetmap/iD/blob/master/FAQ.md#how-can-i-report-an-issue-with-background-imagery')
|
|
.append('span')
|
|
.text(t('background.imagery_source_faq'));
|
|
|
|
var overlayList = content.append('ul')
|
|
.attr('class', 'layer-list');
|
|
|
|
var controls = content.append('div')
|
|
.attr('class', 'controls-list');
|
|
|
|
|
|
/* minimap toggle */
|
|
|
|
var minimapLabel = controls
|
|
.append('label')
|
|
.call(bootstrap.tooltip()
|
|
.html(true)
|
|
.title(iD.ui.tooltipHtml(t('background.minimap.tooltip'), '/'))
|
|
.placement('top')
|
|
);
|
|
|
|
minimapLabel.classed('minimap-toggle', true)
|
|
.append('input')
|
|
.attr('type', 'checkbox')
|
|
.on('change', function() {
|
|
MapInMap.toggle();
|
|
d3.event.preventDefault();
|
|
});
|
|
|
|
minimapLabel.append('span')
|
|
.text(t('background.minimap.description'));
|
|
|
|
|
|
/* imagery offset controls */
|
|
|
|
var adjustments = content.append('div')
|
|
.attr('class', 'adjustments');
|
|
|
|
adjustments.append('a')
|
|
.text(t('background.fix_misalignment'))
|
|
.attr('href', '#')
|
|
.classed('hide-toggle', true)
|
|
.classed('expanded', false)
|
|
.on('click', function() {
|
|
var exp = d3.select(this).classed('expanded');
|
|
nudgeContainer.style('display', exp ? 'none' : 'block');
|
|
d3.select(this).classed('expanded', !exp);
|
|
d3.event.preventDefault();
|
|
});
|
|
|
|
var nudgeContainer = adjustments.append('div')
|
|
.attr('class', 'nudge-container cf')
|
|
.style('display', 'none');
|
|
|
|
nudgeContainer.append('div')
|
|
.attr('class', 'nudge-instructions')
|
|
.text(t('background.offset'));
|
|
|
|
var nudgeRect = nudgeContainer.append('div')
|
|
.attr('class', 'nudge-outer-rect')
|
|
.on('mousedown', dragOffset);
|
|
|
|
nudgeRect.append('div')
|
|
.attr('class', 'nudge-inner-rect')
|
|
.append('input')
|
|
.on('change', inputOffset)
|
|
.on('mousedown', function() {
|
|
d3.event.stopPropagation();
|
|
});
|
|
|
|
nudgeContainer.append('div')
|
|
.selectAll('button')
|
|
.data(directions).enter()
|
|
.append('button')
|
|
.attr('class', function(d) { return d[0] + ' nudge'; })
|
|
.on('mousedown', function(d) {
|
|
buttonOffset(d[1]);
|
|
});
|
|
|
|
nudgeContainer.append('button')
|
|
.attr('title', t('background.reset'))
|
|
.attr('class', 'nudge-reset disabled')
|
|
.on('click', resetOffset)
|
|
.call(iD.svg.Icon('#icon-undo'));
|
|
|
|
context.map()
|
|
.on('move.background-update', _.debounce(update, 1000));
|
|
|
|
context.background()
|
|
.on('change.background-update', update);
|
|
|
|
|
|
update();
|
|
setOpacity(opacityDefault);
|
|
|
|
var keybinding = d3.keybinding('background')
|
|
.on(key, toggle)
|
|
.on(cmd('⌘B'), quickSwitch)
|
|
.on('F', hide)
|
|
.on('H', hide);
|
|
|
|
d3.select(document)
|
|
.call(keybinding);
|
|
|
|
context.surface().on('mousedown.background-outside', hide);
|
|
context.container().on('mousedown.background-outside', hide);
|
|
}
|
|
|
|
return background;
|
|
}
|
|
|
|
function Commit(context) {
|
|
var dispatch = d3.dispatch('cancel', 'save');
|
|
|
|
function commit(selection) {
|
|
var changes = context.history().changes(),
|
|
summary = context.history().difference().summary();
|
|
|
|
function zoomToEntity(change) {
|
|
var entity = change.entity;
|
|
if (change.changeType !== 'deleted' &&
|
|
context.graph().entity(entity.id).geometry(context.graph()) !== 'vertex') {
|
|
context.map().zoomTo(entity);
|
|
context.surface().selectAll(
|
|
iD.util.entityOrMemberSelector([entity.id], context.graph()))
|
|
.classed('hover', true);
|
|
}
|
|
}
|
|
|
|
var header = selection.append('div')
|
|
.attr('class', 'header fillL');
|
|
|
|
header.append('h3')
|
|
.text(t('commit.title'));
|
|
|
|
var body = selection.append('div')
|
|
.attr('class', 'body');
|
|
|
|
|
|
// Comment Section
|
|
var commentSection = body.append('div')
|
|
.attr('class', 'modal-section form-field commit-form');
|
|
|
|
commentSection.append('label')
|
|
.attr('class', 'form-label')
|
|
.text(t('commit.message_label'));
|
|
|
|
var commentField = commentSection.append('textarea')
|
|
.attr('placeholder', t('commit.description_placeholder'))
|
|
.attr('maxlength', 255)
|
|
.property('value', context.storage('comment') || '')
|
|
.on('input.save', checkComment)
|
|
.on('change.save', checkComment)
|
|
.on('blur.save', function() {
|
|
context.storage('comment', this.value);
|
|
});
|
|
|
|
function checkComment() {
|
|
d3.selectAll('.save-section .save-button')
|
|
.attr('disabled', (this.value.length ? null : true));
|
|
|
|
var googleWarning = clippyArea
|
|
.html('')
|
|
.selectAll('a')
|
|
.data(this.value.match(/google/i) ? [true] : []);
|
|
|
|
googleWarning.exit().remove();
|
|
|
|
googleWarning.enter()
|
|
.append('a')
|
|
.attr('target', '_blank')
|
|
.attr('tabindex', -1)
|
|
.call(iD.svg.Icon('#icon-alert', 'inline'))
|
|
.attr('href', t('commit.google_warning_link'))
|
|
.append('span')
|
|
.text(t('commit.google_warning'));
|
|
}
|
|
|
|
commentField.node().select();
|
|
|
|
context.connection().userChangesets(function (err, changesets) {
|
|
if (err) return;
|
|
|
|
var comments = [];
|
|
|
|
for (var i = 0; i < changesets.length; i++) {
|
|
if (changesets[i].tags.comment) {
|
|
comments.push({
|
|
title: changesets[i].tags.comment,
|
|
value: changesets[i].tags.comment
|
|
});
|
|
}
|
|
}
|
|
|
|
commentField.call(d3.combobox().caseSensitive(true).data(comments));
|
|
});
|
|
|
|
var clippyArea = commentSection.append('div')
|
|
.attr('class', 'clippy-area');
|
|
|
|
|
|
var changeSetInfo = commentSection.append('div')
|
|
.attr('class', 'changeset-info');
|
|
|
|
changeSetInfo.append('a')
|
|
.attr('target', '_blank')
|
|
.attr('tabindex', -1)
|
|
.call(iD.svg.Icon('#icon-out-link', 'inline'))
|
|
.attr('href', t('commit.about_changeset_comments_link'))
|
|
.append('span')
|
|
.text(t('commit.about_changeset_comments'));
|
|
|
|
// Warnings
|
|
var warnings = body.selectAll('div.warning-section')
|
|
.data([context.history().validate(changes)])
|
|
.enter()
|
|
.append('div')
|
|
.attr('class', 'modal-section warning-section fillL2')
|
|
.style('display', function(d) { return _.isEmpty(d) ? 'none' : null; })
|
|
.style('background', '#ffb');
|
|
|
|
warnings.append('h3')
|
|
.text(t('commit.warnings'));
|
|
|
|
var warningLi = warnings.append('ul')
|
|
.attr('class', 'changeset-list')
|
|
.selectAll('li')
|
|
.data(function(d) { return d; })
|
|
.enter()
|
|
.append('li')
|
|
.style()
|
|
.on('mouseover', mouseover)
|
|
.on('mouseout', mouseout)
|
|
.on('click', warningClick);
|
|
|
|
warningLi
|
|
.call(iD.svg.Icon('#icon-alert', 'pre-text'));
|
|
|
|
warningLi
|
|
.append('strong').text(function(d) {
|
|
return d.message;
|
|
});
|
|
|
|
warningLi.filter(function(d) { return d.tooltip; })
|
|
.call(bootstrap.tooltip()
|
|
.title(function(d) { return d.tooltip; })
|
|
.placement('top')
|
|
);
|
|
|
|
|
|
// Upload Explanation
|
|
var saveSection = body.append('div')
|
|
.attr('class','modal-section save-section fillL cf');
|
|
|
|
var prose = saveSection.append('p')
|
|
.attr('class', 'commit-info')
|
|
.html(t('commit.upload_explanation'));
|
|
|
|
context.connection().userDetails(function(err, user) {
|
|
if (err) return;
|
|
|
|
var userLink = d3.select(document.createElement('div'));
|
|
|
|
if (user.image_url) {
|
|
userLink.append('img')
|
|
.attr('src', user.image_url)
|
|
.attr('class', 'icon pre-text user-icon');
|
|
}
|
|
|
|
userLink.append('a')
|
|
.attr('class','user-info')
|
|
.text(user.display_name)
|
|
.attr('href', context.connection().userURL(user.display_name))
|
|
.attr('tabindex', -1)
|
|
.attr('target', '_blank');
|
|
|
|
prose.html(t('commit.upload_explanation_with_user', {user: userLink.html()}));
|
|
});
|
|
|
|
|
|
// Buttons
|
|
var buttonSection = saveSection.append('div')
|
|
.attr('class','buttons fillL cf');
|
|
|
|
var cancelButton = buttonSection.append('button')
|
|
.attr('class', 'secondary-action col5 button cancel-button')
|
|
.on('click.cancel', function() { dispatch.cancel(); });
|
|
|
|
cancelButton.append('span')
|
|
.attr('class', 'label')
|
|
.text(t('commit.cancel'));
|
|
|
|
var saveButton = buttonSection.append('button')
|
|
.attr('class', 'action col5 button save-button')
|
|
.attr('disabled', function() {
|
|
var n = d3.select('.commit-form textarea').node();
|
|
return (n && n.value.length) ? null : true;
|
|
})
|
|
.on('click.save', function() {
|
|
dispatch.save({
|
|
comment: commentField.node().value
|
|
});
|
|
});
|
|
|
|
saveButton.append('span')
|
|
.attr('class', 'label')
|
|
.text(t('commit.save'));
|
|
|
|
|
|
// Changes
|
|
var changeSection = body.selectAll('div.commit-section')
|
|
.data([0])
|
|
.enter()
|
|
.append('div')
|
|
.attr('class', 'commit-section modal-section fillL2');
|
|
|
|
changeSection.append('h3')
|
|
.text(t('commit.changes', {count: summary.length}));
|
|
|
|
var li = changeSection.append('ul')
|
|
.attr('class', 'changeset-list')
|
|
.selectAll('li')
|
|
.data(summary)
|
|
.enter()
|
|
.append('li')
|
|
.on('mouseover', mouseover)
|
|
.on('mouseout', mouseout)
|
|
.on('click', zoomToEntity);
|
|
|
|
li.each(function(d) {
|
|
d3.select(this)
|
|
.call(iD.svg.Icon('#icon-' + d.entity.geometry(d.graph), 'pre-text ' + d.changeType));
|
|
});
|
|
|
|
li.append('span')
|
|
.attr('class', 'change-type')
|
|
.text(function(d) {
|
|
return t('commit.' + d.changeType) + ' ';
|
|
});
|
|
|
|
li.append('strong')
|
|
.attr('class', 'entity-type')
|
|
.text(function(d) {
|
|
return context.presets().match(d.entity, d.graph).name();
|
|
});
|
|
|
|
li.append('span')
|
|
.attr('class', 'entity-name')
|
|
.text(function(d) {
|
|
var name = iD.util.displayName(d.entity) || '',
|
|
string = '';
|
|
if (name !== '') string += ':';
|
|
return string += ' ' + name;
|
|
});
|
|
|
|
li.style('opacity', 0)
|
|
.transition()
|
|
.style('opacity', 1);
|
|
|
|
|
|
function mouseover(d) {
|
|
if (d.entity) {
|
|
context.surface().selectAll(
|
|
iD.util.entityOrMemberSelector([d.entity.id], context.graph())
|
|
).classed('hover', true);
|
|
}
|
|
}
|
|
|
|
function mouseout() {
|
|
context.surface().selectAll('.hover')
|
|
.classed('hover', false);
|
|
}
|
|
|
|
function warningClick(d) {
|
|
if (d.entity) {
|
|
context.map().zoomTo(d.entity);
|
|
context.enter(
|
|
iD.modes.Select(context, [d.entity.id])
|
|
.suppressMenu(true));
|
|
}
|
|
}
|
|
|
|
// Call checkComment off the bat, in case a changeset
|
|
// comment is recovered from localStorage
|
|
commentField.trigger('input');
|
|
}
|
|
|
|
return d3.rebind(commit, dispatch, 'on');
|
|
}
|
|
|
|
function modalModule(selection, blocking) {
|
|
var keybinding = d3.keybinding('modal');
|
|
var previous = selection.select('div.modal');
|
|
var animate = previous.empty();
|
|
|
|
previous.transition()
|
|
.duration(200)
|
|
.style('opacity', 0)
|
|
.remove();
|
|
|
|
var shaded = selection
|
|
.append('div')
|
|
.attr('class', 'shaded')
|
|
.style('opacity', 0);
|
|
|
|
shaded.close = function() {
|
|
shaded
|
|
.transition()
|
|
.duration(200)
|
|
.style('opacity',0)
|
|
.remove();
|
|
modal
|
|
.transition()
|
|
.duration(200)
|
|
.style('top','0px');
|
|
|
|
keybinding.off();
|
|
};
|
|
|
|
|
|
var modal = shaded.append('div')
|
|
.attr('class', 'modal fillL col6');
|
|
|
|
if (!blocking) {
|
|
shaded.on('click.remove-modal', function() {
|
|
if (d3.event.target === this) {
|
|
shaded.close();
|
|
}
|
|
});
|
|
|
|
modal.append('button')
|
|
.attr('class', 'close')
|
|
.on('click', shaded.close)
|
|
.call(iD.svg.Icon('#icon-close'));
|
|
|
|
keybinding
|
|
.on('⌫', shaded.close)
|
|
.on('⎋', shaded.close);
|
|
|
|
d3.select(document).call(keybinding);
|
|
}
|
|
|
|
modal.append('div')
|
|
.attr('class', 'content');
|
|
|
|
if (animate) {
|
|
shaded.transition().style('opacity', 1);
|
|
} else {
|
|
shaded.style('opacity', 1);
|
|
}
|
|
|
|
return shaded;
|
|
}
|
|
|
|
function confirm(selection) {
|
|
var modal = modalModule(selection);
|
|
|
|
modal.select('.modal')
|
|
.classed('modal-alert', true);
|
|
|
|
var section = modal.select('.content');
|
|
|
|
section.append('div')
|
|
.attr('class', 'modal-section header');
|
|
|
|
section.append('div')
|
|
.attr('class', 'modal-section message-text');
|
|
|
|
var buttons = section.append('div')
|
|
.attr('class', 'modal-section buttons cf');
|
|
|
|
modal.okButton = function() {
|
|
buttons
|
|
.append('button')
|
|
.attr('class', 'action col4')
|
|
.on('click.confirm', function() {
|
|
modal.remove();
|
|
})
|
|
.text(t('confirm.okay'));
|
|
|
|
return modal;
|
|
};
|
|
|
|
return modal;
|
|
}
|
|
|
|
function Conflicts(context) {
|
|
var dispatch = d3.dispatch('download', 'cancel', 'save'),
|
|
list;
|
|
|
|
function conflicts(selection) {
|
|
var header = selection
|
|
.append('div')
|
|
.attr('class', 'header fillL');
|
|
|
|
header
|
|
.append('button')
|
|
.attr('class', 'fr')
|
|
.on('click', function() { dispatch.cancel(); })
|
|
.call(iD.svg.Icon('#icon-close'));
|
|
|
|
header
|
|
.append('h3')
|
|
.text(t('save.conflict.header'));
|
|
|
|
var body = selection
|
|
.append('div')
|
|
.attr('class', 'body fillL');
|
|
|
|
body
|
|
.append('div')
|
|
.attr('class', 'conflicts-help')
|
|
.text(t('save.conflict.help'))
|
|
.append('a')
|
|
.attr('class', 'conflicts-download')
|
|
.text(t('save.conflict.download_changes'))
|
|
.on('click.download', function() { dispatch.download(); });
|
|
|
|
body
|
|
.append('div')
|
|
.attr('class', 'conflict-container fillL3')
|
|
.call(showConflict, 0);
|
|
|
|
body
|
|
.append('div')
|
|
.attr('class', 'conflicts-done')
|
|
.attr('opacity', 0)
|
|
.style('display', 'none')
|
|
.text(t('save.conflict.done'));
|
|
|
|
var buttons = body
|
|
.append('div')
|
|
.attr('class','buttons col12 joined conflicts-buttons');
|
|
|
|
buttons
|
|
.append('button')
|
|
.attr('disabled', list.length > 1)
|
|
.attr('class', 'action conflicts-button col6')
|
|
.text(t('save.title'))
|
|
.on('click.try_again', function() { dispatch.save(); });
|
|
|
|
buttons
|
|
.append('button')
|
|
.attr('class', 'secondary-action conflicts-button col6')
|
|
.text(t('confirm.cancel'))
|
|
.on('click.cancel', function() { dispatch.cancel(); });
|
|
}
|
|
|
|
|
|
function showConflict(selection, index) {
|
|
if (index < 0 || index >= list.length) return;
|
|
|
|
var parent = d3.select(selection.node().parentNode);
|
|
|
|
// enable save button if this is the last conflict being reviewed..
|
|
if (index === list.length - 1) {
|
|
window.setTimeout(function() {
|
|
parent.select('.conflicts-button')
|
|
.attr('disabled', null);
|
|
|
|
parent.select('.conflicts-done')
|
|
.transition()
|
|
.attr('opacity', 1)
|
|
.style('display', 'block');
|
|
}, 250);
|
|
}
|
|
|
|
var item = selection
|
|
.selectAll('.conflict')
|
|
.data([list[index]]);
|
|
|
|
var enter = item.enter()
|
|
.append('div')
|
|
.attr('class', 'conflict');
|
|
|
|
enter
|
|
.append('h4')
|
|
.attr('class', 'conflict-count')
|
|
.text(t('save.conflict.count', { num: index + 1, total: list.length }));
|
|
|
|
enter
|
|
.append('a')
|
|
.attr('class', 'conflict-description')
|
|
.attr('href', '#')
|
|
.text(function(d) { return d.name; })
|
|
.on('click', function(d) {
|
|
zoomToEntity(d.id);
|
|
d3.event.preventDefault();
|
|
});
|
|
|
|
var details = enter
|
|
.append('div')
|
|
.attr('class', 'conflict-detail-container');
|
|
|
|
details
|
|
.append('ul')
|
|
.attr('class', 'conflict-detail-list')
|
|
.selectAll('li')
|
|
.data(function(d) { return d.details || []; })
|
|
.enter()
|
|
.append('li')
|
|
.attr('class', 'conflict-detail-item')
|
|
.html(function(d) { return d; });
|
|
|
|
details
|
|
.append('div')
|
|
.attr('class', 'conflict-choices')
|
|
.call(addChoices);
|
|
|
|
details
|
|
.append('div')
|
|
.attr('class', 'conflict-nav-buttons joined cf')
|
|
.selectAll('button')
|
|
.data(['previous', 'next'])
|
|
.enter()
|
|
.append('button')
|
|
.text(function(d) { return t('save.conflict.' + d); })
|
|
.attr('class', 'conflict-nav-button action col6')
|
|
.attr('disabled', function(d, i) {
|
|
return (i === 0 && index === 0) ||
|
|
(i === 1 && index === list.length - 1) || null;
|
|
})
|
|
.on('click', function(d, i) {
|
|
var container = parent.select('.conflict-container'),
|
|
sign = (i === 0 ? -1 : 1);
|
|
|
|
container
|
|
.selectAll('.conflict')
|
|
.remove();
|
|
|
|
container
|
|
.call(showConflict, index + sign);
|
|
|
|
d3.event.preventDefault();
|
|
});
|
|
|
|
item.exit()
|
|
.remove();
|
|
|
|
}
|
|
|
|
function addChoices(selection) {
|
|
var choices = selection
|
|
.append('ul')
|
|
.attr('class', 'layer-list')
|
|
.selectAll('li')
|
|
.data(function(d) { return d.choices || []; });
|
|
|
|
var enter = choices.enter()
|
|
.append('li')
|
|
.attr('class', 'layer');
|
|
|
|
var label = enter
|
|
.append('label');
|
|
|
|
label
|
|
.append('input')
|
|
.attr('type', 'radio')
|
|
.attr('name', function(d) { return d.id; })
|
|
.on('change', function(d, i) {
|
|
var ul = this.parentNode.parentNode.parentNode;
|
|
ul.__data__.chosen = i;
|
|
choose(ul, d);
|
|
});
|
|
|
|
label
|
|
.append('span')
|
|
.text(function(d) { return d.text; });
|
|
|
|
choices
|
|
.each(function(d, i) {
|
|
var ul = this.parentNode;
|
|
if (ul.__data__.chosen === i) choose(ul, d);
|
|
});
|
|
}
|
|
|
|
function choose(ul, datum) {
|
|
if (d3.event) d3.event.preventDefault();
|
|
|
|
d3.select(ul)
|
|
.selectAll('li')
|
|
.classed('active', function(d) { return d === datum; })
|
|
.selectAll('input')
|
|
.property('checked', function(d) { return d === datum; });
|
|
|
|
var extent = iD.geo.Extent(),
|
|
entity;
|
|
|
|
entity = context.graph().hasEntity(datum.id);
|
|
if (entity) extent._extend(entity.extent(context.graph()));
|
|
|
|
datum.action();
|
|
|
|
entity = context.graph().hasEntity(datum.id);
|
|
if (entity) extent._extend(entity.extent(context.graph()));
|
|
|
|
zoomToEntity(datum.id, extent);
|
|
}
|
|
|
|
function zoomToEntity(id, extent) {
|
|
context.surface().selectAll('.hover')
|
|
.classed('hover', false);
|
|
|
|
var entity = context.graph().hasEntity(id);
|
|
if (entity) {
|
|
if (extent) {
|
|
context.map().trimmedExtent(extent);
|
|
} else {
|
|
context.map().zoomTo(entity);
|
|
}
|
|
context.surface().selectAll(
|
|
iD.util.entityOrMemberSelector([entity.id], context.graph()))
|
|
.classed('hover', true);
|
|
}
|
|
}
|
|
|
|
|
|
// The conflict list should be an array of objects like:
|
|
// {
|
|
// id: id,
|
|
// name: entityName(local),
|
|
// details: merge.conflicts(),
|
|
// chosen: 1,
|
|
// choices: [
|
|
// choice(id, keepMine, forceLocal),
|
|
// choice(id, keepTheirs, forceRemote)
|
|
// ]
|
|
// }
|
|
conflicts.list = function(_) {
|
|
if (!arguments.length) return list;
|
|
list = _;
|
|
return conflicts;
|
|
};
|
|
|
|
return d3.rebind(conflicts, dispatch, 'on');
|
|
}
|
|
|
|
function Contributors(context) {
|
|
var debouncedUpdate = _.debounce(function() { update(); }, 1000),
|
|
limit = 4,
|
|
hidden = false,
|
|
wrap = d3.select(null);
|
|
|
|
function update() {
|
|
var users = {},
|
|
entities = context.intersects(context.map().extent());
|
|
|
|
entities.forEach(function(entity) {
|
|
if (entity && entity.user) users[entity.user] = true;
|
|
});
|
|
|
|
var u = Object.keys(users),
|
|
subset = u.slice(0, u.length > limit ? limit - 1 : limit);
|
|
|
|
wrap.html('')
|
|
.call(iD.svg.Icon('#icon-nearby', 'pre-text light'));
|
|
|
|
var userList = d3.select(document.createElement('span'));
|
|
|
|
userList.selectAll()
|
|
.data(subset)
|
|
.enter()
|
|
.append('a')
|
|
.attr('class', 'user-link')
|
|
.attr('href', function(d) { return context.connection().userURL(d); })
|
|
.attr('target', '_blank')
|
|
.attr('tabindex', -1)
|
|
.text(String);
|
|
|
|
if (u.length > limit) {
|
|
var count = d3.select(document.createElement('span'));
|
|
|
|
count.append('a')
|
|
.attr('target', '_blank')
|
|
.attr('tabindex', -1)
|
|
.attr('href', function() {
|
|
return context.connection().changesetsURL(context.map().center(), context.map().zoom());
|
|
})
|
|
.text(u.length - limit + 1);
|
|
|
|
wrap.append('span')
|
|
.html(t('contributors.truncated_list', { users: userList.html(), count: count.html() }));
|
|
|
|
} else {
|
|
wrap.append('span')
|
|
.html(t('contributors.list', { users: userList.html() }));
|
|
}
|
|
|
|
if (!u.length) {
|
|
hidden = true;
|
|
wrap
|
|
.transition()
|
|
.style('opacity', 0);
|
|
|
|
} else if (hidden) {
|
|
wrap
|
|
.transition()
|
|
.style('opacity', 1);
|
|
}
|
|
}
|
|
|
|
return function(selection) {
|
|
wrap = selection;
|
|
update();
|
|
|
|
context.connection().on('loaded.contributors', debouncedUpdate);
|
|
context.map().on('move.contributors', debouncedUpdate);
|
|
};
|
|
}
|
|
|
|
// toggles the visibility of ui elements, using a combination of the
|
|
// hide class, which sets display=none, and a d3 transition for opacity.
|
|
// this will cause blinking when called repeatedly, so check that the
|
|
// value actually changes between calls.
|
|
function Toggle(show, callback) {
|
|
return function(selection) {
|
|
selection
|
|
.style('opacity', show ? 0 : 1)
|
|
.classed('hide', false)
|
|
.transition()
|
|
.style('opacity', show ? 1 : 0)
|
|
.each('end', function() {
|
|
d3.select(this)
|
|
.classed('hide', !show)
|
|
.style('opacity', null);
|
|
if (callback) callback.apply(this);
|
|
});
|
|
};
|
|
}
|
|
|
|
function Disclosure() {
|
|
var dispatch = d3.dispatch('toggled'),
|
|
title,
|
|
expanded = false,
|
|
content = function () {};
|
|
|
|
var disclosure = function(selection) {
|
|
var $link = selection.selectAll('.hide-toggle')
|
|
.data([0]);
|
|
|
|
$link.enter().append('a')
|
|
.attr('href', '#')
|
|
.attr('class', 'hide-toggle');
|
|
|
|
$link.text(title)
|
|
.on('click', toggle)
|
|
.classed('expanded', expanded);
|
|
|
|
var $body = selection.selectAll('div')
|
|
.data([0]);
|
|
|
|
$body.enter().append('div');
|
|
|
|
$body.classed('hide', !expanded)
|
|
.call(content);
|
|
|
|
function toggle() {
|
|
expanded = !expanded;
|
|
$link.classed('expanded', expanded);
|
|
$body.call(Toggle(expanded));
|
|
dispatch.toggled(expanded);
|
|
}
|
|
};
|
|
|
|
disclosure.title = function(_) {
|
|
if (!arguments.length) return title;
|
|
title = _;
|
|
return disclosure;
|
|
};
|
|
|
|
disclosure.expanded = function(_) {
|
|
if (!arguments.length) return expanded;
|
|
expanded = _;
|
|
return disclosure;
|
|
};
|
|
|
|
disclosure.content = function(_) {
|
|
if (!arguments.length) return content;
|
|
content = _;
|
|
return disclosure;
|
|
};
|
|
|
|
return d3.rebind(disclosure, dispatch, 'on');
|
|
}
|
|
|
|
function preset(context) {
|
|
var event = d3.dispatch('change'),
|
|
state,
|
|
fields,
|
|
preset,
|
|
tags,
|
|
id;
|
|
|
|
function UIField(field, entity, show) {
|
|
field = _.clone(field);
|
|
|
|
field.input = iD.ui.preset[field.type](field, context)
|
|
.on('change', event.change);
|
|
|
|
if (field.input.entity) field.input.entity(entity);
|
|
|
|
field.keys = field.keys || [field.key];
|
|
|
|
field.show = show;
|
|
|
|
field.shown = function() {
|
|
return field.id === 'name' || field.show || _.some(field.keys, function(key) { return !!tags[key]; });
|
|
};
|
|
|
|
field.modified = function() {
|
|
var original = context.graph().base().entities[entity.id];
|
|
return _.some(field.keys, function(key) {
|
|
return original ? tags[key] !== original.tags[key] : tags[key];
|
|
});
|
|
};
|
|
|
|
field.revert = function() {
|
|
var original = context.graph().base().entities[entity.id],
|
|
t = {};
|
|
field.keys.forEach(function(key) {
|
|
t[key] = original ? original.tags[key] : undefined;
|
|
});
|
|
return t;
|
|
};
|
|
|
|
field.present = function() {
|
|
return _.some(field.keys, function(key) {
|
|
return tags[key];
|
|
});
|
|
};
|
|
|
|
field.remove = function() {
|
|
var t = {};
|
|
field.keys.forEach(function(key) {
|
|
t[key] = undefined;
|
|
});
|
|
return t;
|
|
};
|
|
|
|
return field;
|
|
}
|
|
|
|
function fieldKey(field) {
|
|
return field.id;
|
|
}
|
|
|
|
function presets(selection) {
|
|
selection.call(iD.ui.Disclosure()
|
|
.title(t('inspector.all_fields'))
|
|
.expanded(context.storage('preset_fields.expanded') !== 'false')
|
|
.on('toggled', toggled)
|
|
.content(content));
|
|
|
|
function toggled(expanded) {
|
|
context.storage('preset_fields.expanded', expanded);
|
|
}
|
|
}
|
|
|
|
function content(selection) {
|
|
if (!fields) {
|
|
var entity = context.entity(id),
|
|
geometry = context.geometry(id);
|
|
|
|
fields = [UIField(context.presets().field('name'), entity)];
|
|
|
|
preset.fields.forEach(function(field) {
|
|
if (field.matchGeometry(geometry)) {
|
|
fields.push(UIField(field, entity, true));
|
|
}
|
|
});
|
|
|
|
if (entity.isHighwayIntersection(context.graph())) {
|
|
fields.push(UIField(context.presets().field('restrictions'), entity, true));
|
|
}
|
|
|
|
context.presets().universal().forEach(function(field) {
|
|
if (preset.fields.indexOf(field) < 0) {
|
|
fields.push(UIField(field, entity));
|
|
}
|
|
});
|
|
}
|
|
|
|
var shown = fields.filter(function(field) { return field.shown(); }),
|
|
notShown = fields.filter(function(field) { return !field.shown(); });
|
|
|
|
var $form = selection.selectAll('.preset-form')
|
|
.data([0]);
|
|
|
|
$form.enter().append('div')
|
|
.attr('class', 'preset-form inspector-inner fillL3');
|
|
|
|
var $fields = $form.selectAll('.form-field')
|
|
.data(shown, fieldKey);
|
|
|
|
// Enter
|
|
|
|
var $enter = $fields.enter()
|
|
.append('div')
|
|
.attr('class', function(field) {
|
|
return 'form-field form-field-' + field.id;
|
|
});
|
|
|
|
var $label = $enter.append('label')
|
|
.attr('class', 'form-label')
|
|
.attr('for', function(field) { return 'preset-input-' + field.id; })
|
|
.text(function(field) { return field.label(); });
|
|
|
|
var wrap = $label.append('div')
|
|
.attr('class', 'form-label-button-wrap');
|
|
|
|
wrap.append('button')
|
|
.attr('class', 'remove-icon')
|
|
.attr('tabindex', -1)
|
|
.call(iD.svg.Icon('#operation-delete'));
|
|
|
|
wrap.append('button')
|
|
.attr('class', 'modified-icon')
|
|
.attr('tabindex', -1)
|
|
.call(iD.svg.Icon('#icon-undo'));
|
|
|
|
// Update
|
|
|
|
$fields.select('.form-label-button-wrap .remove-icon')
|
|
.on('click', remove);
|
|
|
|
$fields.select('.modified-icon')
|
|
.on('click', revert);
|
|
|
|
$fields
|
|
.order()
|
|
.classed('modified', function(field) {
|
|
return field.modified();
|
|
})
|
|
.classed('present', function(field) {
|
|
return field.present();
|
|
})
|
|
.each(function(field) {
|
|
var reference = iD.ui.TagReference(field.reference || {key: field.key}, context);
|
|
|
|
if (state === 'hover') {
|
|
reference.showing(false);
|
|
}
|
|
|
|
d3.select(this)
|
|
.call(field.input)
|
|
.selectAll('input')
|
|
.on('keydown', function() {
|
|
// if user presses enter, and combobox is not active, accept edits..
|
|
if (d3.event.keyCode === 13 && d3.select('.combobox').empty()) {
|
|
context.enter(iD.modes.Browse(context));
|
|
}
|
|
})
|
|
.call(reference.body)
|
|
.select('.form-label-button-wrap')
|
|
.call(reference.button);
|
|
|
|
field.input.tags(tags);
|
|
});
|
|
|
|
$fields.exit()
|
|
.remove();
|
|
|
|
notShown = notShown.map(function(field) {
|
|
return {
|
|
title: field.label(),
|
|
value: field.label(),
|
|
field: field
|
|
};
|
|
});
|
|
|
|
var $more = selection.selectAll('.more-fields')
|
|
.data((notShown.length > 0) ? [0] : []);
|
|
|
|
$more.enter().append('div')
|
|
.attr('class', 'more-fields')
|
|
.append('label')
|
|
.text(t('inspector.add_fields'));
|
|
|
|
var $input = $more.selectAll('.value')
|
|
.data([0]);
|
|
|
|
$input.enter().append('input')
|
|
.attr('class', 'value')
|
|
.attr('type', 'text');
|
|
|
|
$input.value('')
|
|
.attr('placeholder', function() {
|
|
var placeholder = [];
|
|
for (var field in notShown) {
|
|
placeholder.push(notShown[field].title);
|
|
}
|
|
return placeholder.slice(0,3).join(', ') + ((placeholder.length > 3) ? '…' : '');
|
|
})
|
|
.call(d3.combobox().data(notShown)
|
|
.minItems(1)
|
|
.on('accept', show));
|
|
|
|
$more.exit()
|
|
.remove();
|
|
|
|
$input.exit()
|
|
.remove();
|
|
|
|
function show(field) {
|
|
field = field.field;
|
|
field.show = true;
|
|
content(selection);
|
|
field.input.focus();
|
|
}
|
|
|
|
function revert(field) {
|
|
d3.event.stopPropagation();
|
|
d3.event.preventDefault();
|
|
event.change(field.revert());
|
|
}
|
|
|
|
function remove(field) {
|
|
d3.event.stopPropagation();
|
|
d3.event.preventDefault();
|
|
event.change(field.remove());
|
|
}
|
|
}
|
|
|
|
presets.preset = function(_) {
|
|
if (!arguments.length) return preset;
|
|
if (preset && preset.id === _.id) return presets;
|
|
preset = _;
|
|
fields = null;
|
|
return presets;
|
|
};
|
|
|
|
presets.state = function(_) {
|
|
if (!arguments.length) return state;
|
|
state = _;
|
|
return presets;
|
|
};
|
|
|
|
presets.tags = function(_) {
|
|
if (!arguments.length) return tags;
|
|
tags = _;
|
|
// Don't reset fields here.
|
|
return presets;
|
|
};
|
|
|
|
presets.entityID = function(_) {
|
|
if (!arguments.length) return id;
|
|
if (id === _) return presets;
|
|
id = _;
|
|
fields = null;
|
|
return presets;
|
|
};
|
|
|
|
return d3.rebind(presets, event, 'on');
|
|
}
|
|
|
|
function PresetIcon() {
|
|
var preset, geometry;
|
|
|
|
function presetIcon(selection) {
|
|
selection.each(render);
|
|
}
|
|
|
|
function render() {
|
|
var selection = d3.select(this),
|
|
p = preset.apply(this, arguments),
|
|
geom = geometry.apply(this, arguments),
|
|
icon = p.icon || (geom === 'line' ? 'other-line' : 'marker-stroked'),
|
|
maki = iD.data.featureIcons.hasOwnProperty(icon + '-24');
|
|
|
|
if (icon === 'dentist') maki = true; // workaround for dentist icon missing in `maki-sprite.json`
|
|
|
|
function tag_classes(p) {
|
|
var s = '';
|
|
for (var i in p.tags) {
|
|
s += ' tag-' + i;
|
|
if (p.tags[i] !== '*') {
|
|
s += ' tag-' + i + '-' + p.tags[i];
|
|
}
|
|
}
|
|
return s;
|
|
}
|
|
|
|
var $fill = selection.selectAll('.preset-icon-fill')
|
|
.data([0]);
|
|
|
|
$fill.enter().append('div');
|
|
|
|
$fill.attr('class', function() {
|
|
return 'preset-icon-fill preset-icon-fill-' + geom + tag_classes(p);
|
|
});
|
|
|
|
var $frame = selection.selectAll('.preset-icon-frame')
|
|
.data([0]);
|
|
|
|
$frame.enter()
|
|
.append('div')
|
|
.call(iD.svg.Icon('#preset-icon-frame'));
|
|
|
|
$frame.attr('class', function() {
|
|
return 'preset-icon-frame ' + (geom === 'area' ? '' : 'hide');
|
|
});
|
|
|
|
|
|
var $icon = selection.selectAll('.preset-icon')
|
|
.data([0]);
|
|
|
|
$icon.enter()
|
|
.append('div')
|
|
.attr('class', 'preset-icon')
|
|
.call(iD.svg.Icon(''));
|
|
|
|
$icon
|
|
.attr('class', 'preset-icon preset-icon-' + (maki ? '32' : (geom === 'area' ? '44' : '60')));
|
|
|
|
$icon.selectAll('svg')
|
|
.attr('class', function() {
|
|
return 'icon ' + icon + tag_classes(p);
|
|
});
|
|
|
|
$icon.selectAll('use') // workaround: maki parking-24 broken?
|
|
.attr('href', '#' + icon + (maki ? ( icon === 'parking' ? '-18' : '-24') : ''));
|
|
}
|
|
|
|
presetIcon.preset = function(_) {
|
|
if (!arguments.length) return preset;
|
|
preset = d3.functor(_);
|
|
return presetIcon;
|
|
};
|
|
|
|
presetIcon.geometry = function(_) {
|
|
if (!arguments.length) return geometry;
|
|
geometry = d3.functor(_);
|
|
return presetIcon;
|
|
};
|
|
|
|
return presetIcon;
|
|
}
|
|
|
|
function TagReference(tag, context) {
|
|
var tagReference = {},
|
|
button,
|
|
body,
|
|
loaded,
|
|
showing;
|
|
|
|
function findLocal(data) {
|
|
var locale = iD.detect().locale.toLowerCase(),
|
|
localized;
|
|
|
|
localized = _.find(data, function(d) {
|
|
return d.lang.toLowerCase() === locale;
|
|
});
|
|
if (localized) return localized;
|
|
|
|
// try the non-regional version of a language, like
|
|
// 'en' if the language is 'en-US'
|
|
if (locale.indexOf('-') !== -1) {
|
|
var first = locale.split('-')[0];
|
|
localized = _.find(data, function(d) {
|
|
return d.lang.toLowerCase() === first;
|
|
});
|
|
if (localized) return localized;
|
|
}
|
|
|
|
// finally fall back to english
|
|
return _.find(data, function(d) {
|
|
return d.lang.toLowerCase() === 'en';
|
|
});
|
|
}
|
|
|
|
function load(param) {
|
|
button.classed('tag-reference-loading', true);
|
|
|
|
context.taginfo().docs(param, function show(err, data) {
|
|
var docs;
|
|
if (!err && data) {
|
|
docs = findLocal(data);
|
|
}
|
|
|
|
body.html('');
|
|
|
|
if (!docs || !docs.description) {
|
|
if (param.hasOwnProperty('value')) {
|
|
load(_.omit(param, 'value')); // retry with key only
|
|
} else {
|
|
body.append('p').text(t('inspector.no_documentation_key'));
|
|
done();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (docs.image && docs.image.thumb_url_prefix) {
|
|
body
|
|
.append('img')
|
|
.attr('class', 'wiki-image')
|
|
.attr('src', docs.image.thumb_url_prefix + '100' + docs.image.thumb_url_suffix)
|
|
.on('load', function() { done(); })
|
|
.on('error', function() { d3.select(this).remove(); done(); });
|
|
} else {
|
|
done();
|
|
}
|
|
|
|
body
|
|
.append('p')
|
|
.text(docs.description);
|
|
|
|
body
|
|
.append('a')
|
|
.attr('target', '_blank')
|
|
.attr('tabindex', -1)
|
|
.attr('href', 'https://wiki.openstreetmap.org/wiki/' + docs.title)
|
|
.call(iD.svg.Icon('#icon-out-link', 'inline'))
|
|
.append('span')
|
|
.text(t('inspector.reference'));
|
|
});
|
|
}
|
|
|
|
function done() {
|
|
loaded = true;
|
|
|
|
button.classed('tag-reference-loading', false);
|
|
|
|
body.transition()
|
|
.duration(200)
|
|
.style('max-height', '200px')
|
|
.style('opacity', '1');
|
|
|
|
showing = true;
|
|
}
|
|
|
|
function hide(selection) {
|
|
selection = selection || body.transition().duration(200);
|
|
|
|
selection
|
|
.style('max-height', '0px')
|
|
.style('opacity', '0');
|
|
|
|
showing = false;
|
|
}
|
|
|
|
tagReference.button = function(selection) {
|
|
button = selection.selectAll('.tag-reference-button')
|
|
.data([0]);
|
|
|
|
button.enter()
|
|
.append('button')
|
|
.attr('class', 'tag-reference-button')
|
|
.attr('tabindex', -1)
|
|
.call(iD.svg.Icon('#icon-inspect'));
|
|
|
|
button.on('click', function () {
|
|
d3.event.stopPropagation();
|
|
d3.event.preventDefault();
|
|
if (showing) {
|
|
hide();
|
|
} else if (loaded) {
|
|
done();
|
|
} else {
|
|
if (context.taginfo()) {
|
|
load(tag);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
tagReference.body = function(selection) {
|
|
body = selection.selectAll('.tag-reference-body')
|
|
.data([0]);
|
|
|
|
body.enter().append('div')
|
|
.attr('class', 'tag-reference-body cf')
|
|
.style('max-height', '0')
|
|
.style('opacity', '0');
|
|
|
|
if (showing === false) {
|
|
hide(body);
|
|
}
|
|
};
|
|
|
|
tagReference.showing = function(_) {
|
|
if (!arguments.length) return showing;
|
|
showing = _;
|
|
return tagReference;
|
|
};
|
|
|
|
return tagReference;
|
|
}
|
|
|
|
function RawTagEditor(context) {
|
|
var event = d3.dispatch('change'),
|
|
showBlank = false,
|
|
state,
|
|
preset,
|
|
tags,
|
|
id;
|
|
|
|
function rawTagEditor(selection) {
|
|
var count = Object.keys(tags).filter(function(d) { return d; }).length;
|
|
|
|
selection.call(Disclosure()
|
|
.title(t('inspector.all_tags') + ' (' + count + ')')
|
|
.expanded(context.storage('raw_tag_editor.expanded') === 'true' || preset.isFallback())
|
|
.on('toggled', toggled)
|
|
.content(content));
|
|
|
|
function toggled(expanded) {
|
|
context.storage('raw_tag_editor.expanded', expanded);
|
|
if (expanded) {
|
|
selection.node().parentNode.scrollTop += 200;
|
|
}
|
|
}
|
|
}
|
|
|
|
function content($wrap) {
|
|
var entries = d3.entries(tags);
|
|
|
|
if (!entries.length || showBlank) {
|
|
showBlank = false;
|
|
entries.push({key: '', value: ''});
|
|
}
|
|
|
|
var $list = $wrap.selectAll('.tag-list')
|
|
.data([0]);
|
|
|
|
$list.enter().append('ul')
|
|
.attr('class', 'tag-list');
|
|
|
|
var $newTag = $wrap.selectAll('.add-tag')
|
|
.data([0]);
|
|
|
|
$newTag.enter()
|
|
.append('button')
|
|
.attr('class', 'add-tag')
|
|
.call(iD.svg.Icon('#icon-plus', 'light'));
|
|
|
|
$newTag.on('click', addTag);
|
|
|
|
var $items = $list.selectAll('li')
|
|
.data(entries, function(d) { return d.key; });
|
|
|
|
// Enter
|
|
|
|
var $enter = $items.enter().append('li')
|
|
.attr('class', 'tag-row cf');
|
|
|
|
$enter.append('div')
|
|
.attr('class', 'key-wrap')
|
|
.append('input')
|
|
.property('type', 'text')
|
|
.attr('class', 'key')
|
|
.attr('maxlength', 255);
|
|
|
|
$enter.append('div')
|
|
.attr('class', 'input-wrap-position')
|
|
.append('input')
|
|
.property('type', 'text')
|
|
.attr('class', 'value')
|
|
.attr('maxlength', 255);
|
|
|
|
$enter.append('button')
|
|
.attr('tabindex', -1)
|
|
.attr('class', 'remove minor')
|
|
.call(iD.svg.Icon('#operation-delete'));
|
|
|
|
if (context.taginfo()) {
|
|
$enter.each(bindTypeahead);
|
|
}
|
|
|
|
// Update
|
|
|
|
$items.order();
|
|
|
|
$items.each(function(tag) {
|
|
var isRelation = (context.entity(id).type === 'relation'),
|
|
reference;
|
|
if (isRelation && tag.key === 'type')
|
|
reference = TagReference({rtype: tag.value}, context);
|
|
else
|
|
reference = TagReference({key: tag.key, value: tag.value}, context);
|
|
|
|
if (state === 'hover') {
|
|
reference.showing(false);
|
|
}
|
|
|
|
d3.select(this)
|
|
.call(reference.button)
|
|
.call(reference.body);
|
|
});
|
|
|
|
$items.select('input.key')
|
|
.attr('title', function(d) { return d.key; })
|
|
.value(function(d) { return d.key; })
|
|
.on('blur', keyChange)
|
|
.on('change', keyChange);
|
|
|
|
$items.select('input.value')
|
|
.attr('title', function(d) { return d.value; })
|
|
.value(function(d) { return d.value; })
|
|
.on('blur', valueChange)
|
|
.on('change', valueChange)
|
|
.on('keydown.push-more', pushMore);
|
|
|
|
$items.select('button.remove')
|
|
.on('click', removeTag);
|
|
|
|
$items.exit()
|
|
.each(unbind)
|
|
.remove();
|
|
|
|
function pushMore() {
|
|
if (d3.event.keyCode === 9 && !d3.event.shiftKey &&
|
|
$list.selectAll('li:last-child input.value').node() === this) {
|
|
addTag();
|
|
}
|
|
}
|
|
|
|
function bindTypeahead() {
|
|
var row = d3.select(this),
|
|
key = row.selectAll('input.key'),
|
|
value = row.selectAll('input.value');
|
|
|
|
function sort(value, data) {
|
|
var sameletter = [],
|
|
other = [];
|
|
for (var i = 0; i < data.length; i++) {
|
|
if (data[i].value.substring(0, value.length) === value) {
|
|
sameletter.push(data[i]);
|
|
} else {
|
|
other.push(data[i]);
|
|
}
|
|
}
|
|
return sameletter.concat(other);
|
|
}
|
|
|
|
key.call(d3.combobox()
|
|
.fetcher(function(value, callback) {
|
|
context.taginfo().keys({
|
|
debounce: true,
|
|
geometry: context.geometry(id),
|
|
query: value
|
|
}, function(err, data) {
|
|
if (!err) callback(sort(value, data));
|
|
});
|
|
}));
|
|
|
|
value.call(d3.combobox()
|
|
.fetcher(function(value, callback) {
|
|
context.taginfo().values({
|
|
debounce: true,
|
|
key: key.value(),
|
|
geometry: context.geometry(id),
|
|
query: value
|
|
}, function(err, data) {
|
|
if (!err) callback(sort(value, data));
|
|
});
|
|
}));
|
|
}
|
|
|
|
function unbind() {
|
|
var row = d3.select(this);
|
|
|
|
row.selectAll('input.key')
|
|
.call(d3.combobox.off);
|
|
|
|
row.selectAll('input.value')
|
|
.call(d3.combobox.off);
|
|
}
|
|
|
|
function keyChange(d) {
|
|
var kOld = d.key,
|
|
kNew = this.value.trim(),
|
|
tag = {};
|
|
|
|
if (kNew && kNew !== kOld) {
|
|
var match = kNew.match(/^(.*?)(?:_(\d+))?$/),
|
|
base = match[1],
|
|
suffix = +(match[2] || 1);
|
|
while (tags[kNew]) { // rename key if already in use
|
|
kNew = base + '_' + suffix++;
|
|
}
|
|
}
|
|
tag[kOld] = undefined;
|
|
tag[kNew] = d.value;
|
|
d.key = kNew; // Maintain DOM identity through the subsequent update.
|
|
this.value = kNew;
|
|
event.change(tag);
|
|
}
|
|
|
|
function valueChange(d) {
|
|
var tag = {};
|
|
tag[d.key] = this.value;
|
|
event.change(tag);
|
|
}
|
|
|
|
function removeTag(d) {
|
|
var tag = {};
|
|
tag[d.key] = undefined;
|
|
event.change(tag);
|
|
d3.select(this.parentNode).remove();
|
|
}
|
|
|
|
function addTag() {
|
|
// Wrapped in a setTimeout in case it's being called from a blur
|
|
// handler. Without the setTimeout, the call to `content` would
|
|
// wipe out the pending value change.
|
|
setTimeout(function() {
|
|
showBlank = true;
|
|
content($wrap);
|
|
$list.selectAll('li:last-child input.key').node().focus();
|
|
}, 0);
|
|
}
|
|
}
|
|
|
|
rawTagEditor.state = function(_) {
|
|
if (!arguments.length) return state;
|
|
state = _;
|
|
return rawTagEditor;
|
|
};
|
|
|
|
rawTagEditor.preset = function(_) {
|
|
if (!arguments.length) return preset;
|
|
preset = _;
|
|
return rawTagEditor;
|
|
};
|
|
|
|
rawTagEditor.tags = function(_) {
|
|
if (!arguments.length) return tags;
|
|
tags = _;
|
|
return rawTagEditor;
|
|
};
|
|
|
|
rawTagEditor.entityID = function(_) {
|
|
if (!arguments.length) return id;
|
|
id = _;
|
|
return rawTagEditor;
|
|
};
|
|
|
|
return d3.rebind(rawTagEditor, event, 'on');
|
|
}
|
|
|
|
function RawMemberEditor(context) {
|
|
var id;
|
|
|
|
function selectMember(d) {
|
|
d3.event.preventDefault();
|
|
context.enter(iD.modes.Select(context, [d.id]));
|
|
}
|
|
|
|
function changeRole(d) {
|
|
var role = d3.select(this).property('value');
|
|
var member = {id: d.id, type: d.type, role: role};
|
|
context.perform(
|
|
iD.actions.ChangeMember(d.relation.id, member, d.index),
|
|
t('operations.change_role.annotation'));
|
|
}
|
|
|
|
function deleteMember(d) {
|
|
context.perform(
|
|
iD.actions.DeleteMember(d.relation.id, d.index),
|
|
t('operations.delete_member.annotation'));
|
|
|
|
if (!context.hasEntity(d.relation.id)) {
|
|
context.enter(iD.modes.Browse(context));
|
|
}
|
|
}
|
|
|
|
function rawMemberEditor(selection) {
|
|
var entity = context.entity(id),
|
|
memberships = [];
|
|
|
|
entity.members.forEach(function(member, index) {
|
|
memberships.push({
|
|
index: index,
|
|
id: member.id,
|
|
type: member.type,
|
|
role: member.role,
|
|
relation: entity,
|
|
member: context.hasEntity(member.id)
|
|
});
|
|
});
|
|
|
|
selection.call(Disclosure()
|
|
.title(t('inspector.all_members') + ' (' + memberships.length + ')')
|
|
.expanded(true)
|
|
.on('toggled', toggled)
|
|
.content(content));
|
|
|
|
function toggled(expanded) {
|
|
if (expanded) {
|
|
selection.node().parentNode.scrollTop += 200;
|
|
}
|
|
}
|
|
|
|
function content($wrap) {
|
|
var $list = $wrap.selectAll('.member-list')
|
|
.data([0]);
|
|
|
|
$list.enter().append('ul')
|
|
.attr('class', 'member-list');
|
|
|
|
var $items = $list.selectAll('li')
|
|
.data(memberships, function(d) {
|
|
return iD.Entity.key(d.relation) + ',' + d.index + ',' +
|
|
(d.member ? iD.Entity.key(d.member) : 'incomplete');
|
|
});
|
|
|
|
var $enter = $items.enter().append('li')
|
|
.attr('class', 'member-row form-field')
|
|
.classed('member-incomplete', function(d) { return !d.member; });
|
|
|
|
$enter.each(function(d) {
|
|
if (d.member) {
|
|
var $label = d3.select(this).append('label')
|
|
.attr('class', 'form-label')
|
|
.append('a')
|
|
.attr('href', '#')
|
|
.on('click', selectMember);
|
|
|
|
$label.append('span')
|
|
.attr('class', 'member-entity-type')
|
|
.text(function(d) { return context.presets().match(d.member, context.graph()).name(); });
|
|
|
|
$label.append('span')
|
|
.attr('class', 'member-entity-name')
|
|
.text(function(d) { return iD.util.displayName(d.member); });
|
|
|
|
} else {
|
|
d3.select(this).append('label')
|
|
.attr('class', 'form-label')
|
|
.text(t('inspector.incomplete'));
|
|
}
|
|
});
|
|
|
|
$enter.append('input')
|
|
.attr('class', 'member-role')
|
|
.property('type', 'text')
|
|
.attr('maxlength', 255)
|
|
.attr('placeholder', t('inspector.role'))
|
|
.property('value', function(d) { return d.role; })
|
|
.on('change', changeRole);
|
|
|
|
$enter.append('button')
|
|
.attr('tabindex', -1)
|
|
.attr('class', 'remove button-input-action member-delete minor')
|
|
.on('click', deleteMember)
|
|
.call(iD.svg.Icon('#operation-delete'));
|
|
|
|
$items.exit()
|
|
.remove();
|
|
}
|
|
}
|
|
|
|
rawMemberEditor.entityID = function(_) {
|
|
if (!arguments.length) return id;
|
|
id = _;
|
|
return rawMemberEditor;
|
|
};
|
|
|
|
return rawMemberEditor;
|
|
}
|
|
|
|
function RawMembershipEditor(context) {
|
|
var id, showBlank;
|
|
|
|
function selectRelation(d) {
|
|
d3.event.preventDefault();
|
|
context.enter(iD.modes.Select(context, [d.relation.id]));
|
|
}
|
|
|
|
function changeRole(d) {
|
|
var role = d3.select(this).property('value');
|
|
context.perform(
|
|
iD.actions.ChangeMember(d.relation.id, _.extend({}, d.member, {role: role}), d.index),
|
|
t('operations.change_role.annotation'));
|
|
}
|
|
|
|
function addMembership(d, role) {
|
|
showBlank = false;
|
|
|
|
if (d.relation) {
|
|
context.perform(
|
|
iD.actions.AddMember(d.relation.id, {id: id, type: context.entity(id).type, role: role}),
|
|
t('operations.add_member.annotation'));
|
|
|
|
} else {
|
|
var relation = iD.Relation();
|
|
|
|
context.perform(
|
|
iD.actions.AddEntity(relation),
|
|
iD.actions.AddMember(relation.id, {id: id, type: context.entity(id).type, role: role}),
|
|
t('operations.add.annotation.relation'));
|
|
|
|
context.enter(iD.modes.Select(context, [relation.id]));
|
|
}
|
|
}
|
|
|
|
function deleteMembership(d) {
|
|
context.perform(
|
|
iD.actions.DeleteMember(d.relation.id, d.index),
|
|
t('operations.delete_member.annotation'));
|
|
}
|
|
|
|
function relations(q) {
|
|
var newRelation = {
|
|
relation: null,
|
|
value: t('inspector.new_relation')
|
|
},
|
|
result = [],
|
|
graph = context.graph();
|
|
|
|
context.intersects(context.extent()).forEach(function(entity) {
|
|
if (entity.type !== 'relation' || entity.id === id)
|
|
return;
|
|
|
|
var presetName = context.presets().match(entity, graph).name(),
|
|
entityName = iD.util.displayName(entity) || '';
|
|
|
|
var value = presetName + ' ' + entityName;
|
|
if (q && value.toLowerCase().indexOf(q.toLowerCase()) === -1)
|
|
return;
|
|
|
|
result.push({
|
|
relation: entity,
|
|
value: value
|
|
});
|
|
});
|
|
|
|
result.sort(function(a, b) {
|
|
return iD.Relation.creationOrder(a.relation, b.relation);
|
|
});
|
|
|
|
// Dedupe identical names by appending relation id - see #2891
|
|
var dupeGroups = _(result)
|
|
.groupBy('value')
|
|
.filter(function(v) { return v.length > 1; })
|
|
.value();
|
|
|
|
dupeGroups.forEach(function(group) {
|
|
group.forEach(function(obj) {
|
|
obj.value += ' ' + obj.relation.id;
|
|
});
|
|
});
|
|
|
|
result.unshift(newRelation);
|
|
|
|
return result;
|
|
}
|
|
|
|
function rawMembershipEditor(selection) {
|
|
var entity = context.entity(id),
|
|
memberships = [];
|
|
|
|
context.graph().parentRelations(entity).forEach(function(relation) {
|
|
relation.members.forEach(function(member, index) {
|
|
if (member.id === entity.id) {
|
|
memberships.push({relation: relation, member: member, index: index});
|
|
}
|
|
});
|
|
});
|
|
|
|
selection.call(Disclosure()
|
|
.title(t('inspector.all_relations') + ' (' + memberships.length + ')')
|
|
.expanded(true)
|
|
.on('toggled', toggled)
|
|
.content(content));
|
|
|
|
function toggled(expanded) {
|
|
if (expanded) {
|
|
selection.node().parentNode.scrollTop += 200;
|
|
}
|
|
}
|
|
|
|
function content($wrap) {
|
|
var $list = $wrap.selectAll('.member-list')
|
|
.data([0]);
|
|
|
|
$list.enter().append('ul')
|
|
.attr('class', 'member-list');
|
|
|
|
var $items = $list.selectAll('li.member-row-normal')
|
|
.data(memberships, function(d) { return iD.Entity.key(d.relation) + ',' + d.index; });
|
|
|
|
var $enter = $items.enter().append('li')
|
|
.attr('class', 'member-row member-row-normal form-field');
|
|
|
|
var $label = $enter.append('label')
|
|
.attr('class', 'form-label')
|
|
.append('a')
|
|
.attr('href', '#')
|
|
.on('click', selectRelation);
|
|
|
|
$label.append('span')
|
|
.attr('class', 'member-entity-type')
|
|
.text(function(d) { return context.presets().match(d.relation, context.graph()).name(); });
|
|
|
|
$label.append('span')
|
|
.attr('class', 'member-entity-name')
|
|
.text(function(d) { return iD.util.displayName(d.relation); });
|
|
|
|
$enter.append('input')
|
|
.attr('class', 'member-role')
|
|
.property('type', 'text')
|
|
.attr('maxlength', 255)
|
|
.attr('placeholder', t('inspector.role'))
|
|
.property('value', function(d) { return d.member.role; })
|
|
.on('change', changeRole);
|
|
|
|
$enter.append('button')
|
|
.attr('tabindex', -1)
|
|
.attr('class', 'remove button-input-action member-delete minor')
|
|
.on('click', deleteMembership)
|
|
.call(iD.svg.Icon('#operation-delete'));
|
|
|
|
$items.exit()
|
|
.remove();
|
|
|
|
if (showBlank) {
|
|
var $new = $list.selectAll('.member-row-new')
|
|
.data([0]);
|
|
|
|
$enter = $new.enter().append('li')
|
|
.attr('class', 'member-row member-row-new form-field');
|
|
|
|
$enter.append('input')
|
|
.attr('type', 'text')
|
|
.attr('class', 'member-entity-input')
|
|
.call(d3.combobox()
|
|
.minItems(1)
|
|
.fetcher(function(value, callback) {
|
|
callback(relations(value));
|
|
})
|
|
.on('accept', function(d) {
|
|
addMembership(d, $new.select('.member-role').property('value'));
|
|
}));
|
|
|
|
$enter.append('input')
|
|
.attr('class', 'member-role')
|
|
.property('type', 'text')
|
|
.attr('maxlength', 255)
|
|
.attr('placeholder', t('inspector.role'))
|
|
.on('change', changeRole);
|
|
|
|
$enter.append('button')
|
|
.attr('tabindex', -1)
|
|
.attr('class', 'remove button-input-action member-delete minor')
|
|
.on('click', deleteMembership)
|
|
.call(iD.svg.Icon('#operation-delete'));
|
|
|
|
} else {
|
|
$list.selectAll('.member-row-new')
|
|
.remove();
|
|
}
|
|
|
|
var $add = $wrap.selectAll('.add-relation')
|
|
.data([0]);
|
|
|
|
$add.enter()
|
|
.append('button')
|
|
.attr('class', 'add-relation')
|
|
.call(iD.svg.Icon('#icon-plus', 'light'));
|
|
|
|
$wrap.selectAll('.add-relation')
|
|
.on('click', function() {
|
|
showBlank = true;
|
|
content($wrap);
|
|
$list.selectAll('.member-entity-input').node().focus();
|
|
});
|
|
}
|
|
}
|
|
|
|
rawMembershipEditor.entityID = function(_) {
|
|
if (!arguments.length) return id;
|
|
id = _;
|
|
return rawMembershipEditor;
|
|
};
|
|
|
|
return rawMembershipEditor;
|
|
}
|
|
|
|
function EntityEditor(context) {
|
|
var dispatch = d3.dispatch('choose'),
|
|
state = 'select',
|
|
coalesceChanges = false,
|
|
modified = false,
|
|
base,
|
|
id,
|
|
preset$$,
|
|
reference;
|
|
|
|
var presetEditor = preset(context)
|
|
.on('change', changeTags);
|
|
var rawTagEditor = RawTagEditor(context)
|
|
.on('change', changeTags);
|
|
|
|
function entityEditor(selection) {
|
|
var entity = context.entity(id),
|
|
tags = _.clone(entity.tags);
|
|
|
|
var $header = selection.selectAll('.header')
|
|
.data([0]);
|
|
|
|
// Enter
|
|
var $enter = $header.enter().append('div')
|
|
.attr('class', 'header fillL cf');
|
|
|
|
$enter.append('button')
|
|
.attr('class', 'fl preset-reset preset-choose')
|
|
.append('span')
|
|
.html('◄');
|
|
|
|
$enter.append('button')
|
|
.attr('class', 'fr preset-close')
|
|
.call(iD.svg.Icon(modified ? '#icon-apply' : '#icon-close'));
|
|
|
|
$enter.append('h3');
|
|
|
|
// Update
|
|
$header.select('h3')
|
|
.text(t('inspector.edit'));
|
|
|
|
$header.select('.preset-close')
|
|
.on('click', function() {
|
|
context.enter(iD.modes.Browse(context));
|
|
});
|
|
|
|
var $body = selection.selectAll('.inspector-body')
|
|
.data([0]);
|
|
|
|
// Enter
|
|
$enter = $body.enter().append('div')
|
|
.attr('class', 'inspector-body');
|
|
|
|
$enter.append('div')
|
|
.attr('class', 'preset-list-item inspector-inner')
|
|
.append('div')
|
|
.attr('class', 'preset-list-button-wrap')
|
|
.append('button')
|
|
.attr('class', 'preset-list-button preset-reset')
|
|
.call(bootstrap.tooltip()
|
|
.title(t('inspector.back_tooltip'))
|
|
.placement('bottom'))
|
|
.append('div')
|
|
.attr('class', 'label');
|
|
|
|
$body.select('.preset-list-button-wrap')
|
|
.call(reference.button);
|
|
|
|
$body.select('.preset-list-item')
|
|
.call(reference.body);
|
|
|
|
$enter.append('div')
|
|
.attr('class', 'inspector-border inspector-preset');
|
|
|
|
$enter.append('div')
|
|
.attr('class', 'inspector-border raw-tag-editor inspector-inner');
|
|
|
|
$enter.append('div')
|
|
.attr('class', 'inspector-border raw-member-editor inspector-inner');
|
|
|
|
$enter.append('div')
|
|
.attr('class', 'raw-membership-editor inspector-inner');
|
|
|
|
selection.selectAll('.preset-reset')
|
|
.on('click', function() {
|
|
dispatch.choose(preset$$);
|
|
});
|
|
|
|
// Update
|
|
$body.select('.preset-list-item button')
|
|
.call(PresetIcon()
|
|
.geometry(context.geometry(id))
|
|
.preset(preset$$));
|
|
|
|
$body.select('.preset-list-item .label')
|
|
.text(preset$$.name());
|
|
|
|
$body.select('.inspector-preset')
|
|
.call(presetEditor
|
|
.preset(preset$$)
|
|
.entityID(id)
|
|
.tags(tags)
|
|
.state(state));
|
|
|
|
$body.select('.raw-tag-editor')
|
|
.call(rawTagEditor
|
|
.preset(preset$$)
|
|
.entityID(id)
|
|
.tags(tags)
|
|
.state(state));
|
|
|
|
if (entity.type === 'relation') {
|
|
$body.select('.raw-member-editor')
|
|
.style('display', 'block')
|
|
.call(RawMemberEditor(context)
|
|
.entityID(id));
|
|
} else {
|
|
$body.select('.raw-member-editor')
|
|
.style('display', 'none');
|
|
}
|
|
|
|
$body.select('.raw-membership-editor')
|
|
.call(RawMembershipEditor(context)
|
|
.entityID(id));
|
|
|
|
function historyChanged() {
|
|
if (state === 'hide') return;
|
|
|
|
var entity = context.hasEntity(id),
|
|
graph = context.graph();
|
|
if (!entity) return;
|
|
|
|
entityEditor.preset(context.presets().match(entity, graph));
|
|
entityEditor.modified(base !== graph);
|
|
entityEditor(selection);
|
|
}
|
|
|
|
context.history()
|
|
.on('change.entity-editor', historyChanged);
|
|
}
|
|
|
|
function clean(o) {
|
|
|
|
function cleanVal(k, v) {
|
|
function keepSpaces(k) {
|
|
var whitelist = ['opening_hours', 'service_times', 'collection_times',
|
|
'operating_times', 'smoking_hours', 'happy_hours'];
|
|
return _.some(whitelist, function(s) { return k.indexOf(s) !== -1; });
|
|
}
|
|
|
|
var blacklist = ['description', 'note', 'fixme'];
|
|
if (_.some(blacklist, function(s) { return k.indexOf(s) !== -1; })) return v;
|
|
|
|
var cleaned = v.split(';')
|
|
.map(function(s) { return s.trim(); })
|
|
.join(keepSpaces(k) ? '; ' : ';');
|
|
|
|
// The code below is not intended to validate websites and emails.
|
|
// It is only intended to prevent obvious copy-paste errors. (#2323)
|
|
|
|
// clean website- and email-like tags
|
|
if (k.indexOf('website') !== -1 ||
|
|
k.indexOf('email') !== -1 ||
|
|
cleaned.indexOf('http') === 0) {
|
|
cleaned = cleaned
|
|
.replace(/[\u200B-\u200F\uFEFF]/g, ''); // strip LRM and other zero width chars
|
|
|
|
}
|
|
|
|
return cleaned;
|
|
}
|
|
|
|
var out = {}, k, v;
|
|
for (k in o) {
|
|
if (k && (v = o[k]) !== undefined) {
|
|
out[k] = cleanVal(k, v);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// Tag changes that fire on input can all get coalesced into a single
|
|
// history operation when the user leaves the field. #2342
|
|
function changeTags(changed, onInput) {
|
|
var entity = context.entity(id),
|
|
annotation = t('operations.change_tags.annotation'),
|
|
tags = _.extend({}, entity.tags, changed);
|
|
|
|
if (!onInput) {
|
|
tags = clean(tags);
|
|
}
|
|
if (!_.isEqual(entity.tags, tags)) {
|
|
if (coalesceChanges) {
|
|
context.overwrite(iD.actions.ChangeTags(id, tags), annotation);
|
|
} else {
|
|
context.perform(iD.actions.ChangeTags(id, tags), annotation);
|
|
coalesceChanges = !!onInput;
|
|
}
|
|
}
|
|
}
|
|
|
|
entityEditor.modified = function(_) {
|
|
if (!arguments.length) return modified;
|
|
modified = _;
|
|
d3.selectAll('button.preset-close use')
|
|
.attr('xlink:href', (modified ? '#icon-apply' : '#icon-close'));
|
|
};
|
|
|
|
entityEditor.state = function(_) {
|
|
if (!arguments.length) return state;
|
|
state = _;
|
|
return entityEditor;
|
|
};
|
|
|
|
entityEditor.entityID = function(_) {
|
|
if (!arguments.length) return id;
|
|
id = _;
|
|
base = context.graph();
|
|
entityEditor.preset(context.presets().match(context.entity(id), base));
|
|
entityEditor.modified(false);
|
|
coalesceChanges = false;
|
|
return entityEditor;
|
|
};
|
|
|
|
entityEditor.preset = function(_) {
|
|
if (!arguments.length) return preset$$;
|
|
if (_ !== preset$$) {
|
|
preset$$ = _;
|
|
reference = TagReference(preset$$.reference(context.geometry(id)), context)
|
|
.showing(false);
|
|
}
|
|
return entityEditor;
|
|
};
|
|
|
|
return d3.rebind(entityEditor, dispatch, 'on');
|
|
}
|
|
|
|
function FeatureInfo(context) {
|
|
function update(selection) {
|
|
var features = context.features(),
|
|
stats = features.stats(),
|
|
count = 0,
|
|
hiddenList = _.compact(_.map(features.hidden(), function(k) {
|
|
if (stats[k]) {
|
|
count += stats[k];
|
|
return String(stats[k]) + ' ' + t('feature.' + k + '.description');
|
|
}
|
|
}));
|
|
|
|
selection.html('');
|
|
|
|
if (hiddenList.length) {
|
|
var tooltip = bootstrap.tooltip()
|
|
.placement('top')
|
|
.html(true)
|
|
.title(function() {
|
|
return iD.ui.tooltipHtml(hiddenList.join('<br/>'));
|
|
});
|
|
|
|
var warning = selection.append('a')
|
|
.attr('href', '#')
|
|
.attr('tabindex', -1)
|
|
.html(t('feature_info.hidden_warning', { count: count }))
|
|
.call(tooltip)
|
|
.on('click', function() {
|
|
tooltip.hide(warning);
|
|
// open map data panel?
|
|
d3.event.preventDefault();
|
|
});
|
|
}
|
|
|
|
selection
|
|
.classed('hide', !hiddenList.length);
|
|
}
|
|
|
|
return function(selection) {
|
|
update(selection);
|
|
|
|
context.features().on('change.feature_info', function() {
|
|
update(selection);
|
|
});
|
|
};
|
|
}
|
|
|
|
function FeatureList(context) {
|
|
var geocodeResults;
|
|
|
|
function featureList(selection) {
|
|
var header = selection.append('div')
|
|
.attr('class', 'header fillL cf');
|
|
|
|
header.append('h3')
|
|
.text(t('inspector.feature_list'));
|
|
|
|
function keypress() {
|
|
var q = search.property('value'),
|
|
items = list.selectAll('.feature-list-item');
|
|
if (d3.event.keyCode === 13 && q.length && items.size()) {
|
|
click(items.datum());
|
|
}
|
|
}
|
|
|
|
function inputevent() {
|
|
geocodeResults = undefined;
|
|
drawList();
|
|
}
|
|
|
|
var searchWrap = selection.append('div')
|
|
.attr('class', 'search-header');
|
|
|
|
var search = searchWrap.append('input')
|
|
.attr('placeholder', t('inspector.search'))
|
|
.attr('type', 'search')
|
|
.on('keypress', keypress)
|
|
.on('input', inputevent);
|
|
|
|
searchWrap
|
|
.call(iD.svg.Icon('#icon-search', 'pre-text'));
|
|
|
|
var listWrap = selection.append('div')
|
|
.attr('class', 'inspector-body');
|
|
|
|
var list = listWrap.append('div')
|
|
.attr('class', 'feature-list cf');
|
|
|
|
context
|
|
.on('exit.feature-list', clearSearch);
|
|
context.map()
|
|
.on('drawn.feature-list', mapDrawn);
|
|
|
|
function clearSearch() {
|
|
search.property('value', '');
|
|
drawList();
|
|
}
|
|
|
|
function mapDrawn(e) {
|
|
if (e.full) {
|
|
drawList();
|
|
}
|
|
}
|
|
|
|
function features() {
|
|
var entities = {},
|
|
result = [],
|
|
graph = context.graph(),
|
|
q = search.property('value').toLowerCase();
|
|
|
|
if (!q) return result;
|
|
|
|
var idMatch = q.match(/^([nwr])([0-9]+)$/);
|
|
|
|
if (idMatch) {
|
|
result.push({
|
|
id: idMatch[0],
|
|
geometry: idMatch[1] === 'n' ? 'point' : idMatch[1] === 'w' ? 'line' : 'relation',
|
|
type: idMatch[1] === 'n' ? t('inspector.node') : idMatch[1] === 'w' ? t('inspector.way') : t('inspector.relation'),
|
|
name: idMatch[2]
|
|
});
|
|
}
|
|
|
|
var locationMatch = sexagesimal.pair(q.toUpperCase()) || q.match(/^(-?\d+\.?\d*)\s+(-?\d+\.?\d*)$/);
|
|
|
|
if (locationMatch) {
|
|
var loc = [parseFloat(locationMatch[0]), parseFloat(locationMatch[1])];
|
|
result.push({
|
|
id: -1,
|
|
geometry: 'point',
|
|
type: t('inspector.location'),
|
|
name: loc[0].toFixed(6) + ', ' + loc[1].toFixed(6),
|
|
location: loc
|
|
});
|
|
}
|
|
|
|
function addEntity(entity) {
|
|
if (entity.id in entities || result.length > 200)
|
|
return;
|
|
|
|
entities[entity.id] = true;
|
|
|
|
var name = iD.util.displayName(entity) || '';
|
|
if (name.toLowerCase().indexOf(q) >= 0) {
|
|
result.push({
|
|
id: entity.id,
|
|
entity: entity,
|
|
geometry: context.geometry(entity.id),
|
|
type: context.presets().match(entity, graph).name(),
|
|
name: name
|
|
});
|
|
}
|
|
|
|
graph.parentRelations(entity).forEach(function(parent) {
|
|
addEntity(parent);
|
|
});
|
|
}
|
|
|
|
var visible = context.surface().selectAll('.point, .line, .area')[0];
|
|
for (var i = 0; i < visible.length && result.length <= 200; i++) {
|
|
addEntity(visible[i].__data__);
|
|
}
|
|
|
|
(geocodeResults || []).forEach(function(d) {
|
|
// https://github.com/openstreetmap/iD/issues/1890
|
|
if (d.osm_type && d.osm_id) {
|
|
result.push({
|
|
id: iD.Entity.id.fromOSM(d.osm_type, d.osm_id),
|
|
geometry: d.osm_type === 'relation' ? 'relation' : d.osm_type === 'way' ? 'line' : 'point',
|
|
type: d.type !== 'yes' ? (d.type.charAt(0).toUpperCase() + d.type.slice(1)).replace('_', ' ')
|
|
: (d.class.charAt(0).toUpperCase() + d.class.slice(1)).replace('_', ' '),
|
|
name: d.display_name,
|
|
extent: new iD.geo.Extent(
|
|
[parseFloat(d.boundingbox[3]), parseFloat(d.boundingbox[0])],
|
|
[parseFloat(d.boundingbox[2]), parseFloat(d.boundingbox[1])])
|
|
});
|
|
}
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
function drawList() {
|
|
var value = search.property('value'),
|
|
results = features();
|
|
|
|
list.classed('filtered', value.length);
|
|
|
|
var noResultsWorldwide = geocodeResults && geocodeResults.length === 0;
|
|
|
|
var resultsIndicator = list.selectAll('.no-results-item')
|
|
.data([0])
|
|
.enter().append('button')
|
|
.property('disabled', true)
|
|
.attr('class', 'no-results-item')
|
|
.call(iD.svg.Icon('#icon-alert', 'pre-text'));
|
|
|
|
resultsIndicator.append('span')
|
|
.attr('class', 'entity-name');
|
|
|
|
list.selectAll('.no-results-item .entity-name')
|
|
.text(noResultsWorldwide ? t('geocoder.no_results_worldwide') : t('geocoder.no_results_visible'));
|
|
|
|
list.selectAll('.geocode-item')
|
|
.data([0])
|
|
.enter().append('button')
|
|
.attr('class', 'geocode-item')
|
|
.on('click', geocode)
|
|
.append('div')
|
|
.attr('class', 'label')
|
|
.append('span')
|
|
.attr('class', 'entity-name')
|
|
.text(t('geocoder.search'));
|
|
|
|
list.selectAll('.no-results-item')
|
|
.style('display', (value.length && !results.length) ? 'block' : 'none');
|
|
|
|
list.selectAll('.geocode-item')
|
|
.style('display', (value && geocodeResults === undefined) ? 'block' : 'none');
|
|
|
|
list.selectAll('.feature-list-item')
|
|
.data([-1])
|
|
.remove();
|
|
|
|
var items = list.selectAll('.feature-list-item')
|
|
.data(results, function(d) { return d.id; });
|
|
|
|
var enter = items.enter()
|
|
.insert('button', '.geocode-item')
|
|
.attr('class', 'feature-list-item')
|
|
.on('mouseover', mouseover)
|
|
.on('mouseout', mouseout)
|
|
.on('click', click);
|
|
|
|
var label = enter
|
|
.append('div')
|
|
.attr('class', 'label');
|
|
|
|
label.each(function(d) {
|
|
d3.select(this)
|
|
.call(iD.svg.Icon('#icon-' + d.geometry, 'pre-text'));
|
|
});
|
|
|
|
label.append('span')
|
|
.attr('class', 'entity-type')
|
|
.text(function(d) { return d.type; });
|
|
|
|
label.append('span')
|
|
.attr('class', 'entity-name')
|
|
.text(function(d) { return d.name; });
|
|
|
|
enter.style('opacity', 0)
|
|
.transition()
|
|
.style('opacity', 1);
|
|
|
|
items.order();
|
|
|
|
items.exit()
|
|
.remove();
|
|
}
|
|
|
|
function mouseover(d) {
|
|
if (d.id === -1) return;
|
|
|
|
context.surface().selectAll(iD.util.entityOrMemberSelector([d.id], context.graph()))
|
|
.classed('hover', true);
|
|
}
|
|
|
|
function mouseout() {
|
|
context.surface().selectAll('.hover')
|
|
.classed('hover', false);
|
|
}
|
|
|
|
function click(d) {
|
|
d3.event.preventDefault();
|
|
if (d.location) {
|
|
context.map().centerZoom([d.location[1], d.location[0]], 20);
|
|
}
|
|
else if (d.entity) {
|
|
if (d.entity.type === 'node') {
|
|
context.map().center(d.entity.loc);
|
|
} else if (d.entity.type === 'way') {
|
|
var center = context.projection(context.map().center()),
|
|
edge = iD.geo.chooseEdge(context.childNodes(d.entity), center, context.projection);
|
|
context.map().center(edge.loc);
|
|
}
|
|
context.enter(iD.modes.Select(context, [d.entity.id]).suppressMenu(true));
|
|
} else {
|
|
context.zoomToEntity(d.id);
|
|
}
|
|
}
|
|
|
|
function geocode() {
|
|
var searchVal = encodeURIComponent(search.property('value'));
|
|
d3.json('https://nominatim.openstreetmap.org/search/' + searchVal + '?limit=10&format=json', function(err, resp) {
|
|
geocodeResults = resp || [];
|
|
drawList();
|
|
});
|
|
}
|
|
}
|
|
|
|
return featureList;
|
|
}
|
|
|
|
function flash(selection) {
|
|
var modal = modalModule(selection);
|
|
|
|
modal.select('.modal').classed('modal-flash', true);
|
|
|
|
modal.select('.content')
|
|
.classed('modal-section', true)
|
|
.append('div')
|
|
.attr('class', 'description');
|
|
|
|
modal.on('click.flash', function() { modal.remove(); });
|
|
|
|
setTimeout(function() {
|
|
modal.remove();
|
|
return true;
|
|
}, 1500);
|
|
|
|
return modal;
|
|
}
|
|
|
|
function FullScreen(context) {
|
|
var element = context.container().node(),
|
|
keybinding = d3.keybinding('full-screen');
|
|
// button;
|
|
|
|
function getFullScreenFn() {
|
|
if (element.requestFullscreen) {
|
|
return element.requestFullscreen;
|
|
} else if (element.msRequestFullscreen) {
|
|
return element.msRequestFullscreen;
|
|
} else if (element.mozRequestFullScreen) {
|
|
return element.mozRequestFullScreen;
|
|
} else if (element.webkitRequestFullscreen) {
|
|
return element.webkitRequestFullscreen;
|
|
}
|
|
}
|
|
|
|
function getExitFullScreenFn() {
|
|
if (document.exitFullscreen) {
|
|
return document.exitFullscreen;
|
|
} else if (document.msExitFullscreen) {
|
|
return document.msExitFullscreen;
|
|
} else if (document.mozCancelFullScreen) {
|
|
return document.mozCancelFullScreen;
|
|
} else if (document.webkitExitFullscreen) {
|
|
return document.webkitExitFullscreen;
|
|
}
|
|
}
|
|
|
|
function isFullScreen() {
|
|
return document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement ||
|
|
document.msFullscreenElement;
|
|
}
|
|
|
|
function isSupported() {
|
|
return !!getFullScreenFn();
|
|
}
|
|
|
|
function fullScreen() {
|
|
d3.event.preventDefault();
|
|
if (!isFullScreen()) {
|
|
// button.classed('active', true);
|
|
getFullScreenFn().apply(element);
|
|
} else {
|
|
// button.classed('active', false);
|
|
getExitFullScreenFn().apply(document);
|
|
}
|
|
}
|
|
|
|
return function() { // selection) {
|
|
if (!isSupported())
|
|
return;
|
|
|
|
// var tooltip = bootstrap.tooltip()
|
|
// .placement('left');
|
|
|
|
// button = selection.append('button')
|
|
// .attr('title', t('full_screen'))
|
|
// .attr('tabindex', -1)
|
|
// .on('click', fullScreen)
|
|
// .call(tooltip);
|
|
|
|
// button.append('span')
|
|
// .attr('class', 'icon full-screen');
|
|
|
|
keybinding
|
|
.on('f11', fullScreen)
|
|
.on(cmd('⌘⇧F'), fullScreen);
|
|
|
|
d3.select(document)
|
|
.call(keybinding);
|
|
};
|
|
}
|
|
|
|
function Loading(context) {
|
|
var message = '',
|
|
blocking = false,
|
|
modal;
|
|
|
|
var loading = function(selection) {
|
|
modal = modalModule(selection, blocking);
|
|
|
|
var loadertext = modal.select('.content')
|
|
.classed('loading-modal', true)
|
|
.append('div')
|
|
.attr('class', 'modal-section fillL');
|
|
|
|
loadertext.append('img')
|
|
.attr('class', 'loader')
|
|
.attr('src', context.imagePath('loader-white.gif'));
|
|
|
|
loadertext.append('h3')
|
|
.text(message);
|
|
|
|
modal.select('button.close')
|
|
.attr('class', 'hide');
|
|
|
|
return loading;
|
|
};
|
|
|
|
loading.message = function(_) {
|
|
if (!arguments.length) return message;
|
|
message = _;
|
|
return loading;
|
|
};
|
|
|
|
loading.blocking = function(_) {
|
|
if (!arguments.length) return blocking;
|
|
blocking = _;
|
|
return loading;
|
|
};
|
|
|
|
loading.close = function() {
|
|
modal.remove();
|
|
};
|
|
|
|
return loading;
|
|
}
|
|
|
|
function Geolocate(context) {
|
|
var geoOptions = { enableHighAccuracy: false, timeout: 6000 /* 6sec */ },
|
|
locating = Loading(context).message(t('geolocate.locating')).blocking(true),
|
|
timeoutId;
|
|
|
|
function click() {
|
|
context.enter(iD.modes.Browse(context));
|
|
context.container().call(locating);
|
|
navigator.geolocation.getCurrentPosition(success, error, geoOptions);
|
|
|
|
// This timeout ensures that we still call finish() even if
|
|
// the user declines to share their location in Firefox
|
|
timeoutId = setTimeout(finish, 10000 /* 10sec */ );
|
|
}
|
|
|
|
function success(position) {
|
|
var map = context.map(),
|
|
extent = iD.geo.Extent([position.coords.longitude, position.coords.latitude])
|
|
.padByMeters(position.coords.accuracy);
|
|
|
|
map.centerZoom(extent.center(), Math.min(20, map.extentZoom(extent)));
|
|
finish();
|
|
}
|
|
|
|
function error() {
|
|
finish();
|
|
}
|
|
|
|
function finish() {
|
|
locating.close(); // unblock ui
|
|
if (timeoutId) { clearTimeout(timeoutId); }
|
|
timeoutId = undefined;
|
|
}
|
|
|
|
return function(selection) {
|
|
if (!navigator.geolocation) return;
|
|
|
|
selection.append('button')
|
|
.attr('tabindex', -1)
|
|
.attr('title', t('geolocate.title'))
|
|
.on('click', click)
|
|
.call(iD.svg.Icon('#icon-geolocate', 'light'))
|
|
.call(bootstrap.tooltip()
|
|
.placement('left'));
|
|
};
|
|
}
|
|
|
|
function intro(context) {
|
|
var step;
|
|
|
|
function intro(selection) {
|
|
|
|
function localizedName(id) {
|
|
var features = {
|
|
n2140018997: 'city_hall',
|
|
n367813436: 'fire_department',
|
|
w203988286: 'memory_isle_park',
|
|
w203972937: 'riverwalk_trail',
|
|
w203972938: 'riverwalk_trail',
|
|
w203972940: 'riverwalk_trail',
|
|
w41785752: 'w_michigan_ave',
|
|
w134150789: 'w_michigan_ave',
|
|
w134150795: 'w_michigan_ave',
|
|
w134150800: 'w_michigan_ave',
|
|
w134150811: 'w_michigan_ave',
|
|
w134150802: 'e_michigan_ave',
|
|
w134150836: 'e_michigan_ave',
|
|
w41074896: 'e_michigan_ave',
|
|
w17965834: 'spring_st',
|
|
w203986457: 'scidmore_park',
|
|
w203049587: 'petting_zoo',
|
|
w17967397: 'n_andrews_st',
|
|
w17967315: 's_andrews_st',
|
|
w17967326: 'n_constantine_st',
|
|
w17966400: 's_constantine_st',
|
|
w170848823: 'rocky_river',
|
|
w170848824: 'rocky_river',
|
|
w170848331: 'rocky_river',
|
|
w17967752: 'railroad_dr',
|
|
w17965998: 'conrail_rr',
|
|
w134150845: 'conrail_rr',
|
|
w170989131: 'st_joseph_river',
|
|
w143497377: 'n_main_st',
|
|
w134150801: 's_main_st',
|
|
w134150830: 's_main_st',
|
|
w17966462: 's_main_st',
|
|
w17967734: 'water_st',
|
|
w17964996: 'foster_st',
|
|
w170848330: 'portage_river',
|
|
w17965351: 'flower_st',
|
|
w17965502: 'elm_st',
|
|
w17965402: 'walnut_st',
|
|
w17964793: 'morris_ave',
|
|
w17967444: 'east_st',
|
|
w17966984: 'portage_ave'
|
|
};
|
|
return features[id] && t('intro.graph.' + features[id]);
|
|
}
|
|
|
|
context.enter(iD.modes.Browse(context));
|
|
|
|
// Save current map state
|
|
var history = context.history().toJSON(),
|
|
hash = window.location.hash,
|
|
center = context.map().center(),
|
|
zoom = context.map().zoom(),
|
|
background = context.background().baseLayerSource(),
|
|
opacity = d3.selectAll('#map .layer-background').style('opacity'),
|
|
loadedTiles = context.connection().loadedTiles(),
|
|
baseEntities = context.history().graph().base().entities,
|
|
introGraph, name;
|
|
|
|
// Block saving
|
|
context.inIntro(true);
|
|
|
|
// Load semi-real data used in intro
|
|
context.connection().toggle(false).flush();
|
|
context.history().reset();
|
|
|
|
introGraph = JSON.parse(iD.introGraph);
|
|
for (var key in introGraph) {
|
|
introGraph[key] = iD.Entity(introGraph[key]);
|
|
name = localizedName(key);
|
|
if (name) {
|
|
introGraph[key].tags.name = name;
|
|
}
|
|
}
|
|
context.history().merge(d3.values(iD.Graph().load(introGraph).entities));
|
|
context.background().bing();
|
|
|
|
d3.selectAll('#map .layer-background').style('opacity', 1);
|
|
|
|
var curtain = d3.curtain();
|
|
selection.call(curtain);
|
|
|
|
function reveal(box, text, options) {
|
|
options = options || {};
|
|
if (text) curtain.reveal(box, text, options.tooltipClass, options.duration);
|
|
else curtain.reveal(box, '', '', options.duration);
|
|
}
|
|
|
|
var steps = ['navigation', 'point', 'area', 'line', 'startEditing'].map(function(step, i) {
|
|
var s = iD.ui.intro[step](context, reveal)
|
|
.on('done', function() {
|
|
entered.filter(function(d) {
|
|
return d.title === s.title;
|
|
}).classed('finished', true);
|
|
enter(steps[i + 1]);
|
|
});
|
|
return s;
|
|
});
|
|
|
|
steps[steps.length - 1].on('startEditing', function() {
|
|
curtain.remove();
|
|
navwrap.remove();
|
|
d3.selectAll('#map .layer-background').style('opacity', opacity);
|
|
context.connection().toggle(true).flush().loadedTiles(loadedTiles);
|
|
context.history().reset().merge(d3.values(baseEntities));
|
|
context.background().baseLayerSource(background);
|
|
if (history) context.history().fromJSON(history, false);
|
|
context.map().centerZoom(center, zoom);
|
|
window.location.replace(hash);
|
|
context.inIntro(false);
|
|
});
|
|
|
|
var navwrap = selection.append('div').attr('class', 'intro-nav-wrap fillD');
|
|
|
|
var buttonwrap = navwrap.append('div')
|
|
.attr('class', 'joined')
|
|
.selectAll('button.step');
|
|
|
|
var entered = buttonwrap
|
|
.data(steps)
|
|
.enter()
|
|
.append('button')
|
|
.attr('class', 'step')
|
|
.on('click', enter);
|
|
|
|
entered
|
|
.call(iD.svg.Icon('#icon-apply', 'pre-text'));
|
|
|
|
entered
|
|
.append('label')
|
|
.text(function(d) { return t(d.title); });
|
|
|
|
enter(steps[0]);
|
|
|
|
function enter (newStep) {
|
|
if (step) { step.exit(); }
|
|
|
|
context.enter(iD.modes.Browse(context));
|
|
|
|
step = newStep;
|
|
step.enter();
|
|
|
|
entered.classed('active', function(d) {
|
|
return d.title === step.title;
|
|
});
|
|
}
|
|
|
|
}
|
|
return intro;
|
|
}
|
|
|
|
function Help(context) {
|
|
var key = 'H';
|
|
|
|
var docKeys = [
|
|
'help.help',
|
|
'help.editing_saving',
|
|
'help.roads',
|
|
'help.gps',
|
|
'help.imagery',
|
|
'help.addresses',
|
|
'help.inspector',
|
|
'help.buildings',
|
|
'help.relations'];
|
|
|
|
var docs = docKeys.map(function(key) {
|
|
var text = t(key);
|
|
return {
|
|
title: text.split('\n')[0].replace('#', '').trim(),
|
|
html: marked(text.split('\n').slice(1).join('\n'))
|
|
};
|
|
});
|
|
|
|
function help(selection) {
|
|
|
|
function hide() {
|
|
setVisible(false);
|
|
}
|
|
|
|
function toggle() {
|
|
if (d3.event) d3.event.preventDefault();
|
|
tooltip.hide(button);
|
|
setVisible(!button.classed('active'));
|
|
}
|
|
|
|
function setVisible(show) {
|
|
if (show !== shown) {
|
|
button.classed('active', show);
|
|
shown = show;
|
|
|
|
if (show) {
|
|
selection.on('mousedown.help-inside', function() {
|
|
return d3.event.stopPropagation();
|
|
});
|
|
pane.style('display', 'block')
|
|
.style('right', '-500px')
|
|
.transition()
|
|
.duration(200)
|
|
.style('right', '0px');
|
|
} else {
|
|
pane.style('right', '0px')
|
|
.transition()
|
|
.duration(200)
|
|
.style('right', '-500px')
|
|
.each('end', function() {
|
|
d3.select(this).style('display', 'none');
|
|
});
|
|
selection.on('mousedown.help-inside', null);
|
|
}
|
|
}
|
|
}
|
|
|
|
function clickHelp(d, i) {
|
|
pane.property('scrollTop', 0);
|
|
doctitle.html(d.title);
|
|
body.html(d.html);
|
|
body.selectAll('a')
|
|
.attr('target', '_blank');
|
|
menuItems.classed('selected', function(m) {
|
|
return m.title === d.title;
|
|
});
|
|
|
|
nav.html('');
|
|
|
|
if (i > 0) {
|
|
var prevLink = nav.append('a')
|
|
.attr('class', 'previous')
|
|
.on('click', function() {
|
|
clickHelp(docs[i - 1], i - 1);
|
|
});
|
|
prevLink.append('span').html('◄ ' + docs[i - 1].title);
|
|
}
|
|
if (i < docs.length - 1) {
|
|
var nextLink = nav.append('a')
|
|
.attr('class', 'next')
|
|
.on('click', function() {
|
|
clickHelp(docs[i + 1], i + 1);
|
|
});
|
|
nextLink.append('span').html(docs[i + 1].title + ' ►');
|
|
}
|
|
}
|
|
|
|
function clickWalkthrough() {
|
|
d3.select(document.body).call(intro(context));
|
|
setVisible(false);
|
|
}
|
|
|
|
|
|
var pane = selection.append('div')
|
|
.attr('class', 'help-wrap map-overlay fillL col5 content hide'),
|
|
tooltip = bootstrap.tooltip()
|
|
.placement('left')
|
|
.html(true)
|
|
.title(iD.ui.tooltipHtml(t('help.title'), key)),
|
|
button = selection.append('button')
|
|
.attr('tabindex', -1)
|
|
.on('click', toggle)
|
|
.call(iD.svg.Icon('#icon-help', 'light'))
|
|
.call(tooltip),
|
|
shown = false;
|
|
|
|
|
|
var toc = pane.append('ul')
|
|
.attr('class', 'toc');
|
|
|
|
var menuItems = toc.selectAll('li')
|
|
.data(docs)
|
|
.enter()
|
|
.append('li')
|
|
.append('a')
|
|
.html(function(d) { return d.title; })
|
|
.on('click', clickHelp);
|
|
|
|
toc.append('li')
|
|
.attr('class','walkthrough')
|
|
.append('a')
|
|
.text(t('splash.walkthrough'))
|
|
.on('click', clickWalkthrough);
|
|
|
|
var content = pane.append('div')
|
|
.attr('class', 'left-content');
|
|
|
|
var doctitle = content.append('h2')
|
|
.text(t('help.title'));
|
|
|
|
var body = content.append('div')
|
|
.attr('class', 'body');
|
|
|
|
var nav = content.append('div')
|
|
.attr('class', 'nav');
|
|
|
|
clickHelp(docs[0], 0);
|
|
|
|
var keybinding = d3.keybinding('help')
|
|
.on(key, toggle)
|
|
.on('B', hide)
|
|
.on('F', hide);
|
|
|
|
d3.select(document)
|
|
.call(keybinding);
|
|
|
|
context.surface().on('mousedown.help-outside', hide);
|
|
context.container().on('mousedown.help-outside', hide);
|
|
}
|
|
|
|
return help;
|
|
}
|
|
|
|
function Info(context) {
|
|
var key = cmd('⌘I'),
|
|
imperial = (iD.detect().locale.toLowerCase() === 'en-us'),
|
|
hidden = true;
|
|
|
|
function info(selection) {
|
|
function radiansToMeters(r) {
|
|
// using WGS84 authalic radius (6371007.1809 m)
|
|
return r * 6371007.1809;
|
|
}
|
|
|
|
function steradiansToSqmeters(r) {
|
|
// http://gis.stackexchange.com/a/124857/40446
|
|
return r / 12.56637 * 510065621724000;
|
|
}
|
|
|
|
function toLineString(feature) {
|
|
if (feature.type === 'LineString') return feature;
|
|
|
|
var result = { type: 'LineString', coordinates: [] };
|
|
if (feature.type === 'Polygon') {
|
|
result.coordinates = feature.coordinates[0];
|
|
} else if (feature.type === 'MultiPolygon') {
|
|
result.coordinates = feature.coordinates[0][0];
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function displayLength(m) {
|
|
var d = m * (imperial ? 3.28084 : 1),
|
|
p, unit;
|
|
|
|
if (imperial) {
|
|
if (d >= 5280) {
|
|
d /= 5280;
|
|
unit = 'mi';
|
|
} else {
|
|
unit = 'ft';
|
|
}
|
|
} else {
|
|
if (d >= 1000) {
|
|
d /= 1000;
|
|
unit = 'km';
|
|
} else {
|
|
unit = 'm';
|
|
}
|
|
}
|
|
|
|
// drop unnecessary precision
|
|
p = d > 1000 ? 0 : d > 100 ? 1 : 2;
|
|
|
|
return String(d.toFixed(p)) + ' ' + unit;
|
|
}
|
|
|
|
function displayArea(m2) {
|
|
var d = m2 * (imperial ? 10.7639111056 : 1),
|
|
d1, d2, p1, p2, unit1, unit2;
|
|
|
|
if (imperial) {
|
|
if (d >= 6969600) { // > 0.25mi² show mi²
|
|
d1 = d / 27878400;
|
|
unit1 = 'mi²';
|
|
} else {
|
|
d1 = d;
|
|
unit1 = 'ft²';
|
|
}
|
|
|
|
if (d > 4356 && d < 43560000) { // 0.1 - 1000 acres
|
|
d2 = d / 43560;
|
|
unit2 = 'ac';
|
|
}
|
|
|
|
} else {
|
|
if (d >= 250000) { // > 0.25km² show km²
|
|
d1 = d / 1000000;
|
|
unit1 = 'km²';
|
|
} else {
|
|
d1 = d;
|
|
unit1 = 'm²';
|
|
}
|
|
|
|
if (d > 1000 && d < 10000000) { // 0.1 - 1000 hectares
|
|
d2 = d / 10000;
|
|
unit2 = 'ha';
|
|
}
|
|
}
|
|
|
|
// drop unnecessary precision
|
|
p1 = d1 > 1000 ? 0 : d1 > 100 ? 1 : 2;
|
|
p2 = d2 > 1000 ? 0 : d2 > 100 ? 1 : 2;
|
|
|
|
return String(d1.toFixed(p1)) + ' ' + unit1 +
|
|
(d2 ? ' (' + String(d2.toFixed(p2)) + ' ' + unit2 + ')' : '');
|
|
}
|
|
|
|
|
|
function redraw() {
|
|
if (hidden) return;
|
|
|
|
var resolver = context.graph(),
|
|
selected = _.filter(context.selectedIDs(), function(e) { return context.hasEntity(e); }),
|
|
singular = selected.length === 1 ? selected[0] : null,
|
|
extent = iD.geo.Extent(),
|
|
entity;
|
|
|
|
wrap.html('');
|
|
wrap.append('h4')
|
|
.attr('class', 'infobox-heading fillD')
|
|
.text(singular || t('infobox.selected', { n: selected.length }));
|
|
|
|
if (!selected.length) return;
|
|
|
|
var center;
|
|
for (var i = 0; i < selected.length; i++) {
|
|
entity = context.entity(selected[i]);
|
|
extent._extend(entity.extent(resolver));
|
|
}
|
|
center = extent.center();
|
|
|
|
|
|
var list = wrap.append('ul');
|
|
|
|
// multiple wrap, just display extent center..
|
|
if (!singular) {
|
|
list.append('li')
|
|
.text(t('infobox.center') + ': ' + center[0].toFixed(5) + ', ' + center[1].toFixed(5));
|
|
return;
|
|
}
|
|
|
|
// single wrap, display details..
|
|
if (!entity) return;
|
|
var geometry = entity.geometry(resolver);
|
|
|
|
if (geometry === 'line' || geometry === 'area') {
|
|
var closed = (entity.type === 'relation') || (entity.isClosed() && !entity.isDegenerate()),
|
|
feature = entity.asGeoJSON(resolver),
|
|
length = radiansToMeters(d3.geo.length(toLineString(feature))),
|
|
lengthLabel = t('infobox.' + (closed ? 'perimeter' : 'length')),
|
|
centroid = d3.geo.centroid(feature);
|
|
|
|
list.append('li')
|
|
.text(t('infobox.geometry') + ': ' +
|
|
(closed ? t('infobox.closed') + ' ' : '') + t('geometry.' + geometry) );
|
|
|
|
if (closed) {
|
|
var area = steradiansToSqmeters(entity.area(resolver));
|
|
list.append('li')
|
|
.text(t('infobox.area') + ': ' + displayArea(area));
|
|
}
|
|
|
|
list.append('li')
|
|
.text(lengthLabel + ': ' + displayLength(length));
|
|
|
|
list.append('li')
|
|
.text(t('infobox.centroid') + ': ' + centroid[0].toFixed(5) + ', ' + centroid[1].toFixed(5));
|
|
|
|
|
|
var toggle = imperial ? 'imperial' : 'metric';
|
|
wrap.append('a')
|
|
.text(t('infobox.' + toggle))
|
|
.attr('href', '#')
|
|
.attr('class', 'button')
|
|
.on('click', function() {
|
|
d3.event.preventDefault();
|
|
imperial = !imperial;
|
|
redraw();
|
|
});
|
|
|
|
} else {
|
|
var centerLabel = t('infobox.' + (entity.type === 'node' ? 'location' : 'center'));
|
|
|
|
list.append('li')
|
|
.text(t('infobox.geometry') + ': ' + t('geometry.' + geometry));
|
|
|
|
list.append('li')
|
|
.text(centerLabel + ': ' + center[0].toFixed(5) + ', ' + center[1].toFixed(5));
|
|
}
|
|
}
|
|
|
|
|
|
function toggle() {
|
|
if (d3.event) d3.event.preventDefault();
|
|
|
|
hidden = !hidden;
|
|
|
|
if (hidden) {
|
|
wrap
|
|
.style('display', 'block')
|
|
.style('opacity', 1)
|
|
.transition()
|
|
.duration(200)
|
|
.style('opacity', 0)
|
|
.each('end', function() {
|
|
d3.select(this).style('display', 'none');
|
|
});
|
|
} else {
|
|
wrap
|
|
.style('display', 'block')
|
|
.style('opacity', 0)
|
|
.transition()
|
|
.duration(200)
|
|
.style('opacity', 1);
|
|
|
|
redraw();
|
|
}
|
|
}
|
|
|
|
|
|
var wrap = selection.selectAll('.infobox')
|
|
.data([0]);
|
|
|
|
wrap.enter()
|
|
.append('div')
|
|
.attr('class', 'infobox fillD2')
|
|
.style('display', (hidden ? 'none' : 'block'));
|
|
|
|
context.map()
|
|
.on('drawn.info', redraw);
|
|
|
|
redraw();
|
|
|
|
var keybinding = d3.keybinding('info')
|
|
.on(key, toggle);
|
|
|
|
d3.select(document)
|
|
.call(keybinding);
|
|
}
|
|
|
|
return info;
|
|
}
|
|
|
|
function PresetList(context) {
|
|
var event = d3.dispatch('choose'),
|
|
id,
|
|
currentPreset,
|
|
autofocus = false;
|
|
|
|
function presetList(selection) {
|
|
var geometry = context.geometry(id),
|
|
presets = context.presets().matchGeometry(geometry);
|
|
|
|
selection.html('');
|
|
|
|
var messagewrap = selection.append('div')
|
|
.attr('class', 'header fillL cf');
|
|
|
|
var message = messagewrap.append('h3')
|
|
.text(t('inspector.choose'));
|
|
|
|
if (context.entity(id).isUsed(context.graph())) {
|
|
messagewrap.append('button')
|
|
.attr('class', 'preset-choose')
|
|
.on('click', function() { event.choose(currentPreset); })
|
|
.append('span')
|
|
.html('►');
|
|
} else {
|
|
messagewrap.append('button')
|
|
.attr('class', 'close')
|
|
.on('click', function() {
|
|
context.enter(iD.modes.Browse(context));
|
|
})
|
|
.call(iD.svg.Icon('#icon-close'));
|
|
}
|
|
|
|
function keydown() {
|
|
// hack to let delete shortcut work when search is autofocused
|
|
if (search.property('value').length === 0 &&
|
|
(d3.event.keyCode === d3.keybinding.keyCodes['⌫'] ||
|
|
d3.event.keyCode === d3.keybinding.keyCodes['⌦'])) {
|
|
d3.event.preventDefault();
|
|
d3.event.stopPropagation();
|
|
iD.operations.Delete([id], context)();
|
|
} else if (search.property('value').length === 0 &&
|
|
(d3.event.ctrlKey || d3.event.metaKey) &&
|
|
d3.event.keyCode === d3.keybinding.keyCodes.z) {
|
|
d3.event.preventDefault();
|
|
d3.event.stopPropagation();
|
|
context.undo();
|
|
} else if (!d3.event.ctrlKey && !d3.event.metaKey) {
|
|
d3.select(this).on('keydown', null);
|
|
}
|
|
}
|
|
|
|
function keypress() {
|
|
// enter
|
|
var value = search.property('value');
|
|
if (d3.event.keyCode === 13 && value.length) {
|
|
list.selectAll('.preset-list-item:first-child').datum().choose();
|
|
}
|
|
}
|
|
|
|
function inputevent() {
|
|
var value = search.property('value');
|
|
list.classed('filtered', value.length);
|
|
if (value.length) {
|
|
var results = presets.search(value, geometry);
|
|
message.text(t('inspector.results', {
|
|
n: results.collection.length,
|
|
search: value
|
|
}));
|
|
list.call(drawList, results);
|
|
} else {
|
|
list.call(drawList, context.presets().defaults(geometry, 36));
|
|
message.text(t('inspector.choose'));
|
|
}
|
|
}
|
|
|
|
var searchWrap = selection.append('div')
|
|
.attr('class', 'search-header');
|
|
|
|
var search = searchWrap.append('input')
|
|
.attr('class', 'preset-search-input')
|
|
.attr('placeholder', t('inspector.search'))
|
|
.attr('type', 'search')
|
|
.on('keydown', keydown)
|
|
.on('keypress', keypress)
|
|
.on('input', inputevent);
|
|
|
|
searchWrap
|
|
.call(iD.svg.Icon('#icon-search', 'pre-text'));
|
|
|
|
if (autofocus) {
|
|
search.node().focus();
|
|
}
|
|
|
|
var listWrap = selection.append('div')
|
|
.attr('class', 'inspector-body');
|
|
|
|
var list = listWrap.append('div')
|
|
.attr('class', 'preset-list fillL cf')
|
|
.call(drawList, context.presets().defaults(geometry, 36));
|
|
}
|
|
|
|
function drawList(list, presets) {
|
|
var collection = presets.collection.map(function(preset) {
|
|
return preset.members ? CategoryItem(preset) : PresetItem(preset);
|
|
});
|
|
|
|
var items = list.selectAll('.preset-list-item')
|
|
.data(collection, function(d) { return d.preset.id; });
|
|
|
|
items.enter().append('div')
|
|
.attr('class', function(item) { return 'preset-list-item preset-' + item.preset.id.replace('/', '-'); })
|
|
.classed('current', function(item) { return item.preset === currentPreset; })
|
|
.each(function(item) {
|
|
d3.select(this).call(item);
|
|
})
|
|
.style('opacity', 0)
|
|
.transition()
|
|
.style('opacity', 1);
|
|
|
|
items.order();
|
|
|
|
items.exit()
|
|
.remove();
|
|
}
|
|
|
|
function CategoryItem(preset) {
|
|
var box, sublist, shown = false;
|
|
|
|
function item(selection) {
|
|
var wrap = selection.append('div')
|
|
.attr('class', 'preset-list-button-wrap category col12');
|
|
|
|
wrap.append('button')
|
|
.attr('class', 'preset-list-button')
|
|
.classed('expanded', false)
|
|
.call(PresetIcon()
|
|
.geometry(context.geometry(id))
|
|
.preset(preset))
|
|
.on('click', function() {
|
|
var isExpanded = d3.select(this).classed('expanded');
|
|
var triangle = isExpanded ? '▶ ' : '▼ ';
|
|
d3.select(this).classed('expanded', !isExpanded);
|
|
d3.select(this).selectAll('.label').text(triangle + preset.name());
|
|
item.choose();
|
|
})
|
|
.append('div')
|
|
.attr('class', 'label')
|
|
.text(function() {
|
|
return '▶ ' + preset.name();
|
|
});
|
|
|
|
box = selection.append('div')
|
|
.attr('class', 'subgrid col12')
|
|
.style('max-height', '0px')
|
|
.style('opacity', 0);
|
|
|
|
box.append('div')
|
|
.attr('class', 'arrow');
|
|
|
|
sublist = box.append('div')
|
|
.attr('class', 'preset-list fillL3 cf fl');
|
|
}
|
|
|
|
item.choose = function() {
|
|
if (!box || !sublist) return;
|
|
|
|
if (shown) {
|
|
shown = false;
|
|
box.transition()
|
|
.duration(200)
|
|
.style('opacity', '0')
|
|
.style('max-height', '0px')
|
|
.style('padding-bottom', '0px');
|
|
} else {
|
|
shown = true;
|
|
sublist.call(drawList, preset.members);
|
|
box.transition()
|
|
.duration(200)
|
|
.style('opacity', '1')
|
|
.style('max-height', 200 + preset.members.collection.length * 80 + 'px')
|
|
.style('padding-bottom', '20px');
|
|
}
|
|
};
|
|
|
|
item.preset = preset;
|
|
|
|
return item;
|
|
}
|
|
|
|
function PresetItem(preset) {
|
|
function item(selection) {
|
|
var wrap = selection.append('div')
|
|
.attr('class', 'preset-list-button-wrap col12');
|
|
|
|
wrap.append('button')
|
|
.attr('class', 'preset-list-button')
|
|
.call(PresetIcon()
|
|
.geometry(context.geometry(id))
|
|
.preset(preset))
|
|
.on('click', item.choose)
|
|
.append('div')
|
|
.attr('class', 'label')
|
|
.text(preset.name());
|
|
|
|
wrap.call(item.reference.button);
|
|
selection.call(item.reference.body);
|
|
}
|
|
|
|
item.choose = function() {
|
|
context.presets().choose(preset);
|
|
|
|
context.perform(
|
|
iD.actions.ChangePreset(id, currentPreset, preset),
|
|
t('operations.change_tags.annotation'));
|
|
|
|
event.choose(preset);
|
|
};
|
|
|
|
item.help = function() {
|
|
d3.event.stopPropagation();
|
|
item.reference.toggle();
|
|
};
|
|
|
|
item.preset = preset;
|
|
item.reference = TagReference(preset.reference(context.geometry(id)), context);
|
|
|
|
return item;
|
|
}
|
|
|
|
presetList.autofocus = function(_) {
|
|
if (!arguments.length) return autofocus;
|
|
autofocus = _;
|
|
return presetList;
|
|
};
|
|
|
|
presetList.entityID = function(_) {
|
|
if (!arguments.length) return id;
|
|
id = _;
|
|
presetList.preset(context.presets().match(context.entity(id), context.graph()));
|
|
return presetList;
|
|
};
|
|
|
|
presetList.preset = function(_) {
|
|
if (!arguments.length) return currentPreset;
|
|
currentPreset = _;
|
|
return presetList;
|
|
};
|
|
|
|
return d3.rebind(presetList, event, 'on');
|
|
}
|
|
|
|
function ViewOnOSM(context) {
|
|
var id;
|
|
|
|
function viewOnOSM(selection) {
|
|
var entity = context.entity(id);
|
|
|
|
selection.style('display', entity.isNew() ? 'none' : null);
|
|
|
|
var $link = selection.selectAll('.view-on-osm')
|
|
.data([0]);
|
|
|
|
$link.enter()
|
|
.append('a')
|
|
.attr('class', 'view-on-osm')
|
|
.attr('target', '_blank')
|
|
.call(iD.svg.Icon('#icon-out-link', 'inline'))
|
|
.append('span')
|
|
.text(t('inspector.view_on_osm'));
|
|
|
|
$link
|
|
.attr('href', context.connection().entityURL(entity));
|
|
}
|
|
|
|
viewOnOSM.entityID = function(_) {
|
|
if (!arguments.length) return id;
|
|
id = _;
|
|
return viewOnOSM;
|
|
};
|
|
|
|
return viewOnOSM;
|
|
}
|
|
|
|
function Inspector(context) {
|
|
var presetList = PresetList(context),
|
|
entityEditor = EntityEditor(context),
|
|
state = 'select',
|
|
entityID,
|
|
newFeature = false;
|
|
|
|
function inspector(selection) {
|
|
presetList
|
|
.entityID(entityID)
|
|
.autofocus(newFeature)
|
|
.on('choose', setPreset);
|
|
|
|
entityEditor
|
|
.state(state)
|
|
.entityID(entityID)
|
|
.on('choose', showList);
|
|
|
|
var $wrap = selection.selectAll('.panewrap')
|
|
.data([0]);
|
|
|
|
var $enter = $wrap.enter().append('div')
|
|
.attr('class', 'panewrap');
|
|
|
|
$enter.append('div')
|
|
.attr('class', 'preset-list-pane pane');
|
|
|
|
$enter.append('div')
|
|
.attr('class', 'entity-editor-pane pane');
|
|
|
|
var $presetPane = $wrap.select('.preset-list-pane');
|
|
var $editorPane = $wrap.select('.entity-editor-pane');
|
|
|
|
var graph = context.graph(),
|
|
entity = context.entity(entityID),
|
|
showEditor = state === 'hover' ||
|
|
entity.isUsed(graph) ||
|
|
entity.isHighwayIntersection(graph);
|
|
|
|
if (showEditor) {
|
|
$wrap.style('right', '0%');
|
|
$editorPane.call(entityEditor);
|
|
} else {
|
|
$wrap.style('right', '-100%');
|
|
$presetPane.call(presetList);
|
|
}
|
|
|
|
var $footer = selection.selectAll('.footer')
|
|
.data([0]);
|
|
|
|
$footer.enter().append('div')
|
|
.attr('class', 'footer');
|
|
|
|
selection.select('.footer')
|
|
.call(ViewOnOSM(context)
|
|
.entityID(entityID));
|
|
|
|
function showList(preset) {
|
|
$wrap.transition()
|
|
.styleTween('right', function() { return d3.interpolate('0%', '-100%'); });
|
|
|
|
$presetPane.call(presetList
|
|
.preset(preset)
|
|
.autofocus(true));
|
|
}
|
|
|
|
function setPreset(preset) {
|
|
$wrap.transition()
|
|
.styleTween('right', function() { return d3.interpolate('-100%', '0%'); });
|
|
|
|
$editorPane.call(entityEditor
|
|
.preset(preset));
|
|
}
|
|
}
|
|
|
|
inspector.state = function(_) {
|
|
if (!arguments.length) return state;
|
|
state = _;
|
|
entityEditor.state(state);
|
|
return inspector;
|
|
};
|
|
|
|
inspector.entityID = function(_) {
|
|
if (!arguments.length) return entityID;
|
|
entityID = _;
|
|
return inspector;
|
|
};
|
|
|
|
inspector.newFeature = function(_) {
|
|
if (!arguments.length) return newFeature;
|
|
newFeature = _;
|
|
return inspector;
|
|
};
|
|
|
|
return inspector;
|
|
}
|
|
|
|
function Lasso(context) {
|
|
var group, polygon;
|
|
|
|
lasso.coordinates = [];
|
|
|
|
function lasso(selection) {
|
|
|
|
context.container().classed('lasso', true);
|
|
|
|
group = selection.append('g')
|
|
.attr('class', 'lasso hide');
|
|
|
|
polygon = group.append('path')
|
|
.attr('class', 'lasso-path');
|
|
|
|
group.call(Toggle(true));
|
|
|
|
}
|
|
|
|
function draw() {
|
|
if (polygon) {
|
|
polygon.data([lasso.coordinates])
|
|
.attr('d', function(d) { return 'M' + d.join(' L') + ' Z'; });
|
|
}
|
|
}
|
|
|
|
lasso.extent = function () {
|
|
return lasso.coordinates.reduce(function(extent, point) {
|
|
return extent.extend(iD.geo.Extent(point));
|
|
}, iD.geo.Extent());
|
|
};
|
|
|
|
lasso.p = function(_) {
|
|
if (!arguments.length) return lasso;
|
|
lasso.coordinates.push(_);
|
|
draw();
|
|
return lasso;
|
|
};
|
|
|
|
lasso.close = function() {
|
|
if (group) {
|
|
group.call(Toggle(false, function() {
|
|
d3.select(this).remove();
|
|
}));
|
|
}
|
|
context.container().classed('lasso', false);
|
|
};
|
|
|
|
return lasso;
|
|
}
|
|
|
|
function MapData(context) {
|
|
var key = 'F',
|
|
features = context.features().keys(),
|
|
layers = context.layers(),
|
|
fills = ['wireframe', 'partial', 'full'],
|
|
fillDefault = context.storage('area-fill') || 'partial',
|
|
fillSelected = fillDefault;
|
|
|
|
|
|
function map_data(selection) {
|
|
|
|
function showsFeature(d) {
|
|
return context.features().enabled(d);
|
|
}
|
|
|
|
function autoHiddenFeature(d) {
|
|
return context.features().autoHidden(d);
|
|
}
|
|
|
|
function clickFeature(d) {
|
|
context.features().toggle(d);
|
|
update();
|
|
}
|
|
|
|
function showsFill(d) {
|
|
return fillSelected === d;
|
|
}
|
|
|
|
function setFill(d) {
|
|
_.each(fills, function(opt) {
|
|
context.surface().classed('fill-' + opt, Boolean(opt === d));
|
|
});
|
|
|
|
fillSelected = d;
|
|
if (d !== 'wireframe') {
|
|
fillDefault = d;
|
|
context.storage('area-fill', d);
|
|
}
|
|
update();
|
|
}
|
|
|
|
function showsLayer(which) {
|
|
var layer = layers.layer(which);
|
|
if (layer) {
|
|
return layer.enabled();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function setLayer(which, enabled) {
|
|
var layer = layers.layer(which);
|
|
if (layer) {
|
|
layer.enabled(enabled);
|
|
update();
|
|
}
|
|
}
|
|
|
|
function toggleLayer(which) {
|
|
setLayer(which, !showsLayer(which));
|
|
}
|
|
|
|
function clickGpx() {
|
|
toggleLayer('gpx');
|
|
}
|
|
|
|
function clickMapillaryImages() {
|
|
toggleLayer('mapillary-images');
|
|
if (!showsLayer('mapillary-images')) {
|
|
setLayer('mapillary-signs', false);
|
|
}
|
|
}
|
|
|
|
function clickMapillarySigns() {
|
|
toggleLayer('mapillary-signs');
|
|
}
|
|
|
|
|
|
function drawMapillaryItems(selection) {
|
|
var mapillaryImages = layers.layer('mapillary-images'),
|
|
mapillarySigns = layers.layer('mapillary-signs'),
|
|
supportsMapillaryImages = mapillaryImages && mapillaryImages.supported(),
|
|
supportsMapillarySigns = mapillarySigns && mapillarySigns.supported(),
|
|
showsMapillaryImages = supportsMapillaryImages && mapillaryImages.enabled(),
|
|
showsMapillarySigns = supportsMapillarySigns && mapillarySigns.enabled();
|
|
|
|
var mapillaryList = selection
|
|
.selectAll('.layer-list-mapillary')
|
|
.data([0]);
|
|
|
|
// Enter
|
|
mapillaryList
|
|
.enter()
|
|
.append('ul')
|
|
.attr('class', 'layer-list layer-list-mapillary');
|
|
|
|
var mapillaryImageLayerItem = mapillaryList
|
|
.selectAll('.list-item-mapillary-images')
|
|
.data(supportsMapillaryImages ? [0] : []);
|
|
|
|
var enterImages = mapillaryImageLayerItem.enter()
|
|
.append('li')
|
|
.attr('class', 'list-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('.list-item-mapillary-signs')
|
|
.data(supportsMapillarySigns ? [0] : []);
|
|
|
|
var enterSigns = mapillarySignLayerItem.enter()
|
|
.append('li')
|
|
.attr('class', 'list-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
|
|
mapillaryImageLayerItem
|
|
.classed('active', showsMapillaryImages)
|
|
.selectAll('input')
|
|
.property('checked', showsMapillaryImages);
|
|
|
|
mapillarySignLayerItem
|
|
.classed('active', showsMapillarySigns)
|
|
.selectAll('input')
|
|
.property('disabled', !showsMapillaryImages)
|
|
.property('checked', showsMapillarySigns);
|
|
|
|
mapillarySignLayerItem
|
|
.selectAll('label')
|
|
.classed('deemphasize', !showsMapillaryImages);
|
|
|
|
// Exit
|
|
mapillaryImageLayerItem.exit()
|
|
.remove();
|
|
mapillarySignLayerItem.exit()
|
|
.remove();
|
|
}
|
|
|
|
|
|
function drawGpxItem(selection) {
|
|
var gpx = layers.layer('gpx'),
|
|
hasGpx = gpx && gpx.hasGpx(),
|
|
showsGpx = hasGpx && gpx.enabled();
|
|
|
|
var gpxLayerItem = selection
|
|
.selectAll('.layer-list-gpx')
|
|
.data(gpx ? [0] : []);
|
|
|
|
// Enter
|
|
var enter = gpxLayerItem.enter()
|
|
.append('ul')
|
|
.attr('class', 'layer-list layer-list-gpx')
|
|
.append('li')
|
|
.classed('list-item-gpx', true);
|
|
|
|
enter.append('button')
|
|
.attr('class', 'list-item-gpx-extent')
|
|
.call(bootstrap.tooltip()
|
|
.title(t('gpx.zoom'))
|
|
.placement('left'))
|
|
.on('click', function() {
|
|
d3.event.preventDefault();
|
|
d3.event.stopPropagation();
|
|
gpx.fitZoom();
|
|
})
|
|
.call(iD.svg.Icon('#icon-search'));
|
|
|
|
enter.append('button')
|
|
.attr('class', 'list-item-gpx-browse')
|
|
.call(bootstrap.tooltip()
|
|
.title(t('gpx.browse'))
|
|
.placement('left'))
|
|
.on('click', function() {
|
|
d3.select(document.createElement('input'))
|
|
.attr('type', 'file')
|
|
.on('change', function() {
|
|
gpx.files(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
|
|
gpxLayerItem
|
|
.classed('active', showsGpx)
|
|
.selectAll('input')
|
|
.property('disabled', !hasGpx)
|
|
.property('checked', showsGpx);
|
|
|
|
gpxLayerItem
|
|
.selectAll('label')
|
|
.classed('deemphasize', !hasGpx);
|
|
|
|
// Exit
|
|
gpxLayerItem.exit()
|
|
.remove();
|
|
}
|
|
|
|
|
|
function drawList(selection, data, type, name, change, active) {
|
|
var items = selection.selectAll('li')
|
|
.data(data);
|
|
|
|
// Enter
|
|
var enter = items.enter()
|
|
.append('li')
|
|
.attr('class', 'layer')
|
|
.call(bootstrap.tooltip()
|
|
.html(true)
|
|
.title(function(d) {
|
|
var tip = t(name + '.' + d + '.tooltip'),
|
|
key = (d === 'wireframe' ? 'W' : null);
|
|
|
|
if (name === 'feature' && autoHiddenFeature(d)) {
|
|
tip += '<div>' + t('map_data.autohidden') + '</div>';
|
|
}
|
|
return iD.ui.tooltipHtml(tip, key);
|
|
})
|
|
.placement('top')
|
|
);
|
|
|
|
var label = enter.append('label');
|
|
|
|
label.append('input')
|
|
.attr('type', type)
|
|
.attr('name', name)
|
|
.on('change', change);
|
|
|
|
label.append('span')
|
|
.text(function(d) { return t(name + '.' + d + '.description'); });
|
|
|
|
// Update
|
|
items
|
|
.classed('active', active)
|
|
.selectAll('input')
|
|
.property('checked', active)
|
|
.property('indeterminate', function(d) {
|
|
return (name === 'feature' && autoHiddenFeature(d));
|
|
});
|
|
|
|
// Exit
|
|
items.exit()
|
|
.remove();
|
|
}
|
|
|
|
|
|
function update() {
|
|
dataLayerContainer.call(drawMapillaryItems);
|
|
dataLayerContainer.call(drawGpxItem);
|
|
|
|
fillList.call(drawList, fills, 'radio', 'area_fill', setFill, showsFill);
|
|
|
|
featureList.call(drawList, features, 'checkbox', 'feature', clickFeature, showsFeature);
|
|
}
|
|
|
|
function hidePanel() {
|
|
setVisible(false);
|
|
}
|
|
|
|
function togglePanel() {
|
|
if (d3.event) d3.event.preventDefault();
|
|
tooltip.hide(button);
|
|
setVisible(!button.classed('active'));
|
|
}
|
|
|
|
function toggleWireframe() {
|
|
if (d3.event) {
|
|
d3.event.preventDefault();
|
|
d3.event.stopPropagation();
|
|
}
|
|
setFill((fillSelected === 'wireframe' ? fillDefault : 'wireframe'));
|
|
context.map().pan([0,0]); // trigger a redraw
|
|
}
|
|
|
|
function setVisible(show) {
|
|
if (show !== shown) {
|
|
button.classed('active', show);
|
|
shown = show;
|
|
|
|
if (show) {
|
|
update();
|
|
selection.on('mousedown.map_data-inside', function() {
|
|
return d3.event.stopPropagation();
|
|
});
|
|
content.style('display', 'block')
|
|
.style('right', '-300px')
|
|
.transition()
|
|
.duration(200)
|
|
.style('right', '0px');
|
|
} else {
|
|
content.style('display', 'block')
|
|
.style('right', '0px')
|
|
.transition()
|
|
.duration(200)
|
|
.style('right', '-300px')
|
|
.each('end', function() {
|
|
d3.select(this).style('display', 'none');
|
|
});
|
|
selection.on('mousedown.map_data-inside', null);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
var content = selection.append('div')
|
|
.attr('class', 'fillL map-overlay col3 content hide'),
|
|
tooltip = bootstrap.tooltip()
|
|
.placement('left')
|
|
.html(true)
|
|
.title(iD.ui.tooltipHtml(t('map_data.description'), key)),
|
|
button = selection.append('button')
|
|
.attr('tabindex', -1)
|
|
.on('click', togglePanel)
|
|
.call(iD.svg.Icon('#icon-data', 'light'))
|
|
.call(tooltip),
|
|
shown = false;
|
|
|
|
content.append('h4')
|
|
.text(t('map_data.title'));
|
|
|
|
|
|
// data layers
|
|
content.append('a')
|
|
.text(t('map_data.data_layers'))
|
|
.attr('href', '#')
|
|
.classed('hide-toggle', true)
|
|
.classed('expanded', true)
|
|
.on('click', function() {
|
|
var exp = d3.select(this).classed('expanded');
|
|
dataLayerContainer.style('display', exp ? 'none' : 'block');
|
|
d3.select(this).classed('expanded', !exp);
|
|
d3.event.preventDefault();
|
|
});
|
|
|
|
var dataLayerContainer = content.append('div')
|
|
.attr('class', 'data-data-layers')
|
|
.style('display', 'block');
|
|
|
|
|
|
// area fills
|
|
content.append('a')
|
|
.text(t('map_data.fill_area'))
|
|
.attr('href', '#')
|
|
.classed('hide-toggle', true)
|
|
.classed('expanded', false)
|
|
.on('click', function() {
|
|
var exp = d3.select(this).classed('expanded');
|
|
fillContainer.style('display', exp ? 'none' : 'block');
|
|
d3.select(this).classed('expanded', !exp);
|
|
d3.event.preventDefault();
|
|
});
|
|
|
|
var fillContainer = content.append('div')
|
|
.attr('class', 'data-area-fills')
|
|
.style('display', 'none');
|
|
|
|
var fillList = fillContainer.append('ul')
|
|
.attr('class', 'layer-list layer-fill-list');
|
|
|
|
|
|
// feature filters
|
|
content.append('a')
|
|
.text(t('map_data.map_features'))
|
|
.attr('href', '#')
|
|
.classed('hide-toggle', true)
|
|
.classed('expanded', false)
|
|
.on('click', function() {
|
|
var exp = d3.select(this).classed('expanded');
|
|
featureContainer.style('display', exp ? 'none' : 'block');
|
|
d3.select(this).classed('expanded', !exp);
|
|
d3.event.preventDefault();
|
|
});
|
|
|
|
var featureContainer = content.append('div')
|
|
.attr('class', 'data-feature-filters')
|
|
.style('display', 'none');
|
|
|
|
var featureList = featureContainer.append('ul')
|
|
.attr('class', 'layer-list layer-feature-list');
|
|
|
|
|
|
context.features()
|
|
.on('change.map_data-update', update);
|
|
|
|
setFill(fillDefault);
|
|
|
|
var keybinding = d3.keybinding('features')
|
|
.on(key, togglePanel)
|
|
.on('W', toggleWireframe)
|
|
.on('B', hidePanel)
|
|
.on('H', hidePanel);
|
|
|
|
d3.select(document)
|
|
.call(keybinding);
|
|
|
|
context.surface().on('mousedown.map_data-outside', hidePanel);
|
|
context.container().on('mousedown.map_data-outside', hidePanel);
|
|
}
|
|
|
|
return map_data;
|
|
}
|
|
|
|
function Modes(context) {
|
|
var modes = [
|
|
iD.modes.AddPoint(context),
|
|
iD.modes.AddLine(context),
|
|
iD.modes.AddArea(context)];
|
|
|
|
function editable() {
|
|
return context.editable() && context.mode().id !== 'save';
|
|
}
|
|
|
|
return function(selection) {
|
|
var buttons = selection.selectAll('button.add-button')
|
|
.data(modes);
|
|
|
|
buttons.enter().append('button')
|
|
.attr('tabindex', -1)
|
|
.attr('class', function(mode) { return mode.id + ' add-button col4'; })
|
|
.on('click.mode-buttons', function(mode) {
|
|
if (mode.id === context.mode().id) {
|
|
context.enter(iD.modes.Browse(context));
|
|
} else {
|
|
context.enter(mode);
|
|
}
|
|
})
|
|
.call(bootstrap.tooltip()
|
|
.placement('bottom')
|
|
.html(true)
|
|
.title(function(mode) {
|
|
return iD.ui.tooltipHtml(mode.description, mode.key);
|
|
}));
|
|
|
|
context.map()
|
|
.on('move.modes', _.debounce(update, 500));
|
|
|
|
context
|
|
.on('enter.modes', update);
|
|
|
|
buttons.each(function(d) {
|
|
d3.select(this)
|
|
.call(iD.svg.Icon('#icon-' + d.button, 'pre-text'));
|
|
});
|
|
|
|
buttons.append('span')
|
|
.attr('class', 'label')
|
|
.text(function(mode) { return mode.title; });
|
|
|
|
context.on('enter.editor', function(entered) {
|
|
buttons.classed('active', function(mode) { return entered.button === mode.button; });
|
|
context.container()
|
|
.classed('mode-' + entered.id, true);
|
|
});
|
|
|
|
context.on('exit.editor', function(exited) {
|
|
context.container()
|
|
.classed('mode-' + exited.id, false);
|
|
});
|
|
|
|
var keybinding = d3.keybinding('mode-buttons');
|
|
|
|
modes.forEach(function(m) {
|
|
keybinding.on(m.key, function() { if (editable()) context.enter(m); });
|
|
});
|
|
|
|
d3.select(document)
|
|
.call(keybinding);
|
|
|
|
function update() {
|
|
buttons.property('disabled', !editable());
|
|
}
|
|
};
|
|
}
|
|
|
|
function Notice(context) {
|
|
return function(selection) {
|
|
var div = selection.append('div')
|
|
.attr('class', 'notice');
|
|
|
|
var button = div.append('button')
|
|
.attr('class', 'zoom-to notice')
|
|
.on('click', function() { context.map().zoom(context.minEditableZoom()); });
|
|
|
|
button
|
|
.call(iD.svg.Icon('#icon-plus', 'pre-text'))
|
|
.append('span')
|
|
.attr('class', 'label')
|
|
.text(t('zoom_in_edit'));
|
|
|
|
function disableTooHigh() {
|
|
div.style('display', context.editable() ? 'none' : 'block');
|
|
}
|
|
|
|
context.map()
|
|
.on('move.notice', _.debounce(disableTooHigh, 500));
|
|
|
|
disableTooHigh();
|
|
};
|
|
}
|
|
|
|
function RadialMenu(context, operations) {
|
|
var menu,
|
|
center = [0, 0],
|
|
tooltip;
|
|
|
|
var radialMenu = function(selection) {
|
|
if (!operations.length)
|
|
return;
|
|
|
|
selection.node().parentNode.focus();
|
|
|
|
function click(operation) {
|
|
d3.event.stopPropagation();
|
|
if (operation.disabled())
|
|
return;
|
|
operation();
|
|
radialMenu.close();
|
|
}
|
|
|
|
menu = selection.append('g')
|
|
.attr('class', 'radial-menu')
|
|
.attr('transform', 'translate(' + center + ')')
|
|
.attr('opacity', 0);
|
|
|
|
menu.transition()
|
|
.attr('opacity', 1);
|
|
|
|
var r = 50,
|
|
a = Math.PI / 4,
|
|
a0 = -Math.PI / 4,
|
|
a1 = a0 + (operations.length - 1) * a;
|
|
|
|
menu.append('path')
|
|
.attr('class', 'radial-menu-background')
|
|
.attr('d', 'M' + r * Math.sin(a0) + ',' +
|
|
r * Math.cos(a0) +
|
|
' A' + r + ',' + r + ' 0 ' + (operations.length > 5 ? '1' : '0') + ',0 ' +
|
|
(r * Math.sin(a1) + 1e-3) + ',' +
|
|
(r * Math.cos(a1) + 1e-3)) // Force positive-length path (#1305)
|
|
.attr('stroke-width', 50)
|
|
.attr('stroke-linecap', 'round');
|
|
|
|
var button = menu.selectAll()
|
|
.data(operations)
|
|
.enter()
|
|
.append('g')
|
|
.attr('class', function(d) { return 'radial-menu-item radial-menu-item-' + d.id; })
|
|
.classed('disabled', function(d) { return d.disabled(); })
|
|
.attr('transform', function(d, i) {
|
|
return 'translate(' + iD.geo.roundCoords([
|
|
r * Math.sin(a0 + i * a),
|
|
r * Math.cos(a0 + i * a)]).join(',') + ')';
|
|
});
|
|
|
|
button.append('circle')
|
|
.attr('r', 15)
|
|
.on('click', click)
|
|
.on('mousedown', mousedown)
|
|
.on('mouseover', mouseover)
|
|
.on('mouseout', mouseout);
|
|
|
|
button.append('use')
|
|
.attr('transform', 'translate(-10,-10)')
|
|
.attr('width', '20')
|
|
.attr('height', '20')
|
|
.attr('xlink:href', function(d) { return '#operation-' + d.id; });
|
|
|
|
tooltip = d3.select(document.body)
|
|
.append('div')
|
|
.attr('class', 'tooltip-inner radial-menu-tooltip');
|
|
|
|
function mousedown() {
|
|
d3.event.stopPropagation(); // https://github.com/openstreetmap/iD/issues/1869
|
|
}
|
|
|
|
function mouseover(d, i) {
|
|
var rect = context.surfaceRect(),
|
|
angle = a0 + i * a,
|
|
top = rect.top + (r + 25) * Math.cos(angle) + center[1] + 'px',
|
|
left = rect.left + (r + 25) * Math.sin(angle) + center[0] + 'px',
|
|
bottom = rect.height - (r + 25) * Math.cos(angle) - center[1] + 'px',
|
|
right = rect.width - (r + 25) * Math.sin(angle) - center[0] + 'px';
|
|
|
|
tooltip
|
|
.style('top', null)
|
|
.style('left', null)
|
|
.style('bottom', null)
|
|
.style('right', null)
|
|
.style('display', 'block')
|
|
.html(iD.ui.tooltipHtml(d.tooltip(), d.keys[0]));
|
|
|
|
if (i === 0) {
|
|
tooltip
|
|
.style('right', right)
|
|
.style('top', top);
|
|
} else if (i >= 4) {
|
|
tooltip
|
|
.style('left', left)
|
|
.style('bottom', bottom);
|
|
} else {
|
|
tooltip
|
|
.style('left', left)
|
|
.style('top', top);
|
|
}
|
|
}
|
|
|
|
function mouseout() {
|
|
tooltip.style('display', 'none');
|
|
}
|
|
};
|
|
|
|
radialMenu.close = function() {
|
|
if (menu) {
|
|
menu
|
|
.style('pointer-events', 'none')
|
|
.transition()
|
|
.attr('opacity', 0)
|
|
.remove();
|
|
}
|
|
|
|
if (tooltip) {
|
|
tooltip.remove();
|
|
}
|
|
};
|
|
|
|
radialMenu.center = function(_) {
|
|
if (!arguments.length) return center;
|
|
center = _;
|
|
return radialMenu;
|
|
};
|
|
|
|
return radialMenu;
|
|
}
|
|
|
|
function Restore(context) {
|
|
return function(selection) {
|
|
if (!context.history().lock() || !context.history().restorableChanges())
|
|
return;
|
|
|
|
var modal = modalModule(selection, true);
|
|
|
|
modal.select('.modal')
|
|
.attr('class', 'modal fillL col6');
|
|
|
|
var introModal = modal.select('.content');
|
|
|
|
introModal.attr('class','cf');
|
|
|
|
introModal.append('div')
|
|
.attr('class', 'modal-section')
|
|
.append('h3')
|
|
.text(t('restore.heading'));
|
|
|
|
introModal.append('div')
|
|
.attr('class','modal-section')
|
|
.append('p')
|
|
.text(t('restore.description'));
|
|
|
|
var buttonWrap = introModal.append('div')
|
|
.attr('class', 'modal-actions cf');
|
|
|
|
var restore = buttonWrap.append('button')
|
|
.attr('class', 'restore col6')
|
|
.text(t('restore.restore'))
|
|
.on('click', function() {
|
|
context.history().restore();
|
|
modal.remove();
|
|
});
|
|
|
|
buttonWrap.append('button')
|
|
.attr('class', 'reset col6')
|
|
.text(t('restore.reset'))
|
|
.on('click', function() {
|
|
context.history().clearSaved();
|
|
modal.remove();
|
|
});
|
|
|
|
restore.node().focus();
|
|
};
|
|
}
|
|
|
|
function Save(context) {
|
|
var history = context.history(),
|
|
key = cmd('⌘S');
|
|
|
|
|
|
function saving() {
|
|
return context.mode().id === 'save';
|
|
}
|
|
|
|
function save() {
|
|
d3.event.preventDefault();
|
|
if (!context.inIntro() && !saving() && history.hasChanges()) {
|
|
context.enter(iD.modes.Save(context));
|
|
}
|
|
}
|
|
|
|
function getBackground(numChanges) {
|
|
var step;
|
|
if (numChanges === 0) {
|
|
return null;
|
|
} else if (numChanges <= 50) {
|
|
step = numChanges / 50;
|
|
return d3.interpolateRgb('#fff', '#ff8')(step); // white -> yellow
|
|
} else {
|
|
step = Math.min((numChanges - 50) / 50, 1.0);
|
|
return d3.interpolateRgb('#ff8', '#f88')(step); // yellow -> red
|
|
}
|
|
}
|
|
|
|
return function(selection) {
|
|
var tooltip = bootstrap.tooltip()
|
|
.placement('bottom')
|
|
.html(true)
|
|
.title(iD.ui.tooltipHtml(t('save.no_changes'), key));
|
|
|
|
var button = selection.append('button')
|
|
.attr('class', 'save col12 disabled')
|
|
.attr('tabindex', -1)
|
|
.on('click', save)
|
|
.call(tooltip);
|
|
|
|
button.append('span')
|
|
.attr('class', 'label')
|
|
.text(t('save.title'));
|
|
|
|
button.append('span')
|
|
.attr('class', 'count')
|
|
.text('0');
|
|
|
|
var keybinding = d3.keybinding('undo-redo')
|
|
.on(key, save, true);
|
|
|
|
d3.select(document)
|
|
.call(keybinding);
|
|
|
|
var numChanges = 0;
|
|
|
|
context.history().on('change.save', function() {
|
|
var _ = history.difference().summary().length;
|
|
if (_ === numChanges)
|
|
return;
|
|
numChanges = _;
|
|
|
|
tooltip.title(iD.ui.tooltipHtml(t(numChanges > 0 ?
|
|
'save.help' : 'save.no_changes'), key));
|
|
|
|
var background = getBackground(numChanges);
|
|
|
|
button
|
|
.classed('disabled', numChanges === 0)
|
|
.classed('has-count', numChanges > 0)
|
|
.style('background', background);
|
|
|
|
button.select('span.count')
|
|
.text(numChanges)
|
|
.style('background', background)
|
|
.style('border-color', background);
|
|
});
|
|
|
|
context.on('enter.save', function() {
|
|
button.property('disabled', saving());
|
|
if (saving()) button.call(tooltip.hide);
|
|
});
|
|
};
|
|
}
|
|
|
|
function Scale(context) {
|
|
var projection = context.projection,
|
|
imperial = (iD.detect().locale.toLowerCase() === 'en-us'),
|
|
maxLength = 180,
|
|
tickHeight = 8;
|
|
|
|
function scaleDefs(loc1, loc2) {
|
|
var lat = (loc2[1] + loc1[1]) / 2,
|
|
conversion = (imperial ? 3.28084 : 1),
|
|
dist = iD.geo.lonToMeters(loc2[0] - loc1[0], lat) * conversion,
|
|
scale = { dist: 0, px: 0, text: '' },
|
|
buckets, i, val, dLon;
|
|
|
|
if (imperial) {
|
|
buckets = [5280000, 528000, 52800, 5280, 500, 50, 5, 1];
|
|
} else {
|
|
buckets = [5000000, 500000, 50000, 5000, 500, 50, 5, 1];
|
|
}
|
|
|
|
// determine a user-friendly endpoint for the scale
|
|
for (i = 0; i < buckets.length; i++) {
|
|
val = buckets[i];
|
|
if (dist >= val) {
|
|
scale.dist = Math.floor(dist / val) * val;
|
|
break;
|
|
}
|
|
}
|
|
|
|
dLon = iD.geo.metersToLon(scale.dist / conversion, lat);
|
|
scale.px = Math.round(projection([loc1[0] + dLon, loc1[1]])[0]);
|
|
|
|
if (imperial) {
|
|
if (scale.dist >= 5280) {
|
|
scale.dist /= 5280;
|
|
scale.text = String(scale.dist) + ' mi';
|
|
} else {
|
|
scale.text = String(scale.dist) + ' ft';
|
|
}
|
|
} else {
|
|
if (scale.dist >= 1000) {
|
|
scale.dist /= 1000;
|
|
scale.text = String(scale.dist) + ' km';
|
|
} else {
|
|
scale.text = String(scale.dist) + ' m';
|
|
}
|
|
}
|
|
|
|
return scale;
|
|
}
|
|
|
|
function update(selection) {
|
|
// choose loc1, loc2 along bottom of viewport (near where the scale will be drawn)
|
|
var dims = context.map().dimensions(),
|
|
loc1 = projection.invert([0, dims[1]]),
|
|
loc2 = projection.invert([maxLength, dims[1]]),
|
|
scale = scaleDefs(loc1, loc2);
|
|
|
|
selection.select('#scalepath')
|
|
.attr('d', 'M0.5,0.5v' + tickHeight + 'h' + scale.px + 'v-' + tickHeight);
|
|
|
|
selection.select('#scaletext')
|
|
.attr('x', scale.px + 8)
|
|
.attr('y', tickHeight)
|
|
.text(scale.text);
|
|
}
|
|
|
|
|
|
return function(selection) {
|
|
function switchUnits() {
|
|
imperial = !imperial;
|
|
selection.call(update);
|
|
}
|
|
|
|
var g = selection.append('svg')
|
|
.attr('id', 'scale')
|
|
.on('click', switchUnits)
|
|
.append('g')
|
|
.attr('transform', 'translate(10,11)');
|
|
|
|
g.append('path').attr('id', 'scalepath');
|
|
g.append('text').attr('id', 'scaletext');
|
|
|
|
selection.call(update);
|
|
|
|
context.map().on('move.scale', function() {
|
|
update(selection);
|
|
});
|
|
};
|
|
}
|
|
|
|
function SelectionList(context, selectedIDs) {
|
|
|
|
function selectEntity(entity) {
|
|
context.enter(iD.modes.Select(context, [entity.id]).suppressMenu(true));
|
|
}
|
|
|
|
|
|
function selectionList(selection) {
|
|
selection.classed('selection-list-pane', true);
|
|
|
|
var header = selection.append('div')
|
|
.attr('class', 'header fillL cf');
|
|
|
|
header.append('h3')
|
|
.text(t('inspector.multiselect'));
|
|
|
|
var listWrap = selection.append('div')
|
|
.attr('class', 'inspector-body');
|
|
|
|
var list = listWrap.append('div')
|
|
.attr('class', 'feature-list cf');
|
|
|
|
context.history().on('change.selection-list', drawList);
|
|
drawList();
|
|
|
|
function drawList() {
|
|
var entities = selectedIDs
|
|
.map(function(id) { return context.hasEntity(id); })
|
|
.filter(function(entity) { return entity; });
|
|
|
|
var items = list.selectAll('.feature-list-item')
|
|
.data(entities, iD.Entity.key);
|
|
|
|
var enter = items.enter().append('button')
|
|
.attr('class', 'feature-list-item')
|
|
.on('click', selectEntity);
|
|
|
|
// Enter
|
|
var label = enter.append('div')
|
|
.attr('class', 'label')
|
|
.call(iD.svg.Icon('', 'pre-text'));
|
|
|
|
label.append('span')
|
|
.attr('class', 'entity-type');
|
|
|
|
label.append('span')
|
|
.attr('class', 'entity-name');
|
|
|
|
// Update
|
|
items.selectAll('use')
|
|
.attr('href', function() {
|
|
var entity = this.parentNode.parentNode.__data__;
|
|
return '#icon-' + context.geometry(entity.id);
|
|
});
|
|
|
|
items.selectAll('.entity-type')
|
|
.text(function(entity) { return context.presets().match(entity, context.graph()).name(); });
|
|
|
|
items.selectAll('.entity-name')
|
|
.text(function(entity) { return iD.util.displayName(entity); });
|
|
|
|
// Exit
|
|
items.exit()
|
|
.remove();
|
|
}
|
|
}
|
|
|
|
return selectionList;
|
|
|
|
}
|
|
|
|
function Sidebar(context) {
|
|
var inspector = Inspector(context),
|
|
current;
|
|
|
|
function sidebar(selection) {
|
|
var featureListWrap = selection.append('div')
|
|
.attr('class', 'feature-list-pane')
|
|
.call(FeatureList(context));
|
|
|
|
selection.call(Notice(context));
|
|
|
|
var inspectorWrap = selection.append('div')
|
|
.attr('class', 'inspector-hidden inspector-wrap fr');
|
|
|
|
function hover(id) {
|
|
if (!current && context.hasEntity(id)) {
|
|
featureListWrap.classed('inspector-hidden', true);
|
|
inspectorWrap.classed('inspector-hidden', false)
|
|
.classed('inspector-hover', true);
|
|
|
|
if (inspector.entityID() !== id || inspector.state() !== 'hover') {
|
|
inspector
|
|
.state('hover')
|
|
.entityID(id);
|
|
|
|
inspectorWrap.call(inspector);
|
|
}
|
|
} else if (!current) {
|
|
featureListWrap.classed('inspector-hidden', false);
|
|
inspectorWrap.classed('inspector-hidden', true);
|
|
inspector.state('hide');
|
|
}
|
|
}
|
|
|
|
sidebar.hover = _.throttle(hover, 200);
|
|
|
|
sidebar.select = function(id, newFeature) {
|
|
if (!current && id) {
|
|
featureListWrap.classed('inspector-hidden', true);
|
|
inspectorWrap.classed('inspector-hidden', false)
|
|
.classed('inspector-hover', false);
|
|
|
|
if (inspector.entityID() !== id || inspector.state() !== 'select') {
|
|
inspector
|
|
.state('select')
|
|
.entityID(id)
|
|
.newFeature(newFeature);
|
|
|
|
inspectorWrap.call(inspector);
|
|
}
|
|
} else if (!current) {
|
|
featureListWrap.classed('inspector-hidden', false);
|
|
inspectorWrap.classed('inspector-hidden', true);
|
|
inspector.state('hide');
|
|
}
|
|
};
|
|
|
|
sidebar.show = function(component) {
|
|
featureListWrap.classed('inspector-hidden', true);
|
|
inspectorWrap.classed('inspector-hidden', true);
|
|
if (current) current.remove();
|
|
current = selection.append('div')
|
|
.attr('class', 'sidebar-component')
|
|
.call(component);
|
|
};
|
|
|
|
sidebar.hide = function() {
|
|
featureListWrap.classed('inspector-hidden', false);
|
|
inspectorWrap.classed('inspector-hidden', true);
|
|
if (current) current.remove();
|
|
current = null;
|
|
};
|
|
}
|
|
|
|
sidebar.hover = function() {};
|
|
sidebar.hover.cancel = function() {};
|
|
sidebar.select = function() {};
|
|
sidebar.show = function() {};
|
|
sidebar.hide = function() {};
|
|
|
|
return sidebar;
|
|
}
|
|
|
|
function SourceSwitch(context) {
|
|
var keys;
|
|
|
|
function click() {
|
|
d3.event.preventDefault();
|
|
|
|
if (context.history().hasChanges() &&
|
|
!window.confirm(t('source_switch.lose_changes'))) return;
|
|
|
|
var live = d3.select(this)
|
|
.classed('live');
|
|
|
|
context.connection()
|
|
.switch(live ? keys[1] : keys[0]);
|
|
|
|
context.enter(iD.modes.Browse(context));
|
|
context.flush();
|
|
|
|
d3.select(this)
|
|
.text(live ? t('source_switch.dev') : t('source_switch.live'))
|
|
.classed('live', !live);
|
|
}
|
|
|
|
var sourceSwitch = function(selection) {
|
|
selection.append('a')
|
|
.attr('href', '#')
|
|
.text(t('source_switch.live'))
|
|
.classed('live', true)
|
|
.attr('tabindex', -1)
|
|
.on('click', click);
|
|
};
|
|
|
|
sourceSwitch.keys = function(_) {
|
|
if (!arguments.length) return keys;
|
|
keys = _;
|
|
return sourceSwitch;
|
|
};
|
|
|
|
return sourceSwitch;
|
|
}
|
|
|
|
function Spinner(context) {
|
|
var connection = context.connection();
|
|
|
|
return function(selection) {
|
|
var img = selection.append('img')
|
|
.attr('src', context.imagePath('loader-black.gif'))
|
|
.style('opacity', 0);
|
|
|
|
connection.on('loading.spinner', function() {
|
|
img.transition()
|
|
.style('opacity', 1);
|
|
});
|
|
|
|
connection.on('loaded.spinner', function() {
|
|
img.transition()
|
|
.style('opacity', 0);
|
|
});
|
|
};
|
|
}
|
|
|
|
function Splash(context) {
|
|
return function(selection) {
|
|
if (context.storage('sawSplash'))
|
|
return;
|
|
|
|
context.storage('sawSplash', true);
|
|
|
|
var modal = modalModule(selection);
|
|
|
|
modal.select('.modal')
|
|
.attr('class', 'modal-splash modal col6');
|
|
|
|
var introModal = modal.select('.content')
|
|
.append('div')
|
|
.attr('class', 'fillL');
|
|
|
|
introModal.append('div')
|
|
.attr('class','modal-section cf')
|
|
.append('h3').text(t('splash.welcome'));
|
|
|
|
introModal.append('div')
|
|
.attr('class','modal-section')
|
|
.append('p')
|
|
.html(t('splash.text', {
|
|
version: iD.version,
|
|
website: '<a href="http://ideditor.com/">ideditor.com</a>',
|
|
github: '<a href="https://github.com/openstreetmap/iD">github.com</a>'
|
|
}));
|
|
|
|
var buttons = introModal.append('div').attr('class', 'modal-actions cf');
|
|
|
|
buttons.append('button')
|
|
.attr('class', 'col6 walkthrough')
|
|
.text(t('splash.walkthrough'))
|
|
.on('click', function() {
|
|
d3.select(document.body).call(intro(context));
|
|
modal.close();
|
|
});
|
|
|
|
buttons.append('button')
|
|
.attr('class', 'col6 start')
|
|
.text(t('splash.start'))
|
|
.on('click', modal.close);
|
|
|
|
modal.select('button.close').attr('class','hide');
|
|
|
|
};
|
|
}
|
|
|
|
function Status(context) {
|
|
var connection = context.connection(),
|
|
errCount = 0;
|
|
|
|
return function(selection) {
|
|
|
|
function update() {
|
|
|
|
connection.status(function(err, apiStatus) {
|
|
|
|
selection.html('');
|
|
|
|
if (err && errCount++ < 2) return;
|
|
|
|
if (err) {
|
|
selection.text(t('status.error'));
|
|
|
|
} else if (apiStatus === 'readonly') {
|
|
selection.text(t('status.readonly'));
|
|
|
|
} else if (apiStatus === 'offline') {
|
|
selection.text(t('status.offline'));
|
|
}
|
|
|
|
selection.attr('class', 'api-status ' + (err ? 'error' : apiStatus));
|
|
if (!err) errCount = 0;
|
|
|
|
});
|
|
}
|
|
|
|
connection.on('auth', function() { update(selection); });
|
|
window.setInterval(update, 90000);
|
|
update(selection);
|
|
};
|
|
}
|
|
|
|
function Success(context) {
|
|
var dispatch = d3.dispatch('cancel'),
|
|
changeset;
|
|
|
|
function success(selection) {
|
|
var message = (changeset.comment || t('success.edited_osm')).substring(0, 130) +
|
|
' ' + context.connection().changesetURL(changeset.id);
|
|
|
|
var header = selection.append('div')
|
|
.attr('class', 'header fillL');
|
|
|
|
header.append('button')
|
|
.attr('class', 'fr')
|
|
.on('click', function() { dispatch.cancel(); })
|
|
.call(iD.svg.Icon('#icon-close'));
|
|
|
|
header.append('h3')
|
|
.text(t('success.just_edited'));
|
|
|
|
var body = selection.append('div')
|
|
.attr('class', 'body save-success fillL');
|
|
|
|
body.append('p')
|
|
.html(t('success.help_html'));
|
|
|
|
body.append('a')
|
|
.attr('class', 'details')
|
|
.attr('target', '_blank')
|
|
.attr('tabindex', -1)
|
|
.call(iD.svg.Icon('#icon-out-link', 'inline'))
|
|
.attr('href', t('success.help_link_url'))
|
|
.append('span')
|
|
.text(t('success.help_link_text'));
|
|
|
|
var changesetURL = context.connection().changesetURL(changeset.id);
|
|
|
|
body.append('a')
|
|
.attr('class', 'button col12 osm')
|
|
.attr('target', '_blank')
|
|
.attr('href', changesetURL)
|
|
.text(t('success.view_on_osm'));
|
|
|
|
var sharing = {
|
|
facebook: 'https://facebook.com/sharer/sharer.php?u=' + encodeURIComponent(changesetURL),
|
|
twitter: 'https://twitter.com/intent/tweet?source=webclient&text=' + encodeURIComponent(message),
|
|
google: 'https://plus.google.com/share?url=' + encodeURIComponent(changesetURL)
|
|
};
|
|
|
|
body.selectAll('.button.social')
|
|
.data(d3.entries(sharing))
|
|
.enter()
|
|
.append('a')
|
|
.attr('class', 'button social col4')
|
|
.attr('target', '_blank')
|
|
.attr('href', function(d) { return d.value; })
|
|
.call(bootstrap.tooltip()
|
|
.title(function(d) { return t('success.' + d.key); })
|
|
.placement('bottom'))
|
|
.each(function(d) { d3.select(this).call(iD.svg.Icon('#logo-' + d.key, 'social')); });
|
|
}
|
|
|
|
success.changeset = function(_) {
|
|
if (!arguments.length) return changeset;
|
|
changeset = _;
|
|
return success;
|
|
};
|
|
|
|
return d3.rebind(success, dispatch, 'on');
|
|
}
|
|
|
|
function UndoRedo(context) {
|
|
var commands = [{
|
|
id: 'undo',
|
|
cmd: cmd('⌘Z'),
|
|
action: function() { if (!(context.inIntro() || saving())) context.undo(); },
|
|
annotation: function() { return context.history().undoAnnotation(); }
|
|
}, {
|
|
id: 'redo',
|
|
cmd: cmd('⌘⇧Z'),
|
|
action: function() {if (!(context.inIntro() || saving())) context.redo(); },
|
|
annotation: function() { return context.history().redoAnnotation(); }
|
|
}];
|
|
|
|
function saving() {
|
|
return context.mode().id === 'save';
|
|
}
|
|
|
|
return function(selection) {
|
|
var tooltip = bootstrap.tooltip()
|
|
.placement('bottom')
|
|
.html(true)
|
|
.title(function (d) {
|
|
return iD.ui.tooltipHtml(d.annotation() ?
|
|
t(d.id + '.tooltip', {action: d.annotation()}) :
|
|
t(d.id + '.nothing'), d.cmd);
|
|
});
|
|
|
|
var buttons = selection.selectAll('button')
|
|
.data(commands)
|
|
.enter().append('button')
|
|
.attr('class', 'col6 disabled')
|
|
.on('click', function(d) { return d.action(); })
|
|
.call(tooltip);
|
|
|
|
buttons.each(function(d) {
|
|
d3.select(this)
|
|
.call(iD.svg.Icon('#icon-' + d.id));
|
|
});
|
|
|
|
var keybinding = d3.keybinding('undo')
|
|
.on(commands[0].cmd, function() { d3.event.preventDefault(); commands[0].action(); })
|
|
.on(commands[1].cmd, function() { d3.event.preventDefault(); commands[1].action(); });
|
|
|
|
d3.select(document)
|
|
.call(keybinding);
|
|
|
|
context.history()
|
|
.on('change.undo_redo', update);
|
|
|
|
context
|
|
.on('enter.undo_redo', update);
|
|
|
|
function update() {
|
|
buttons
|
|
.property('disabled', saving())
|
|
.classed('disabled', function(d) { return !d.annotation(); })
|
|
.each(function() {
|
|
var selection = d3.select(this);
|
|
if (selection.property('tooltipVisible')) {
|
|
selection.call(tooltip.show);
|
|
}
|
|
});
|
|
}
|
|
};
|
|
}
|
|
|
|
function Zoom(context) {
|
|
var zooms = [{
|
|
id: 'zoom-in',
|
|
icon: 'plus',
|
|
title: t('zoom.in'),
|
|
action: context.zoomIn,
|
|
key: '+'
|
|
}, {
|
|
id: 'zoom-out',
|
|
icon: 'minus',
|
|
title: t('zoom.out'),
|
|
action: context.zoomOut,
|
|
key: '-'
|
|
}];
|
|
|
|
function zoomIn() {
|
|
d3.event.preventDefault();
|
|
if (!context.inIntro()) context.zoomIn();
|
|
}
|
|
|
|
function zoomOut() {
|
|
d3.event.preventDefault();
|
|
if (!context.inIntro()) context.zoomOut();
|
|
}
|
|
|
|
function zoomInFurther() {
|
|
d3.event.preventDefault();
|
|
if (!context.inIntro()) context.zoomInFurther();
|
|
}
|
|
|
|
function zoomOutFurther() {
|
|
d3.event.preventDefault();
|
|
if (!context.inIntro()) context.zoomOutFurther();
|
|
}
|
|
|
|
|
|
return function(selection) {
|
|
var button = selection.selectAll('button')
|
|
.data(zooms)
|
|
.enter().append('button')
|
|
.attr('tabindex', -1)
|
|
.attr('class', function(d) { return d.id; })
|
|
.on('click.editor', function(d) { d.action(); })
|
|
.call(bootstrap.tooltip()
|
|
.placement('left')
|
|
.html(true)
|
|
.title(function(d) {
|
|
return iD.ui.tooltipHtml(d.title, d.key);
|
|
}));
|
|
|
|
button.each(function(d) {
|
|
d3.select(this)
|
|
.call(iD.svg.Icon('#icon-' + d.icon, 'light'));
|
|
});
|
|
|
|
var keybinding = d3.keybinding('zoom');
|
|
|
|
_.each(['=','ffequals','plus','ffplus'], function(key) {
|
|
keybinding.on(key, zoomIn);
|
|
keybinding.on('⇧' + key, zoomIn);
|
|
keybinding.on(cmd('⌘' + key), zoomInFurther);
|
|
keybinding.on(cmd('⌘⇧' + key), zoomInFurther);
|
|
});
|
|
_.each(['-','ffminus','_','dash'], function(key) {
|
|
keybinding.on(key, zoomOut);
|
|
keybinding.on('⇧' + key, zoomOut);
|
|
keybinding.on(cmd('⌘' + key), zoomOutFurther);
|
|
keybinding.on(cmd('⌘⇧' + key), zoomOutFurther);
|
|
});
|
|
|
|
d3.select(document)
|
|
.call(keybinding);
|
|
};
|
|
}
|
|
|
|
exports.Account = Account;
|
|
exports.Attribution = Attribution;
|
|
exports.Background = Background;
|
|
exports.cmd = cmd;
|
|
exports.Commit = Commit;
|
|
exports.confirm = confirm;
|
|
exports.Conflicts = Conflicts;
|
|
exports.Contributors = Contributors;
|
|
exports.Disclosure = Disclosure;
|
|
exports.EntityEditor = EntityEditor;
|
|
exports.FeatureInfo = FeatureInfo;
|
|
exports.FeatureList = FeatureList;
|
|
exports.flash = flash;
|
|
exports.FullScreen = FullScreen;
|
|
exports.Geolocate = Geolocate;
|
|
exports.Help = Help;
|
|
exports.Info = Info;
|
|
exports.Inspector = Inspector;
|
|
exports.intro = intro;
|
|
exports.Lasso = Lasso;
|
|
exports.Loading = Loading;
|
|
exports.MapData = MapData;
|
|
exports.MapInMap = MapInMap;
|
|
exports.modal = modalModule;
|
|
exports.Modes = Modes;
|
|
exports.Notice = Notice;
|
|
exports.preset = preset;
|
|
exports.PresetIcon = PresetIcon;
|
|
exports.PresetList = PresetList;
|
|
exports.RadialMenu = RadialMenu;
|
|
exports.RawMemberEditor = RawMemberEditor;
|
|
exports.RawMembershipEditor = RawMembershipEditor;
|
|
exports.RawTagEditor = RawTagEditor;
|
|
exports.Restore = Restore;
|
|
exports.Save = Save;
|
|
exports.Scale = Scale;
|
|
exports.SelectionList = SelectionList;
|
|
exports.Sidebar = Sidebar;
|
|
exports.SourceSwitch = SourceSwitch;
|
|
exports.Spinner = Spinner;
|
|
exports.Splash = Splash;
|
|
exports.Status = Status;
|
|
exports.Success = Success;
|
|
exports.TagReference = TagReference;
|
|
exports.Toggle = Toggle;
|
|
exports.UndoRedo = UndoRedo;
|
|
exports.ViewOnOSM = ViewOnOSM;
|
|
exports.Zoom = Zoom;
|
|
|
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
|
})); |