From 07e5e908f2024db6baf1d3536f45c2579bbacd31 Mon Sep 17 00:00:00 2001 From: Kushan Joshi <0o3ko0@gmail.com> Date: Fri, 17 Jun 2016 15:57:45 +0530 Subject: [PATCH] making test run --- index.html | 1 - js/lib/id/ui.js | 6005 +++++++++++++++++++++++++++++++++++++++++++++++ test/index.html | 41 +- 3 files changed, 6007 insertions(+), 40 deletions(-) create mode 100644 js/lib/id/ui.js diff --git a/index.html b/index.html index 3cacf3310..112df6491 100644 --- a/index.html +++ b/index.html @@ -44,7 +44,6 @@ - diff --git a/js/lib/id/ui.js b/js/lib/id/ui.js new file mode 100644 index 000000000..d252c2226 --- /dev/null +++ b/js/lib/id/ui.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/test/index.html b/test/index.html index 8b2b0f539..63babcfe5 100644 --- a/test/index.html +++ b/test/index.html @@ -55,45 +55,8 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +