diff --git a/Makefile b/Makefile
index 1818e10bf..b079b12d5 100644
--- a/Makefile
+++ b/Makefile
@@ -52,7 +52,12 @@ MODULE_TARGETS = \
js/lib/id/presets.js \
js/lib/id/renderer.js \
js/lib/id/services.js \
+ js/lib/id/ui/intro.js \
js/lib/id/svg.js \
+ js/lib/id/ui/index.js \
+ js/lib/id/ui/core.js \
+ js/lib/id/ui/intro.js \
+ js/lib/id/ui/preset.js \
js/lib/id/util.js \
js/lib/id/validations.js
@@ -96,6 +101,22 @@ js/lib/id/svg.js: $(shell find modules/svg -type f)
@rm -f $@
node_modules/.bin/rollup -f umd -n iD.svg modules/svg/index.js --no-strict -o $@
+js/lib/id/ui/index.js: $(shell find modules/ui -type f)
+ @rm -f $@
+ node_modules/.bin/rollup -f umd -n iD modules/ui/ui.js --no-strict -o $@
+
+js/lib/id/ui/core.js: $(shell find modules/ui/core -type f)
+ @rm -f $@
+ node_modules/.bin/rollup -f umd -n iD.ui modules/ui/core/index.js --no-strict -o $@
+
+js/lib/id/ui/intro.js: $(shell find modules/ui/intro -type f)
+ @rm -f $@
+ node_modules/.bin/rollup -f umd -n iD.ui.intro modules/ui/intro/index.js --no-strict -o $@
+
+js/lib/id/ui/preset.js: $(shell find modules/ui/preset -type f)
+ @rm -f $@
+ node_modules/.bin/rollup -f umd -n iD.ui.preset modules/ui/preset/index.js --no-strict -o $@
+
js/lib/id/util.js: $(shell find modules/util -type f)
@rm -f $@
node_modules/.bin/rollup -f umd -n iD.util modules/util/index.js --no-strict -o $@
@@ -127,72 +148,6 @@ dist/iD.js: \
js/id/start.js \
js/id/id.js \
$(MODULE_TARGETS) \
- js/id/ui.js \
- js/id/ui/account.js \
- js/id/ui/attribution.js \
- js/id/ui/background.js \
- js/id/ui/cmd.js \
- js/id/ui/commit.js \
- js/id/ui/confirm.js \
- js/id/ui/conflicts.js \
- js/id/ui/contributors.js \
- js/id/ui/disclosure.js \
- js/id/ui/entity_editor.js \
- js/id/ui/feature_info.js \
- js/id/ui/feature_list.js \
- js/id/ui/flash.js \
- js/id/ui/full_screen.js \
- js/id/ui/geolocate.js \
- js/id/ui/help.js \
- js/id/ui/info.js \
- js/id/ui/inspector.js \
- js/id/ui/intro.js \
- js/id/ui/lasso.js \
- js/id/ui/loading.js \
- js/id/ui/map_data.js \
- js/id/ui/map_in_map.js \
- js/id/ui/modal.js \
- js/id/ui/modes.js \
- js/id/ui/notice.js \
- js/id/ui/preset_icon.js \
- js/id/ui/preset.js \
- js/id/ui/preset_list.js \
- js/id/ui/radial_menu.js \
- js/id/ui/raw_member_editor.js \
- js/id/ui/raw_membership_editor.js \
- js/id/ui/raw_tag_editor.js \
- js/id/ui/restore.js \
- js/id/ui/save.js \
- js/id/ui/scale.js \
- js/id/ui/selection_list.js \
- js/id/ui/sidebar.js \
- js/id/ui/source_switch.js \
- js/id/ui/spinner.js \
- js/id/ui/splash.js \
- js/id/ui/status.js \
- js/id/ui/success.js \
- js/id/ui/tag_reference.js \
- js/id/ui/toggle.js \
- js/id/ui/undo_redo.js \
- js/id/ui/view_on_osm.js \
- js/id/ui/zoom.js \
- js/id/ui/preset/access.js \
- js/id/ui/preset/address.js \
- js/id/ui/preset/check.js \
- js/id/ui/preset/combo.js \
- js/id/ui/preset/cycleway.js \
- js/id/ui/preset/input.js \
- js/id/ui/preset/localized.js \
- js/id/ui/preset/maxspeed.js \
- js/id/ui/preset/radio.js \
- js/id/ui/preset/restrictions.js \
- js/id/ui/preset/textarea.js \
- js/id/ui/preset/wikipedia.js \
- js/id/ui/intro/area.js \
- js/id/ui/intro/line.js \
- js/id/ui/intro/navigation.js \
- js/id/ui/intro/point.js \
- js/id/ui/intro/start_editing.js \
js/id/end.js \
js/lib/locale.js \
data/introGraph.js
diff --git a/index.html b/index.html
index fcb62f626..64befdbcb 100644
--- a/index.html
+++ b/index.html
@@ -45,79 +45,16 @@
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/js/lib/id/ui/core.js b/js/lib/id/ui/core.js
new file mode 100644
index 000000000..d252c2226
--- /dev/null
+++ b/js/lib/id/ui/core.js
@@ -0,0 +1,6005 @@
+(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 = '
';
+ }
+
+ 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 = '
' + t('background.switch') + '
';
+ 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('')
+ .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('
'));
+ });
+
+ 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 += '' + t('map_data.autohidden') + '
';
+ }
+ 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: 'ideditor.com',
+ github: 'github.com'
+ }));
+
+ 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 });
+
+}));
\ No newline at end of file
diff --git a/js/lib/id/ui/index.js b/js/lib/id/ui/index.js
new file mode 100644
index 000000000..1df22b9df
--- /dev/null
+++ b/js/lib/id/ui/index.js
@@ -0,0 +1,251 @@
+(function (global, factory) {
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
+ typeof define === 'function' && define.amd ? define(['exports'], factory) :
+ (factory((global.iD = global.iD || {})));
+}(this, function (exports) { 'use strict';
+
+ function ui(context) {
+ function render(container) {
+ var map = context.map();
+
+ if (iD.detect().opera) container.classed('opera', true);
+
+ var hash = iD.behavior.Hash(context);
+
+ hash();
+
+ if (!hash.hadHash) {
+ map.centerZoom([0, 0], 2);
+ }
+
+ container.append('svg')
+ .attr('id', 'defs')
+ .call(iD.svg.Defs(context));
+
+ container.append('div')
+ .attr('id', 'sidebar')
+ .attr('class', 'col4')
+ .call(ui.sidebar);
+
+ var content = container.append('div')
+ .attr('id', 'content');
+
+ var bar = content.append('div')
+ .attr('id', 'bar')
+ .attr('class', 'fillD');
+
+ content.append('div')
+ .attr('id', 'map')
+ .call(map);
+
+ content
+ .call(iD.ui.MapInMap(context));
+
+ content.append('div')
+ .call(iD.ui.Info(context));
+
+ bar.append('div')
+ .attr('class', 'spacer col4');
+
+ var limiter = bar.append('div')
+ .attr('class', 'limiter');
+
+ limiter.append('div')
+ .attr('class', 'button-wrap joined col3')
+ .call(iD.ui.Modes(context), limiter);
+
+ limiter.append('div')
+ .attr('class', 'button-wrap joined col1')
+ .call(iD.ui.UndoRedo(context));
+
+ limiter.append('div')
+ .attr('class', 'button-wrap col1')
+ .call(iD.ui.Save(context));
+
+ bar.append('div')
+ .attr('class', 'full-screen')
+ .call(iD.ui.FullScreen(context));
+
+ bar.append('div')
+ .attr('class', 'spinner')
+ .call(iD.ui.Spinner(context));
+
+ var controls = bar.append('div')
+ .attr('class', 'map-controls');
+
+ controls.append('div')
+ .attr('class', 'map-control zoombuttons')
+ .call(iD.ui.Zoom(context));
+
+ controls.append('div')
+ .attr('class', 'map-control geolocate-control')
+ .call(iD.ui.Geolocate(context));
+
+ controls.append('div')
+ .attr('class', 'map-control background-control')
+ .call(iD.ui.Background(context));
+
+ controls.append('div')
+ .attr('class', 'map-control map-data-control')
+ .call(iD.ui.MapData(context));
+
+ controls.append('div')
+ .attr('class', 'map-control help-control')
+ .call(iD.ui.Help(context));
+
+ var about = content.append('div')
+ .attr('id', 'about');
+
+ about.append('div')
+ .attr('id', 'attrib')
+ .call(iD.ui.Attribution(context));
+
+ var footer = about.append('div')
+ .attr('id', 'footer')
+ .attr('class', 'fillD');
+
+ footer.append('div')
+ .attr('class', 'api-status')
+ .call(iD.ui.Status(context));
+
+ footer.append('div')
+ .attr('id', 'scale-block')
+ .call(iD.ui.Scale(context));
+
+ var aboutList = footer.append('div')
+ .attr('id', 'info-block')
+ .append('ul')
+ .attr('id', 'about-list');
+
+ if (!context.embed()) {
+ aboutList.call(iD.ui.Account(context));
+ }
+
+ aboutList.append('li')
+ .append('a')
+ .attr('target', '_blank')
+ .attr('tabindex', -1)
+ .attr('href', 'https://github.com/openstreetmap/iD')
+ .text(iD.version);
+
+ var issueLinks = aboutList.append('li');
+
+ issueLinks.append('a')
+ .attr('target', '_blank')
+ .attr('tabindex', -1)
+ .attr('href', 'https://github.com/openstreetmap/iD/issues')
+ .call(iD.svg.Icon('#icon-bug', 'light'))
+ .call(bootstrap.tooltip()
+ .title(t('report_a_bug'))
+ .placement('top')
+ );
+
+ issueLinks.append('a')
+ .attr('target', '_blank')
+ .attr('tabindex', -1)
+ .attr('href', 'https://github.com/openstreetmap/iD/blob/master/CONTRIBUTING.md#translating')
+ .call(iD.svg.Icon('#icon-translate', 'light'))
+ .call(bootstrap.tooltip()
+ .title(t('help_translate'))
+ .placement('top')
+ );
+
+ aboutList.append('li')
+ .attr('class', 'feature-warning')
+ .attr('tabindex', -1)
+ .call(iD.ui.FeatureInfo(context));
+
+ aboutList.append('li')
+ .attr('class', 'user-list')
+ .attr('tabindex', -1)
+ .call(iD.ui.Contributors(context));
+
+ window.onbeforeunload = function() {
+ return context.save();
+ };
+
+ window.onunload = function() {
+ context.history().unlock();
+ };
+
+ var mapDimensions = map.dimensions();
+
+ d3.select(window).on('resize.editor', function() {
+ mapDimensions = content.dimensions(null);
+ map.dimensions(mapDimensions);
+ });
+
+ function pan(d) {
+ return function() {
+ d3.event.preventDefault();
+ if (!context.inIntro()) context.pan(d);
+ };
+ }
+
+ // pan amount
+ var pa = 10;
+
+ var keybinding = d3.keybinding('main')
+ .on('⌫', function() { d3.event.preventDefault(); })
+ .on('←', pan([pa, 0]))
+ .on('↑', pan([0, pa]))
+ .on('→', pan([-pa, 0]))
+ .on('↓', pan([0, -pa]))
+ .on('⇧←', pan([mapDimensions[0], 0]))
+ .on('⇧↑', pan([0, mapDimensions[1]]))
+ .on('⇧→', pan([-mapDimensions[0], 0]))
+ .on('⇧↓', pan([0, -mapDimensions[1]]))
+ .on(iD.ui.cmd('⌘←'), pan([mapDimensions[0], 0]))
+ .on(iD.ui.cmd('⌘↑'), pan([0, mapDimensions[1]]))
+ .on(iD.ui.cmd('⌘→'), pan([-mapDimensions[0], 0]))
+ .on(iD.ui.cmd('⌘↓'), pan([0, -mapDimensions[1]]));
+
+ d3.select(document)
+ .call(keybinding);
+
+ context.enter(iD.modes.Browse(context));
+
+ context.container()
+ .call(iD.ui.Splash(context))
+ .call(iD.ui.Restore(context));
+
+ var authenticating = iD.ui.Loading(context)
+ .message(t('loading_auth'));
+
+ context.connection()
+ .on('authenticating.ui', function() {
+ context.container()
+ .call(authenticating);
+ })
+ .on('authenticated.ui', function() {
+ authenticating.close();
+ });
+ }
+
+ function ui(container) {
+ context.container(container);
+ context.loadLocale(function() {
+ render(container);
+ });
+ }
+
+ ui.sidebar = iD.ui.Sidebar(context);
+
+ return ui;
+ }
+
+ ui.tooltipHtml = function(text, key) {
+ var s = '' + text + '';
+ if (key) {
+ s += '' +
+ ' ' + (t('tooltip_keyhint')) + ' ' +
+ ' ' + key + '
';
+ }
+ return s;
+ };
+
+ exports.ui = ui;
+
+ Object.defineProperty(exports, '__esModule', { value: true });
+
+}));
\ No newline at end of file
diff --git a/js/lib/id/ui/intro.js b/js/lib/id/ui/intro.js
new file mode 100644
index 000000000..465216d98
--- /dev/null
+++ b/js/lib/id/ui/intro.js
@@ -0,0 +1,619 @@
+(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 || {}, global.iD.ui.intro = global.iD.ui.intro || {})));
+}(this, function (exports) { 'use strict';
+
+ function area(context, reveal) {
+ var event = d3.dispatch('done'),
+ timeout;
+
+ var step = {
+ title: 'intro.areas.title'
+ };
+
+ step.enter = function() {
+ var playground = [-85.63552, 41.94159],
+ corner = [-85.63565411045074, 41.9417715536927];
+ context.map().centerZoom(playground, 19);
+ reveal('button.add-area',
+ t('intro.areas.add', { button: iD.ui.intro.icon('#icon-area', 'pre-text') }),
+ { tooltipClass: 'intro-areas-add' });
+
+ context.on('enter.intro', addArea);
+
+ function addArea(mode) {
+ if (mode.id !== 'add-area') return;
+ context.on('enter.intro', drawArea);
+
+ var padding = 120 * Math.pow(2, context.map().zoom() - 19);
+ var pointBox = iD.ui.intro.pad(corner, padding, context);
+ reveal(pointBox, t('intro.areas.corner'));
+
+ context.map().on('move.intro', function() {
+ padding = 120 * Math.pow(2, context.map().zoom() - 19);
+ pointBox = iD.ui.intro.pad(corner, padding, context);
+ reveal(pointBox, t('intro.areas.corner'), {duration: 0});
+ });
+ }
+
+ function drawArea(mode) {
+ if (mode.id !== 'draw-area') return;
+ context.on('enter.intro', enterSelect);
+
+ var padding = 150 * Math.pow(2, context.map().zoom() - 19);
+ var pointBox = iD.ui.intro.pad(playground, padding, context);
+ reveal(pointBox, t('intro.areas.place'));
+
+ context.map().on('move.intro', function() {
+ padding = 150 * Math.pow(2, context.map().zoom() - 19);
+ pointBox = iD.ui.intro.pad(playground, padding, context);
+ reveal(pointBox, t('intro.areas.place'), {duration: 0});
+ });
+ }
+
+ function enterSelect(mode) {
+ if (mode.id !== 'select') return;
+ context.map().on('move.intro', null);
+ context.on('enter.intro', null);
+
+ timeout = setTimeout(function() {
+ reveal('.preset-search-input',
+ t('intro.areas.search',
+ { name: context.presets().item('leisure/playground').name() }));
+ d3.select('.preset-search-input').on('keyup.intro', keySearch);
+ }, 500);
+ }
+
+ function keySearch() {
+ var first = d3.select('.preset-list-item:first-child');
+ if (first.classed('preset-leisure-playground')) {
+ reveal(first.select('.preset-list-button').node(), t('intro.areas.choose'));
+ d3.selection.prototype.one.call(context.history(), 'change.intro', selectedPreset);
+ d3.select('.preset-search-input').on('keyup.intro', null);
+ }
+ }
+
+ function selectedPreset() {
+ reveal('.pane',
+ t('intro.areas.describe', { button: iD.ui.intro.icon('#icon-apply', 'pre-text') }));
+ context.on('exit.intro', event.done);
+ }
+ };
+
+ step.exit = function() {
+ window.clearTimeout(timeout);
+ context.on('enter.intro', null);
+ context.on('exit.intro', null);
+ context.history().on('change.intro', null);
+ context.map().on('move.intro', null);
+ d3.select('.preset-search-input').on('keyup.intro', null);
+ };
+
+ return d3.rebind(step, event, 'on');
+ }
+
+ function line(context, reveal) {
+ var event = d3.dispatch('done'),
+ timeouts = [];
+
+ var step = {
+ title: 'intro.lines.title'
+ };
+
+ function timeout(f, t) {
+ timeouts.push(window.setTimeout(f, t));
+ }
+
+ function eventCancel() {
+ d3.event.stopPropagation();
+ d3.event.preventDefault();
+ }
+
+ step.enter = function() {
+ var centroid = [-85.62830, 41.95699];
+ var midpoint = [-85.62975395449628, 41.95787501510204];
+ var start = [-85.6297754121684, 41.95805253325314];
+ var intersection = [-85.62974496187628, 41.95742515554585];
+
+ context.map().centerZoom(start, 18);
+ reveal('button.add-line',
+ t('intro.lines.add', { button: iD.ui.intro.icon('#icon-line', 'pre-text') }),
+ { tooltipClass: 'intro-lines-add' });
+
+ context.on('enter.intro', addLine);
+
+ function addLine(mode) {
+ if (mode.id !== 'add-line') return;
+ context.on('enter.intro', drawLine);
+
+ var padding = 150 * Math.pow(2, context.map().zoom() - 18);
+ var pointBox = iD.ui.intro.pad(start, padding, context);
+ reveal(pointBox, t('intro.lines.start'));
+
+ context.map().on('move.intro', function() {
+ padding = 150 * Math.pow(2, context.map().zoom() - 18);
+ pointBox = iD.ui.intro.pad(start, padding, context);
+ reveal(pointBox, t('intro.lines.start'), {duration: 0});
+ });
+ }
+
+ function drawLine(mode) {
+ if (mode.id !== 'draw-line') return;
+ context.history().on('change.intro', addIntersection);
+ context.on('enter.intro', retry);
+
+ var padding = 300 * Math.pow(2, context.map().zoom() - 19);
+ var pointBox = iD.ui.intro.pad(midpoint, padding, context);
+ reveal(pointBox, t('intro.lines.intersect', {name: t('intro.graph.flower_st')}));
+
+ context.map().on('move.intro', function() {
+ padding = 300 * Math.pow(2, context.map().zoom() - 19);
+ pointBox = iD.ui.intro.pad(midpoint, padding, context);
+ reveal(pointBox, t('intro.lines.intersect', {name: t('intro.graph.flower_st')}), {duration: 0});
+ });
+ }
+
+ // ended line before creating intersection
+ function retry(mode) {
+ if (mode.id !== 'select') return;
+ var pointBox = iD.ui.intro.pad(intersection, 30, context),
+ ids = mode.selectedIDs();
+ reveal(pointBox, t('intro.lines.restart', {name: t('intro.graph.flower_st')}));
+ d3.select(window).on('mousedown.intro', eventCancel, true);
+
+ timeout(function() {
+ context.replace(iD.actions.DeleteMultiple(ids));
+ step.exit();
+ step.enter();
+ }, 3000);
+ }
+
+ function addIntersection(changes) {
+ if ( _.some(changes.created(), function(d) {
+ return d.type === 'node' && context.graph().parentWays(d).length > 1;
+ })) {
+ context.history().on('change.intro', null);
+ context.on('enter.intro', enterSelect);
+
+ var padding = 900 * Math.pow(2, context.map().zoom() - 19);
+ var pointBox = iD.ui.intro.pad(centroid, padding, context);
+ reveal(pointBox, t('intro.lines.finish'));
+
+ context.map().on('move.intro', function() {
+ padding = 900 * Math.pow(2, context.map().zoom() - 19);
+ pointBox = iD.ui.intro.pad(centroid, padding, context);
+ reveal(pointBox, t('intro.lines.finish'), {duration: 0});
+ });
+ }
+ }
+
+ function enterSelect(mode) {
+ if (mode.id !== 'select') return;
+ context.map().on('move.intro', null);
+ context.on('enter.intro', null);
+ d3.select('#curtain').style('pointer-events', 'all');
+
+ presetCategory();
+ }
+
+ function presetCategory() {
+ timeout(function() {
+ d3.select('#curtain').style('pointer-events', 'none');
+ var road = d3.select('.preset-category-road .preset-list-button');
+ reveal(road.node(), t('intro.lines.road'));
+ road.one('click.intro', roadCategory);
+ }, 500);
+ }
+
+ function roadCategory() {
+ timeout(function() {
+ var grid = d3.select('.subgrid');
+ reveal(grid.node(), t('intro.lines.residential'));
+ grid.selectAll(':not(.preset-highway-residential) .preset-list-button')
+ .one('click.intro', retryPreset);
+ grid.selectAll('.preset-highway-residential .preset-list-button')
+ .one('click.intro', roadDetails);
+ }, 500);
+ }
+
+ // selected wrong road type
+ function retryPreset() {
+ timeout(function() {
+ var preset = d3.select('.entity-editor-pane .preset-list-button');
+ reveal(preset.node(), t('intro.lines.wrong_preset'));
+ preset.one('click.intro', presetCategory);
+ }, 500);
+ }
+
+ function roadDetails() {
+ reveal('.pane',
+ t('intro.lines.describe', { button: iD.ui.intro.icon('#icon-apply', 'pre-text') }));
+ context.on('exit.intro', event.done);
+ }
+
+ };
+
+ step.exit = function() {
+ d3.select(window).on('mousedown.intro', null, true);
+ d3.select('#curtain').style('pointer-events', 'none');
+ timeouts.forEach(window.clearTimeout);
+ context.on('enter.intro', null);
+ context.on('exit.intro', null);
+ context.map().on('move.intro', null);
+ context.history().on('change.intro', null);
+ };
+
+ return d3.rebind(step, event, 'on');
+ }
+
+
+ function pointBox(point, context) {
+ var rect = context.surfaceRect();
+ point = context.projection(point);
+ return {
+ left: point[0] + rect.left - 30,
+ top: point[1] + rect.top - 50,
+ width: 60,
+ height: 70
+ };
+ }
+
+ function pad(box, padding, context) {
+ if (box instanceof Array) {
+ var rect = context.surfaceRect();
+ box = context.projection(box);
+ box = {
+ left: box[0] + rect.left,
+ top: box[1] + rect.top
+ };
+ }
+ return {
+ left: box.left - padding,
+ top: box.top - padding,
+ width: (box.width || 0) + 2 * padding,
+ height: (box.width || 0) + 2 * padding
+ };
+ }
+
+ function icon(name, svgklass) {
+ return '';
+ }
+
+ function navigation(context, reveal) {
+ var event = d3.dispatch('done'),
+ timeouts = [];
+
+ var step = {
+ title: 'intro.navigation.title'
+ };
+
+ function set(f, t) {
+ timeouts.push(window.setTimeout(f, t));
+ }
+
+ function eventCancel() {
+ d3.event.stopPropagation();
+ d3.event.preventDefault();
+ }
+
+ step.enter = function() {
+ var rect = context.surfaceRect(),
+ map = {
+ left: rect.left + 10,
+ top: rect.top + 70,
+ width: rect.width - 70,
+ height: rect.height - 170
+ };
+
+ context.map().centerZoom([-85.63591, 41.94285], 19);
+
+ reveal(map, t('intro.navigation.drag'));
+
+ context.map().on('move.intro', _.debounce(function() {
+ context.map().on('move.intro', null);
+ townhall();
+ context.on('enter.intro', inspectTownHall);
+ }, 400));
+
+ function townhall() {
+ var hall = [-85.63645945147184, 41.942986488012565];
+
+ var point = context.projection(hall);
+ if (point[0] < 0 || point[0] > rect.width ||
+ point[1] < 0 || point[1] > rect.height) {
+ context.map().center(hall);
+ }
+
+ var box = iD.ui.intro.pointBox(hall, context);
+ reveal(box, t('intro.navigation.select'));
+
+ context.map().on('move.intro', function() {
+ var box = iD.ui.intro.pointBox(hall, context);
+ reveal(box, t('intro.navigation.select'), {duration: 0});
+ });
+ }
+
+ function inspectTownHall(mode) {
+ if (mode.id !== 'select') return;
+ context.on('enter.intro', null);
+ context.map().on('move.intro', null);
+ set(function() {
+ reveal('.entity-editor-pane',
+ t('intro.navigation.pane', { button: iD.ui.intro.icon('#icon-close', 'pre-text') }));
+ context.on('exit.intro', streetSearch);
+ }, 700);
+ }
+
+ function streetSearch() {
+ context.on('exit.intro', null);
+ reveal('.search-header input',
+ t('intro.navigation.search', { name: t('intro.graph.spring_st') }));
+ d3.select('.search-header input').on('keyup.intro', searchResult);
+ }
+
+ function searchResult() {
+ var first = d3.select('.feature-list-item:nth-child(0n+2)'), // skip No Results item
+ firstName = first.select('.entity-name'),
+ name = t('intro.graph.spring_st');
+
+ if (!firstName.empty() && firstName.text() === name) {
+ reveal(first.node(), t('intro.navigation.choose', { name: name }));
+ context.on('exit.intro', selectedStreet);
+ d3.select('.search-header input')
+ .on('keydown.intro', eventCancel, true)
+ .on('keyup.intro', null);
+ }
+ }
+
+ function selectedStreet() {
+ var springSt = [-85.63585099140167, 41.942506848938926];
+ context.map().center(springSt);
+ context.on('exit.intro', event.done);
+ set(function() {
+ reveal('.entity-editor-pane',
+ t('intro.navigation.chosen', {
+ name: t('intro.graph.spring_st'),
+ button: iD.ui.intro.icon('#icon-close', 'pre-text')
+ }));
+ }, 400);
+ }
+ };
+
+ step.exit = function() {
+ timeouts.forEach(window.clearTimeout);
+ context.map().on('move.intro', null);
+ context.on('enter.intro', null);
+ context.on('exit.intro', null);
+ d3.select('.search-header input')
+ .on('keydown.intro', null)
+ .on('keyup.intro', null);
+ };
+
+ return d3.rebind(step, event, 'on');
+ }
+
+ function point(context, reveal) {
+ var event = d3.dispatch('done'),
+ timeouts = [];
+
+ var step = {
+ title: 'intro.points.title'
+ };
+
+ function setTimeout(f, t) {
+ timeouts.push(window.setTimeout(f, t));
+ }
+
+ function eventCancel() {
+ d3.event.stopPropagation();
+ d3.event.preventDefault();
+ }
+
+ step.enter = function() {
+ context.map().centerZoom([-85.63279, 41.94394], 19);
+ reveal('button.add-point',
+ t('intro.points.add', { button: iD.ui.intro.icon('#icon-point', 'pre-text') }),
+ { tooltipClass: 'intro-points-add' });
+
+ var corner = [-85.632481,41.944094];
+
+ context.on('enter.intro', addPoint);
+
+ function addPoint(mode) {
+ if (mode.id !== 'add-point') return;
+ context.on('enter.intro', enterSelect);
+
+ var pointBox = iD.ui.intro.pad(corner, 150, context);
+ reveal(pointBox, t('intro.points.place'));
+
+ context.map().on('move.intro', function() {
+ pointBox = iD.ui.intro.pad(corner, 150, context);
+ reveal(pointBox, t('intro.points.place'), {duration: 0});
+ });
+ }
+
+ function enterSelect(mode) {
+ if (mode.id !== 'select') return;
+ context.map().on('move.intro', null);
+ context.on('enter.intro', null);
+
+ setTimeout(function() {
+ reveal('.preset-search-input',
+ t('intro.points.search', {name: context.presets().item('amenity/cafe').name()}));
+ d3.select('.preset-search-input').on('keyup.intro', keySearch);
+ }, 500);
+ }
+
+ function keySearch() {
+ var first = d3.select('.preset-list-item:first-child');
+ if (first.classed('preset-amenity-cafe')) {
+ reveal(first.select('.preset-list-button').node(), t('intro.points.choose'));
+ d3.selection.prototype.one.call(context.history(), 'change.intro', selectedPreset);
+ d3.select('.preset-search-input')
+ .on('keydown.intro', eventCancel, true)
+ .on('keyup.intro', null);
+ }
+ }
+
+ function selectedPreset() {
+ setTimeout(function() {
+ reveal('.entity-editor-pane', t('intro.points.describe'), {tooltipClass: 'intro-points-describe'});
+ context.history().on('change.intro', closeEditor);
+ context.on('exit.intro', selectPoint);
+ }, 400);
+ }
+
+ function closeEditor() {
+ d3.select('.preset-search-input').on('keydown.intro', null);
+ context.history().on('change.intro', null);
+ reveal('.entity-editor-pane',
+ t('intro.points.close', { button: iD.ui.intro.icon('#icon-apply', 'pre-text') }));
+ }
+
+ function selectPoint() {
+ context.on('exit.intro', null);
+ context.history().on('change.intro', null);
+ context.on('enter.intro', enterReselect);
+
+ var pointBox = iD.ui.intro.pad(corner, 150, context);
+ reveal(pointBox, t('intro.points.reselect'));
+
+ context.map().on('move.intro', function() {
+ pointBox = iD.ui.intro.pad(corner, 150, context);
+ reveal(pointBox, t('intro.points.reselect'), {duration: 0});
+ });
+ }
+
+ function enterReselect(mode) {
+ if (mode.id !== 'select') return;
+ context.map().on('move.intro', null);
+ context.on('enter.intro', null);
+
+ setTimeout(function() {
+ reveal('.entity-editor-pane',
+ t('intro.points.fixname', { button: iD.ui.intro.icon('#icon-apply', 'pre-text') }));
+ context.on('exit.intro', deletePoint);
+ }, 500);
+ }
+
+ function deletePoint() {
+ context.on('exit.intro', null);
+ context.on('enter.intro', enterDelete);
+
+ var pointBox = iD.ui.intro.pad(corner, 150, context);
+ reveal(pointBox, t('intro.points.reselect_delete'));
+
+ context.map().on('move.intro', function() {
+ pointBox = iD.ui.intro.pad(corner, 150, context);
+ reveal(pointBox, t('intro.points.reselect_delete'), {duration: 0});
+ });
+ }
+
+ function enterDelete(mode) {
+ if (mode.id !== 'select') return;
+ context.map().on('move.intro', null);
+ context.on('enter.intro', null);
+ context.on('exit.intro', deletePoint);
+ context.map().on('move.intro', deletePoint);
+ context.history().on('change.intro', deleted);
+
+ setTimeout(function() {
+ var node = d3.select('.radial-menu-item-delete').node();
+ var pointBox = iD.ui.intro.pad(node.getBoundingClientRect(), 50, context);
+ reveal(pointBox,
+ t('intro.points.delete', { button: iD.ui.intro.icon('#operation-delete', 'pre-text') }));
+ }, 300);
+ }
+
+ function deleted(changed) {
+ if (changed.deleted().length) event.done();
+ }
+
+ };
+
+ step.exit = function() {
+ timeouts.forEach(window.clearTimeout);
+ context.on('exit.intro', null);
+ context.on('enter.intro', null);
+ context.map().on('move.intro', null);
+ context.history().on('change.intro', null);
+ d3.select('.preset-search-input')
+ .on('keyup.intro', null)
+ .on('keydown.intro', null);
+ };
+
+ return d3.rebind(step, event, 'on');
+ }
+
+ function startEditing(context, reveal) {
+ var event = d3.dispatch('done', 'startEditing'),
+ modal,
+ timeouts = [];
+
+ var step = {
+ title: 'intro.startediting.title'
+ };
+
+ function timeout(f, t) {
+ timeouts.push(window.setTimeout(f, t));
+ }
+
+ step.enter = function() {
+ reveal('.map-control.help-control',
+ t('intro.startediting.help', { button: iD.ui.intro.icon('#icon-help', 'pre-text') }));
+
+ timeout(function() {
+ reveal('#bar button.save', t('intro.startediting.save'));
+ }, 5000);
+
+ timeout(function() {
+ reveal('#surface');
+ }, 10000);
+
+ timeout(function() {
+ modal = iD.ui.modal(context.container());
+
+ modal.select('.modal')
+ .attr('class', 'modal-splash modal col6');
+
+ modal.selectAll('.close').remove();
+
+ var startbutton = modal.select('.content')
+ .attr('class', 'fillL')
+ .append('button')
+ .attr('class', 'modal-section huge-modal-button')
+ .on('click', function() {
+ modal.remove();
+ });
+
+ startbutton.append('div')
+ .attr('class','illustration');
+ startbutton.append('h2')
+ .text(t('intro.startediting.start'));
+
+ event.startEditing();
+ }, 10500);
+ };
+
+ step.exit = function() {
+ if (modal) modal.remove();
+ timeouts.forEach(window.clearTimeout);
+ };
+
+ return d3.rebind(step, event, 'on');
+ }
+
+ exports.area = area;
+ exports.line = line;
+ exports.pad = pad;
+ exports.pointBox = pointBox;
+ exports.icon = icon;
+ exports.navigation = navigation;
+ exports.point = point;
+ exports.startEditing = startEditing;
+
+ Object.defineProperty(exports, '__esModule', { value: true });
+
+}));
\ No newline at end of file
diff --git a/js/lib/id/ui/preset.js b/js/lib/id/ui/preset.js
new file mode 100644
index 000000000..5d3ff168b
--- /dev/null
+++ b/js/lib/id/ui/preset.js
@@ -0,0 +1,1862 @@
+(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 || {}, global.iD.ui.preset = global.iD.ui.preset || {})));
+}(this, function (exports) { 'use strict';
+
+ function access(field) {
+ var dispatch = d3.dispatch('change'),
+ items;
+
+ function access(selection) {
+ var wrap = selection.selectAll('.preset-input-wrap')
+ .data([0]);
+
+ wrap.enter().append('div')
+ .attr('class', 'cf preset-input-wrap')
+ .append('ul');
+
+ items = wrap.select('ul').selectAll('li')
+ .data(field.keys);
+
+ // Enter
+
+ var enter = items.enter().append('li')
+ .attr('class', function(d) { return 'cf preset-access-' + d; });
+
+ enter.append('span')
+ .attr('class', 'col6 label preset-label-access')
+ .attr('for', function(d) { return 'preset-input-access-' + d; })
+ .text(function(d) { return field.t('types.' + d); });
+
+ enter.append('div')
+ .attr('class', 'col6 preset-input-access-wrap')
+ .append('input')
+ .attr('type', 'text')
+ .attr('class', 'preset-input-access')
+ .attr('id', function(d) { return 'preset-input-access-' + d; })
+ .each(function(d) {
+ d3.select(this)
+ .call(d3.combobox()
+ .data(access.options(d)));
+ });
+
+ // Update
+
+ wrap.selectAll('.preset-input-access')
+ .on('change', change)
+ .on('blur', change);
+ }
+
+ function change(d) {
+ var tag = {};
+ tag[d] = d3.select(this).value() || undefined;
+ dispatch.change(tag);
+ }
+
+ access.options = function(type) {
+ var options = ['no', 'permissive', 'private', 'destination'];
+
+ if (type !== 'access') {
+ options.unshift('yes');
+ options.push('designated');
+
+ if (type === 'bicycle') {
+ options.push('dismount');
+ }
+ }
+
+ return options.map(function(option) {
+ return {
+ title: field.t('options.' + option + '.description'),
+ value: option
+ };
+ });
+ };
+
+ var placeholders = {
+ footway: {
+ foot: 'designated',
+ motor_vehicle: 'no'
+ },
+ steps: {
+ foot: 'yes',
+ motor_vehicle: 'no',
+ bicycle: 'no',
+ horse: 'no'
+ },
+ pedestrian: {
+ foot: 'yes',
+ motor_vehicle: 'no'
+ },
+ cycleway: {
+ motor_vehicle: 'no',
+ bicycle: 'designated'
+ },
+ bridleway: {
+ motor_vehicle: 'no',
+ horse: 'designated'
+ },
+ path: {
+ foot: 'yes',
+ motor_vehicle: 'no',
+ bicycle: 'yes',
+ horse: 'yes'
+ },
+ motorway: {
+ foot: 'no',
+ motor_vehicle: 'yes',
+ bicycle: 'no',
+ horse: 'no'
+ },
+ trunk: {
+ motor_vehicle: 'yes'
+ },
+ primary: {
+ foot: 'yes',
+ motor_vehicle: 'yes',
+ bicycle: 'yes',
+ horse: 'yes'
+ },
+ secondary: {
+ foot: 'yes',
+ motor_vehicle: 'yes',
+ bicycle: 'yes',
+ horse: 'yes'
+ },
+ tertiary: {
+ foot: 'yes',
+ motor_vehicle: 'yes',
+ bicycle: 'yes',
+ horse: 'yes'
+ },
+ residential: {
+ foot: 'yes',
+ motor_vehicle: 'yes',
+ bicycle: 'yes',
+ horse: 'yes'
+ },
+ unclassified: {
+ foot: 'yes',
+ motor_vehicle: 'yes',
+ bicycle: 'yes',
+ horse: 'yes'
+ },
+ service: {
+ foot: 'yes',
+ motor_vehicle: 'yes',
+ bicycle: 'yes',
+ horse: 'yes'
+ },
+ motorway_link: {
+ foot: 'no',
+ motor_vehicle: 'yes',
+ bicycle: 'no',
+ horse: 'no'
+ },
+ trunk_link: {
+ motor_vehicle: 'yes'
+ },
+ primary_link: {
+ foot: 'yes',
+ motor_vehicle: 'yes',
+ bicycle: 'yes',
+ horse: 'yes'
+ },
+ secondary_link: {
+ foot: 'yes',
+ motor_vehicle: 'yes',
+ bicycle: 'yes',
+ horse: 'yes'
+ },
+ tertiary_link: {
+ foot: 'yes',
+ motor_vehicle: 'yes',
+ bicycle: 'yes',
+ horse: 'yes'
+ }
+ };
+
+ access.tags = function(tags) {
+ items.selectAll('.preset-input-access')
+ .value(function(d) { return tags[d] || ''; })
+ .attr('placeholder', function() {
+ return tags.access ? tags.access : field.placeholder();
+ });
+
+ // items.selectAll('#preset-input-access-access')
+ // .attr('placeholder', 'yes');
+
+ _.forEach(placeholders[tags.highway], function(v, k) {
+ items.selectAll('#preset-input-access-' + k)
+ .attr('placeholder', function() { return (tags.access || v); });
+ });
+ };
+
+ access.focus = function() {
+ items.selectAll('.preset-input-access')
+ .node().focus();
+ };
+
+ return d3.rebind(access, dispatch, 'on');
+ }
+
+ function address(field, context) {
+ var dispatch = d3.dispatch('init', 'change'),
+ wrap,
+ entity,
+ isInitialized;
+
+ var widths = {
+ housenumber: 1/3,
+ street: 2/3,
+ city: 2/3,
+ state: 1/4,
+ postcode: 1/3
+ };
+
+ function getStreets() {
+ var extent = entity.extent(context.graph()),
+ l = extent.center(),
+ box = iD.geo.Extent(l).padByMeters(200);
+
+ return context.intersects(box)
+ .filter(isAddressable)
+ .map(function(d) {
+ var loc = context.projection([
+ (extent[0][0] + extent[1][0]) / 2,
+ (extent[0][1] + extent[1][1]) / 2]),
+ choice = iD.geo.chooseEdge(context.childNodes(d), loc, context.projection);
+ return {
+ title: d.tags.name,
+ value: d.tags.name,
+ dist: choice.distance
+ };
+ }).sort(function(a, b) {
+ return a.dist - b.dist;
+ });
+
+ function isAddressable(d) {
+ return d.tags.highway && d.tags.name && d.type === 'way';
+ }
+ }
+
+ function getCities() {
+ var extent = entity.extent(context.graph()),
+ l = extent.center(),
+ box = iD.geo.Extent(l).padByMeters(200);
+
+ return context.intersects(box)
+ .filter(isAddressable)
+ .map(function(d) {
+ return {
+ title: d.tags['addr:city'] || d.tags.name,
+ value: d.tags['addr:city'] || d.tags.name,
+ dist: iD.geo.sphericalDistance(d.extent(context.graph()).center(), l)
+ };
+ }).sort(function(a, b) {
+ return a.dist - b.dist;
+ });
+
+ function isAddressable(d) {
+ if (d.tags.name &&
+ (d.tags.admin_level === '8' || d.tags.border_type === 'city'))
+ return true;
+
+ if (d.tags.place && d.tags.name && (
+ d.tags.place === 'city' ||
+ d.tags.place === 'town' ||
+ d.tags.place === 'village'))
+ return true;
+
+ if (d.tags['addr:city']) return true;
+
+ return false;
+ }
+ }
+
+ function getPostCodes() {
+ var extent = entity.extent(context.graph()),
+ l = extent.center(),
+ box = iD.geo.Extent(l).padByMeters(200);
+
+ return context.intersects(box)
+ .filter(isAddressable)
+ .map(function(d) {
+ return {
+ title: d.tags['addr:postcode'],
+ value: d.tags['addr:postcode'],
+ dist: iD.geo.sphericalDistance(d.extent(context.graph()).center(), l)
+ };
+ }).sort(function(a, b) {
+ return a.dist - b.dist;
+ });
+
+ function isAddressable(d) {
+ return d.tags['addr:postcode'];
+ }
+ }
+
+ function address(selection) {
+ isInitialized = false;
+
+ wrap = selection.selectAll('.preset-input-wrap')
+ .data([0]);
+
+ // Enter
+
+ wrap.enter()
+ .append('div')
+ .attr('class', 'preset-input-wrap');
+
+ var center = entity.extent(context.graph()).center(),
+ addressFormat;
+
+ iD.services.nominatim().countryCode(center, function (err, countryCode) {
+ addressFormat = _.find(iD.data.addressFormats, function (a) {
+ return a && a.countryCodes && _.includes(a.countryCodes, countryCode);
+ }) || _.first(iD.data.addressFormats);
+
+ function row(r) {
+ // Normalize widths.
+ var total = _.reduce(r, function(sum, field) {
+ return sum + (widths[field] || 0.5);
+ }, 0);
+
+ return r.map(function (field) {
+ return {
+ id: field,
+ width: (widths[field] || 0.5) / total
+ };
+ });
+ }
+
+ wrap.selectAll('div')
+ .data(addressFormat.format)
+ .enter()
+ .append('div')
+ .attr('class', 'addr-row')
+ .selectAll('input')
+ .data(row)
+ .enter()
+ .append('input')
+ .property('type', 'text')
+ .attr('placeholder', function (d) { return field.t('placeholders.' + d.id); })
+ .attr('class', function (d) { return 'addr-' + d.id; })
+ .style('width', function (d) { return d.width * 100 + '%'; });
+
+ // Update
+
+ wrap.selectAll('.addr-street')
+ .call(d3.combobox()
+ .fetcher(function(value, callback) {
+ callback(getStreets());
+ }));
+
+ wrap.selectAll('.addr-city')
+ .call(d3.combobox()
+ .fetcher(function(value, callback) {
+ callback(getCities());
+ }));
+
+ wrap.selectAll('.addr-postcode')
+ .call(d3.combobox()
+ .fetcher(function(value, callback) {
+ callback(getPostCodes());
+ }));
+
+ wrap.selectAll('input')
+ .on('blur', change())
+ .on('change', change());
+
+ wrap.selectAll('input:not(.combobox-input)')
+ .on('input', change(true));
+
+ dispatch.init();
+ isInitialized = true;
+ });
+ }
+
+ function change(onInput) {
+ return function() {
+ var tags = {};
+
+ wrap.selectAll('input')
+ .each(function (field) {
+ tags['addr:' + field.id] = this.value || undefined;
+ });
+
+ dispatch.change(tags, onInput);
+ };
+ }
+
+ function updateTags(tags) {
+ wrap.selectAll('input')
+ .value(function (field) {
+ return tags['addr:' + field.id] || '';
+ });
+ }
+
+ address.entity = function(_) {
+ if (!arguments.length) return entity;
+ entity = _;
+ return address;
+ };
+
+ address.tags = function(tags) {
+ if (isInitialized) {
+ updateTags(tags);
+ } else {
+ dispatch.on('init', function () {
+ updateTags(tags);
+ });
+ }
+ };
+
+ address.focus = function() {
+ var node = wrap.selectAll('input').node();
+ if (node) node.focus();
+ };
+
+ return d3.rebind(address, dispatch, 'on');
+ }
+
+ function check(field) {
+ var dispatch = d3.dispatch('change'),
+ options = field.strings && field.strings.options,
+ values = [],
+ texts = [],
+ entity, value, box, text, label;
+
+ if (options) {
+ for (var k in options) {
+ values.push(k === 'undefined' ? undefined : k);
+ texts.push(field.t('options.' + k, { 'default': options[k] }));
+ }
+ } else {
+ values = [undefined, 'yes'];
+ texts = [t('inspector.unknown'), t('inspector.check.yes')];
+ if (field.type === 'check') {
+ values.push('no');
+ texts.push(t('inspector.check.no'));
+ }
+ }
+
+ var check = function(selection) {
+ // hack: pretend oneway field is a oneway_yes field
+ // where implied oneway tag exists (e.g. `junction=roundabout`) #2220, #1841
+ if (field.id === 'oneway') {
+ for (var key in entity.tags) {
+ if (key in iD.oneWayTags && (entity.tags[key] in iD.oneWayTags[key])) {
+ texts[0] = t('presets.fields.oneway_yes.options.undefined');
+ break;
+ }
+ }
+ }
+
+ selection.classed('checkselect', 'true');
+
+ label = selection.selectAll('.preset-input-wrap')
+ .data([0]);
+
+ var enter = label.enter().append('label')
+ .attr('class', 'preset-input-wrap');
+
+ enter.append('input')
+ .property('indeterminate', field.type === 'check')
+ .attr('type', 'checkbox')
+ .attr('id', 'preset-input-' + field.id);
+
+ enter.append('span')
+ .text(texts[0])
+ .attr('class', 'value');
+
+ box = label.select('input')
+ .on('click', function() {
+ var t = {};
+ t[field.key] = values[(values.indexOf(value) + 1) % values.length];
+ dispatch.change(t);
+ d3.event.stopPropagation();
+ });
+
+ text = label.select('span.value');
+ };
+
+ check.entity = function(_) {
+ if (!arguments.length) return entity;
+ entity = _;
+ return check;
+ };
+
+ check.tags = function(tags) {
+ value = tags[field.key];
+ box.property('indeterminate', field.type === 'check' && !value);
+ box.property('checked', value === 'yes');
+ text.text(texts[values.indexOf(value)]);
+ label.classed('set', !!value);
+ };
+
+ check.focus = function() {
+ box.node().focus();
+ };
+
+ return d3.rebind(check, dispatch, 'on');
+ }
+
+ function combo(field, context) {
+ var dispatch = d3.dispatch('change'),
+ isMulti = (field.type === 'multiCombo'),
+ optstrings = field.strings && field.strings.options,
+ optarray = field.options,
+ snake_case = (field.snake_case || (field.snake_case === undefined)),
+ combobox = d3.combobox().minItems(isMulti ? 1 : 2),
+ comboData = [],
+ multiData = [],
+ container,
+ input,
+ entity;
+
+ // ensure multiCombo field.key ends with a ':'
+ if (isMulti && field.key.match(/:$/) === null) {
+ field.key += ':';
+ }
+
+
+ function snake(s) {
+ return s.replace(/\s+/g, '_');
+ }
+
+ function unsnake(s) {
+ return s.replace(/_+/g, ' ');
+ }
+
+ function clean(s) {
+ return s.split(';')
+ .map(function(s) { return s.trim(); })
+ .join(';');
+ }
+
+
+ // returns the tag value for a display value
+ // (for multiCombo, dval should be the key suffix, not the entire key)
+ function tagValue(dval) {
+ dval = clean(dval || '');
+
+ if (optstrings) {
+ var match = _.find(comboData, function(o) {
+ return o.key && clean(o.value) === dval;
+ });
+ if (match) {
+ return match.key;
+ }
+ }
+
+ if (field.type === 'typeCombo' && !dval) {
+ return 'yes';
+ }
+
+ return (snake_case ? snake(dval) : dval) || undefined;
+ }
+
+
+ // returns the display value for a tag value
+ // (for multiCombo, tval should be the key suffix, not the entire key)
+ function displayValue(tval) {
+ tval = tval || '';
+
+ if (optstrings) {
+ var match = _.find(comboData, function(o) { return o.key === tval && o.value; });
+ if (match) {
+ return match.value;
+ }
+ }
+
+ if (field.type === 'typeCombo' && tval.toLowerCase() === 'yes') {
+ return '';
+ }
+
+ return snake_case ? unsnake(tval) : tval;
+ }
+
+
+ function objectDifference(a, b) {
+ return _.reject(a, function(d1) {
+ return _.some(b, function(d2) { return d1.value === d2.value; });
+ });
+ }
+
+
+ function initCombo(selection, attachTo) {
+ if (optstrings) {
+ selection.attr('readonly', 'readonly');
+ selection.call(combobox, attachTo);
+ setStaticValues(setPlaceholder);
+
+ } else if (optarray) {
+ selection.call(combobox, attachTo);
+ setStaticValues(setPlaceholder);
+
+ } else if (context.taginfo()) {
+ selection.call(combobox.fetcher(setTaginfoValues), attachTo);
+ setTaginfoValues('', setPlaceholder);
+ }
+ }
+
+
+ function setStaticValues(callback) {
+ if (!(optstrings || optarray)) return;
+
+ if (optstrings) {
+ comboData = Object.keys(optstrings).map(function(k) {
+ var v = field.t('options.' + k, { 'default': optstrings[k] });
+ return {
+ key: k,
+ value: v,
+ title: v
+ };
+ });
+
+ } else if (optarray) {
+ comboData = optarray.map(function(k) {
+ var v = snake_case ? unsnake(k) : k;
+ return {
+ key: k,
+ value: v,
+ title: v
+ };
+ });
+ }
+
+ combobox.data(objectDifference(comboData, multiData));
+ if (callback) callback(comboData);
+ }
+
+
+ function setTaginfoValues(q, callback) {
+ var fn = isMulti ? 'multikeys' : 'values';
+ context.taginfo()[fn]({
+ debounce: true,
+ key: field.key,
+ geometry: context.geometry(entity.id),
+ query: (isMulti ? field.key : '') + q
+ }, function(err, data) {
+ if (err) return;
+ comboData = _.map(data, 'value').map(function(k) {
+ if (isMulti) k = k.replace(field.key, '');
+ var v = snake_case ? unsnake(k) : k;
+ return {
+ key: k,
+ value: v,
+ title: v
+ };
+ });
+ comboData = objectDifference(comboData, multiData);
+ if (callback) callback(comboData);
+ });
+ }
+
+
+ function setPlaceholder(d) {
+ var ph;
+ if (isMulti) {
+ ph = field.placeholder() || t('inspector.add');
+ } else {
+ var vals = _.map(d, 'value').filter(function(s) { return s.length < 20; }),
+ placeholders = vals.length > 1 ? vals : _.map(d, 'key');
+ ph = field.placeholder() || placeholders.slice(0, 3).join(', ');
+ }
+
+ input.attr('placeholder', ph + '…');
+ }
+
+
+ function change() {
+ var val = tagValue(input.value()),
+ t = {};
+
+ if (isMulti) {
+ if (!val) return;
+ container.classed('active', false);
+ input.value('');
+ field.keys.push(field.key + val);
+ t[field.key + val] = 'yes';
+ window.setTimeout(function() { input.node().focus(); }, 10);
+
+ } else {
+ t[field.key] = val;
+ }
+
+ dispatch.change(t);
+ }
+
+
+ function removeMultikey(d) {
+ d3.event.stopPropagation();
+ var t = {};
+ t[d.key] = undefined;
+ dispatch.change(t);
+ }
+
+
+ function combo(selection) {
+ if (isMulti) {
+ container = selection.selectAll('ul').data([0]);
+
+ container.enter()
+ .append('ul')
+ .attr('class', 'form-field-multicombo')
+ .on('click', function() {
+ window.setTimeout(function() { input.node().focus(); }, 10);
+ });
+
+ } else {
+ container = selection;
+ }
+
+ input = container.selectAll('input')
+ .data([0]);
+
+ input.enter()
+ .append('input')
+ .attr('type', 'text')
+ .attr('id', 'preset-input-' + field.id)
+ .call(initCombo, selection);
+
+ input
+ .on('change', change)
+ .on('blur', change);
+
+ if (isMulti) {
+ combobox
+ .on('accept', function() {
+ input.node().blur();
+ input.node().focus();
+ });
+
+ input
+ .on('focus', function() { container.classed('active', true); });
+ }
+ }
+
+
+ combo.tags = function(tags) {
+ if (isMulti) {
+ multiData = [];
+
+ // Build multiData array containing keys already set..
+ Object.keys(tags).forEach(function(key) {
+ if (key.indexOf(field.key) !== 0 || tags[key].toLowerCase() !== 'yes') return;
+
+ var suffix = key.substring(field.key.length);
+ multiData.push({
+ key: key,
+ value: displayValue(suffix)
+ });
+ });
+
+ // Set keys for form-field modified (needed for undo and reset buttons)..
+ field.keys = _.map(multiData, 'key');
+
+ // Exclude existing multikeys from combo options..
+ var available = objectDifference(comboData, multiData);
+ combobox.data(available);
+
+ // Hide "Add" button if this field uses fixed set of
+ // translateable optstrings and they're all currently used..
+ container.selectAll('.combobox-input, .combobox-caret')
+ .classed('hide', optstrings && !available.length);
+
+
+ // Render chips
+ var chips = container.selectAll('.chips').data(multiData);
+
+ var enter = chips.enter()
+ .insert('li', 'input')
+ .attr('class', 'chips');
+
+ enter.append('span');
+ enter.append('a');
+
+ chips.select('span')
+ .text(function(d) { return d.value; });
+
+ chips.select('a')
+ .on('click', removeMultikey)
+ .attr('class', 'remove')
+ .text('×');
+
+ chips.exit()
+ .remove();
+
+ } else {
+ input.value(displayValue(tags[field.key]));
+ }
+ };
+
+
+ combo.focus = function() {
+ input.node().focus();
+ };
+
+
+ combo.entity = function(_) {
+ if (!arguments.length) return entity;
+ entity = _;
+ return combo;
+ };
+
+
+ return d3.rebind(combo, dispatch, 'on');
+ }
+
+ function cycleway(field) {
+ var dispatch = d3.dispatch('change'),
+ items;
+
+ function cycleway(selection) {
+ var wrap = selection.selectAll('.preset-input-wrap')
+ .data([0]);
+
+ wrap.enter().append('div')
+ .attr('class', 'cf preset-input-wrap')
+ .append('ul');
+
+ items = wrap.select('ul').selectAll('li')
+ .data(field.keys);
+
+ // Enter
+
+ var enter = items.enter().append('li')
+ .attr('class', function(d) { return 'cf preset-cycleway-' + d; });
+
+ enter.append('span')
+ .attr('class', 'col6 label preset-label-cycleway')
+ .attr('for', function(d) { return 'preset-input-cycleway-' + d; })
+ .text(function(d) { return field.t('types.' + d); });
+
+ enter.append('div')
+ .attr('class', 'col6 preset-input-cycleway-wrap')
+ .append('input')
+ .attr('type', 'text')
+ .attr('class', 'preset-input-cycleway')
+ .attr('id', function(d) { return 'preset-input-cycleway-' + d; })
+ .each(function(d) {
+ d3.select(this)
+ .call(d3.combobox()
+ .data(cycleway.options(d)));
+ });
+
+ // Update
+
+ wrap.selectAll('.preset-input-cycleway')
+ .on('change', change)
+ .on('blur', change);
+ }
+
+ function change() {
+ var inputs = d3.selectAll('.preset-input-cycleway')[0],
+ left = d3.select(inputs[0]).value(),
+ right = d3.select(inputs[1]).value(),
+ tag = {};
+ if (left === 'none' || left === '') { left = undefined; }
+ if (right === 'none' || right === '') { right = undefined; }
+
+ // Always set both left and right as changing one can affect the other
+ tag = {
+ cycleway: undefined,
+ 'cycleway:left': left,
+ 'cycleway:right': right
+ };
+
+ // If the left and right tags match, use the cycleway tag to tag both
+ // sides the same way
+ if (left === right) {
+ tag = {
+ cycleway: left,
+ 'cycleway:left': undefined,
+ 'cycleway:right': undefined
+ };
+ }
+
+ dispatch.change(tag);
+ }
+
+ cycleway.options = function() {
+ return d3.keys(field.strings.options).map(function(option) {
+ return {
+ title: field.t('options.' + option + '.description'),
+ value: option
+ };
+ });
+ };
+
+ cycleway.tags = function(tags) {
+ items.selectAll('.preset-input-cycleway')
+ .value(function(d) {
+ // If cycleway is set, always return that
+ if (tags.cycleway) {
+ return tags.cycleway;
+ }
+ return tags[d] || '';
+ })
+ .attr('placeholder', field.placeholder());
+ };
+
+ cycleway.focus = function() {
+ items.selectAll('.preset-input-cycleway')
+ .node().focus();
+ };
+
+ return d3.rebind(cycleway, dispatch, 'on');
+ }
+
+ function url(field, context) {
+
+ var dispatch = d3.dispatch('change'),
+ input,
+ entity;
+
+ function i(selection) {
+ var fieldId = 'preset-input-' + field.id;
+
+ input = selection.selectAll('input')
+ .data([0]);
+
+ input.enter().append('input')
+ .attr('type', field.type)
+ .attr('id', fieldId)
+ .attr('placeholder', field.placeholder() || t('inspector.unknown'));
+
+ input
+ .on('input', change(true))
+ .on('blur', change())
+ .on('change', change());
+
+ if (field.type === 'tel') {
+ var center = entity.extent(context.graph()).center();
+ iD.services.nominatim().countryCode(center, function (err, countryCode) {
+ if (err || !iD.data.phoneFormats[countryCode]) return;
+ selection.selectAll('#' + fieldId)
+ .attr('placeholder', iD.data.phoneFormats[countryCode]);
+ });
+
+ } else if (field.type === 'number') {
+ input.attr('type', 'text');
+
+ var spinControl = selection.selectAll('.spin-control')
+ .data([0]);
+
+ var enter = spinControl.enter().append('div')
+ .attr('class', 'spin-control');
+
+ enter.append('button')
+ .datum(1)
+ .attr('class', 'increment')
+ .attr('tabindex', -1);
+
+ enter.append('button')
+ .datum(-1)
+ .attr('class', 'decrement')
+ .attr('tabindex', -1);
+
+ spinControl.selectAll('button')
+ .on('click', function(d) {
+ d3.event.preventDefault();
+ var num = parseInt(input.node().value || 0, 10);
+ if (!isNaN(num)) input.node().value = num + d;
+ change()();
+ });
+ }
+ }
+
+ function change(onInput) {
+ return function() {
+ var t = {};
+ t[field.key] = input.value() || undefined;
+ dispatch.change(t, onInput);
+ };
+ }
+
+ i.entity = function(_) {
+ if (!arguments.length) return entity;
+ entity = _;
+ return i;
+ };
+
+ i.tags = function(tags) {
+ input.value(tags[field.key] || '');
+ };
+
+ i.focus = function() {
+ var node = input.node();
+ if (node) node.focus();
+ };
+
+ return d3.rebind(i, dispatch, 'on');
+ }
+
+ function localized(field, context) {
+ var dispatch = d3.dispatch('change', 'input'),
+ wikipedia = iD.services.wikipedia(),
+ input, localizedInputs, wikiTitles,
+ entity;
+
+ function localized(selection) {
+ input = selection.selectAll('.localized-main')
+ .data([0]);
+
+ input.enter().append('input')
+ .attr('type', 'text')
+ .attr('id', 'preset-input-' + field.id)
+ .attr('class', 'localized-main')
+ .attr('placeholder', field.placeholder());
+
+ if (field.id === 'name') {
+ var preset = context.presets().match(entity, context.graph());
+ input.call(d3.combobox().fetcher(
+ iD.util.SuggestNames(preset, iD.data.suggestions)
+ ));
+ }
+
+ input
+ .on('input', change(true))
+ .on('blur', change())
+ .on('change', change());
+
+ var translateButton = selection.selectAll('.localized-add')
+ .data([0]);
+
+ translateButton.enter()
+ .append('button')
+ .attr('class', 'button-input-action localized-add minor')
+ .attr('tabindex', -1)
+ .call(iD.svg.Icon('#icon-plus'))
+ .call(bootstrap.tooltip()
+ .title(t('translate.translate'))
+ .placement('left'));
+
+ translateButton
+ .on('click', addNew);
+
+ localizedInputs = selection.selectAll('.localized-wrap')
+ .data([0]);
+
+ localizedInputs.enter().append('div')
+ .attr('class', 'localized-wrap');
+ }
+
+ function addNew() {
+ d3.event.preventDefault();
+ var data = localizedInputs.selectAll('div.entry').data();
+ var defaultLang = iD.detect().locale.toLowerCase().split('-')[0];
+ var langExists = _.find(data, function(datum) { return datum.lang === defaultLang;});
+ var isLangEn = defaultLang.indexOf('en') > -1;
+ if (isLangEn || langExists) {
+ defaultLang = '';
+ }
+ data.push({ lang: defaultLang, value: '' });
+ localizedInputs.call(render, data);
+ }
+
+ function change(onInput) {
+ return function() {
+ var t = {};
+ t[field.key] = d3.select(this).value() || undefined;
+ dispatch.change(t, onInput);
+ };
+ }
+
+ function key(lang) { return field.key + ':' + lang; }
+
+ function changeLang(d) {
+ var lang = d3.select(this).value(),
+ t = {},
+ language = _.find(iD.data.wikipedia, function(d) {
+ return d[0].toLowerCase() === lang.toLowerCase() ||
+ d[1].toLowerCase() === lang.toLowerCase();
+ });
+
+ if (language) lang = language[2];
+
+ if (d.lang && d.lang !== lang) {
+ t[key(d.lang)] = undefined;
+ }
+
+ var value = d3.select(this.parentNode)
+ .selectAll('.localized-value')
+ .value();
+
+ if (lang && value) {
+ t[key(lang)] = value;
+ } else if (lang && wikiTitles && wikiTitles[d.lang]) {
+ t[key(lang)] = wikiTitles[d.lang];
+ }
+
+ d.lang = lang;
+ dispatch.change(t);
+ }
+
+ function changeValue(d) {
+ if (!d.lang) return;
+ var t = {};
+ t[key(d.lang)] = d3.select(this).value() || undefined;
+ dispatch.change(t);
+ }
+
+ function fetcher(value, cb) {
+ var v = value.toLowerCase();
+
+ cb(iD.data.wikipedia.filter(function(d) {
+ return d[0].toLowerCase().indexOf(v) >= 0 ||
+ d[1].toLowerCase().indexOf(v) >= 0 ||
+ d[2].toLowerCase().indexOf(v) >= 0;
+ }).map(function(d) {
+ return { value: d[1] };
+ }));
+ }
+
+ function render(selection, data) {
+ var wraps = selection.selectAll('div.entry').
+ data(data, function(d) { return d.lang; });
+
+ var innerWrap = wraps.enter()
+ .insert('div', ':first-child');
+
+ innerWrap.attr('class', 'entry')
+ .each(function() {
+ var wrap = d3.select(this);
+ var langcombo = d3.combobox().fetcher(fetcher).minItems(0);
+
+ var label = wrap.append('label')
+ .attr('class','form-label')
+ .text(t('translate.localized_translation_label'))
+ .attr('for','localized-lang');
+
+ label.append('button')
+ .attr('class', 'minor remove')
+ .on('click', function(d){
+ d3.event.preventDefault();
+ var t = {};
+ t[key(d.lang)] = undefined;
+ dispatch.change(t);
+ d3.select(this.parentNode.parentNode)
+ .style('top','0')
+ .style('max-height','240px')
+ .transition()
+ .style('opacity', '0')
+ .style('max-height','0px')
+ .remove();
+ })
+ .call(iD.svg.Icon('#operation-delete'));
+
+ wrap.append('input')
+ .attr('class', 'localized-lang')
+ .attr('type', 'text')
+ .attr('placeholder',t('translate.localized_translation_language'))
+ .on('blur', changeLang)
+ .on('change', changeLang)
+ .call(langcombo);
+
+ wrap.append('input')
+ .on('blur', changeValue)
+ .on('change', changeValue)
+ .attr('type', 'text')
+ .attr('placeholder', t('translate.localized_translation_name'))
+ .attr('class', 'localized-value');
+ });
+
+ innerWrap
+ .style('margin-top', '0px')
+ .style('max-height', '0px')
+ .style('opacity', '0')
+ .transition()
+ .duration(200)
+ .style('margin-top', '10px')
+ .style('max-height', '240px')
+ .style('opacity', '1')
+ .each('end', function() {
+ d3.select(this)
+ .style('max-height', '')
+ .style('overflow', 'visible');
+ });
+
+ wraps.exit()
+ .transition()
+ .duration(200)
+ .style('max-height','0px')
+ .style('opacity', '0')
+ .style('top','-10px')
+ .remove();
+
+ var entry = selection.selectAll('.entry');
+
+ entry.select('.localized-lang')
+ .value(function(d) {
+ var lang = _.find(iD.data.wikipedia, function(lang) { return lang[2] === d.lang; });
+ return lang ? lang[1] : d.lang;
+ });
+
+ entry.select('.localized-value')
+ .value(function(d) { return d.value; });
+ }
+
+ localized.tags = function(tags) {
+ // Fetch translations from wikipedia
+ if (tags.wikipedia && !wikiTitles) {
+ wikiTitles = {};
+ var wm = tags.wikipedia.match(/([^:]+):(.+)/);
+ if (wm && wm[0] && wm[1]) {
+ wikipedia.translations(wm[1], wm[2], function(d) {
+ wikiTitles = d;
+ });
+ }
+ }
+
+ input.value(tags[field.key] || '');
+
+ var postfixed = [], k, m;
+ for (k in tags) {
+ m = k.match(/^(.*):([a-zA-Z_-]+)$/);
+ if (m && m[1] === field.key && m[2]) {
+ postfixed.push({ lang: m[2], value: tags[k] });
+ }
+ }
+
+ localizedInputs.call(render, postfixed.reverse());
+ };
+
+ localized.focus = function() {
+ input.node().focus();
+ };
+
+ localized.entity = function(_) {
+ if (!arguments.length) return entity;
+ entity = _;
+ return localized;
+ };
+
+ return d3.rebind(localized, dispatch, 'on');
+ }
+
+ function maxspeed(field, context) {
+ var dispatch = d3.dispatch('change'),
+ entity,
+ imperial,
+ unitInput,
+ combobox,
+ input;
+
+ var metricValues = [20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120],
+ imperialValues = [20, 25, 30, 35, 40, 45, 50, 55, 65, 70];
+
+ function maxspeed(selection) {
+ combobox = d3.combobox();
+ var unitCombobox = d3.combobox().data(['km/h', 'mph'].map(comboValues));
+
+ input = selection.selectAll('#preset-input-' + field.id)
+ .data([0]);
+
+ input.enter().append('input')
+ .attr('type', 'text')
+ .attr('id', 'preset-input-' + field.id)
+ .attr('placeholder', field.placeholder());
+
+ input
+ .call(combobox)
+ .on('change', change)
+ .on('blur', change);
+
+ var childNodes = context.graph().childNodes(context.entity(entity.id)),
+ loc = childNodes[~~(childNodes.length/2)].loc;
+
+ imperial = _.some(iD.data.imperial.features, function(f) {
+ return _.some(f.geometry.coordinates, function(d) {
+ return iD.geo.pointInPolygon(loc, d);
+ });
+ });
+
+ unitInput = selection.selectAll('input.maxspeed-unit')
+ .data([0]);
+
+ unitInput.enter().append('input')
+ .attr('type', 'text')
+ .attr('class', 'maxspeed-unit');
+
+ unitInput
+ .on('blur', changeUnits)
+ .on('change', changeUnits)
+ .call(unitCombobox);
+
+ function changeUnits() {
+ imperial = unitInput.value() === 'mph';
+ unitInput.value(imperial ? 'mph' : 'km/h');
+ setSuggestions();
+ change();
+ }
+
+ }
+
+ function setSuggestions() {
+ combobox.data((imperial ? imperialValues : metricValues).map(comboValues));
+ unitInput.value(imperial ? 'mph' : 'km/h');
+ }
+
+ function comboValues(d) {
+ return {
+ value: d.toString(),
+ title: d.toString()
+ };
+ }
+
+ function change() {
+ var tag = {},
+ value = input.value();
+
+ if (!value) {
+ tag[field.key] = undefined;
+ } else if (isNaN(value) || !imperial) {
+ tag[field.key] = value;
+ } else {
+ tag[field.key] = value + ' mph';
+ }
+
+ dispatch.change(tag);
+ }
+
+ maxspeed.tags = function(tags) {
+ var value = tags[field.key];
+
+ if (value && value.indexOf('mph') >= 0) {
+ value = parseInt(value, 10);
+ imperial = true;
+ } else if (value) {
+ imperial = false;
+ }
+
+ setSuggestions();
+
+ input.value(value || '');
+ };
+
+ maxspeed.focus = function() {
+ input.node().focus();
+ };
+
+ maxspeed.entity = function(_) {
+ entity = _;
+ };
+
+ return d3.rebind(maxspeed, dispatch, 'on');
+ }
+
+ function radio(field) {
+ var dispatch = d3.dispatch('change'),
+ labels, radios, placeholder;
+
+ function radio(selection) {
+ selection.classed('preset-radio', true);
+
+ var wrap = selection.selectAll('.preset-input-wrap')
+ .data([0]);
+
+ var buttonWrap = wrap.enter().append('div')
+ .attr('class', 'preset-input-wrap toggle-list');
+
+ buttonWrap.append('span')
+ .attr('class', 'placeholder');
+
+ placeholder = selection.selectAll('.placeholder');
+
+ labels = wrap.selectAll('label')
+ .data(field.options || field.keys);
+
+ var enter = labels.enter().append('label');
+
+ enter.append('input')
+ .attr('type', 'radio')
+ .attr('name', field.id)
+ .attr('value', function(d) { return field.t('options.' + d, { 'default': d }); })
+ .attr('checked', false);
+
+ enter.append('span')
+ .text(function(d) { return field.t('options.' + d, { 'default': d }); });
+
+ radios = labels.selectAll('input')
+ .on('change', change);
+ }
+
+ function change() {
+ var t = {};
+ if (field.key) t[field.key] = undefined;
+ radios.each(function(d) {
+ var active = d3.select(this).property('checked');
+ if (field.key) {
+ if (active) t[field.key] = d;
+ } else {
+ t[d] = active ? 'yes' : undefined;
+ }
+ });
+ dispatch.change(t);
+ }
+
+ radio.tags = function(tags) {
+ function checked(d) {
+ if (field.key) {
+ return tags[field.key] === d;
+ } else {
+ return !!(tags[d] && tags[d] !== 'no');
+ }
+ }
+
+ labels.classed('active', checked);
+ radios.property('checked', checked);
+ var selection = radios.filter(function() { return this.checked; });
+ if (selection.empty()) {
+ placeholder.text(t('inspector.none'));
+ } else {
+ placeholder.text(selection.attr('value'));
+ }
+ };
+
+ radio.focus = function() {
+ radios.node().focus();
+ };
+
+ return d3.rebind(radio, dispatch, 'on');
+ }
+
+ function restrictions(field, context) {
+ var dispatch = d3.dispatch('change'),
+ hover = iD.behavior.Hover(context),
+ vertexID,
+ fromNodeID;
+
+
+ function restrictions(selection) {
+ // if form field is hidden or has detached from dom, clean up.
+ if (!d3.select('.inspector-wrap.inspector-hidden').empty() || !selection.node().parentNode) {
+ selection.call(restrictions.off);
+ return;
+ }
+
+ var wrap = selection.selectAll('.preset-input-wrap')
+ .data([0]);
+
+ var enter = wrap.enter()
+ .append('div')
+ .attr('class', 'preset-input-wrap');
+
+ enter
+ .append('div')
+ .attr('class', 'restriction-help');
+
+
+ var intersection = iD.geo.Intersection(context.graph(), vertexID),
+ graph = intersection.graph,
+ vertex = graph.entity(vertexID),
+ filter = d3.functor(true),
+ extent = iD.geo.Extent(),
+ projection = iD.geo.RawMercator();
+
+ var d = wrap.dimensions(),
+ c = [d[0] / 2, d[1] / 2],
+ z = 24;
+
+ projection
+ .scale(256 * Math.pow(2, z) / (2 * Math.PI));
+
+ var s = projection(vertex.loc);
+
+ projection
+ .translate([c[0] - s[0], c[1] - s[1]])
+ .clipExtent([[0, 0], d]);
+
+ var drawLayers = iD.svg.Layers(projection, context).only('osm').dimensions(d),
+ drawVertices = iD.svg.Vertices(projection, context),
+ drawLines = iD.svg.Lines(projection, context),
+ drawTurns = iD.svg.Turns(projection, context);
+
+ enter
+ .call(drawLayers)
+ .selectAll('.surface')
+ .call(hover);
+
+
+ var surface = wrap.selectAll('.surface');
+
+ surface
+ .dimensions(d)
+ .call(drawVertices, graph, [vertex], filter, extent, z)
+ .call(drawLines, graph, intersection.ways, filter)
+ .call(drawTurns, graph, intersection.turns(fromNodeID));
+
+ surface
+ .on('click.restrictions', click)
+ .on('mouseover.restrictions', mouseover)
+ .on('mouseout.restrictions', mouseout);
+
+ surface
+ .selectAll('.selected')
+ .classed('selected', false);
+
+ if (fromNodeID) {
+ surface
+ .selectAll('.' + intersection.highways[fromNodeID].id)
+ .classed('selected', true);
+ }
+
+ mouseout();
+
+ context.history()
+ .on('change.restrictions', render);
+
+ d3.select(window)
+ .on('resize.restrictions', function() {
+ wrap.dimensions(null);
+ render();
+ });
+
+ function click() {
+ var datum = d3.event.target.__data__;
+ if (datum instanceof iD.Entity) {
+ fromNodeID = intersection.adjacentNodeId(datum.id);
+ render();
+ } else if (datum instanceof iD.geo.Turn) {
+ if (datum.restriction) {
+ context.perform(
+ iD.actions.UnrestrictTurn(datum, projection),
+ t('operations.restriction.annotation.delete'));
+ } else {
+ context.perform(
+ iD.actions.RestrictTurn(datum, projection),
+ t('operations.restriction.annotation.create'));
+ }
+ }
+ }
+
+ function mouseover() {
+ var datum = d3.event.target.__data__;
+ if (datum instanceof iD.geo.Turn) {
+ var graph = context.graph(),
+ presets = context.presets(),
+ preset;
+
+ if (datum.restriction) {
+ preset = presets.match(graph.entity(datum.restriction), graph);
+ } else {
+ preset = presets.item('type/restriction/' +
+ iD.geo.inferRestriction(
+ graph,
+ datum.from,
+ datum.via,
+ datum.to,
+ projection));
+ }
+
+ wrap.selectAll('.restriction-help')
+ .text(t('operations.restriction.help.' +
+ (datum.restriction ? 'toggle_off' : 'toggle_on'),
+ {restriction: preset.name()}));
+ }
+ }
+
+ function mouseout() {
+ wrap.selectAll('.restriction-help')
+ .text(t('operations.restriction.help.' +
+ (fromNodeID ? 'toggle' : 'select')));
+ }
+
+ function render() {
+ if (context.hasEntity(vertexID)) {
+ restrictions(selection);
+ }
+ }
+ }
+
+ restrictions.entity = function(_) {
+ if (!vertexID || vertexID !== _.id) {
+ fromNodeID = null;
+ vertexID = _.id;
+ }
+ };
+
+ restrictions.tags = function() {};
+ restrictions.focus = function() {};
+
+ restrictions.off = function(selection) {
+ selection.selectAll('.surface')
+ .call(hover.off)
+ .on('click.restrictions', null)
+ .on('mouseover.restrictions', null)
+ .on('mouseout.restrictions', null);
+
+ context.history()
+ .on('change.restrictions', null);
+
+ d3.select(window)
+ .on('resize.restrictions', null);
+ };
+
+ return d3.rebind(restrictions, dispatch, 'on');
+ }
+
+ function textarea(field) {
+ var dispatch = d3.dispatch('change'),
+ input;
+
+ function textarea(selection) {
+ input = selection.selectAll('textarea')
+ .data([0]);
+
+ input.enter().append('textarea')
+ .attr('id', 'preset-input-' + field.id)
+ .attr('placeholder', field.placeholder() || t('inspector.unknown'))
+ .attr('maxlength', 255);
+
+ input
+ .on('input', change(true))
+ .on('blur', change())
+ .on('change', change());
+ }
+
+ function change(onInput) {
+ return function() {
+ var t = {};
+ t[field.key] = input.value() || undefined;
+ dispatch.change(t, onInput);
+ };
+ }
+
+ textarea.tags = function(tags) {
+ input.value(tags[field.key] || '');
+ };
+
+ textarea.focus = function() {
+ input.node().focus();
+ };
+
+ return d3.rebind(textarea, dispatch, 'on');
+ }
+
+ function wikipedia(field, context) {
+ var dispatch = d3.dispatch('change'),
+ wikipedia = iD.services.wikipedia(),
+ wikidata = iD.services.wikidata(),
+ link, entity, lang, title;
+
+ function wiki(selection) {
+ var langcombo = d3.combobox()
+ .fetcher(function(value, cb) {
+ var v = value.toLowerCase();
+
+ cb(iD.data.wikipedia.filter(function(d) {
+ return d[0].toLowerCase().indexOf(v) >= 0 ||
+ d[1].toLowerCase().indexOf(v) >= 0 ||
+ d[2].toLowerCase().indexOf(v) >= 0;
+ }).map(function(d) {
+ return { value: d[1] };
+ }));
+ });
+
+ var titlecombo = d3.combobox()
+ .fetcher(function(value, cb) {
+
+ if (!value) value = context.entity(entity.id).tags.name || '';
+ var searchfn = value.length > 7 ? wikipedia.search : wikipedia.suggestions;
+
+ searchfn(language()[2], value, function(query, data) {
+ cb(data.map(function(d) {
+ return { value: d };
+ }));
+ });
+ });
+
+ lang = selection.selectAll('input.wiki-lang')
+ .data([0]);
+
+ lang.enter().append('input')
+ .attr('type', 'text')
+ .attr('class', 'wiki-lang')
+ .attr('placeholder', t('translate.localized_translation_language'))
+ .value('English');
+
+ lang
+ .call(langcombo)
+ .on('blur', changeLang)
+ .on('change', changeLang);
+
+ title = selection.selectAll('input.wiki-title')
+ .data([0]);
+
+ title.enter().append('input')
+ .attr('type', 'text')
+ .attr('class', 'wiki-title')
+ .attr('id', 'preset-input-' + field.id);
+
+ title
+ .call(titlecombo)
+ .on('blur', blur)
+ .on('change', change);
+
+ link = selection.selectAll('a.wiki-link')
+ .data([0]);
+
+ link.enter().append('a')
+ .attr('class', 'wiki-link button-input-action minor')
+ .attr('tabindex', -1)
+ .attr('target', '_blank')
+ .call(iD.svg.Icon('#icon-out-link', 'inline'));
+ }
+
+ function language() {
+ var value = lang.value().toLowerCase();
+ var locale = iD.detect().locale.toLowerCase();
+ var localeLanguage;
+ return _.find(iD.data.wikipedia, function(d) {
+ if (d[2] === locale) localeLanguage = d;
+ return d[0].toLowerCase() === value ||
+ d[1].toLowerCase() === value ||
+ d[2] === value;
+ }) || localeLanguage || ['English', 'English', 'en'];
+ }
+
+ function changeLang() {
+ lang.value(language()[1]);
+ change(true);
+ }
+
+ function blur() {
+ change(true);
+ }
+
+ function change(skipWikidata) {
+ var value = title.value(),
+ m = value.match(/https?:\/\/([-a-z]+)\.wikipedia\.org\/(?:wiki|\1-[-a-z]+)\/([^#]+)(?:#(.+))?/),
+ l = m && _.find(iD.data.wikipedia, function(d) { return m[1] === d[2]; }),
+ anchor,
+ syncTags = {};
+
+ if (l) {
+ // Normalize title http://www.mediawiki.org/wiki/API:Query#Title_normalization
+ value = decodeURIComponent(m[2]).replace(/_/g, ' ');
+ if (m[3]) {
+ try {
+ // Best-effort `anchordecode:` implementation
+ anchor = decodeURIComponent(m[3].replace(/\.([0-9A-F]{2})/g, '%$1'));
+ } catch (e) {
+ anchor = decodeURIComponent(m[3]);
+ }
+ value += '#' + anchor.replace(/_/g, ' ');
+ }
+ value = value.slice(0, 1).toUpperCase() + value.slice(1);
+ lang.value(l[1]);
+ title.value(value);
+ }
+
+ syncTags.wikipedia = value ? language()[2] + ':' + value : undefined;
+ if (!skipWikidata) {
+ syncTags.wikidata = undefined;
+ }
+
+ dispatch.change(syncTags);
+
+
+ if (skipWikidata || !value || !language()[2]) return;
+
+ // attempt asynchronous update of wikidata tag..
+ var initEntityId = entity.id,
+ initWikipedia = context.entity(initEntityId).tags.wikipedia;
+
+ wikidata.itemsByTitle(language()[2], value, function (title, data) {
+ // 1. most recent change was a tag change
+ var annotation = t('operations.change_tags.annotation'),
+ currAnnotation = context.history().undoAnnotation();
+ if (currAnnotation !== annotation) return;
+
+ // 2. same entity exists and still selected
+ var selectedIds = context.selectedIDs(),
+ currEntityId = selectedIds.length > 0 && selectedIds[0];
+ if (currEntityId !== initEntityId) return;
+
+ // 3. wikipedia value has not changed
+ var currTags = _.clone(context.entity(currEntityId).tags),
+ qids = data && Object.keys(data);
+ if (initWikipedia !== currTags.wikipedia) return;
+
+ // ok to coalesce the update of wikidata tag into the previous tag change
+ currTags.wikidata = qids && _.find(qids, function (id) {
+ return id.match(/^Q\d+$/);
+ });
+
+ context.overwrite(iD.actions.ChangeTags(currEntityId, currTags), annotation);
+ dispatch.change(currTags);
+ });
+ }
+
+ wiki.tags = function(tags) {
+ var value = tags[field.key] || '',
+ m = value.match(/([^:]+):([^#]+)(?:#(.+))?/),
+ l = m && _.find(iD.data.wikipedia, function(d) { return m[1] === d[2]; }),
+ anchor = m && m[3];
+
+ // value in correct format
+ if (l) {
+ lang.value(l[1]);
+ title.value(m[2] + (anchor ? ('#' + anchor) : ''));
+ if (anchor) {
+ try {
+ // Best-effort `anchorencode:` implementation
+ anchor = encodeURIComponent(anchor.replace(/ /g, '_')).replace(/%/g, '.');
+ } catch (e) {
+ anchor = anchor.replace(/ /g, '_');
+ }
+ }
+ link.attr('href', 'https://' + m[1] + '.wikipedia.org/wiki/' +
+ m[2].replace(/ /g, '_') + (anchor ? ('#' + anchor) : ''));
+
+ // unrecognized value format
+ } else {
+ title.value(value);
+ if (value && value !== '') {
+ lang.value('');
+ }
+ link.attr('href', 'https://en.wikipedia.org/wiki/Special:Search?search=' + value);
+ }
+ };
+
+ wiki.entity = function(_) {
+ if (!arguments.length) return entity;
+ entity = _;
+ return wiki;
+ };
+
+ wiki.focus = function() {
+ title.node().focus();
+ };
+
+ return d3.rebind(wiki, dispatch, 'on');
+ }
+
+ exports.access = access;
+ exports.address = address;
+ exports.check = check;
+ exports.defaultcheck = check;
+ exports.combo = combo;
+ exports.typeCombo = combo;
+ exports.multiCombo = combo;
+ exports.cycleway = cycleway;
+ exports.text = url;
+ exports.url = url;
+ exports.number = url;
+ exports.email = url;
+ exports.tel = url;
+ exports.localized = localized;
+ exports.maxspeed = maxspeed;
+ exports.radio = radio;
+ exports.restrictions = restrictions;
+ exports.textarea = textarea;
+ exports.wikipedia = wikipedia;
+
+ Object.defineProperty(exports, '__esModule', { value: true });
+
+}));
\ No newline at end of file
diff --git a/js/id/ui/account.js b/modules/ui/core/account.js
similarity index 98%
rename from js/id/ui/account.js
rename to modules/ui/core/account.js
index a3d9e4ab3..223df11aa 100644
--- a/js/id/ui/account.js
+++ b/modules/ui/core/account.js
@@ -1,4 +1,4 @@
-iD.ui.Account = function(context) {
+export function Account(context) {
var connection = context.connection();
function update(selection) {
@@ -63,4 +63,4 @@ iD.ui.Account = function(context) {
connection.on('auth.account', function() { update(selection); });
update(selection);
};
-};
+}
diff --git a/js/id/ui/attribution.js b/modules/ui/core/attribution.js
similarity index 98%
rename from js/id/ui/attribution.js
rename to modules/ui/core/attribution.js
index 8f3c11209..80089a599 100644
--- a/js/id/ui/attribution.js
+++ b/modules/ui/core/attribution.js
@@ -1,4 +1,4 @@
-iD.ui.Attribution = function(context) {
+export function Attribution(context) {
var selection;
function attribution(data, klass) {
@@ -77,4 +77,4 @@ iD.ui.Attribution = function(context) {
update();
};
-};
+}
diff --git a/js/id/ui/background.js b/modules/ui/core/background.js
similarity index 98%
rename from js/id/ui/background.js
rename to modules/ui/core/background.js
index 015642ec8..c62c10da1 100644
--- a/js/id/ui/background.js
+++ b/modules/ui/core/background.js
@@ -1,4 +1,7 @@
-iD.ui.Background = function(context) {
+import { cmd } from './cmd';
+import { MapInMap } from './map_in_map';
+
+export function Background(context) {
var key = 'B',
opacities = [1, 0.75, 0.5, 0.25],
directions = [
@@ -405,7 +408,7 @@ iD.ui.Background = function(context) {
.append('input')
.attr('type', 'checkbox')
.on('change', function() {
- iD.ui.MapInMap.toggle();
+ MapInMap.toggle();
d3.event.preventDefault();
});
@@ -477,7 +480,7 @@ iD.ui.Background = function(context) {
var keybinding = d3.keybinding('background')
.on(key, toggle)
- .on(iD.ui.cmd('⌘B'), quickSwitch)
+ .on(cmd('⌘B'), quickSwitch)
.on('F', hide)
.on('H', hide);
@@ -489,4 +492,4 @@ iD.ui.Background = function(context) {
}
return background;
-};
+}
diff --git a/js/id/ui/cmd.js b/modules/ui/core/cmd.js
similarity index 95%
rename from js/id/ui/cmd.js
rename to modules/ui/core/cmd.js
index 5708d5c4c..2a2761f8f 100644
--- a/js/id/ui/cmd.js
+++ b/modules/ui/core/cmd.js
@@ -1,6 +1,6 @@
// Translate a MacOS key command into the appropriate Windows/Linux equivalent.
// For example, ⌘Z -> Ctrl+Z
-iD.ui.cmd = function(code) {
+export function cmd(code) {
if (iD.detect().os === 'mac') {
return code;
}
@@ -27,4 +27,4 @@ iD.ui.cmd = function(code) {
}
return result;
-};
+}
diff --git a/js/id/ui/commit.js b/modules/ui/core/commit.js
similarity index 99%
rename from js/id/ui/commit.js
rename to modules/ui/core/commit.js
index 2c4de614e..94b1b2de3 100644
--- a/js/id/ui/commit.js
+++ b/modules/ui/core/commit.js
@@ -1,4 +1,4 @@
-iD.ui.Commit = function(context) {
+export function Commit(context) {
var dispatch = d3.dispatch('cancel', 'save');
function commit(selection) {
@@ -275,4 +275,4 @@ iD.ui.Commit = function(context) {
}
return d3.rebind(commit, dispatch, 'on');
-};
+}
diff --git a/js/id/ui/confirm.js b/modules/ui/core/confirm.js
similarity index 83%
rename from js/id/ui/confirm.js
rename to modules/ui/core/confirm.js
index 88569470a..00b54da65 100644
--- a/js/id/ui/confirm.js
+++ b/modules/ui/core/confirm.js
@@ -1,5 +1,7 @@
-iD.ui.confirm = function(selection) {
- var modal = iD.ui.modal(selection);
+import { modal as modalModule } from './modal';
+
+export function confirm(selection) {
+ var modal = modalModule(selection);
modal.select('.modal')
.classed('modal-alert', true);
@@ -28,4 +30,4 @@ iD.ui.confirm = function(selection) {
};
return modal;
-};
+}
diff --git a/js/id/ui/conflicts.js b/modules/ui/core/conflicts.js
similarity index 99%
rename from js/id/ui/conflicts.js
rename to modules/ui/core/conflicts.js
index 098b38c1d..2c22bcb3e 100644
--- a/js/id/ui/conflicts.js
+++ b/modules/ui/core/conflicts.js
@@ -1,4 +1,4 @@
-iD.ui.Conflicts = function(context) {
+export function Conflicts(context) {
var dispatch = d3.dispatch('download', 'cancel', 'save'),
list;
@@ -247,4 +247,4 @@ iD.ui.Conflicts = function(context) {
};
return d3.rebind(conflicts, dispatch, 'on');
-};
+}
diff --git a/js/id/ui/contributors.js b/modules/ui/core/contributors.js
similarity index 98%
rename from js/id/ui/contributors.js
rename to modules/ui/core/contributors.js
index 8bf709e71..d2fbae78c 100644
--- a/js/id/ui/contributors.js
+++ b/modules/ui/core/contributors.js
@@ -1,4 +1,4 @@
-iD.ui.Contributors = function(context) {
+export function Contributors(context) {
var debouncedUpdate = _.debounce(function() { update(); }, 1000),
limit = 4,
hidden = false,
@@ -69,4 +69,4 @@ iD.ui.Contributors = function(context) {
context.connection().on('loaded.contributors', debouncedUpdate);
context.map().on('move.contributors', debouncedUpdate);
};
-};
+}
diff --git a/js/id/ui/disclosure.js b/modules/ui/core/disclosure.js
similarity index 92%
rename from js/id/ui/disclosure.js
rename to modules/ui/core/disclosure.js
index 2370d3bb3..8ab1d8f2c 100644
--- a/js/id/ui/disclosure.js
+++ b/modules/ui/core/disclosure.js
@@ -1,4 +1,6 @@
-iD.ui.Disclosure = function() {
+import { Toggle } from './toggle';
+
+export function Disclosure() {
var dispatch = d3.dispatch('toggled'),
title,
expanded = false,
@@ -27,7 +29,7 @@ iD.ui.Disclosure = function() {
function toggle() {
expanded = !expanded;
$link.classed('expanded', expanded);
- $body.call(iD.ui.Toggle(expanded));
+ $body.call(Toggle(expanded));
dispatch.toggled(expanded);
}
};
@@ -51,4 +53,4 @@ iD.ui.Disclosure = function() {
};
return d3.rebind(disclosure, dispatch, 'on');
-};
+}
diff --git a/js/id/ui/entity_editor.js b/modules/ui/core/entity_editor.js
similarity index 91%
rename from js/id/ui/entity_editor.js
rename to modules/ui/core/entity_editor.js
index 2e64539d4..b1173fff5 100644
--- a/js/id/ui/entity_editor.js
+++ b/modules/ui/core/entity_editor.js
@@ -1,4 +1,11 @@
-iD.ui.EntityEditor = function(context) {
+import { preset as presetModule } from './preset';
+import { PresetIcon } from './preset_icon';
+import { RawTagEditor } from './raw_tag_editor';
+import { RawMemberEditor } from './raw_member_editor';
+import { RawMembershipEditor } from './raw_membership_editor';
+import { TagReference } from './tag_reference';
+
+export function EntityEditor(context) {
var dispatch = d3.dispatch('choose'),
state = 'select',
coalesceChanges = false,
@@ -8,9 +15,9 @@ iD.ui.EntityEditor = function(context) {
preset,
reference;
- var presetEditor = iD.ui.preset(context)
+ var presetEditor = presetModule(context)
.on('change', changeTags);
- var rawTagEditor = iD.ui.RawTagEditor(context)
+ var rawTagEditor = RawTagEditor(context)
.on('change', changeTags);
function entityEditor(selection) {
@@ -88,7 +95,7 @@ iD.ui.EntityEditor = function(context) {
// Update
$body.select('.preset-list-item button')
- .call(iD.ui.PresetIcon()
+ .call(PresetIcon()
.geometry(context.geometry(id))
.preset(preset));
@@ -112,7 +119,7 @@ iD.ui.EntityEditor = function(context) {
if (entity.type === 'relation') {
$body.select('.raw-member-editor')
.style('display', 'block')
- .call(iD.ui.RawMemberEditor(context)
+ .call(RawMemberEditor(context)
.entityID(id));
} else {
$body.select('.raw-member-editor')
@@ -120,7 +127,7 @@ iD.ui.EntityEditor = function(context) {
}
$body.select('.raw-membership-editor')
- .call(iD.ui.RawMembershipEditor(context)
+ .call(RawMembershipEditor(context)
.entityID(id));
function historyChanged() {
@@ -226,11 +233,11 @@ iD.ui.EntityEditor = function(context) {
if (!arguments.length) return preset;
if (_ !== preset) {
preset = _;
- reference = iD.ui.TagReference(preset.reference(context.geometry(id)), context)
+ reference = TagReference(preset.reference(context.geometry(id)), context)
.showing(false);
}
return entityEditor;
};
return d3.rebind(entityEditor, dispatch, 'on');
-};
+}
diff --git a/js/id/ui/feature_info.js b/modules/ui/core/feature_info.js
similarity index 97%
rename from js/id/ui/feature_info.js
rename to modules/ui/core/feature_info.js
index b80a2dd40..2951f5e07 100644
--- a/js/id/ui/feature_info.js
+++ b/modules/ui/core/feature_info.js
@@ -1,4 +1,4 @@
-iD.ui.FeatureInfo = function(context) {
+export function FeatureInfo(context) {
function update(selection) {
var features = context.features(),
stats = features.stats(),
@@ -43,4 +43,4 @@ iD.ui.FeatureInfo = function(context) {
update(selection);
});
};
-};
+}
diff --git a/js/id/ui/feature_list.js b/modules/ui/core/feature_list.js
similarity index 99%
rename from js/id/ui/feature_list.js
rename to modules/ui/core/feature_list.js
index cf3b8eef5..057330330 100644
--- a/js/id/ui/feature_list.js
+++ b/modules/ui/core/feature_list.js
@@ -1,4 +1,4 @@
-iD.ui.FeatureList = function(context) {
+export function FeatureList(context) {
var geocodeResults;
function featureList(selection) {
@@ -253,4 +253,4 @@ iD.ui.FeatureList = function(context) {
}
return featureList;
-};
+}
diff --git a/js/id/ui/flash.js b/modules/ui/core/flash.js
similarity index 74%
rename from js/id/ui/flash.js
rename to modules/ui/core/flash.js
index 416b4a144..6afc8414f 100644
--- a/js/id/ui/flash.js
+++ b/modules/ui/core/flash.js
@@ -1,5 +1,7 @@
-iD.ui.flash = function(selection) {
- var modal = iD.ui.modal(selection);
+import { modal as modalModule } from './modal';
+
+export function flash(selection) {
+ var modal = modalModule(selection);
modal.select('.modal').classed('modal-flash', true);
@@ -16,4 +18,4 @@ iD.ui.flash = function(selection) {
}, 1500);
return modal;
-};
+}
diff --git a/js/id/ui/full_screen.js b/modules/ui/core/full_screen.js
similarity index 94%
rename from js/id/ui/full_screen.js
rename to modules/ui/core/full_screen.js
index 948ba8b4b..4e2e60279 100644
--- a/js/id/ui/full_screen.js
+++ b/modules/ui/core/full_screen.js
@@ -1,4 +1,6 @@
-iD.ui.FullScreen = function(context) {
+import { cmd } from './cmd';
+
+export function FullScreen(context) {
var element = context.container().node(),
keybinding = d3.keybinding('full-screen');
// button;
@@ -65,9 +67,9 @@ iD.ui.FullScreen = function(context) {
keybinding
.on('f11', fullScreen)
- .on(iD.ui.cmd('⌘⇧F'), fullScreen);
+ .on(cmd('⌘⇧F'), fullScreen);
d3.select(document)
.call(keybinding);
};
-};
+}
diff --git a/js/id/ui/geolocate.js b/modules/ui/core/geolocate.js
similarity index 89%
rename from js/id/ui/geolocate.js
rename to modules/ui/core/geolocate.js
index de191941c..06a64c807 100644
--- a/js/id/ui/geolocate.js
+++ b/modules/ui/core/geolocate.js
@@ -1,6 +1,8 @@
-iD.ui.Geolocate = function(context) {
+import { Loading } from './loading';
+
+export function Geolocate(context) {
var geoOptions = { enableHighAccuracy: false, timeout: 6000 /* 6sec */ },
- locating = iD.ui.Loading(context).message(t('geolocate.locating')).blocking(true),
+ locating = Loading(context).message(t('geolocate.locating')).blocking(true),
timeoutId;
function click() {
@@ -43,4 +45,4 @@ iD.ui.Geolocate = function(context) {
.call(bootstrap.tooltip()
.placement('left'));
};
-};
+}
diff --git a/js/id/ui/help.js b/modules/ui/core/help.js
similarity index 97%
rename from js/id/ui/help.js
rename to modules/ui/core/help.js
index cf66ade47..d630b4c69 100644
--- a/js/id/ui/help.js
+++ b/modules/ui/core/help.js
@@ -1,4 +1,6 @@
-iD.ui.Help = function(context) {
+import { intro } from './intro';
+
+export function Help(context) {
var key = 'H';
var docKeys = [
@@ -90,7 +92,7 @@ iD.ui.Help = function(context) {
}
function clickWalkthrough() {
- d3.select(document.body).call(iD.ui.intro(context));
+ d3.select(document.body).call(intro(context));
setVisible(false);
}
@@ -153,4 +155,4 @@ iD.ui.Help = function(context) {
}
return help;
-};
+}
diff --git a/modules/ui/core/index.js b/modules/ui/core/index.js
new file mode 100644
index 000000000..1537d70e9
--- /dev/null
+++ b/modules/ui/core/index.js
@@ -0,0 +1,48 @@
+export { Account } from './account';
+export { Attribution } from './attribution';
+export { Background } from './background';
+export { cmd } from './cmd';
+export { Commit } from './commit';
+export { confirm } from './confirm';
+export { Conflicts } from './conflicts';
+export { Contributors } from './contributors';
+export { Disclosure } from './disclosure';
+export { EntityEditor } from './entity_editor';
+export { FeatureInfo } from './feature_info';
+export { FeatureList } from './feature_list';
+export { flash } from './flash';
+export { FullScreen } from './full_screen';
+export { Geolocate } from './geolocate';
+export { Help } from './help';
+export { Info } from './info';
+export { Inspector } from './inspector';
+export { intro } from './intro';
+export { Lasso } from './lasso';
+export { Loading } from './loading';
+export { MapData } from './map_data';
+export { MapInMap } from './map_in_map';
+export { modal } from './modal';
+export { Modes } from './modes';
+export { Notice } from './notice';
+export { preset } from './preset';
+export { PresetIcon } from './preset_icon';
+export { PresetList } from './preset_list';
+export { RadialMenu } from './radial_menu';
+export { RawMemberEditor } from './raw_member_editor';
+export { RawMembershipEditor } from './raw_membership_editor';
+export { RawTagEditor } from './raw_tag_editor';
+export { Restore } from './restore';
+export { Save } from './save';
+export { Scale } from './scale';
+export { SelectionList } from './selection_list';
+export { Sidebar } from './sidebar';
+export { SourceSwitch } from './source_switch';
+export { Spinner } from './spinner';
+export { Splash } from './splash';
+export { Status } from './status';
+export { Success } from './success';
+export { TagReference } from './tag_reference';
+export { Toggle } from './toggle';
+export { UndoRedo } from './undo_redo';
+export { ViewOnOSM } from './view_on_osm';
+export { Zoom } from './zoom';
diff --git a/js/id/ui/info.js b/modules/ui/core/info.js
similarity index 98%
rename from js/id/ui/info.js
rename to modules/ui/core/info.js
index 948d08f19..f82fec3a9 100644
--- a/js/id/ui/info.js
+++ b/modules/ui/core/info.js
@@ -1,5 +1,7 @@
-iD.ui.Info = function(context) {
- var key = iD.ui.cmd('⌘I'),
+import { cmd } from './cmd';
+
+export function Info(context) {
+ var key = cmd('⌘I'),
imperial = (iD.detect().locale.toLowerCase() === 'en-us'),
hidden = true;
@@ -228,4 +230,4 @@ iD.ui.Info = function(context) {
}
return info;
-};
+}
diff --git a/js/id/ui/inspector.js b/modules/ui/core/inspector.js
similarity index 89%
rename from js/id/ui/inspector.js
rename to modules/ui/core/inspector.js
index 28a5c9fd3..f77443e91 100644
--- a/js/id/ui/inspector.js
+++ b/modules/ui/core/inspector.js
@@ -1,6 +1,10 @@
-iD.ui.Inspector = function(context) {
- var presetList = iD.ui.PresetList(context),
- entityEditor = iD.ui.EntityEditor(context),
+import { PresetList } from './preset_list';
+import { EntityEditor } from './entity_editor';
+import { ViewOnOSM } from './view_on_osm';
+
+export function Inspector(context) {
+ var presetList = PresetList(context),
+ entityEditor = EntityEditor(context),
state = 'select',
entityID,
newFeature = false;
@@ -52,7 +56,7 @@ iD.ui.Inspector = function(context) {
.attr('class', 'footer');
selection.select('.footer')
- .call(iD.ui.ViewOnOSM(context)
+ .call(ViewOnOSM(context)
.entityID(entityID));
function showList(preset) {
@@ -93,4 +97,4 @@ iD.ui.Inspector = function(context) {
};
return inspector;
-};
+}
diff --git a/js/id/ui/intro.js b/modules/ui/core/intro.js
similarity index 85%
rename from js/id/ui/intro.js
rename to modules/ui/core/intro.js
index 3938cad2d..1941f6431 100644
--- a/js/id/ui/intro.js
+++ b/modules/ui/core/intro.js
@@ -1,4 +1,4 @@
-iD.ui.intro = function(context) {
+export function intro(context) {
var step;
function intro(selection) {
@@ -153,37 +153,4 @@ iD.ui.intro = function(context) {
}
return intro;
-};
-
-iD.ui.intro.pointBox = function(point, context) {
- var rect = context.surfaceRect();
- point = context.projection(point);
- return {
- left: point[0] + rect.left - 30,
- top: point[1] + rect.top - 50,
- width: 60,
- height: 70
- };
-};
-
-iD.ui.intro.pad = function(box, padding, context) {
- if (box instanceof Array) {
- var rect = context.surfaceRect();
- box = context.projection(box);
- box = {
- left: box[0] + rect.left,
- top: box[1] + rect.top
- };
- }
- return {
- left: box.left - padding,
- top: box.top - padding,
- width: (box.width || 0) + 2 * padding,
- height: (box.width || 0) + 2 * padding
- };
-};
-
-iD.ui.intro.icon = function(name, svgklass) {
- return '';
-};
+}
diff --git a/js/id/ui/lasso.js b/modules/ui/core/lasso.js
similarity index 87%
rename from js/id/ui/lasso.js
rename to modules/ui/core/lasso.js
index 237c500e0..8df6da675 100644
--- a/js/id/ui/lasso.js
+++ b/modules/ui/core/lasso.js
@@ -1,4 +1,6 @@
-iD.ui.Lasso = function(context) {
+import { Toggle } from './toggle';
+
+export function Lasso(context) {
var group, polygon;
lasso.coordinates = [];
@@ -13,7 +15,7 @@ iD.ui.Lasso = function(context) {
polygon = group.append('path')
.attr('class', 'lasso-path');
- group.call(iD.ui.Toggle(true));
+ group.call(Toggle(true));
}
@@ -39,7 +41,7 @@ iD.ui.Lasso = function(context) {
lasso.close = function() {
if (group) {
- group.call(iD.ui.Toggle(false, function() {
+ group.call(Toggle(false, function() {
d3.select(this).remove();
}));
}
@@ -47,4 +49,4 @@ iD.ui.Lasso = function(context) {
};
return lasso;
-};
+}
diff --git a/js/id/ui/loading.js b/modules/ui/core/loading.js
similarity index 87%
rename from js/id/ui/loading.js
rename to modules/ui/core/loading.js
index c4ad1d55c..681d77070 100644
--- a/js/id/ui/loading.js
+++ b/modules/ui/core/loading.js
@@ -1,10 +1,12 @@
-iD.ui.Loading = function(context) {
+import { modal as modalModule } from './modal';
+
+export function Loading(context) {
var message = '',
blocking = false,
modal;
var loading = function(selection) {
- modal = iD.ui.modal(selection, blocking);
+ modal = modalModule(selection, blocking);
var loadertext = modal.select('.content')
.classed('loading-modal', true)
@@ -41,4 +43,4 @@ iD.ui.Loading = function(context) {
};
return loading;
-};
+}
diff --git a/js/id/ui/map_data.js b/modules/ui/core/map_data.js
similarity index 99%
rename from js/id/ui/map_data.js
rename to modules/ui/core/map_data.js
index 70b001eed..55cc02e8e 100644
--- a/js/id/ui/map_data.js
+++ b/modules/ui/core/map_data.js
@@ -1,4 +1,4 @@
-iD.ui.MapData = function(context) {
+export function MapData(context) {
var key = 'F',
features = context.features().keys(),
layers = context.layers(),
@@ -431,4 +431,4 @@ iD.ui.MapData = function(context) {
}
return map_data;
-};
+}
diff --git a/js/id/ui/map_in_map.js b/modules/ui/core/map_in_map.js
similarity index 99%
rename from js/id/ui/map_in_map.js
rename to modules/ui/core/map_in_map.js
index 2d3d525cd..8642d795c 100644
--- a/js/id/ui/map_in_map.js
+++ b/modules/ui/core/map_in_map.js
@@ -1,4 +1,4 @@
-iD.ui.MapInMap = function(context) {
+export function MapInMap(context) {
var key = '/';
function map_in_map(selection) {
@@ -268,7 +268,7 @@ iD.ui.MapInMap = function(context) {
}
}
- iD.ui.MapInMap.toggle = toggle;
+ MapInMap.toggle = toggle;
var wrap = selection.selectAll('.map-in-map')
.data([0]);
@@ -297,4 +297,4 @@ iD.ui.MapInMap = function(context) {
}
return map_in_map;
-};
+}
diff --git a/js/id/ui/modal.js b/modules/ui/core/modal.js
similarity index 96%
rename from js/id/ui/modal.js
rename to modules/ui/core/modal.js
index 1e6578454..be155a354 100644
--- a/js/id/ui/modal.js
+++ b/modules/ui/core/modal.js
@@ -1,4 +1,4 @@
-iD.ui.modal = function(selection, blocking) {
+export function modal(selection, blocking) {
var keybinding = d3.keybinding('modal');
var previous = selection.select('div.modal');
var animate = previous.empty();
@@ -60,4 +60,4 @@ iD.ui.modal = function(selection, blocking) {
}
return shaded;
-};
+}
diff --git a/js/id/ui/modes.js b/modules/ui/core/modes.js
similarity index 98%
rename from js/id/ui/modes.js
rename to modules/ui/core/modes.js
index 3e568bbf8..ec4de94b5 100644
--- a/js/id/ui/modes.js
+++ b/modules/ui/core/modes.js
@@ -1,4 +1,4 @@
-iD.ui.Modes = function(context) {
+export function Modes(context) {
var modes = [
iD.modes.AddPoint(context),
iD.modes.AddLine(context),
@@ -68,4 +68,4 @@ iD.ui.Modes = function(context) {
buttons.property('disabled', !editable());
}
};
-};
+}
diff --git a/js/id/ui/notice.js b/modules/ui/core/notice.js
similarity index 94%
rename from js/id/ui/notice.js
rename to modules/ui/core/notice.js
index e0668b7e7..ddb16586a 100644
--- a/js/id/ui/notice.js
+++ b/modules/ui/core/notice.js
@@ -1,4 +1,4 @@
-iD.ui.Notice = function(context) {
+export function Notice(context) {
return function(selection) {
var div = selection.append('div')
.attr('class', 'notice');
@@ -22,4 +22,4 @@ iD.ui.Notice = function(context) {
disableTooHigh();
};
-};
+}
diff --git a/js/id/ui/preset.js b/modules/ui/core/preset.js
similarity index 99%
rename from js/id/ui/preset.js
rename to modules/ui/core/preset.js
index 0d408fbc0..cbda008c3 100644
--- a/js/id/ui/preset.js
+++ b/modules/ui/core/preset.js
@@ -1,4 +1,4 @@
-iD.ui.preset = function(context) {
+export function preset(context) {
var event = d3.dispatch('change'),
state,
fields,
@@ -266,4 +266,4 @@ iD.ui.preset = function(context) {
};
return d3.rebind(presets, event, 'on');
-};
+}
diff --git a/js/id/ui/preset_icon.js b/modules/ui/core/preset_icon.js
similarity index 98%
rename from js/id/ui/preset_icon.js
rename to modules/ui/core/preset_icon.js
index 584657069..7749dba75 100644
--- a/js/id/ui/preset_icon.js
+++ b/modules/ui/core/preset_icon.js
@@ -1,4 +1,4 @@
-iD.ui.PresetIcon = function() {
+export function PresetIcon() {
var preset, geometry;
function presetIcon(selection) {
@@ -79,4 +79,4 @@ iD.ui.PresetIcon = function() {
};
return presetIcon;
-};
+}
diff --git a/js/id/ui/preset_list.js b/modules/ui/core/preset_list.js
similarity index 96%
rename from js/id/ui/preset_list.js
rename to modules/ui/core/preset_list.js
index bef5f215a..04d7f529b 100644
--- a/js/id/ui/preset_list.js
+++ b/modules/ui/core/preset_list.js
@@ -1,4 +1,7 @@
-iD.ui.PresetList = function(context) {
+import { PresetIcon } from './preset_icon';
+import { TagReference } from './tag_reference';
+
+export function PresetList(context) {
var event = d3.dispatch('choose'),
id,
currentPreset,
@@ -134,7 +137,7 @@ iD.ui.PresetList = function(context) {
wrap.append('button')
.attr('class', 'preset-list-button')
.classed('expanded', false)
- .call(iD.ui.PresetIcon()
+ .call(PresetIcon()
.geometry(context.geometry(id))
.preset(preset))
.on('click', function() {
@@ -195,7 +198,7 @@ iD.ui.PresetList = function(context) {
wrap.append('button')
.attr('class', 'preset-list-button')
- .call(iD.ui.PresetIcon()
+ .call(PresetIcon()
.geometry(context.geometry(id))
.preset(preset))
.on('click', item.choose)
@@ -223,7 +226,7 @@ iD.ui.PresetList = function(context) {
};
item.preset = preset;
- item.reference = iD.ui.TagReference(preset.reference(context.geometry(id)), context);
+ item.reference = TagReference(preset.reference(context.geometry(id)), context);
return item;
}
@@ -248,4 +251,4 @@ iD.ui.PresetList = function(context) {
};
return d3.rebind(presetList, event, 'on');
-};
+}
diff --git a/js/id/ui/radial_menu.js b/modules/ui/core/radial_menu.js
similarity index 98%
rename from js/id/ui/radial_menu.js
rename to modules/ui/core/radial_menu.js
index 72914b30c..51d3ac719 100644
--- a/js/id/ui/radial_menu.js
+++ b/modules/ui/core/radial_menu.js
@@ -1,4 +1,4 @@
-iD.ui.RadialMenu = function(context, operations) {
+export function RadialMenu(context, operations) {
var menu,
center = [0, 0],
tooltip;
@@ -130,4 +130,4 @@ iD.ui.RadialMenu = function(context, operations) {
};
return radialMenu;
-};
+}
diff --git a/js/id/ui/raw_member_editor.js b/modules/ui/core/raw_member_editor.js
similarity index 96%
rename from js/id/ui/raw_member_editor.js
rename to modules/ui/core/raw_member_editor.js
index 311400fa4..5c21133af 100644
--- a/js/id/ui/raw_member_editor.js
+++ b/modules/ui/core/raw_member_editor.js
@@ -1,4 +1,6 @@
-iD.ui.RawMemberEditor = function(context) {
+import { Disclosure } from './disclosure';
+
+export function RawMemberEditor(context) {
var id;
function selectMember(d) {
@@ -39,7 +41,7 @@ iD.ui.RawMemberEditor = function(context) {
});
});
- selection.call(iD.ui.Disclosure()
+ selection.call(Disclosure()
.title(t('inspector.all_members') + ' (' + memberships.length + ')')
.expanded(true)
.on('toggled', toggled)
@@ -117,4 +119,4 @@ iD.ui.RawMemberEditor = function(context) {
};
return rawMemberEditor;
-};
+}
diff --git a/js/id/ui/raw_membership_editor.js b/modules/ui/core/raw_membership_editor.js
similarity index 98%
rename from js/id/ui/raw_membership_editor.js
rename to modules/ui/core/raw_membership_editor.js
index 2ca442552..4078a5d31 100644
--- a/js/id/ui/raw_membership_editor.js
+++ b/modules/ui/core/raw_membership_editor.js
@@ -1,4 +1,6 @@
-iD.ui.RawMembershipEditor = function(context) {
+import { Disclosure } from './disclosure';
+
+export function RawMembershipEditor(context) {
var id, showBlank;
function selectRelation(d) {
@@ -97,7 +99,7 @@ iD.ui.RawMembershipEditor = function(context) {
});
});
- selection.call(iD.ui.Disclosure()
+ selection.call(Disclosure()
.title(t('inspector.all_relations') + ' (' + memberships.length + ')')
.expanded(true)
.on('toggled', toggled)
@@ -214,4 +216,4 @@ iD.ui.RawMembershipEditor = function(context) {
};
return rawMembershipEditor;
-};
+}
diff --git a/js/id/ui/raw_tag_editor.js b/modules/ui/core/raw_tag_editor.js
similarity index 95%
rename from js/id/ui/raw_tag_editor.js
rename to modules/ui/core/raw_tag_editor.js
index 87ff0ebf2..ea9249091 100644
--- a/js/id/ui/raw_tag_editor.js
+++ b/modules/ui/core/raw_tag_editor.js
@@ -1,4 +1,7 @@
-iD.ui.RawTagEditor = function(context) {
+import { Disclosure } from './disclosure';
+import { TagReference } from './tag_reference';
+
+export function RawTagEditor(context) {
var event = d3.dispatch('change'),
showBlank = false,
state,
@@ -9,7 +12,7 @@ iD.ui.RawTagEditor = function(context) {
function rawTagEditor(selection) {
var count = Object.keys(tags).filter(function(d) { return d; }).length;
- selection.call(iD.ui.Disclosure()
+ selection.call(Disclosure()
.title(t('inspector.all_tags') + ' (' + count + ')')
.expanded(context.storage('raw_tag_editor.expanded') === 'true' || preset.isFallback())
.on('toggled', toggled)
@@ -86,9 +89,9 @@ iD.ui.RawTagEditor = function(context) {
var isRelation = (context.entity(id).type === 'relation'),
reference;
if (isRelation && tag.key === 'type')
- reference = iD.ui.TagReference({rtype: tag.value}, context);
+ reference = TagReference({rtype: tag.value}, context);
else
- reference = iD.ui.TagReference({key: tag.key, value: tag.value}, context);
+ reference = TagReference({key: tag.key, value: tag.value}, context);
if (state === 'hover') {
reference.showing(false);
@@ -248,4 +251,4 @@ iD.ui.RawTagEditor = function(context) {
};
return d3.rebind(rawTagEditor, event, 'on');
-};
+}
diff --git a/js/id/ui/restore.js b/modules/ui/core/restore.js
similarity index 90%
rename from js/id/ui/restore.js
rename to modules/ui/core/restore.js
index 6c6f0ec22..70a5419eb 100644
--- a/js/id/ui/restore.js
+++ b/modules/ui/core/restore.js
@@ -1,9 +1,11 @@
-iD.ui.Restore = function(context) {
+import { modal as modalModule } from './modal';
+
+export function Restore(context) {
return function(selection) {
if (!context.history().lock() || !context.history().restorableChanges())
return;
- var modal = iD.ui.modal(selection, true);
+ var modal = modalModule(selection, true);
modal.select('.modal')
.attr('class', 'modal fillL col6');
@@ -43,4 +45,4 @@ iD.ui.Restore = function(context) {
restore.node().focus();
};
-};
+}
diff --git a/js/id/ui/save.js b/modules/ui/core/save.js
similarity index 96%
rename from js/id/ui/save.js
rename to modules/ui/core/save.js
index faa27adc2..14d3abe51 100644
--- a/js/id/ui/save.js
+++ b/modules/ui/core/save.js
@@ -1,6 +1,8 @@
-iD.ui.Save = function(context) {
+import { cmd } from './cmd';
+
+export function Save(context) {
var history = context.history(),
- key = iD.ui.cmd('⌘S');
+ key = cmd('⌘S');
function saving() {
@@ -82,4 +84,4 @@ iD.ui.Save = function(context) {
if (saving()) button.call(tooltip.hide);
});
};
-};
+}
diff --git a/js/id/ui/scale.js b/modules/ui/core/scale.js
similarity index 98%
rename from js/id/ui/scale.js
rename to modules/ui/core/scale.js
index 35122d73a..06ca2bdb0 100644
--- a/js/id/ui/scale.js
+++ b/modules/ui/core/scale.js
@@ -1,4 +1,4 @@
-iD.ui.Scale = function(context) {
+export function Scale(context) {
var projection = context.projection,
imperial = (iD.detect().locale.toLowerCase() === 'en-us'),
maxLength = 180,
@@ -86,4 +86,4 @@ iD.ui.Scale = function(context) {
update(selection);
});
};
-};
+}
diff --git a/js/id/ui/selection_list.js b/modules/ui/core/selection_list.js
similarity index 97%
rename from js/id/ui/selection_list.js
rename to modules/ui/core/selection_list.js
index e16d3bbe3..66cf322e2 100644
--- a/js/id/ui/selection_list.js
+++ b/modules/ui/core/selection_list.js
@@ -1,4 +1,4 @@
-iD.ui.SelectionList = function(context, selectedIDs) {
+export function SelectionList(context, selectedIDs) {
function selectEntity(entity) {
context.enter(iD.modes.Select(context, [entity.id]).suppressMenu(true));
@@ -67,4 +67,4 @@ iD.ui.SelectionList = function(context, selectedIDs) {
return selectionList;
-};
+}
diff --git a/js/id/ui/sidebar.js b/modules/ui/core/sidebar.js
similarity index 90%
rename from js/id/ui/sidebar.js
rename to modules/ui/core/sidebar.js
index 4d7c726d6..9531244f4 100644
--- a/js/id/ui/sidebar.js
+++ b/modules/ui/core/sidebar.js
@@ -1,13 +1,17 @@
-iD.ui.Sidebar = function(context) {
- var inspector = iD.ui.Inspector(context),
+import { Inspector } from './inspector';
+import { FeatureList } from './feature_list';
+import { Notice } from './notice';
+
+export function Sidebar(context) {
+ var inspector = Inspector(context),
current;
function sidebar(selection) {
var featureListWrap = selection.append('div')
.attr('class', 'feature-list-pane')
- .call(iD.ui.FeatureList(context));
+ .call(FeatureList(context));
- selection.call(iD.ui.Notice(context));
+ selection.call(Notice(context));
var inspectorWrap = selection.append('div')
.attr('class', 'inspector-hidden inspector-wrap fr');
@@ -79,4 +83,4 @@ iD.ui.Sidebar = function(context) {
sidebar.hide = function() {};
return sidebar;
-};
+}
diff --git a/js/id/ui/source_switch.js b/modules/ui/core/source_switch.js
similarity index 95%
rename from js/id/ui/source_switch.js
rename to modules/ui/core/source_switch.js
index eb38a2849..d5b0c4c20 100644
--- a/js/id/ui/source_switch.js
+++ b/modules/ui/core/source_switch.js
@@ -1,4 +1,4 @@
-iD.ui.SourceSwitch = function(context) {
+export function SourceSwitch(context) {
var keys;
function click() {
@@ -37,4 +37,4 @@ iD.ui.SourceSwitch = function(context) {
};
return sourceSwitch;
-};
+}
diff --git a/js/id/ui/spinner.js b/modules/ui/core/spinner.js
similarity index 92%
rename from js/id/ui/spinner.js
rename to modules/ui/core/spinner.js
index 5f8b026f7..37c41ab1f 100644
--- a/js/id/ui/spinner.js
+++ b/modules/ui/core/spinner.js
@@ -1,4 +1,4 @@
-iD.ui.Spinner = function(context) {
+export function Spinner(context) {
var connection = context.connection();
return function(selection) {
@@ -16,4 +16,4 @@ iD.ui.Spinner = function(context) {
.style('opacity', 0);
});
};
-};
+}
diff --git a/js/id/ui/splash.js b/modules/ui/core/splash.js
similarity index 85%
rename from js/id/ui/splash.js
rename to modules/ui/core/splash.js
index 72385ed4a..41a84b986 100644
--- a/js/id/ui/splash.js
+++ b/modules/ui/core/splash.js
@@ -1,11 +1,14 @@
-iD.ui.Splash = function(context) {
+import { modal as modalModule } from './modal';
+import { intro } from './intro';
+
+export function Splash(context) {
return function(selection) {
if (context.storage('sawSplash'))
return;
context.storage('sawSplash', true);
- var modal = iD.ui.modal(selection);
+ var modal = modalModule(selection);
modal.select('.modal')
.attr('class', 'modal-splash modal col6');
@@ -33,7 +36,7 @@ iD.ui.Splash = function(context) {
.attr('class', 'col6 walkthrough')
.text(t('splash.walkthrough'))
.on('click', function() {
- d3.select(document.body).call(iD.ui.intro(context));
+ d3.select(document.body).call(intro(context));
modal.close();
});
@@ -45,4 +48,4 @@ iD.ui.Splash = function(context) {
modal.select('button.close').attr('class','hide');
};
-};
+}
diff --git a/js/id/ui/status.js b/modules/ui/core/status.js
similarity index 95%
rename from js/id/ui/status.js
rename to modules/ui/core/status.js
index 81702dab9..337b5fe7a 100644
--- a/js/id/ui/status.js
+++ b/modules/ui/core/status.js
@@ -1,4 +1,4 @@
-iD.ui.Status = function(context) {
+export function Status(context) {
var connection = context.connection(),
errCount = 0;
@@ -32,4 +32,4 @@ iD.ui.Status = function(context) {
window.setInterval(update, 90000);
update(selection);
};
-};
+}
diff --git a/js/id/ui/success.js b/modules/ui/core/success.js
similarity index 98%
rename from js/id/ui/success.js
rename to modules/ui/core/success.js
index 05799cc71..906a4b679 100644
--- a/js/id/ui/success.js
+++ b/modules/ui/core/success.js
@@ -1,4 +1,4 @@
-iD.ui.Success = function(context) {
+export function Success(context) {
var dispatch = d3.dispatch('cancel'),
changeset;
@@ -66,4 +66,4 @@ iD.ui.Success = function(context) {
};
return d3.rebind(success, dispatch, 'on');
-};
+}
diff --git a/js/id/ui/tag_reference.js b/modules/ui/core/tag_reference.js
similarity index 98%
rename from js/id/ui/tag_reference.js
rename to modules/ui/core/tag_reference.js
index fd95faa61..3b5568d2b 100644
--- a/js/id/ui/tag_reference.js
+++ b/modules/ui/core/tag_reference.js
@@ -1,4 +1,4 @@
-iD.ui.TagReference = function(tag, context) {
+export function TagReference(tag, context) {
var tagReference = {},
button,
body,
@@ -146,4 +146,4 @@ iD.ui.TagReference = function(tag, context) {
};
return tagReference;
-};
+}
diff --git a/js/id/ui/toggle.js b/modules/ui/core/toggle.js
similarity index 93%
rename from js/id/ui/toggle.js
rename to modules/ui/core/toggle.js
index 7833fffd4..9e06ecf8e 100644
--- a/js/id/ui/toggle.js
+++ b/modules/ui/core/toggle.js
@@ -2,7 +2,7 @@
// 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.
-iD.ui.Toggle = function(show, callback) {
+export function Toggle(show, callback) {
return function(selection) {
selection
.style('opacity', show ? 0 : 1)
@@ -16,4 +16,4 @@ iD.ui.Toggle = function(show, callback) {
if (callback) callback.apply(this);
});
};
-};
+}
diff --git a/js/id/ui/undo_redo.js b/modules/ui/core/undo_redo.js
similarity index 94%
rename from js/id/ui/undo_redo.js
rename to modules/ui/core/undo_redo.js
index 7064d5d9e..bd1a79825 100644
--- a/js/id/ui/undo_redo.js
+++ b/modules/ui/core/undo_redo.js
@@ -1,12 +1,14 @@
-iD.ui.UndoRedo = function(context) {
+import { cmd } from './cmd';
+
+export function UndoRedo(context) {
var commands = [{
id: 'undo',
- cmd: iD.ui.cmd('⌘Z'),
+ cmd: cmd('⌘Z'),
action: function() { if (!(context.inIntro() || saving())) context.undo(); },
annotation: function() { return context.history().undoAnnotation(); }
}, {
id: 'redo',
- cmd: iD.ui.cmd('⌘⇧Z'),
+ cmd: cmd('⌘⇧Z'),
action: function() {if (!(context.inIntro() || saving())) context.redo(); },
annotation: function() { return context.history().redoAnnotation(); }
}];
@@ -62,4 +64,4 @@ iD.ui.UndoRedo = function(context) {
});
}
};
-};
+}
diff --git a/js/id/ui/view_on_osm.js b/modules/ui/core/view_on_osm.js
similarity index 94%
rename from js/id/ui/view_on_osm.js
rename to modules/ui/core/view_on_osm.js
index 003876671..b9d17d087 100644
--- a/js/id/ui/view_on_osm.js
+++ b/modules/ui/core/view_on_osm.js
@@ -1,4 +1,4 @@
-iD.ui.ViewOnOSM = function(context) {
+export function ViewOnOSM(context) {
var id;
function viewOnOSM(selection) {
@@ -28,4 +28,4 @@ iD.ui.ViewOnOSM = function(context) {
};
return viewOnOSM;
-};
+}
diff --git a/js/id/ui/zoom.js b/modules/ui/core/zoom.js
similarity index 85%
rename from js/id/ui/zoom.js
rename to modules/ui/core/zoom.js
index b163b4160..718dbac45 100644
--- a/js/id/ui/zoom.js
+++ b/modules/ui/core/zoom.js
@@ -1,4 +1,6 @@
-iD.ui.Zoom = function(context) {
+import { cmd } from './cmd';
+
+export function Zoom(context) {
var zooms = [{
id: 'zoom-in',
icon: 'plus',
@@ -58,17 +60,17 @@ iD.ui.Zoom = function(context) {
_.each(['=','ffequals','plus','ffplus'], function(key) {
keybinding.on(key, zoomIn);
keybinding.on('⇧' + key, zoomIn);
- keybinding.on(iD.ui.cmd('⌘' + key), zoomInFurther);
- keybinding.on(iD.ui.cmd('⌘⇧' + key), zoomInFurther);
+ 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(iD.ui.cmd('⌘' + key), zoomOutFurther);
- keybinding.on(iD.ui.cmd('⌘⇧' + key), zoomOutFurther);
+ keybinding.on(cmd('⌘' + key), zoomOutFurther);
+ keybinding.on(cmd('⌘⇧' + key), zoomOutFurther);
});
d3.select(document)
.call(keybinding);
};
-};
+}
diff --git a/js/id/ui/intro/area.js b/modules/ui/intro/area.js
similarity index 98%
rename from js/id/ui/intro/area.js
rename to modules/ui/intro/area.js
index 0f1340822..e7aa6396d 100644
--- a/js/id/ui/intro/area.js
+++ b/modules/ui/intro/area.js
@@ -1,4 +1,4 @@
-iD.ui.intro.area = function(context, reveal) {
+export function area(context, reveal) {
var event = d3.dispatch('done'),
timeout;
@@ -85,4 +85,4 @@ iD.ui.intro.area = function(context, reveal) {
};
return d3.rebind(step, event, 'on');
-};
+}
diff --git a/modules/ui/intro/index.js b/modules/ui/intro/index.js
new file mode 100644
index 000000000..e29f2c24d
--- /dev/null
+++ b/modules/ui/intro/index.js
@@ -0,0 +1,5 @@
+export { area } from './area';
+export { line } from './line';
+export { navigation } from './navigation';
+export { point } from './point';
+export { startEditing } from './start_editing';
diff --git a/js/id/ui/intro/line.js b/modules/ui/intro/line.js
similarity index 99%
rename from js/id/ui/intro/line.js
rename to modules/ui/intro/line.js
index 66b72f5f2..970d1844f 100644
--- a/js/id/ui/intro/line.js
+++ b/modules/ui/intro/line.js
@@ -1,4 +1,4 @@
-iD.ui.intro.line = function(context, reveal) {
+export function line(context, reveal) {
var event = d3.dispatch('done'),
timeouts = [];
@@ -150,4 +150,4 @@ iD.ui.intro.line = function(context, reveal) {
};
return d3.rebind(step, event, 'on');
-};
+}
diff --git a/js/id/ui/intro/navigation.js b/modules/ui/intro/navigation.js
similarity index 98%
rename from js/id/ui/intro/navigation.js
rename to modules/ui/intro/navigation.js
index e07e028b5..337d4246a 100644
--- a/js/id/ui/intro/navigation.js
+++ b/modules/ui/intro/navigation.js
@@ -1,4 +1,4 @@
-iD.ui.intro.navigation = function(context, reveal) {
+export function navigation(context, reveal) {
var event = d3.dispatch('done'),
timeouts = [];
@@ -109,4 +109,4 @@ iD.ui.intro.navigation = function(context, reveal) {
};
return d3.rebind(step, event, 'on');
-};
+}
diff --git a/js/id/ui/intro/point.js b/modules/ui/intro/point.js
similarity index 99%
rename from js/id/ui/intro/point.js
rename to modules/ui/intro/point.js
index 56eee2bb6..4078fdc98 100644
--- a/js/id/ui/intro/point.js
+++ b/modules/ui/intro/point.js
@@ -1,4 +1,4 @@
-iD.ui.intro.point = function(context, reveal) {
+export function point(context, reveal) {
var event = d3.dispatch('done'),
timeouts = [];
@@ -149,4 +149,4 @@ iD.ui.intro.point = function(context, reveal) {
};
return d3.rebind(step, event, 'on');
-};
+}
diff --git a/js/id/ui/intro/start_editing.js b/modules/ui/intro/start_editing.js
similarity index 96%
rename from js/id/ui/intro/start_editing.js
rename to modules/ui/intro/start_editing.js
index c690a20c0..c33a36124 100644
--- a/js/id/ui/intro/start_editing.js
+++ b/modules/ui/intro/start_editing.js
@@ -1,4 +1,4 @@
-iD.ui.intro.startEditing = function(context, reveal) {
+export function startEditing(context, reveal) {
var event = d3.dispatch('done', 'startEditing'),
modal,
timeouts = [];
@@ -54,4 +54,4 @@ iD.ui.intro.startEditing = function(context, reveal) {
};
return d3.rebind(step, event, 'on');
-};
+}
diff --git a/js/id/ui/preset/access.js b/modules/ui/preset/access.js
similarity index 99%
rename from js/id/ui/preset/access.js
rename to modules/ui/preset/access.js
index 51354758c..2b9f6525c 100644
--- a/js/id/ui/preset/access.js
+++ b/modules/ui/preset/access.js
@@ -1,4 +1,4 @@
-iD.ui.preset.access = function(field) {
+export function access(field) {
var dispatch = d3.dispatch('change'),
items;
@@ -193,4 +193,4 @@ iD.ui.preset.access = function(field) {
};
return d3.rebind(access, dispatch, 'on');
-};
+}
diff --git a/js/id/ui/preset/address.js b/modules/ui/preset/address.js
similarity index 99%
rename from js/id/ui/preset/address.js
rename to modules/ui/preset/address.js
index e125f6002..56358338a 100644
--- a/js/id/ui/preset/address.js
+++ b/modules/ui/preset/address.js
@@ -1,4 +1,4 @@
-iD.ui.preset.address = function(field, context) {
+export function address(field, context) {
var dispatch = d3.dispatch('init', 'change'),
wrap,
entity,
@@ -216,4 +216,4 @@ iD.ui.preset.address = function(field, context) {
};
return d3.rebind(address, dispatch, 'on');
-};
+}
diff --git a/js/id/ui/preset/check.js b/modules/ui/preset/check.js
similarity index 97%
rename from js/id/ui/preset/check.js
rename to modules/ui/preset/check.js
index 7765aba36..9bcf02d1b 100644
--- a/js/id/ui/preset/check.js
+++ b/modules/ui/preset/check.js
@@ -1,5 +1,5 @@
-iD.ui.preset.check =
-iD.ui.preset.defaultcheck = function(field) {
+export { check as defaultcheck };
+export function check(field) {
var dispatch = d3.dispatch('change'),
options = field.strings && field.strings.options,
values = [],
@@ -79,4 +79,4 @@ iD.ui.preset.defaultcheck = function(field) {
};
return d3.rebind(check, dispatch, 'on');
-};
+}
diff --git a/js/id/ui/preset/combo.js b/modules/ui/preset/combo.js
similarity index 98%
rename from js/id/ui/preset/combo.js
rename to modules/ui/preset/combo.js
index cafd3e70b..cebe8741d 100644
--- a/js/id/ui/preset/combo.js
+++ b/modules/ui/preset/combo.js
@@ -1,6 +1,5 @@
-iD.ui.preset.combo =
-iD.ui.preset.typeCombo =
-iD.ui.preset.multiCombo = function(field, context) {
+export { combo as typeCombo, combo as multiCombo };
+export function combo(field, context) {
var dispatch = d3.dispatch('change'),
isMulti = (field.type === 'multiCombo'),
optstrings = field.strings && field.strings.options,
@@ -304,4 +303,4 @@ iD.ui.preset.multiCombo = function(field, context) {
return d3.rebind(combo, dispatch, 'on');
-};
+}
diff --git a/js/id/ui/preset/cycleway.js b/modules/ui/preset/cycleway.js
similarity index 98%
rename from js/id/ui/preset/cycleway.js
rename to modules/ui/preset/cycleway.js
index bf56c94b7..24af9e893 100644
--- a/js/id/ui/preset/cycleway.js
+++ b/modules/ui/preset/cycleway.js
@@ -1,4 +1,4 @@
-iD.ui.preset.cycleway = function(field) {
+export function cycleway(field) {
var dispatch = d3.dispatch('change'),
items;
@@ -97,4 +97,4 @@ iD.ui.preset.cycleway = function(field) {
};
return d3.rebind(cycleway, dispatch, 'on');
-};
+}
diff --git a/modules/ui/preset/index.js b/modules/ui/preset/index.js
new file mode 100644
index 000000000..264462df9
--- /dev/null
+++ b/modules/ui/preset/index.js
@@ -0,0 +1,12 @@
+export { access } from './access';
+export { address } from './address';
+export { check, defaultcheck} from './check';
+export { combo, typeCombo, multiCombo } from './combo';
+export { cycleway } from './cycleway';
+export { text, url, number, email, tel } from './input';
+export { localized } from './localized';
+export { maxspeed } from './maxspeed';
+export { radio } from './radio';
+export { restrictions } from './restrictions';
+export { textarea } from './textarea';
+export { wikipedia } from './wikipedia';
diff --git a/js/id/ui/preset/input.js b/modules/ui/preset/input.js
similarity index 94%
rename from js/id/ui/preset/input.js
rename to modules/ui/preset/input.js
index 926b9a8a2..8d222ecc4 100644
--- a/js/id/ui/preset/input.js
+++ b/modules/ui/preset/input.js
@@ -1,8 +1,10 @@
-iD.ui.preset.text =
-iD.ui.preset.number =
-iD.ui.preset.tel =
-iD.ui.preset.email =
-iD.ui.preset.url = function(field, context) {
+export {
+ url as text,
+ url as number,
+ url as tel,
+ url as email
+};
+export function url(field, context) {
var dispatch = d3.dispatch('change'),
input,
@@ -85,4 +87,4 @@ iD.ui.preset.url = function(field, context) {
};
return d3.rebind(i, dispatch, 'on');
-};
+}
diff --git a/js/id/ui/preset/localized.js b/modules/ui/preset/localized.js
similarity index 99%
rename from js/id/ui/preset/localized.js
rename to modules/ui/preset/localized.js
index f41d2c191..6c7de3bcd 100644
--- a/js/id/ui/preset/localized.js
+++ b/modules/ui/preset/localized.js
@@ -1,4 +1,4 @@
-iD.ui.preset.localized = function(field, context) {
+export function localized(field, context) {
var dispatch = d3.dispatch('change', 'input'),
wikipedia = iD.services.wikipedia(),
input, localizedInputs, wikiTitles,
@@ -239,4 +239,4 @@ iD.ui.preset.localized = function(field, context) {
};
return d3.rebind(localized, dispatch, 'on');
-};
+}
diff --git a/js/id/ui/preset/maxspeed.js b/modules/ui/preset/maxspeed.js
similarity index 98%
rename from js/id/ui/preset/maxspeed.js
rename to modules/ui/preset/maxspeed.js
index a11ee6855..7a4848a97 100644
--- a/js/id/ui/preset/maxspeed.js
+++ b/modules/ui/preset/maxspeed.js
@@ -1,4 +1,4 @@
-iD.ui.preset.maxspeed = function(field, context) {
+export function maxspeed(field, context) {
var dispatch = d3.dispatch('change'),
entity,
imperial,
@@ -107,4 +107,4 @@ iD.ui.preset.maxspeed = function(field, context) {
};
return d3.rebind(maxspeed, dispatch, 'on');
-};
+}
diff --git a/js/id/ui/preset/radio.js b/modules/ui/preset/radio.js
similarity index 98%
rename from js/id/ui/preset/radio.js
rename to modules/ui/preset/radio.js
index 11241e37c..d1a3374eb 100644
--- a/js/id/ui/preset/radio.js
+++ b/modules/ui/preset/radio.js
@@ -1,4 +1,4 @@
-iD.ui.preset.radio = function(field) {
+export function radio(field) {
var dispatch = d3.dispatch('change'),
labels, radios, placeholder;
@@ -72,4 +72,4 @@ iD.ui.preset.radio = function(field) {
};
return d3.rebind(radio, dispatch, 'on');
-};
+}
diff --git a/js/id/ui/preset/restrictions.js b/modules/ui/preset/restrictions.js
similarity index 98%
rename from js/id/ui/preset/restrictions.js
rename to modules/ui/preset/restrictions.js
index fe95a9138..2874a9787 100644
--- a/js/id/ui/preset/restrictions.js
+++ b/modules/ui/preset/restrictions.js
@@ -1,4 +1,4 @@
-iD.ui.preset.restrictions = function(field, context) {
+export function restrictions(field, context) {
var dispatch = d3.dispatch('change'),
hover = iD.behavior.Hover(context),
vertexID,
@@ -171,4 +171,4 @@ iD.ui.preset.restrictions = function(field, context) {
};
return d3.rebind(restrictions, dispatch, 'on');
-};
+}
diff --git a/js/id/ui/preset/textarea.js b/modules/ui/preset/textarea.js
similarity index 95%
rename from js/id/ui/preset/textarea.js
rename to modules/ui/preset/textarea.js
index 92d18aa63..f00f668b3 100644
--- a/js/id/ui/preset/textarea.js
+++ b/modules/ui/preset/textarea.js
@@ -1,4 +1,4 @@
-iD.ui.preset.textarea = function(field) {
+export function textarea(field) {
var dispatch = d3.dispatch('change'),
input;
@@ -34,4 +34,4 @@ iD.ui.preset.textarea = function(field) {
};
return d3.rebind(textarea, dispatch, 'on');
-};
+}
diff --git a/js/id/ui/preset/wikipedia.js b/modules/ui/preset/wikipedia.js
similarity index 99%
rename from js/id/ui/preset/wikipedia.js
rename to modules/ui/preset/wikipedia.js
index b129eb9ba..6f7cf4f3b 100644
--- a/js/id/ui/preset/wikipedia.js
+++ b/modules/ui/preset/wikipedia.js
@@ -1,4 +1,4 @@
-iD.ui.preset.wikipedia = function(field, context) {
+export function wikipedia(field, context) {
var dispatch = d3.dispatch('change'),
wikipedia = iD.services.wikipedia(),
wikidata = iD.services.wikidata(),
@@ -195,4 +195,4 @@ iD.ui.preset.wikipedia = function(field, context) {
};
return d3.rebind(wiki, dispatch, 'on');
-};
+}
diff --git a/js/id/ui.js b/modules/ui/ui.js
similarity index 98%
rename from js/id/ui.js
rename to modules/ui/ui.js
index f06f3a845..74b3d07bb 100644
--- a/js/id/ui.js
+++ b/modules/ui/ui.js
@@ -1,4 +1,4 @@
-iD.ui = function(context) {
+export function ui(context) {
function render(container) {
var map = context.map();
@@ -226,9 +226,9 @@ iD.ui = function(context) {
ui.sidebar = iD.ui.Sidebar(context);
return ui;
-};
+}
-iD.ui.tooltipHtml = function(text, key) {
+ui.tooltipHtml = function(text, key) {
var s = '' + text + '';
if (key) {
s += '' +
diff --git a/test/index.html b/test/index.html
index 8b2b0f539..125e39bab 100644
--- a/test/index.html
+++ b/test/index.html
@@ -52,57 +52,13 @@
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-