diff --git a/css/80_app.css b/css/80_app.css index bffaa68b7..2e14ac1d0 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -130,7 +130,6 @@ a, button, input, textarea { a, button, .checkselect label:hover, -.opacity-options li, .radial-menu-item { cursor: pointer; } @@ -268,7 +267,7 @@ table th { } table.tags, table.tags td, table.tags th { - border: 1px solid #CCC; + border: 1px solid #ccc; padding: 4px; } @@ -304,7 +303,7 @@ ul li { list-style: none;} display: block; height: 30px; background-color: white; - color: #7092FF; + color: #7092ff; cursor: pointer; } @@ -740,6 +739,30 @@ button.save.has-count .count::before { position: absolute; } + +/* Hide-Toggle +------------------------------------------------------- */ + +.hide-toggle .icon.pre-text { + vertical-align: text-top; + width: 16px; + height: 16px; + margin-left: -3px; +} +[dir='rtl'] .hide-toggle .icon.pre-text { + margin-left: 0; + margin-right: -3px; +} + +a:visited.hide-toggle, +a.hide-toggle { + display: inline-block; + font-size: 14px; + font-weight: bold; + padding-bottom: 5px; +} + + /* Inspector ------------------------------------------------------- */ @@ -782,7 +805,6 @@ button.save.has-count .count::before { bottom: 0; } - .feature-list-pane .inspector-body { top: 120px; } @@ -1071,7 +1093,7 @@ button.save.has-count .count::before { .preset-list-item button.tag-reference-button { height: 100%; - border: 1px solid #CCC; + border: 1px solid #ccc; border-radius: 0 3px 3px 0; position: absolute; top: 0; @@ -1143,7 +1165,7 @@ button.save.has-count .count::before { } .preset-editor a.hide-toggle { - margin: 0 20px 10px 20px; + margin: 0 20px 5px 20px; } .preset-editor .form-fields-container { @@ -1218,7 +1240,7 @@ button.save.has-count .count::before { } [dir='rtl'] .form-label button { border-left: none; - border-right: 1px solid #CCC; + border-right: 1px solid #ccc; border-radius: 4px 0 0 0; width: 31px; } @@ -1574,13 +1596,13 @@ input[type=number] { float: left; height: 100%; width: 32px; - border-left: 1px solid #CCC; + border-left: 1px solid #ccc; border-radius: 0; background: rgba(0, 0, 0, 0); } [dir='rtl'] .spin-control button{ border-left: 0; - border-right: 1px solid #CCC; + border-right: 1px solid #ccc; } .spin-control button.decrement { @@ -1604,13 +1626,13 @@ input[type=number] { } .spin-control button.decrement::after { - border-top: 5px solid #CCC; + border-top: 5px solid #ccc; border-left: 5px solid transparent; border-right: 5px solid transparent; } .spin-control button.increment::after { - border-bottom: 5px solid #CCC; + border-bottom: 5px solid #ccc; border-left: 5px solid transparent; border-right: 5px solid transparent; } @@ -1622,7 +1644,7 @@ input[type=number] { display: block; background: white; padding: 5px 10px; - color: #7092FF; + color: #7092ff; } .checkselect label:hover { @@ -1720,7 +1742,7 @@ input[type=number] { right: 1px; width: 32px; margin-left: -32px; - border: 1px solid #CCC; + border: 1px solid #ccc; border-top-width: 0; border-right-width: 0; border-radius: 0 0 4px 0; @@ -1938,12 +1960,12 @@ div.combobox { height: 31px; border: 0; border-radius: 0; - border-bottom: 1px solid #CCC; - border-left: 1px solid #CCC; + border-bottom: 1px solid #ccc; + border-left: 1px solid #ccc; } [dir='rtl'] .tag-row input { border-left: none; - border-right: 1px solid #CCC; + border-right: 1px solid #ccc; } .tag-row .key-wrap, @@ -1963,14 +1985,14 @@ div.combobox { } .tag-row input.value { - border-right: 1px solid #CCC; + border-right: 1px solid #ccc; } [dir='rtl'] .tag-row input.value { - border-left: 1px solid #CCC; + border-left: 1px solid #ccc; } .tag-row:first-child input.key { - border-top: 1px solid #CCC; + border-top: 1px solid #ccc; border-top-left-radius: 4px; } [dir='rtl'] .tag-row:first-child input.key { @@ -1979,14 +2001,14 @@ div.combobox { } .tag-row:first-child input.value { - border-top: 1px solid #CCC; + border-top: 1px solid #ccc; } .tag-row button { position: absolute; height: 31px; right: 10%; - border: 1px solid #CCC; + border: 1px solid #ccc; border-top-width: 0; border-left-width: 0; } @@ -2246,16 +2268,12 @@ div.full-screen > button:hover { .imagery-faq { margin-bottom: 10px; -} - -.map-data-control .hide-toggle, -.background-control .hide-toggle { - padding-bottom: 10px; + white-space: nowrap; } .layer-list, .controls-list { margin-bottom: 10px; - border: 1px solid #CCC; + border: 1px solid #ccc; border-radius: 4px; } @@ -2263,7 +2281,7 @@ div.full-screen > button:hover { position: relative; height: 30px; background-color: white; - color: #7092FF; + color: #7092ff; } .layer-list:empty { @@ -2292,7 +2310,7 @@ div.full-screen > button:hover { .layer-list li.active, .layer-list li.switch { - background: #E8EBFF; + background: #e8ebff; } .layer-list li.best > div.best { @@ -2324,60 +2342,38 @@ div.full-screen > button:hover { text-overflow: ellipsis; } -.minimap-toggle { - display: block; - padding: 5px 10px; - cursor: pointer; - color: #7092FF; - border-radius: 3px; + +/* Background Display Options */ + +.display-options-container { + padding: 10px; } -.minimap-toggle.active { - background: #E8EBFF; +.display-control h5 { + padding-bottom: 0; + padding-top: 10px; } -.minimap-toggle:hover { - background-color: #ececec; +.display-control h5 span { + margin: 5px; } -.hide-toggle { - display: block; - padding-left: 12px; - position: relative; -} -[dir='rtl'] .hide-toggle { - padding-left: 0; - padding-right: 12px; +.display-control .display-option-input { + height: 20px; + width: 160px; } -.hide-toggle:before { - content: ''; - display: block; - position: absolute; - height: 0; - width: 0; - left: 0; - top: 5px; - border-top: 4px solid transparent; - border-bottom: 4px solid transparent; - border-left: 8px solid #7092ff; +.display-control button { + height: 30px; + width: 30px; + margin-left: 5px; + margin-right: 0px; + vertical-align: text-bottom; + border-radius: 4px; } -[dir='rtl'] .hide-toggle:before { - left: auto; - right: 0; - border-left: none; - border-right: 8px solid #7092ff; -} - -.hide-toggle.expanded:before { - border-top: 8px solid #7092ff; - border-bottom: 0; - border-right: 4px solid transparent; - border-left: 4px solid transparent; -} -[dir='rtl'] .hide-toggle.expanded:before { - border-left: 4px solid transparent; - border-right: 4px solid transparent; +[dir='rtl'] .display-control button { + margin-left: 0px; + margin-right: 5px; } @@ -2433,7 +2429,7 @@ div.full-screen > button:hover { } .nudge-container input.error { - border: 1px solid #FF7878; + border: 1px solid #ff7878; border-radius: 2px; background: #ffb; } @@ -2485,92 +2481,43 @@ div.full-screen > button:hover { } .background-control .nudge.right::after { - border-top: 5px solid transparent; - border-bottom: 5px solid transparent; - border-left: 5px solid #222; + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-left: 5px solid #222; } .background-control .nudge.left::after { - border-top: 5px solid transparent; - border-bottom: 5px solid transparent; - border-right: 5px solid #222; + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-right: 5px solid #222; } .background-control .nudge.top::after { - border-right: 5px solid transparent; - border-left: 5px solid transparent; - border-bottom: 5px solid #222; + border-right: 5px solid transparent; + border-left: 5px solid transparent; + border-bottom: 5px solid #222; } .background-control .nudge.bottom::after { - border-right: 5px solid transparent; - border-left: 5px solid transparent; - border-top: 5px solid #222; + border-right: 5px solid transparent; + border-left: 5px solid transparent; + border-top: 5px solid #222; } -.opacity-options { - background: url(img/background-pattern-opacity.png) 0 0 repeat; - height: 20px; - width: 82px; - position: absolute; - right: 50px; - top: 20px; - border: 1px solid #ccc; -} -[dir='rtl'] .opacity-options { - left: 50px; - right: auto; -} - -.opacity-options li { - height: 100%; - display: block; - float: left; -} - -.opacity-options li .select-box{ - position: absolute; - width: 20px; - height: 18px; - z-index: 9999; -} - -.map-data-control li:hover .select-box, -.map-data-control li.selected .select-box, -.background-control li:hover .select-box, -.background-control li.selected .select-box { - border: 2px solid #7092ff; - background: rgba(89, 123, 231, .5); - opacity: .5; -} - -.map-data-control li.selected:hover .select-box, -.map-data-control li.selected .select-box, -.background-control li.selected:hover .select-box, -.background-control li.selected .select-box { - opacity: 1; -} - -.background-control .opacity { - background:#222; - display:inline-block; - width:20px; - height:18px; -} .map-data-control .layer-list button, .background-control .layer-list button { float: right; height: 100%; width: 10%; - border-left: 1px solid #CCC; + border-left: 1px solid #ccc; border-radius: 0; } [dir='rtl'] .map-data-control .layer-list button, [dir='rtl'] .background-control .layer-list button { float: left; border-left: none; - border-right: 1px solid #CCC; + border-right: 1px solid #ccc; } .map-data-control .layer-list button .icon, @@ -2603,12 +2550,12 @@ div.full-screen > button:hover { border-radius: 0 0 0 4px; } [dir='rtl'] .geolocate-control button { - border-radius: 0 0 4px 0; + border-radius: 0 0 4px 0; } .map-overlay.content { position: fixed; - top:60px; + top: 60px; bottom: 30px; padding: 20px 50px 20px 20px; right: 0; @@ -2620,13 +2567,17 @@ div.full-screen > button:hover { right: auto !important; } +.map-overlay.content > div { + padding-bottom: 15px; +} + /* Help */ .help-control button { border-radius: 0 0 0 4px; } [dir='rtl'] .help-control button { - border-radius: 0 0 4px 0; + border-radius: 0 0 4px 0; } .help-wrap p { @@ -2655,7 +2606,7 @@ div.full-screen > button:hover { } .help-wrap .toc { - width:40%; + width: 40%; float:right; margin-left: 20px; margin-bottom: 20px; @@ -2732,12 +2683,12 @@ div.full-screen > button:hover { ------------------------------------------------------- */ img.tile { - position:absolute; - transform-origin:0 0; - -ms-transform-origin:0 0; - -webkit-transform-origin:0 0; - -moz-transform-origin:0 0; - -o-transform-origin:0 0; + position: absolute; + transform-origin: 0 0; + -ms-transform-origin: 0 0; + -webkit-transform-origin: 0 0; + -moz-transform-origin: 0 0; + -o-transform-origin: 0 0; -moz-user-select: none; -webkit-user-select: none; @@ -2747,8 +2698,16 @@ img.tile { opacity: 0; -webkit-transition: opacity 200ms linear; - transition: opacity 200ms linear; -moz-transition: opacity 200ms linear; + transition: opacity 200ms linear; +} + +img.tile-loaded { + opacity: 1; +} + +img.tile-removing { + opacity: 0; } .tile-label-debug { @@ -2763,11 +2722,11 @@ img.tile { margin-left: -70px; margin-top: -20px; - transform-origin:0 0; - -ms-transform-origin:0 0; - -webkit-transform-origin:0 0; - -moz-transform-origin:0 0; - -o-transform-origin:0 0; + transform-origin: 0 0; + -ms-transform-origin: 0 0; + -webkit-transform-origin: 0 0; + -moz-transform-origin: 0 0; + -o-transform-origin: 0 0; -moz-user-select: none; -webkit-user-select: none; @@ -2779,23 +2738,15 @@ img.tile-debug { outline: 1px solid red; } -img.tile-loaded { - opacity: 1; -} - -img.tile-removing { - opacity: 0; -} - /* Map ------------------------------------------------------- */ #map { - position:relative; - overflow:hidden; - height:100%; - background:#000; + position: relative; + overflow: hidden; + height: 100%; + background: #000; -moz-user-select: none; -webkit-user-select: none; -ms-user-select: none; @@ -2803,11 +2754,11 @@ img.tile-removing { } #supersurface { - transform-origin:0 0; - -ms-transform-origin:0 0; - -webkit-transform-origin:0 0; - -moz-transform-origin:0 0; - -o-transform-origin:0 0; + transform-origin: 0 0; + -ms-transform-origin: 0 0; + -webkit-transform-origin: 0 0; + -moz-transform-origin: 0 0; + -o-transform-origin: 0 0; } #supersurface, .layer { @@ -3368,7 +3319,7 @@ img.tile-removing { .modal-section { padding: 20px; - border-bottom: 1px solid #CCC; + border-bottom: 1px solid #ccc; } .modal-section.header h3 { @@ -3401,8 +3352,8 @@ img.tile-removing { .modal-actions button, .save-success a.button { font-weight: normal; - color: #7092FF; - border-bottom: 1px solid #CCC; + color: #7092ff; + border-bottom: 1px solid #ccc; border-radius: 0; height: 160px; text-align: center; @@ -3422,7 +3373,7 @@ img.tile-removing { } .modal-actions > :first-child { - border-right: 1px solid #CCC; + border-right: 1px solid #ccc; } .modal-section:last-child { @@ -3432,7 +3383,7 @@ img.tile-removing { /* Restore Modal ------------------------------------------------------- */ .modal-actions .logo-restore { - color: #7092FF; + color: #7092ff; } .modal-actions .logo-reset { color: #E06C5E; @@ -3450,7 +3401,7 @@ img.tile-removing { padding-top: 15px; } .save-success .logo-osm { - color: #7092FF; + color: #7092ff; margin-bottom: 10px; } .save-success a.button.social { @@ -3460,14 +3411,14 @@ img.tile-removing { .save-success .icon.social { height: 80px; width: 80px; - color: #7092FF; + color: #7092ff; } /* Splash Modal ------------------------------------------------------- */ .modal-actions .logo-walkthrough, .modal-actions .logo-features { - color: #7092FF; + color: #7092ff; } @@ -3658,7 +3609,7 @@ svg.mouseclick use.right { } .mode-save .commit-section .changeset-list button { - border-left: 1px solid #CCC; + border-left: 1px solid #ccc; } .changeset-list li span.count:before { content: '('; } @@ -4215,7 +4166,7 @@ li.hide + li.version .badge .tooltip .tooltip-arrow { .curtain-tooltip .tooltip-inner .instruction { font-weight: bold; display: block; - border-top: 1px solid #CCC; + border-top: 1px solid #ccc; margin-top: 10px; margin-left: -20px; margin-right: -20px; @@ -4293,5 +4244,5 @@ li.hide + li.version .badge .tooltip .tooltip-arrow { .huge-modal-button .illustration { height: 100px; width: 100px; - color: #7092FF; + color: #7092ff; } diff --git a/data/core.yaml b/data/core.yaml index d087073a1..9261276af 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -375,21 +375,27 @@ en: title: Background description: Background settings key: B - percent_brightness: "{opacity}% brightness" + backgrounds: Backgrounds none: None best_imagery: Best known imagery source for this location switch: Switch back to this background custom: Custom custom_button: Edit custom background custom_prompt: "Enter a tile URL template. Valid tokens are:\n - {zoom}/{z}, {x}, {y} for Z/X/Y tile scheme\n - {ty} for flipped TMS-style Y coordinates\n - {u} for quadtile scheme\n - {switch:a,b,c} for DNS server multiplexing\n\nExample:\n{example}" - fix_misalignment: Adjust imagery offset - imagery_source_faq: Where does this imagery come from? + overlays: Overlays + imagery_source_faq: Imagery Info / Report a Problem reset: reset - offset: "Drag anywhere in the gray area below to adjust the imagery offset, or enter the offset values in meters." + display_options: Display Options + brightness: Brightness + contrast: Contrast + saturation: Saturation + sharpness: Sharpness minimap: - description: Minimap + description: Show Minimap tooltip: Show a zoomed out map to help locate the area currently displayed. key: '/' + fix_misalignment: Adjust imagery offset + offset: "Drag anywhere in the gray area below to adjust the imagery offset, or enter the offset values in meters." map_data: title: Map Data description: Map Data diff --git a/dist/locales/en.json b/dist/locales/en.json index 092534873..e009184a1 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -464,22 +464,28 @@ "title": "Background", "description": "Background settings", "key": "B", - "percent_brightness": "{opacity}% brightness", + "backgrounds": "Backgrounds", "none": "None", "best_imagery": "Best known imagery source for this location", "switch": "Switch back to this background", "custom": "Custom", "custom_button": "Edit custom background", "custom_prompt": "Enter a tile URL template. Valid tokens are:\n - {zoom}/{z}, {x}, {y} for Z/X/Y tile scheme\n - {ty} for flipped TMS-style Y coordinates\n - {u} for quadtile scheme\n - {switch:a,b,c} for DNS server multiplexing\n\nExample:\n{example}", - "fix_misalignment": "Adjust imagery offset", - "imagery_source_faq": "Where does this imagery come from?", + "overlays": "Overlays", + "imagery_source_faq": "Imagery Info / Report a Problem", "reset": "reset", - "offset": "Drag anywhere in the gray area below to adjust the imagery offset, or enter the offset values in meters.", + "display_options": "Display Options", + "brightness": "Brightness", + "contrast": "Contrast", + "saturation": "Saturation", + "sharpness": "Sharpness", "minimap": { - "description": "Minimap", + "description": "Show Minimap", "tooltip": "Show a zoomed out map to help locate the area currently displayed.", "key": "/" - } + }, + "fix_misalignment": "Adjust imagery offset", + "offset": "Drag anywhere in the gray area below to adjust the imagery offset, or enter the offset values in meters." }, "map_data": { "title": "Map Data", diff --git a/modules/renderer/background.js b/modules/renderer/background.js index d1d59042e..3d9f645b4 100644 --- a/modules/renderer/background.js +++ b/modules/renderer/background.js @@ -1,6 +1,7 @@ import _find from 'lodash-es/find'; import { dispatch as d3_dispatch } from 'd3-dispatch'; +import { interpolateNumber as d3_interpolateNumber } from 'd3-interpolate'; import { select as d3_select } from 'd3-selection'; import { data } from '../../data'; @@ -12,24 +13,83 @@ import { utilRebind } from '../util/rebind'; export function rendererBackground(context) { - var dispatch = d3_dispatch('change'), - baseLayer = rendererTileLayer(context).projection(context.projection), - overlayLayers = [], - backgroundSources; + var dispatch = d3_dispatch('change'); + var baseLayer = rendererTileLayer(context).projection(context.projection); + var _overlayLayers = []; + var _backgroundSources = []; + var _brightness = 1; + var _contrast = 1; + var _saturation = 1; + var _sharpness = 1; function background(selection) { + var baseFilter = ''; + + if (_brightness !== 1) { + baseFilter += 'brightness(' + _brightness + ')'; + } + if (_contrast !== 1) { + baseFilter += 'contrast(' + _contrast + ')'; + } + if (_saturation !== 1) { + baseFilter += 'saturate(' + _saturation + ')'; + } + if (_sharpness < 1) { // gaussian blur + var blur = d3_interpolateNumber(0.5, 5)(1 - _sharpness); + baseFilter += 'blur(' + blur + 'px)'; + } + var base = selection.selectAll('.layer-background') .data([0]); - base.enter() + base = base.enter() .insert('div', '.layer-data') .attr('class', 'layer layer-background') .merge(base) + .style('filter', baseFilter || null); + + + var imagery = base.selectAll('.layer-imagery') + .data([0]); + + imagery.enter() + .append('div') + .attr('class', 'layer layer-imagery') + .merge(imagery) .call(baseLayer); + + var maskFilter = ''; + var mixBlendMode = ''; + if (_sharpness > 1) { // apply unsharp mask + mixBlendMode = 'overlay'; + maskFilter = 'saturate(0) blur(3px) invert(1)'; + + var contrast = _sharpness - 1; + maskFilter += ' contrast(' + contrast + ')'; + + var brightness = d3_interpolateNumber(1, 0.85)(_sharpness - 1); + maskFilter += ' brightness(' + brightness + ')'; + } + + var mask = base.selectAll('.layer-unsharp-mask') + .data(_sharpness > 1 ? [0] : []); + + mask.exit() + .remove(); + + mask.enter() + .append('div') + .attr('class', 'layer layer-mask layer-unsharp-mask') + .merge(mask) + .call(baseLayer) + .style('filter', maskFilter || null) + .style('mix-blend-mode', mixBlendMode || null); + + var overlays = selection.selectAll('.layer-overlay') - .data(overlayLayers, function(d) { return d.source().name(); }); + .data(_overlayLayers, function(d) { return d.source().name(); }); overlays.exit() .remove(); @@ -46,7 +106,7 @@ export function rendererBackground(context) { if (context.inIntro()) return; var b = background.baseLayerSource(), - o = overlayLayers + o = _overlayLayers .filter(function (d) { return !d.source().isLocatorOverlay() && !d.source().isHidden(); }) .map(function (d) { return d.source().id; }) .join(','), @@ -85,7 +145,7 @@ export function rendererBackground(context) { var imageryUsed = [b.imageryUsed()]; - overlayLayers + _overlayLayers .filter(function (d) { return !d.source().isLocatorOverlay() && !d.source().isHidden(); }) .forEach(function (d) { imageryUsed.push(d.source().imageryUsed()); }); @@ -117,7 +177,7 @@ export function rendererBackground(context) { background.sources = function(extent) { - return backgroundSources.filter(function(source) { + return _backgroundSources.filter(function(source) { return source.intersects(extent); }); }; @@ -127,7 +187,7 @@ export function rendererBackground(context) { if (!_) return; baseLayer.dimensions(_); - overlayLayers.forEach(function(layer) { + _overlayLayers.forEach(function(layer) { layer.dimensions(_); }); }; @@ -172,7 +232,7 @@ export function rendererBackground(context) { background.findSource = function(id) { - return _find(backgroundSources, function(d) { + return _find(_backgroundSources, function(d) { return d.id && d.id === id; }); }; @@ -185,22 +245,22 @@ export function rendererBackground(context) { background.showsLayer = function(d) { return d.id === baseLayer.source().id || - overlayLayers.some(function(layer) { return d.id === layer.source().id; }); + _overlayLayers.some(function(layer) { return d.id === layer.source().id; }); }; background.overlayLayerSources = function() { - return overlayLayers.map(function (l) { return l.source(); }); + return _overlayLayers.map(function (l) { return l.source(); }); }; background.toggleOverlayLayer = function(d) { var layer; - for (var i = 0; i < overlayLayers.length; i++) { - layer = overlayLayers[i]; + for (var i = 0; i < _overlayLayers.length; i++) { + layer = _overlayLayers[i]; if (layer.source() === d) { - overlayLayers.splice(i, 1); + _overlayLayers.splice(i, 1); dispatch.call('change'); background.updateImagery(); return; @@ -210,9 +270,10 @@ export function rendererBackground(context) { layer = rendererTileLayer(context) .source(d) .projection(context.projection) - .dimensions(baseLayer.dimensions()); + .dimensions(baseLayer.dimensions() + ); - overlayLayers.push(layer); + _overlayLayers.push(layer); dispatch.call('change'); background.updateImagery(); }; @@ -235,6 +296,38 @@ export function rendererBackground(context) { }; + background.brightness = function(d) { + if (!arguments.length) return _brightness; + _brightness = d; + if (context.mode()) dispatch.call('change'); + return background; + }; + + + background.contrast = function(d) { + if (!arguments.length) return _contrast; + _contrast = d; + if (context.mode()) dispatch.call('change'); + return background; + }; + + + background.saturation = function(d) { + if (!arguments.length) return _saturation; + _saturation = d; + if (context.mode()) dispatch.call('change'); + return background; + }; + + + background.sharpness = function(d) { + if (!arguments.length) return _sharpness; + _sharpness = d; + if (context.mode()) dispatch.call('change'); + return background; + }; + + background.init = function() { function parseMap(qmap) { if (!qmap) return false; @@ -251,7 +344,7 @@ export function rendererBackground(context) { best; // Add all the available imagery sources - backgroundSources = dataImagery.map(function(source) { + _backgroundSources = dataImagery.map(function(source) { if (source.type === 'bing') { return rendererBackgroundSource.Bing(source, dispatch); } else if (source.id === 'EsriWorldImagery') { @@ -261,15 +354,15 @@ export function rendererBackground(context) { } }); - first = backgroundSources.length && backgroundSources[0]; + first = _backgroundSources.length && _backgroundSources[0]; // Add 'None' - backgroundSources.unshift(rendererBackgroundSource.None()); + _backgroundSources.unshift(rendererBackgroundSource.None()); // Add 'Custom' var template = context.storage('background-custom-template') || ''; var custom = rendererBackgroundSource.Custom(template); - backgroundSources.unshift(custom); + _backgroundSources.unshift(custom); // Decide which background layer to display @@ -290,7 +383,7 @@ export function rendererBackground(context) { ); } - var locator = _find(backgroundSources, function(d) { + var locator = _find(_backgroundSources, function(d) { return d.overlay && d.default; }); diff --git a/modules/ui/background.js b/modules/ui/background.js index 9b58c79d5..d41bd9d24 100644 --- a/modules/ui/background.js +++ b/modules/ui/background.js @@ -7,439 +7,237 @@ import { import { event as d3_event, - select as d3_select, - selectAll as d3_selectAll + select as d3_select } from 'd3-selection'; import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js'; import { t, textDirection } from '../util/locale'; -import { geoMetersToOffset, geoOffsetToMeters } from '../geo'; -import { utilDetect } from '../util/detect'; -import { utilSetTransform, utilCallWhenIdle } from '../util'; import { svgIcon } from '../svg'; -import { uiMapInMap } from './map_in_map'; +import { uiBackgroundDisplayOptions } from './background_display_options'; +import { uiBackgroundOffset } from './background_offset'; import { uiCmd } from './cmd'; +import { uiDisclosure } from './disclosure'; +import { uiHelp } from './help'; +import { uiMapData } from './map_data'; +import { uiMapInMap } from './map_in_map'; import { uiTooltipHtml } from './tooltipHtml'; +import { utilCallWhenIdle } from '../util'; import { tooltip } from '../util/tooltip'; export function uiBackground(context) { - var key = t('background.key'), - detected = utilDetect(), - 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, - customSource = context.background().findSource('custom'), - previous; + var key = t('background.key'); - // Can be 0 from <1.3.0 use or due to issue #1923. - if (opacityDefault === 0) opacityDefault = 1.0; + var _customSource = context.background().findSource('custom'); + var _previousBackground; + var _shown = false; + + var _backgroundList = d3_select(null); + var _overlayList = d3_select(null); + var _displayOptionsContainer = d3_select(null); + var _offsetContainer = d3_select(null); + + var backgroundDisplayOptions = uiBackgroundDisplayOptions(context); + var backgroundOffset = uiBackgroundOffset(context); - function background(selection) { + function setTooltips(selection) { + selection.each(function(d, i, nodes) { + var item = d3_select(this).select('label'); + var span = item.select('span'); + var placement = (i < nodes.length / 2) ? 'bottom' : 'top'; + var description = d.description(); + var isOverflowing = (span.property('clientWidth') !== span.property('scrollWidth')); + + if (d === _previousBackground) { + item.call(tooltip() + .placement(placement) + .html(true) + .title(function() { + var tip = '
' + t('background.switch') + '
'; + return uiTooltipHtml(tip, uiCmd('⌘' + key)); + }) + ); + } else if (description || isOverflowing) { + item.call(tooltip() + .placement(placement) + .title(description || d.name()) + ); + } else { + item.call(tooltip().destroy); + } + }); + } + + + function updateLayerSelections(selection) { + function active(d) { + return context.background().showsLayer(d); + } + + selection.selectAll('.layer') + .classed('active', active) + .classed('switch', function(d) { return d === _previousBackground; }) + .call(setTooltips) + .selectAll('input') + .property('checked', active); + } + + + function chooseBackground(d) { + if (d.id === 'custom' && !d.template()) { + return editCustom(); + } + + d3_event.preventDefault(); + _previousBackground = context.background().baseLayerSource(); + context.background().baseLayerSource(d); + _backgroundList.call(updateLayerSelections); + document.activeElement.blur(); + } + + + function editCustom() { + d3_event.preventDefault(); + var example = 'https://{switch:a,b,c}.tile.openstreetmap.org/{zoom}/{x}/{y}.png'; + var template = window.prompt( + t('background.custom_prompt', { example: example }), + _customSource.template() || example + ); + + if (template) { + context.storage('background-custom-template', template); + _customSource.template(template); + chooseBackground(_customSource); + } else { + _backgroundList.call(updateLayerSelections); + } + } + + + function chooseOverlay(d) { + d3_event.preventDefault(); + context.background().toggleOverlayLayer(d); + _overlayList.call(updateLayerSelections); + document.activeElement.blur(); + } + + + function drawListItems(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(); }); + + layerLinks.exit() + .remove(); + + var enter = layerLinks.enter() + .append('li') + .attr('class', 'layer') + .classed('layer-custom', function(d) { return d.id === 'custom'; }) + .classed('best', function(d) { return d.best(); }); + + enter.filter(function(d) { return d.id === 'custom'; }) + .append('button') + .attr('class', 'layer-browse') + .call(tooltip() + .title(t('background.custom_button')) + .placement((textDirection === 'rtl') ? 'right' : 'left') + ) + .on('click', editCustom) + .call(svgIcon('#icon-search')); + + enter.filter(function(d) { return d.best(); }) + .append('div') + .attr('class', 'best') + .call(tooltip() + .title(t('background.best_imagery')) + .placement((textDirection === 'rtl') ? 'right' : '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(); }); + + + layerList.selectAll('li.layer') + .sort(sortSources) + .style('display', layerList.selectAll('li.layer').data().length > 0 ? 'block' : 'none'); + + layerList + .call(updateLayerSelections); + 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); + function renderBackgroundList(selection) { - if (!detected.opera) { - utilSetTransform(bg, 0, 0); - } + // the background list + var container = selection.selectAll('.layer-background-list') + .data([0]); - opacityList.selectAll('li') - .classed('active', function(_) { return _ === d; }); - - context.storage('background-opacity', d); - } + _backgroundList = container.enter() + .append('ul') + .attr('class', 'layer-list layer-background-list') + .attr('dir', 'auto') + .merge(container); - function setTooltips(selection) { - selection.each(function(d, i, nodes) { - var item = d3_select(this).select('label'), - span = item.select('span'), - placement = (i < nodes.length / 2) ? 'bottom' : 'top', - description = d.description(), - isOverflowing = (span.property('clientWidth') !== span.property('scrollWidth')); + // add minimap toggle below list + var minimapEnter = selection.selectAll('.minimap-toggle-list') + .data([0]) + .enter() + .append('ul') + .attr('class', 'layer-list minimap-toggle-list') + .append('li') + .attr('class', 'layer minimap-toggle-item'); - if (d === previous) { - item.call(tooltip() - .placement(placement) - .html(true) - .title(function() { - var tip = '
' + t('background.switch') + '
'; - return uiTooltipHtml(tip, uiCmd('⌘' + key)); - }) - ); - } else if (description || isOverflowing) { - item.call(tooltip() - .placement(placement) - .title(description || d.name()) - ); - } else { - item.call(tooltip().destroy); - } - }); - } - - - function selectLayer() { - function active(d) { - return context.background().showsLayer(d); - } - - content.selectAll('.layer') - .classed('active', active) - .classed('switch', function(d) { return d === previous; }) - .call(setTooltips) - .selectAll('input') - .property('checked', active); - } - - - function clickSetSource(d) { - if (d.id === 'custom' && !d.template()) { - return editCustom(); - } - - d3_event.preventDefault(); - previous = context.background().baseLayerSource(); - context.background().baseLayerSource(d); - selectLayer(); - document.activeElement.blur(); - } - - - function editCustom() { - d3_event.preventDefault(); - var example = 'https://{switch:a,b,c}.tile.openstreetmap.org/{zoom}/{x}/{y}.png'; - var template = window.prompt( - t('background.custom_prompt', { example: example }), - customSource.template() || example + var minimapLabelEnter = minimapEnter + .append('label') + .call(tooltip() + .html(true) + .title(uiTooltipHtml(t('background.minimap.tooltip'), t('background.minimap.key'))) + .placement('top') ); - if (template) { - context.storage('background-custom-template', template); - customSource.template(template); - clickSetSource(customSource); - } else { - selectLayer(); - } - } - - - 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(); }); - - layerLinks.exit() - .remove(); - - var enter = layerLinks.enter() - .append('li') - .attr('class', 'layer') - .classed('layer-custom', function(d) { return d.id === 'custom'; }) - .classed('best', function(d) { return d.best(); }); - - enter.filter(function(d) { return d.id === 'custom'; }) - .append('button') - .attr('class', 'layer-browse') - .call(tooltip() - .title(t('background.custom_button')) - .placement((textDirection === 'rtl') ? 'right' : 'left')) - .on('click', editCustom) - .call(svgIcon('#icon-search')); - - enter.filter(function(d) { return d.best(); }) - .append('div') - .attr('class', 'best') - .call(tooltip() - .title(t('background.best_imagery')) - .placement((textDirection === 'rtl') ? 'right' : '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(); }); - - - 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.isHidden() && !d.overlay; }); - overlayList.call(drawList, 'checkbox', clickSetOverlay, function(d) { return !d.isHidden() && d.overlay; }); - - selectLayer(); - updateOffsetVal(); - } - - - function updateOffsetVal() { - var meters = geoOffsetToMeters(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() { - if (d3_event.button !== 0) return; - context.background().offset([0, 0]); - updateOffsetVal(); - } - - - function nudge(d) { - context.background().nudge(d, context.map().zoom()); - updateOffsetVal(); - } - - - function buttonOffset(d) { - if (d3_event.button !== 0) return; - var timeout = window.setTimeout(function() { - interval = window.setInterval(nudge.bind(null, d), 100); - }, 500), - interval; - - function doneNudge() { - window.clearTimeout(timeout); - window.clearInterval(interval); - d3_select(window) - .on('mouseup.buttonoffset', null, true) - .on('mousedown.buttonoffset', null, true); - } - - d3_select(window) - .on('mouseup.buttonoffset', doneNudge, true) - .on('mousedown.buttonoffset', doneNudge, true); - - nudge(d); - } - - - function inputOffset() { - if (d3_event.button !== 0) return; - 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; + minimapLabelEnter + .append('input') + .attr('type', 'checkbox') + .on('change', function() { + d3_event.preventDefault(); + uiMapInMap.toggle(); }); - if (d.length !== 2 || !d[0] || !d[1]) { - input.classed('error', true); - return; - } - - context.background().offset(geoMetersToOffset(d)); - updateOffsetVal(); - } + minimapLabelEnter + .append('span') + .text(t('background.minimap.description')); - function dragOffset() { - if (d3_event.button !== 0) return; - 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() { - if (d3_event.button !== 0) return; - 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(); - } - tooltipBehavior.hide(button); - setVisible(!button.classed('active')); - } - - - function quickSwitch() { - if (d3_event) { - d3_event.stopImmediatePropagation(); - d3_event.preventDefault(); - } - if (previous) { - clickSetSource(previous); - } - } - - - function setVisible(show) { - if (show !== shown) { - button.classed('active', show); - shown = show; - - if (show) { - selection - .on('mousedown.background-inside', function() { - d3_event.stopPropagation(); - }); - - content - .style('display', 'block') - .style('right', '-300px') - .transition() - .duration(200) - .style('right', '0px'); - - content.selectAll('.layer') - .call(setTooltips); - - } else { - content - .style('display', 'block') - .style('right', '0px') - .transition() - .duration(200) - .style('right', '-300px') - .on('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'), - tooltipBehavior = tooltip() - .placement((textDirection === 'rtl') ? 'right' : 'left') - .html(true) - .title(uiTooltipHtml(t('background.description'), key)), - button = selection - .append('button') - .attr('tabindex', -1) - .on('click', toggle) - .call(svgIcon('#icon-layers', 'light')) - .call(tooltipBehavior), - shown = false; - - - /* opacity switcher */ - - var opawrap = content - .append('div') - .attr('class', 'opacity-options-wrapper'); - - opawrap - .append('h4') - .text(t('background.title')); - - var opacityList = opawrap - .append('ul') - .attr('class', 'opacity-options'); - - opacityList.selectAll('div.opacity') - .data(opacities) + // "Info / Report a Problem" link + selection.selectAll('.imagery-faq') + .data([0]) .enter() - .append('li') - .attr('data-original-title', function(d) { - return t('background.percent_brightness', { opacity: (d * 100) }); - }) - .on('click.set-opacity', setOpacity) - .html('
') - .call(tooltip() - .placement((textDirection === 'rtl') ? 'right' : 'left')) - .append('div') - .attr('class', 'opacity') - .style('opacity', function(d) { return 1.25 - d; }); - - - /* background list */ - - var backgroundList = content - .append('ul') - .attr('class', 'layer-list') - .attr('dir', 'auto'); - - content .append('div') .attr('class', 'imagery-faq') .append('a') @@ -449,108 +247,141 @@ export function uiBackground(context) { .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')); + } - /* overlay list */ + function renderOverlayList(selection) { + var container = selection.selectAll('.layer-overlay-list') + .data([0]); - var overlayList = content + _overlayList = container.enter() .append('ul') - .attr('class', 'layer-list'); + .attr('class', 'layer-list layer-overlay-list') + .attr('dir', 'auto') + .merge(container); + } - var controls = content + + function update() { + _backgroundList + .call(drawListItems, 'radio', chooseBackground, function(d) { return !d.isHidden() && !d.overlay; }); + + _overlayList + .call(drawListItems, 'checkbox', chooseOverlay, function(d) { return !d.isHidden() && d.overlay; }); + + _displayOptionsContainer + .call(backgroundDisplayOptions); + + _offsetContainer + .call(backgroundOffset); + } + + + function quickSwitch() { + if (d3_event) { + d3_event.stopImmediatePropagation(); + d3_event.preventDefault(); + } + if (_previousBackground) { + chooseBackground(_previousBackground); + } + } + + + function background(selection) { + + function hidePane() { + setVisible(false); + } + + function togglePane() { + if (d3_event) d3_event.preventDefault(); + paneTooltip.hide(button); + setVisible(!button.classed('active')); + } + + function setVisible(show) { + if (show !== _shown) { + button.classed('active', show); + _shown = show; + + if (show) { + uiMapData.hidePane(); + uiHelp.hidePane(); + update(); + + pane + .style('display', 'block') + .style('right', '-300px') + .transition() + .duration(200) + .style('right', '0px'); + + } else { + pane + .style('display', 'block') + .style('right', '0px') + .transition() + .duration(200) + .style('right', '-300px') + .on('end', function() { + d3_select(this).style('display', 'none'); + }); + } + } + } + + + var pane = selection .append('div') - .attr('class', 'controls-list'); + .attr('class', 'fillL map-overlay col3 content hide'); + var paneTooltip = tooltip() + .placement((textDirection === 'rtl') ? 'right' : 'left') + .html(true) + .title(uiTooltipHtml(t('background.description'), key)); - /* minimap toggle */ + var button = selection + .append('button') + .attr('tabindex', -1) + .on('click', togglePane) + .call(svgIcon('#icon-layers', 'light')) + .call(paneTooltip); - var minimapLabel = controls - .append('label') - .call(tooltip() - .html(true) - .title(uiTooltipHtml(t('background.minimap.tooltip'), t('background.minimap.key'))) - .placement('top') + pane + .append('h2') + .text(t('background.title')); + + // background list + pane + .append('div') + .attr('class', 'background-background-list-container') + .call(uiDisclosure(context, 'background_list', true) + .title(t('background.backgrounds')) + .content(renderBackgroundList) ); - minimapLabel - .classed('minimap-toggle', true) - .append('input') - .attr('type', 'checkbox') - .on('change', function() { - uiMapInMap.toggle(); - d3_event.preventDefault(); - }); - - minimapLabel - .append('span') - .text(t('background.minimap.description')); - - - /* imagery offset controls */ - - var adjustments = content + // overlay list + pane .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() { - if (d3_event.button !== 0) return; - 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() { - if (d3_event.button !== 0) return; - 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) { - if (d3_event.button !== 0) return; - buttonOffset(d[1]); - }); - - nudgeContainer - .append('button') - .attr('title', t('background.reset')) - .attr('class', 'nudge-reset disabled') - .on('click', resetOffset) - .call( - (textDirection === 'rtl') ? svgIcon('#icon-redo') : svgIcon('#icon-undo') + .attr('class', 'background-overlay-list-container') + .call(uiDisclosure(context, 'overlay_list', true) + .title(t('background.overlays')) + .content(renderOverlayList) ); + // display options + _displayOptionsContainer = pane + .append('div') + .attr('class', 'background-display-options'); + + // offset controls + _offsetContainer = pane + .append('div') + .attr('class', 'background-offset'); + + + // add listeners context.map() .on('move.background-update', _debounce(utilCallWhenIdle(update), 1000)); @@ -559,15 +390,18 @@ export function uiBackground(context) { update(); - setOpacity(opacityDefault); var keybinding = d3_keybinding('background') - .on(key, toggle) + .on(key, togglePane) .on(uiCmd('⌘' + key), quickSwitch) - .on([t('map_data.key'), t('help.key')], hide); + .on([t('map_data.key'), t('help.key')], hidePane); d3_select(document) .call(keybinding); + + uiBackground.hidePane = hidePane; + uiBackground.togglePane = togglePane; + uiBackground.setVisible = setVisible; } return background; diff --git a/modules/ui/background_display_options.js b/modules/ui/background_display_options.js new file mode 100644 index 000000000..9dfbfc80c --- /dev/null +++ b/modules/ui/background_display_options.js @@ -0,0 +1,125 @@ +import { + event as d3_event, + select as d3_select +} from 'd3-selection'; + + +import { t, textDirection } from '../util/locale'; +import { svgIcon } from '../svg'; +import { uiDisclosure } from './disclosure'; + + +export function uiBackgroundDisplayOptions(context) { + var _selection = d3_select(null); + var sliders = ['brightness', 'contrast', 'saturation', 'sharpness']; + var storedOpacity = context.storage('background-opacity'); + + var _options = { + brightness: (storedOpacity !== null ? (+storedOpacity) : 1), + contrast: 1, + saturation: 1, + sharpness: 1 + }; + + + function clamp(x, min, max) { + return Math.max(min, Math.min(x, max)); + } + + + function updateValue(d, val) { + if (!val && d3_event && d3_event.target) { + val = d3_event.target.value; + } + + val = clamp(val, 0.25, 2); + + _options[d] = val; + context.background()[d](val); + + if (d === 'brightness') { + context.storage('background-opacity', val); + } + + _selection + .call(render); + } + + + function render(selection) { + var container = selection.selectAll('.display-options-container') + .data([0]); + + var containerEnter = container.enter() + .append('div') + .attr('class', 'display-options-container controls-list'); + + // add slider controls + var slidersEnter = containerEnter.selectAll('.display-control') + .data(sliders) + .enter() + .append('div') + .attr('class', function(d) { return 'display-control display-control-' + d; }); + + slidersEnter + .append('h5') + .text(function(d) { return t('background.' + d); }) + .append('span') + .attr('class', function(d) { return 'display-option-value display-option-value-' + d; }); + + slidersEnter + .append('input') + .attr('class', function(d) { return 'display-option-input display-option-input-' + d; }) + .attr('type', 'range') + .attr('min', '0.25') + .attr('max', '2') + .attr('step', '0.05') + .on('input', function(d) { + var val = d3_select(this).property('value'); + updateValue(d, val); + }); + + slidersEnter + .append('button') + .attr('title', t('background.reset')) + .attr('class', function(d) { return 'display-option-reset display-option-reset-' + d; }) + .on('click', function(d) { + if (d3_event.button !== 0) return; + updateValue(d, 1); + }) + .call(svgIcon('#icon-' + (textDirection === 'rtl' ? 'redo' : 'undo'))); + + + // update + container = containerEnter + .merge(container); + + container.selectAll('.display-option-input') + .property('value', function(d) { return _options[d]; }); + + container.selectAll('.display-option-value') + .text(function(d) { return Math.floor(_options[d] * 100) + '%'; }); + + container.selectAll('.display-option-reset') + .classed('disabled', function(d) { return _options[d] === 1; }); + + // first time only, set brightness if needed + if (containerEnter.size() && _options.brightness !== 1) { + context.background().brightness(_options.brightness); + } + } + + + function backgroundDisplayOptions(selection) { + _selection = selection; + + selection + .call(uiDisclosure(context, 'background_display_options', true) + .title(t('background.display_options')) + .content(render) + ); + } + + + return backgroundDisplayOptions; +} diff --git a/modules/ui/background_offset.js b/modules/ui/background_offset.js new file mode 100644 index 000000000..295557de2 --- /dev/null +++ b/modules/ui/background_offset.js @@ -0,0 +1,197 @@ +import { + event as d3_event, + select as d3_select, + selectAll as d3_selectAll +} from 'd3-selection'; + +import { t, textDirection } from '../util/locale'; +import { geoMetersToOffset, geoOffsetToMeters } from '../geo'; +import { svgIcon } from '../svg'; +import { uiDisclosure } from './disclosure'; + + +export function uiBackgroundOffset(context) { + var directions = [ + ['right', [0.5, 0]], + ['top', [0, -0.5]], + ['left', [-0.5, 0]], + ['bottom', [0, 0.5]] + ]; + + + function d3_eventCancel() { + d3_event.stopPropagation(); + d3_event.preventDefault(); + } + + + function updateValue() { + var meters = geoOffsetToMeters(context.background().offset()); + var x = +meters[0].toFixed(2); + var 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]); + updateValue(); + } + + + function nudge(d) { + context.background().nudge(d, context.map().zoom()); + updateValue(); + } + + + function clickNudgeButton(d) { + var interval; + var timeout = window.setTimeout(function() { + interval = window.setInterval(nudge.bind(null, d), 100); + }, 500); + + function doneNudge() { + window.clearTimeout(timeout); + window.clearInterval(interval); + d3_select(window) + .on('mouseup.buttonoffset', null, true) + .on('mousedown.buttonoffset', null, true); + } + + d3_select(window) + .on('mouseup.buttonoffset', doneNudge, true) + .on('mousedown.buttonoffset', doneNudge, true); + + 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(geoMetersToOffset(d)); + updateValue(); + } + + + function dragOffset() { + d3_event.preventDefault(); + if (d3_event.button !== 0) return; + + 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() { + if (d3_event.button !== 0) return; + d3_selectAll('.nudge-surface') + .remove(); + + d3_select(window) + .on('mousemove.offset', null) + .on('mouseup.offset', null); + }); + } + + + function render(selection) { + var container = selection.selectAll('.nudge-container') + .data([0]); + + var containerEnter = container.enter() + .append('div') + .attr('class', 'nudge-container cf'); + + containerEnter + .append('div') + .attr('class', 'nudge-instructions') + .text(t('background.offset')); + + var nudgeEnter = containerEnter + .append('div') + .attr('class', 'nudge-outer-rect') + .on('mousedown', dragOffset); + + nudgeEnter + .append('div') + .attr('class', 'nudge-inner-rect') + .append('input') + .on('change', inputOffset); + + containerEnter + .append('div') + .selectAll('button') + .data(directions).enter() + .append('button') + .attr('class', function(d) { return d[0] + ' nudge'; }) + .on('contextmenu', d3_eventCancel) + .on('mousedown', function(d) { + if (d3_event.button !== 0) return; + clickNudgeButton(d[1]); + }); + + containerEnter + .append('button') + .attr('title', t('background.reset')) + .attr('class', 'nudge-reset disabled') + .on('contextmenu', d3_eventCancel) + .on('click', function() { + if (d3_event.button !== 0) return; + resetOffset(); + }) + .call(svgIcon('#icon-' + (textDirection === 'rtl' ? 'redo' : 'undo'))); + + updateValue(); + } + + + function backgroundOffset(selection) { + selection + .call(uiDisclosure(context, 'background_offset', false) + .title(t('background.fix_misalignment')) + .content(render) + ); + } + + + context.background() + .on('change.backgroundOffset-update', updateValue); + + return backgroundOffset; +} diff --git a/modules/ui/commit.js b/modules/ui/commit.js index 52696ca30..e7d3837f5 100644 --- a/modules/ui/commit.js +++ b/modules/ui/commit.js @@ -269,7 +269,6 @@ export function uiCommit(context) { updateChangeset({ review_requested: (rr ? 'yes' : undefined) }); var expanded = !tagSection.selectAll('a.hide-toggle.expanded').empty(); - tagSection .call(rawTagEditor .expanded(expanded) diff --git a/modules/ui/disclosure.js b/modules/ui/disclosure.js index 82ee9f0b4..6c56e37c6 100644 --- a/modules/ui/disclosure.js +++ b/modules/ui/disclosure.js @@ -1,70 +1,115 @@ import { dispatch as d3_dispatch } from 'd3-dispatch'; +import { event as d3_event } from 'd3-selection'; +import { svgIcon } from '../svg'; import { utilRebind } from '../util/rebind'; import { uiToggle } from './toggle'; +import { textDirection } from '../util/locale'; -export function uiDisclosure() { +export function uiDisclosure(context, key, expandedDefault) { var dispatch = d3_dispatch('toggled'), - title, - expanded = false, - content = function () {}; + _preference = (context.storage('disclosure.' + key + '.expanded')), + _expanded = (_preference === null ? !!expandedDefault : (_preference === 'true')), + _title, + _updatePreference = true, + _content = function () {}; var disclosure = function(selection) { - var hideToggle = selection.selectAll('.hide-toggle') + var hideToggle = selection.selectAll('.hide-toggle-' + key) .data([0]); - hideToggle = hideToggle.enter() + // enter + var hideToggleEnter = hideToggle.enter() .append('a') .attr('href', '#') - .attr('class', 'hide-toggle') + .attr('class', 'hide-toggle hide-toggle-' + key) + .call(svgIcon('', 'pre-text', 'hide-toggle-icon')); + + hideToggleEnter + .append('span') + .attr('class', 'hide-toggle-text'); + + // update + hideToggle = hideToggleEnter .merge(hideToggle); hideToggle - .text(title) .on('click', toggle) - .classed('expanded', expanded); + .classed('expanded', _expanded); + + hideToggle.selectAll('.hide-toggle-text') + .text(_title); + + hideToggle.selectAll('.hide-toggle-icon') + .attr('xlink:href', _expanded ? '#icon-down' + : (textDirection === 'rtl') ? '#icon-backward' : '#icon-forward' + ); - var wrap = selection.selectAll('div') + var wrap = selection.selectAll('.disclosure-wrap') .data([0]); wrap = wrap.enter() .append('div') + .attr('class', 'disclosure-wrap disclosure-wrap-' + key) .merge(wrap); wrap - .classed('hide', !expanded) - .call(content); + .classed('hide', !_expanded) + .call(_content); function toggle() { - expanded = !expanded; - hideToggle.classed('expanded', expanded); - wrap.call(uiToggle(expanded)); - dispatch.call('toggled', this, expanded); + d3_event.preventDefault(); + + _expanded = !_expanded; + + if (_updatePreference) { + context.storage('disclosure.' + key + '.expanded', _expanded); + } + + hideToggle + .classed('expanded', _expanded); + + hideToggle.selectAll('.hide-toggle-icon') + .attr('xlink:href', _expanded ? '#icon-down' + : (textDirection === 'rtl') ? '#icon-backward' : '#icon-forward' + ); + + wrap + .call(uiToggle(_expanded)); + + dispatch.call('toggled', this, _expanded); } }; disclosure.title = function(_) { - if (!arguments.length) return title; - title = _; + if (!arguments.length) return _title; + _title = _; return disclosure; }; disclosure.expanded = function(_) { - if (!arguments.length) return expanded; - expanded = _; + if (!arguments.length) return _expanded; + _expanded = _; + return disclosure; + }; + + + disclosure.updatePreference = function(_) { + if (!arguments.length) return _updatePreference; + _updatePreference = _; return disclosure; }; disclosure.content = function(_) { - if (!arguments.length) return content; - content = _; + if (!arguments.length) return _content; + _content = _; return disclosure; }; diff --git a/modules/ui/help.js b/modules/ui/help.js index 6b1e6b435..01196b3a7 100644 --- a/modules/ui/help.js +++ b/modules/ui/help.js @@ -9,7 +9,9 @@ import marked from 'marked'; import { t, textDirection } from '../util/locale'; import { svgIcon } from '../svg'; import { uiCmd } from './cmd'; +import { uiBackground } from './background'; import { uiIntro } from './intro'; +import { uiMapData } from './map_data'; import { uiShortcuts } from './shortcuts'; import { uiTooltipHtml } from './tooltipHtml'; import { tooltip } from '../util/tooltip'; @@ -260,12 +262,12 @@ export function uiHelp(context) { function help(selection) { - function hide() { + function hidePane() { setVisible(false); } - function toggle() { + function togglePane() { if (d3_event) d3_event.preventDefault(); tooltipBehavior.hide(button); setVisible(!button.classed('active')); @@ -278,14 +280,15 @@ export function uiHelp(context) { shown = show; if (show) { - selection.on('mousedown.help-inside', function() { - return d3_event.stopPropagation(); - }); + uiBackground.hidePane(); + uiMapData.hidePane(); + pane.style('display', 'block') .style('right', '-500px') .transition() .duration(200) .style('right', '0px'); + } else { pane.style('right', '0px') .transition() @@ -294,7 +297,6 @@ export function uiHelp(context) { .on('end', function() { d3_select(this).style('display', 'none'); }); - selection.on('mousedown.help-inside', null); } } } @@ -375,13 +377,14 @@ export function uiHelp(context) { .title(uiTooltipHtml(t('help.title'), key)), button = selection.append('button') .attr('tabindex', -1) - .on('click', toggle) + .on('click', togglePane) .call(svgIcon('#icon-help', 'light')) .call(tooltipBehavior), shown = false; - var toc = pane.append('ul') + var toc = pane + .append('ul') .attr('class', 'toc'); var menuItems = toc.selectAll('li') @@ -424,26 +427,34 @@ export function uiHelp(context) { .text(t('splash.walkthrough')); - var content = pane.append('div') + var content = pane + .append('div') .attr('class', 'left-content'); - var doctitle = content.append('h2') + var doctitle = content + .append('h2') .text(t('help.title')); - var body = content.append('div') + var body = content + .append('div') .attr('class', 'body'); - var nav = content.append('div') + var nav = content + .append('div') .attr('class', 'nav'); clickHelp(docs[0], 0); var keybinding = d3_keybinding('help') - .on(key, toggle) - .on([t('background.key'), t('map_data.key')], hide); + .on(key, togglePane) + .on([t('background.key'), t('map_data.key')], hidePane); d3_select(document) .call(keybinding); + + uiHelp.hidePane = hidePane; + uiHelp.togglePane = togglePane; + uiHelp.setVisible = setVisible; } return help; diff --git a/modules/ui/index.js b/modules/ui/index.js index 742aa7b1c..8a3dd80c6 100644 --- a/modules/ui/index.js +++ b/modules/ui/index.js @@ -2,6 +2,8 @@ export { uiInit } from './init'; export { uiAccount } from './account'; export { uiAttribution } from './attribution'; export { uiBackground } from './background'; +export { uiBackgroundDisplayOptions } from './background_display_options'; +export { uiBackgroundOffset } from './background_offset'; export { uiChangesetEditor } from './changeset_editor'; export { uiCmd } from './cmd'; export { uiCommit } from './commit'; diff --git a/modules/ui/map_data.js b/modules/ui/map_data.js index fb522c098..aef91c01b 100644 --- a/modules/ui/map_data.js +++ b/modules/ui/map_data.js @@ -5,380 +5,423 @@ import { import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js'; -import { t, textDirection } from '../util/locale'; import { svgIcon } from '../svg'; -import { uiTooltipHtml } from './tooltipHtml'; +import { t, textDirection } from '../util/locale'; import { tooltip } from '../util/tooltip'; +import { uiBackground } from './background'; +import { uiDisclosure } from './disclosure'; +import { uiHelp } from './help'; +import { uiTooltipHtml } from './tooltipHtml'; export function uiMapData(context) { - var key = t('map_data.key'), - features = context.features().keys(), - layers = context.layers(), - fills = ['wireframe', 'partial', 'full'], - fillDefault = context.storage('area-fill') || 'partial', - fillSelected = fillDefault; + var key = t('map_data.key'); + var features = context.features().keys(); + var layers = context.layers(); + var fills = ['wireframe', 'partial', 'full']; + + var _fillDefault = context.storage('area-fill') || 'partial'; + var _fillSelected = _fillDefault; + var _shown = false; + var _dataLayerContainer = d3_select(null); + var _fillList = d3_select(null); + var _featureList = d3_select(null); - function map_data(selection) { + function showsFeature(d) { + return context.features().enabled(d); + } - 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) { + fills.forEach(function(opt) { + context.surface().classed('fill-' + opt, Boolean(opt === d)); + }); + + _fillSelected = d; + if (d !== 'wireframe') { + _fillDefault = d; + context.storage('area-fill', d); } + update(); + } - function autoHiddenFeature(d) { - return context.features().autoHidden(d); + function showsLayer(which) { + var layer = layers.layer(which); + if (layer) { + return layer.enabled(); } + return false; + } - function clickFeature(d) { - context.features().toggle(d); + function setLayer(which, enabled) { + var layer = layers.layer(which); + if (layer) { + layer.enabled(enabled); update(); } + } - function showsFill(d) { - return fillSelected === d; + function toggleLayer(which) { + setLayer(which, !showsLayer(which)); + } + + + function drawPhotoItems(selection) { + var photoKeys = ['mapillary-images', 'mapillary-signs', 'openstreetcam-images']; + var photoLayers = layers.all().filter(function(obj) { return photoKeys.indexOf(obj.id) !== -1; }); + var data = photoLayers.filter(function(obj) { return obj.layer.supported(); }); + + function layerSupported(d) { + return d.layer && d.layer.supported(); + } + function layerEnabled(d) { + return layerSupported(d) && d.layer.enabled(); } + var ul = selection + .selectAll('.layer-list-photos') + .data([0]); - function setFill(d) { - fills.forEach(function(opt) { - context.surface().classed('fill-' + opt, Boolean(opt === d)); + ul = ul.enter() + .append('ul') + .attr('class', 'layer-list layer-list-photos') + .merge(ul); + + var li = ul.selectAll('.list-item-photos') + .data(data); + + li.exit() + .remove(); + + var liEnter = li.enter() + .append('li') + .attr('class', function(d) { return 'list-item-photos list-item-' + d.id; }); + + var labelEnter = liEnter + .append('label') + .each(function(d) { + d3_select(this) + .call(tooltip() + .title(t(d.id.replace('-', '_') + '.tooltip')) + .placement('top') + ); }); - fillSelected = d; - if (d !== 'wireframe') { - fillDefault = d; - context.storage('area-fill', d); - } - update(); - } + labelEnter + .append('input') + .attr('type', 'checkbox') + .on('change', function(d) { toggleLayer(d.id); }); + + labelEnter + .append('span') + .text(function(d) { return t(d.id.replace('-', '_') + '.title'); }); - function showsLayer(which) { - var layer = layers.layer(which); - if (layer) { - return layer.enabled(); - } - return false; - } + // Update + li = li + .merge(liEnter); + + li + .classed('active', layerEnabled) + .selectAll('input') + .property('checked', layerEnabled); + } - function setLayer(which, enabled) { - var layer = layers.layer(which); - if (layer) { - layer.enabled(enabled); - update(); - } - } + function drawOsmItem(selection) { + var osm = layers.layer('osm'), + showsOsm = osm.enabled(); + + var ul = selection + .selectAll('.layer-list-osm') + .data(osm ? [0] : []); + + // Exit + ul.exit() + .remove(); + + // Enter + var ulEnter = ul.enter() + .append('ul') + .attr('class', 'layer-list layer-list-osm'); + + var liEnter = ulEnter + .append('li') + .attr('class', 'list-item-osm'); + + var labelEnter = liEnter + .append('label') + .call(tooltip() + .title(t('map_data.layers.osm.tooltip')) + .placement('top') + ); + + labelEnter + .append('input') + .attr('type', 'checkbox') + .on('change', function() { toggleLayer('osm'); }); + + labelEnter + .append('span') + .text(t('map_data.layers.osm.title')); + + // Update + ul = ul + .merge(ulEnter); + + ul.selectAll('.list-item-osm') + .classed('active', showsOsm) + .selectAll('input') + .property('checked', showsOsm); + } - function toggleLayer(which) { - setLayer(which, !showsLayer(which)); - } + function drawGpxItem(selection) { + var gpx = layers.layer('gpx'), + hasGpx = gpx && gpx.hasGpx(), + showsGpx = hasGpx && gpx.enabled(); + var ul = selection + .selectAll('.layer-list-gpx') + .data(gpx ? [0] : []); - function drawPhotoItems(selection) { - var photoKeys = ['mapillary-images', 'mapillary-signs', 'openstreetcam-images']; - var photoLayers = layers.all().filter(function(obj) { return photoKeys.indexOf(obj.id) !== -1; }); - var data = photoLayers.filter(function(obj) { return obj.layer.supported(); }); + // Exit + ul.exit() + .remove(); - function layerSupported(d) { - return d.layer && d.layer.supported(); - } - function layerEnabled(d) { - return layerSupported(d) && d.layer.enabled(); - } + // Enter + var ulEnter = ul.enter() + .append('ul') + .attr('class', 'layer-list layer-list-gpx'); - var ul = selection - .selectAll('.layer-list-photos') - .data([0]); + var liEnter = ulEnter + .append('li') + .attr('class', 'list-item-gpx'); - ul = ul.enter() - .append('ul') - .attr('class', 'layer-list layer-list-photos') - .merge(ul); + liEnter + .append('button') + .attr('class', 'list-item-gpx-extent') + .call(tooltip() + .title(t('gpx.zoom')) + .placement((textDirection === 'rtl') ? 'right' : 'left') + ) + .on('click', function() { + d3_event.preventDefault(); + d3_event.stopPropagation(); + gpx.fitZoom(); + }) + .call(svgIcon('#icon-search')); - var li = ul.selectAll('.list-item-photos') - .data(data); - - li.exit() - .remove(); - - var liEnter = li.enter() - .append('li') - .attr('class', function(d) { return 'list-item-photos list-item-' + d.id; }); - - var labelEnter = liEnter - .append('label') - .each(function(d) { - d3_select(this) - .call(tooltip() - .title(t(d.id.replace('-', '_') + '.tooltip')) - .placement('top') - ); - }); - - labelEnter - .append('input') - .attr('type', 'checkbox') - .on('change', function(d) { toggleLayer(d.id); }); - - labelEnter - .append('span') - .text(function(d) { return t(d.id.replace('-', '_') + '.title'); }); - - - // Update - li = li - .merge(liEnter); - - li - .classed('active', layerEnabled) - .selectAll('input') - .property('checked', layerEnabled); - } - - - function drawOsmItem(selection) { - var osm = layers.layer('osm'), - showsOsm = osm.enabled(); - - var ul = selection - .selectAll('.layer-list-osm') - .data(osm ? [0] : []); - - // Exit - ul.exit() - .remove(); - - // Enter - var ulEnter = ul.enter() - .append('ul') - .attr('class', 'layer-list layer-list-osm'); - - var liEnter = ulEnter - .append('li') - .attr('class', 'list-item-osm'); - - var labelEnter = liEnter - .append('label') - .call(tooltip() - .title(t('map_data.layers.osm.tooltip')) - .placement('top') - ); - - labelEnter - .append('input') - .attr('type', 'checkbox') - .on('change', function() { toggleLayer('osm'); }); - - labelEnter - .append('span') - .text(t('map_data.layers.osm.title')); - - // Update - ul = ul - .merge(ulEnter); - - ul.selectAll('.list-item-osm') - .classed('active', showsOsm) - .selectAll('input') - .property('checked', showsOsm); - } - - - function drawGpxItem(selection) { - var gpx = layers.layer('gpx'), - hasGpx = gpx && gpx.hasGpx(), - showsGpx = hasGpx && gpx.enabled(); - - var ul = selection - .selectAll('.layer-list-gpx') - .data(gpx ? [0] : []); - - // Exit - ul.exit() - .remove(); - - // Enter - var ulEnter = ul.enter() - .append('ul') - .attr('class', 'layer-list layer-list-gpx'); - - var liEnter = ulEnter - .append('li') - .attr('class', 'list-item-gpx'); - - liEnter - .append('button') - .attr('class', 'list-item-gpx-extent') - .call(tooltip() - .title(t('gpx.zoom')) - .placement((textDirection === 'rtl') ? 'right' : 'left')) - .on('click', function() { - d3_event.preventDefault(); - d3_event.stopPropagation(); - gpx.fitZoom(); - }) - .call(svgIcon('#icon-search')); - - liEnter - .append('button') - .attr('class', 'list-item-gpx-browse') - .call(tooltip() - .title(t('gpx.browse')) - .placement((textDirection === 'rtl') ? 'right' : 'left') - ) - .on('click', function() { - d3_select(document.createElement('input')) - .attr('type', 'file') - .on('change', function() { - gpx.files(d3_event.target.files); - }) - .node().click(); - }) - .call(svgIcon('#icon-geolocate')); - - var labelEnter = liEnter - .append('label') - .call(tooltip() - .title(t('gpx.drag_drop')) - .placement('top') - ); - - labelEnter - .append('input') - .attr('type', 'checkbox') - .on('change', function() { toggleLayer('gpx'); }); - - labelEnter - .append('span') - .text(t('gpx.local_layer')); - - // Update - ul = ul - .merge(ulEnter); - - ul.selectAll('.list-item-gpx') - .classed('active', showsGpx) - .selectAll('label') - .classed('deemphasize', !hasGpx) - .selectAll('input') - .property('disabled', !hasGpx) - .property('checked', showsGpx); - } - - - function drawList(selection, data, type, name, change, active) { - var items = selection.selectAll('li') - .data(data); - - // Exit - items.exit() - .remove(); - - // Enter - var enter = items.enter() - .append('li') - .attr('class', 'layer') - .call(tooltip() - .html(true) - .title(function(d) { - var tip = t(name + '.' + d + '.tooltip'), - key = (d === 'wireframe' ? t('area_fill.wireframe.key') : null); - - if (name === 'feature' && autoHiddenFeature(d)) { - var msg = showsLayer('osm') ? t('map_data.autohidden') : t('map_data.osmhidden'); - tip += '
' + msg + '
'; - } - return uiTooltipHtml(tip, key); + liEnter + .append('button') + .attr('class', 'list-item-gpx-browse') + .call(tooltip() + .title(t('gpx.browse')) + .placement((textDirection === 'rtl') ? 'right' : 'left') + ) + .on('click', function() { + d3_select(document.createElement('input')) + .attr('type', 'file') + .on('change', function() { + gpx.files(d3_event.target.files); }) - .placement('top') - ); + .node().click(); + }) + .call(svgIcon('#icon-geolocate')); - var label = enter - .append('label'); + var labelEnter = liEnter + .append('label') + .call(tooltip() + .title(t('gpx.drag_drop')) + .placement('top') + ); - label - .append('input') - .attr('type', type) - .attr('name', name) - .on('change', change); + labelEnter + .append('input') + .attr('type', 'checkbox') + .on('change', function() { toggleLayer('gpx'); }); - label - .append('span') - .text(function(d) { return t(name + '.' + d + '.description'); }); + labelEnter + .append('span') + .text(t('gpx.local_layer')); - // Update - items = items - .merge(enter); + // Update + ul = ul + .merge(ulEnter); - items - .classed('active', active) - .selectAll('input') - .property('checked', active) - .property('indeterminate', function(d) { - return (name === 'feature' && autoHiddenFeature(d)); - }); + ul.selectAll('.list-item-gpx') + .classed('active', showsGpx) + .selectAll('label') + .classed('deemphasize', !hasGpx) + .selectAll('input') + .property('disabled', !hasGpx) + .property('checked', showsGpx); + } + + + function drawListItems(selection, data, type, name, change, active) { + var items = selection.selectAll('li') + .data(data); + + // Exit + items.exit() + .remove(); + + // Enter + var enter = items.enter() + .append('li') + .attr('class', 'layer') + .call(tooltip() + .html(true) + .title(function(d) { + var tip = t(name + '.' + d + '.tooltip'), + key = (d === 'wireframe' ? t('area_fill.wireframe.key') : null); + + if (name === 'feature' && autoHiddenFeature(d)) { + var msg = showsLayer('osm') ? t('map_data.autohidden') : t('map_data.osmhidden'); + tip += '
' + msg + '
'; + } + return uiTooltipHtml(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 = items + .merge(enter); + + items + .classed('active', active) + .selectAll('input') + .property('checked', active) + .property('indeterminate', function(d) { + return (name === 'feature' && autoHiddenFeature(d)); + }); + } + + + function renderDataLayers(selection) { + var container = selection.selectAll('data-layer-container') + .data([0]); + + _dataLayerContainer = container.enter() + .append('div') + .attr('class', 'data-layer-container') + .merge(container); + } + + + function renderFillList(selection) { + var container = selection.selectAll('layer-fill-list') + .data([0]); + + _fillList = container.enter() + .append('ul') + .attr('class', 'layer-list layer-fill-list') + .merge(container); + } + + + function renderFeatureList(selection) { + var container = selection.selectAll('layer-feature-list') + .data([0]); + + _featureList = container.enter() + .append('ul') + .attr('class', 'layer-list layer-feature-list') + .merge(container); + } + + + function update() { + _dataLayerContainer + .call(drawOsmItem) + .call(drawPhotoItems) + .call(drawGpxItem); + + _fillList + .call(drawListItems, fills, 'radio', 'area_fill', setFill, showsFill); + + _featureList + .call(drawListItems, features, 'checkbox', 'feature', clickFeature, showsFeature); + } + + + 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 update() { - dataLayerContainer - .call(drawOsmItem) - .call(drawPhotoItems) - .call(drawGpxItem); + function mapData(selection) { - fillList - .call(drawList, fills, 'radio', 'area_fill', setFill, showsFill); - - featureList - .call(drawList, features, 'checkbox', 'feature', clickFeature, showsFeature); - } - - - function hidePanel() { + function hidePane() { setVisible(false); } - - function togglePanel() { + function togglePane() { if (d3_event) d3_event.preventDefault(); - tooltipBehavior.hide(button); + paneTooltip.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) { + if (show !== _shown) { button.classed('active', show); - shown = show; + _shown = show; if (show) { + uiBackground.hidePane(); + uiHelp.hidePane(); update(); - selection.on('mousedown.map_data-inside', function() { - return d3_event.stopPropagation(); - }); - content.style('display', 'block') + + pane + .style('display', 'block') .style('right', '-300px') .transition() .duration(200) .style('right', '0px'); + } else { - content.style('display', 'block') + pane + .style('display', 'block') .style('right', '0px') .transition() .duration(200) @@ -386,113 +429,80 @@ export function uiMapData(context) { .on('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'), - tooltipBehavior = tooltip() - .placement((textDirection === 'rtl') ? 'right' : 'left') - .html(true) - .title(uiTooltipHtml(t('map_data.description'), key)), - button = selection - .append('button') - .attr('tabindex', -1) - .on('click', togglePanel) - .call(svgIcon('#icon-data', 'light')) - .call(tooltipBehavior), - shown = false; + var pane = selection + .append('div') + .attr('class', 'fillL map-overlay col3 content hide'); - content - .append('h4') + var paneTooltip = tooltip() + .placement((textDirection === 'rtl') ? 'right' : 'left') + .html(true) + .title(uiTooltipHtml(t('map_data.description'), key)); + + var button = selection + .append('button') + .attr('tabindex', -1) + .on('click', togglePane) + .call(svgIcon('#icon-data', 'light')) + .call(paneTooltip); + + + pane + .append('h2') .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 + pane .append('div') - .attr('class', 'data-data-layers') - .style('display', 'block'); - + .attr('class', 'map-data-data-layers') + .call(uiDisclosure(context, 'data_layers', true) + .title(t('map_data.data_layers')) + .content(renderDataLayers) + ); // 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 + pane .append('div') - .attr('class', 'data-area-fills') - .style('display', 'none'); - - var fillList = fillContainer - .append('ul') - .attr('class', 'layer-list layer-fill-list'); - + .attr('class', 'map-data-area-fills') + .call(uiDisclosure(context, 'fill_area', false) + .title(t('map_data.fill_area')) + .content(renderFillList) + ); // 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 + pane .append('div') - .attr('class', 'data-feature-filters') - .style('display', 'none'); - - var featureList = featureContainer - .append('ul') - .attr('class', 'layer-list layer-feature-list'); + .attr('class', 'map-data-feature-filters') + .call(uiDisclosure(context, 'map_features', false) + .title(t('map_data.map_features')) + .content(renderFeatureList) + ); + // add listeners context.features() .on('change.map_data-update', update); - setFill(fillDefault); + update(); + setFill(_fillDefault); var keybinding = d3_keybinding('features') - .on(key, togglePanel) + .on(key, togglePane) .on(t('area_fill.wireframe.key'), toggleWireframe) - .on([t('background.key'), t('help.key')], hidePanel); + .on([t('background.key'), t('help.key')], hidePane); d3_select(document) .call(keybinding); + + uiMapData.hidePane = hidePane; + uiMapData.togglePane = togglePane; + uiMapData.setVisible = setVisible; } - return map_data; + return mapData; } diff --git a/modules/ui/map_in_map.js b/modules/ui/map_in_map.js index 08ca8b44d..3e056e4b0 100644 --- a/modules/ui/map_in_map.js +++ b/modules/ui/map_in_map.js @@ -284,9 +284,10 @@ export function uiMapInMap(context) { isHidden = !isHidden; - var label = d3_select('.minimap-toggle'); - label.classed('active', !isHidden) - .select('input').property('checked', !isHidden); + d3_select('.minimap-toggle-item') + .classed('active', !isHidden) + .select('input') + .property('checked', !isHidden); if (isHidden) { wrap diff --git a/modules/ui/preset_editor.js b/modules/ui/preset_editor.js index 9944c8c97..602278507 100644 --- a/modules/ui/preset_editor.js +++ b/modules/ui/preset_editor.js @@ -16,7 +16,6 @@ import { utilRebind } from '../util'; export function uiPresetEditor(context) { var dispatch = d3_dispatch('change'), formFields = uiFormFields(context), - expandedPreference = (context.storage('preset_fields.expanded') !== 'false'), state, fieldsArr, preset, @@ -25,17 +24,10 @@ export function uiPresetEditor(context) { function presetEditor(selection) { - selection.call(uiDisclosure() + selection.call(uiDisclosure(context, 'preset_fields', true) .title(t('inspector.all_fields')) - .expanded(expandedPreference) - .on('toggled', toggled) .content(render) ); - - function toggled(expanded) { - expandedPreference = expanded; - context.storage('preset_fields.expanded', expanded); - } } diff --git a/modules/ui/raw_member_editor.js b/modules/ui/raw_member_editor.js index 73c7de159..e8e47b058 100644 --- a/modules/ui/raw_member_editor.js +++ b/modules/ui/raw_member_editor.js @@ -20,8 +20,8 @@ import { export function uiRawMemberEditor(context) { - var id, - taginfo = services.taginfo; + var taginfo = services.taginfo, + _entityID; function selectMember(d) { @@ -53,7 +53,7 @@ export function uiRawMemberEditor(context) { function rawMemberEditor(selection) { - var entity = context.entity(id), + var entity = context.entity(_entityID), memberships = []; entity.members.slice(0, 1000).forEach(function(member, index) { @@ -68,21 +68,17 @@ export function uiRawMemberEditor(context) { }); var gt = entity.members.length > 1000 ? '>' : ''; - selection.call(uiDisclosure() + selection.call(uiDisclosure(context, 'raw_member_editor', true) .title(t('inspector.all_members') + ' (' + gt + memberships.length + ')') .expanded(true) - .on('toggled', toggled) + .updatePreference(false) + .on('toggled', function(expanded) { + if (expanded) { selection.node().parentNode.scrollTop += 200; } + }) .content(content) ); - function toggled(expanded) { - if (expanded) { - selection.node().parentNode.scrollTop += 200; - } - } - - function content(wrap) { var list = wrap.selectAll('.member-list') .data([0]); @@ -201,8 +197,8 @@ export function uiRawMemberEditor(context) { rawMemberEditor.entityID = function(_) { - if (!arguments.length) return id; - id = _; + if (!arguments.length) return _entityID; + _entityID = _; return rawMemberEditor; }; diff --git a/modules/ui/raw_membership_editor.js b/modules/ui/raw_membership_editor.js index 21952ab08..9b67c79d6 100644 --- a/modules/ui/raw_membership_editor.js +++ b/modules/ui/raw_membership_editor.js @@ -28,7 +28,8 @@ import { utilDisplayName, utilNoAuto } from '../util'; export function uiRawMembershipEditor(context) { var taginfo = services.taginfo, - id, showBlank; + _entityID, + _showBlank; function selectRelation(d) { @@ -47,11 +48,13 @@ export function uiRawMembershipEditor(context) { function addMembership(d, role) { - showBlank = false; + _showBlank = false; + + var member = { id: _entityID, type: context.entity(_entityID).type, role: role }; if (d.relation) { context.perform( - actionAddMember(d.relation.id, { id: id, type: context.entity(id).type, role: role }), + actionAddMember(d.relation.id, member), t('operations.add_member.annotation') ); @@ -59,7 +62,7 @@ export function uiRawMembershipEditor(context) { var relation = osmRelation(); context.perform( actionAddEntity(relation), - actionAddMember(relation.id, { id: id, type: context.entity(id).type, role: role }), + actionAddMember(relation.id, member), t('operations.add.annotation.relation') ); @@ -77,15 +80,12 @@ export function uiRawMembershipEditor(context) { function relations(q) { - var newRelation = { - relation: null, - value: t('inspector.new_relation') - }, - result = [], - graph = context.graph(); + var newRelation = { relation: null, value: t('inspector.new_relation') }; + var result = []; + var graph = context.graph(); context.intersects(context.extent()).forEach(function(entity) { - if (entity.type !== 'relation' || entity.id === id) + if (entity.type !== 'relation' || entity.id === _entityID) return; var matched = context.presets().match(entity, graph), @@ -96,10 +96,7 @@ export function uiRawMembershipEditor(context) { if (q && value.toLowerCase().indexOf(q.toLowerCase()) === -1) return; - result.push({ - relation: entity, - value: value - }); + result.push({ relation: entity, value: value }); }); result.sort(function(a, b) { @@ -124,7 +121,7 @@ export function uiRawMembershipEditor(context) { function rawMembershipEditor(selection) { - var entity = context.entity(id), + var entity = context.entity(_entityID), parents = context.graph().parentRelations(entity), memberships = []; @@ -137,21 +134,17 @@ export function uiRawMembershipEditor(context) { }); var gt = parents.length > 1000 ? '>' : ''; - selection.call(uiDisclosure() + selection.call(uiDisclosure(context, 'raw_membership_editor', true) .title(t('inspector.all_relations') + ' (' + gt + memberships.length + ')') .expanded(true) - .on('toggled', toggled) + .updatePreference(false) + .on('toggled', function(expanded) { + if (expanded) { selection.node().parentNode.scrollTop += 200; } + }) .content(content) ); - function toggled(expanded) { - if (expanded) { - selection.node().parentNode.scrollTop += 200; - } - } - - function content(wrap) { var list = wrap.selectAll('.member-list') .data([0]); @@ -218,7 +211,7 @@ export function uiRawMembershipEditor(context) { var newrow = list.selectAll('.member-row-new') - .data(showBlank ? [0] : []); + .data(_showBlank ? [0] : []); newrow.exit() .remove(); @@ -272,7 +265,7 @@ export function uiRawMembershipEditor(context) { addrel .call(svgIcon('#icon-plus', 'light')) .on('click', function() { - showBlank = true; + _showBlank = true; content(wrap); list.selectAll('.member-entity-input').node().focus(); }); @@ -308,7 +301,7 @@ export function uiRawMembershipEditor(context) { taginfo.roles({ debounce: true, rtype: rtype || '', - geometry: context.geometry(id), + geometry: context.geometry(_entityID), query: role }, function(err, data) { if (!err) callback(sort(role, data)); @@ -328,8 +321,8 @@ export function uiRawMembershipEditor(context) { rawMembershipEditor.entityID = function(_) { - if (!arguments.length) return id; - id = _; + if (!arguments.length) return _entityID; + _entityID = _; return rawMembershipEditor; }; diff --git a/modules/ui/raw_tag_editor.js b/modules/ui/raw_tag_editor.js index e3cf7b6e4..9e5bccc70 100644 --- a/modules/ui/raw_tag_editor.js +++ b/modules/ui/raw_tag_editor.js @@ -25,34 +25,36 @@ import { export function uiRawTagEditor(context) { var taginfo = services.taginfo, dispatch = d3_dispatch('change'), - expandedPreference = (context.storage('raw_tag_editor.expanded') === 'true'), - expandedCurrent = expandedPreference, - updatePreference = true, - readOnlyTags = [], - showBlank = false, - newRow, - state, - preset, - tags, - id; + _readOnlyTags = [], + _showBlank = false, + _updatePreference = true, + _expanded = false, + _newRow, + _state, + _preset, + _tags, + _entityID; function rawTagEditor(selection) { - var count = Object.keys(tags).filter(function(d) { return d; }).length; + var count = Object.keys(_tags).filter(function(d) { return d; }).length; - selection.call(uiDisclosure() + var disclosure = uiDisclosure(context, 'raw_tag_editor', false) .title(t('inspector.all_tags') + ' (' + count + ')') - .expanded(expandedCurrent) .on('toggled', toggled) - .content(content) - ); + .updatePreference(_updatePreference) + .content(content); + + // Sometimes we want to force the raw_tag_editor to be opened/closed.. + // When undefined, uiDisclosure will use the user's stored preference. + if (_expanded !== undefined) { + disclosure.expanded(_expanded); + } + + selection.call(disclosure); function toggled(expanded) { - expandedCurrent = expanded; - if (updatePreference) { - expandedPreference = expanded; - context.storage('raw_tag_editor.expanded', expanded); - } + _expanded = expanded; if (expanded) { selection.node().parentNode.scrollTop += 200; } @@ -61,14 +63,14 @@ export function uiRawTagEditor(context) { function content(wrap) { - var entries = _map(tags, function(v, k) { + var entries = _map(_tags, function(v, k) { return { key: k, value: v }; }); - if (!entries.length || showBlank) { - showBlank = false; + if (!entries.length || _showBlank) { + _showBlank = false; entries.push({key: '', value: ''}); - newRow = ''; + _newRow = ''; } var list = wrap.selectAll('.tag-list') @@ -138,8 +140,8 @@ export function uiRawTagEditor(context) { items = items .merge(enter) .sort(function(a, b) { - return (a.key === newRow && b.key !== newRow) ? 1 - : (a.key !== newRow && b.key === newRow) ? -1 + return (a.key === _newRow && b.key !== _newRow) ? 1 + : (a.key !== _newRow && b.key === _newRow) ? -1 : d3_ascending(a.key, b.key); }); @@ -149,11 +151,11 @@ export function uiRawTagEditor(context) { key = row.select('input.key'), // propagate bound data to child value = row.select('input.value'); // propagate bound data to child - if (id && taginfo) { + if (_entityID && taginfo) { bindTypeahead(key, value); } - var isRelation = (id && context.entity(id).type === 'relation'), + var isRelation = (_entityID && context.entity(_entityID).type === 'relation'), reference; if (isRelation && tag.key === 'type') { @@ -162,7 +164,7 @@ export function uiRawTagEditor(context) { reference = uiTagReference({ key: tag.key, value: tag.value }, context); } - if (state === 'hover') { + if (_state === 'hover') { reference.showing(false); } @@ -187,8 +189,8 @@ export function uiRawTagEditor(context) { function isReadOnly(d) { - for (var i = 0; i < readOnlyTags.length; i++) { - if (d.key.match(readOnlyTags[i]) !== null) { + for (var i = 0; i < _readOnlyTags.length; i++) { + if (d.key.match(_readOnlyTags[i]) !== null) { return true; } } @@ -206,7 +208,7 @@ export function uiRawTagEditor(context) { function bindTypeahead(key, value) { if (isReadOnly({ key: key })) return; - var geometry = context.geometry(id); + var geometry = context.geometry(_entityID); key.call(d3_combobox() .container(context.container()) @@ -275,7 +277,7 @@ export function uiRawTagEditor(context) { var match = kNew.match(/^(.*?)(?:_(\d+))?$/), base = match[1], suffix = +(match[2] || 1); - while (tags[kNew]) { // rename key if already in use + while (_tags[kNew]) { // rename key if already in use kNew = base + '_' + suffix++; } } @@ -284,8 +286,8 @@ export function uiRawTagEditor(context) { d.key = kNew; // Maintain DOM identity through the subsequent update. - if (newRow === kOld) { // see if this row is still a new row - newRow = ((d.value === '' || kNew === '') ? kNew : undefined); + if (_newRow === kOld) { // see if this row is still a new row + _newRow = ((d.value === '' || kNew === '') ? kNew : undefined); } this.value = kNew; @@ -298,8 +300,8 @@ export function uiRawTagEditor(context) { var tag = {}; tag[d.key] = this.value; - if (newRow === d.key && d.key !== '' && d.value !== '') { // not a new row anymore - newRow = undefined; + if (_newRow === d.key && d.key !== '' && d.value !== '') { // not a new row anymore + _newRow = undefined; } dispatch.call('change', this, tag); @@ -320,7 +322,7 @@ export function uiRawTagEditor(context) { // handler. Without the setTimeout, the call to `content` would // wipe out the pending value change. setTimeout(function() { - showBlank = true; + _showBlank = true; content(wrap); list.selectAll('li:last-child input.key').node().focus(); }, 0); @@ -329,51 +331,51 @@ export function uiRawTagEditor(context) { rawTagEditor.state = function(_) { - if (!arguments.length) return state; - state = _; + if (!arguments.length) return _state; + _state = _; return rawTagEditor; }; rawTagEditor.preset = function(_) { - if (!arguments.length) return preset; - preset = _; - if (preset.isFallback()) { - expandedCurrent = true; - updatePreference = false; + if (!arguments.length) return _preset; + _preset = _; + if (_preset.isFallback()) { + _expanded = true; + _updatePreference = false; } else { - expandedCurrent = expandedPreference; - updatePreference = true; + _expanded = undefined; + _updatePreference = true; } return rawTagEditor; }; rawTagEditor.tags = function(_) { - if (!arguments.length) return tags; - tags = _; + if (!arguments.length) return _tags; + _tags = _; return rawTagEditor; }; rawTagEditor.entityID = function(_) { - if (!arguments.length) return id; - id = _; + if (!arguments.length) return _entityID; + _entityID = _; return rawTagEditor; }; rawTagEditor.expanded = function(_) { - if (!arguments.length) return expandedCurrent; - expandedCurrent = _; - updatePreference = false; + if (!arguments.length) return _expanded; + _expanded = _; + _updatePreference = false; return rawTagEditor; }; rawTagEditor.readOnlyTags = function(_) { - if (!arguments.length) return readOnlyTags; - readOnlyTags = _; + if (!arguments.length) return _readOnlyTags; + _readOnlyTags = _; return rawTagEditor; };