From 68c2b9f1a83926fb28b83daeef0933252b1436ac Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Wed, 19 Feb 2020 14:31:32 -0800 Subject: [PATCH] Move the data layers section to its own object (re: #7368) --- modules/ui/panes/map_data.js | 537 ++--------------------------- modules/ui/sections/data_layers.js | 391 +++++++++++++++++++++ 2 files changed, 419 insertions(+), 509 deletions(-) create mode 100644 modules/ui/sections/data_layers.js diff --git a/modules/ui/panes/map_data.js b/modules/ui/panes/map_data.js index 5797edbd9..11d2d7051 100644 --- a/modules/ui/panes/map_data.js +++ b/modules/ui/panes/map_data.js @@ -1,522 +1,18 @@ import { - event as d3_event, - select as d3_select + event as d3_event } from 'd3-selection'; -import { svgIcon } from '../../svg/icon'; -import { t, textDirection } from '../../util/locale'; -import { tooltip } from '../../util/tooltip'; -import { geoExtent } from '../../geo'; +import { t } from '../../util/locale'; import { modeBrowse } from '../../modes/browse'; -import { uiDisclosure } from '../disclosure'; -import { uiSettingsCustomData } from '../settings/custom_data'; -import { uiTooltipHtml } from '../tooltipHtml'; import { uiCmd } from '../cmd'; import { uiPane } from '../pane'; +import { uiSectionDataLayers } from '../sections/data_layers'; import { uiSectionMapFeatures } from '../sections/map_features'; import { uiSectionMapStyleOptions } from '../sections/map_style_options'; import { uiSectionPhotoOverlays } from '../sections/photo_overlays'; export function uiPaneMapData(context) { - var osmDataToggleKey = uiCmd('⌥' + t('area_fill.wireframe.key')); - var layers = context.layers(); - - var settingsCustomData = uiSettingsCustomData(context) - .on('change', customChanged); - - var _dataLayerContainer = d3_select(null); - var _QAList = d3_select(null); - - var _photoOverlaysSection = uiSectionPhotoOverlays(context); - var _mapStyleOptionsSection = uiSectionMapStyleOptions(context); - var _mapFeaturesSection = uiSectionMapFeatures(context); - - - function showsQA(d) { - var QAKeys = [d]; - var QALayers = layers.all().filter(function(obj) { return QAKeys.indexOf(obj.id) !== -1; }); - var data = QALayers.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(); - } - - return layerEnabled(data[0]); - } - - - function showsLayer(which) { - var layer = layers.layer(which); - if (layer) { - return layer.enabled(); - } - return false; - } - - - function setLayer(which, enabled) { - // Don't allow layer changes while drawing - #6584 - var mode = context.mode(); - if (mode && /^draw/.test(mode.id)) return; - - var layer = layers.layer(which); - if (layer) { - layer.enabled(enabled); - - if (!enabled && (which === 'osm' || which === 'notes')) { - context.enter(modeBrowse(context)); - } - - update(); - } - } - - - function toggleLayer(which) { - setLayer(which, !showsLayer(which)); - } - - - function drawOsmItems(selection) { - var osmKeys = ['osm', 'notes']; - var osmLayers = layers.all().filter(function(obj) { return osmKeys.indexOf(obj.id) !== -1; }); - - var ul = selection - .selectAll('.layer-list-osm') - .data([0]); - - ul = ul.enter() - .append('ul') - .attr('class', 'layer-list layer-list-osm') - .merge(ul); - - var li = ul.selectAll('.list-item') - .data(osmLayers); - - li.exit() - .remove(); - - var liEnter = li.enter() - .append('li') - .attr('class', function(d) { return 'list-item list-item-' + d.id; }); - - var labelEnter = liEnter - .append('label') - .each(function(d) { - if (d.id === 'osm') { - d3_select(this) - .call(tooltip() - .html(true) - .title(uiTooltipHtml(t('map_data.layers.' + d.id + '.tooltip'), osmDataToggleKey)) - .placement('bottom') - ); - } else { - d3_select(this) - .call(tooltip() - .title(t('map_data.layers.' + d.id + '.tooltip')) - .placement('bottom') - ); - } - }); - - labelEnter - .append('input') - .attr('type', 'checkbox') - .on('change', function(d) { toggleLayer(d.id); }); - - labelEnter - .append('span') - .text(function(d) { return t('map_data.layers.' + d.id + '.title'); }); - - - // Update - li - .merge(liEnter) - .classed('active', function (d) { return d.layer.enabled(); }) - .selectAll('input') - .property('checked', function (d) { return d.layer.enabled(); }); - } - - - function drawQAItems(selection) { - var qaKeys = ['keepRight', 'improveOSM', 'osmose']; - var qaLayers = layers.all().filter(function(obj) { return qaKeys.indexOf(obj.id) !== -1; }); - - var ul = selection - .selectAll('.layer-list-qa') - .data([0]); - - ul = ul.enter() - .append('ul') - .attr('class', 'layer-list layer-list-qa') - .merge(ul); - - var li = ul.selectAll('.list-item') - .data(qaLayers); - - li.exit() - .remove(); - - var liEnter = li.enter() - .append('li') - .attr('class', function(d) { return 'list-item list-item-' + d.id; }); - - var labelEnter = liEnter - .append('label') - .each(function(d) { - d3_select(this) - .call(tooltip() - .title(t('map_data.layers.' + d.id + '.tooltip')) - .placement('bottom') - ); - }); - - labelEnter - .append('input') - .attr('type', 'checkbox') - .on('change', function(d) { toggleLayer(d.id); }); - - labelEnter - .append('span') - .text(function(d) { return t('map_data.layers.' + d.id + '.title'); }); - - - // Update - li - .merge(liEnter) - .classed('active', function (d) { return d.layer.enabled(); }) - .selectAll('input') - .property('checked', function (d) { return d.layer.enabled(); }); - } - - - // Beta feature - sample vector layers to support Detroit Mapping Challenge - // https://github.com/osmus/detroit-mapping-challenge - function drawVectorItems(selection) { - var dataLayer = layers.layer('data'); - var vtData = [ - { - name: 'Detroit Neighborhoods/Parks', - src: 'neighborhoods-parks', - tooltip: 'Neighborhood boundaries and parks as compiled by City of Detroit in concert with community groups.', - template: 'https://{switch:a,b,c,d}.tiles.mapbox.com/v4/jonahadkins.cjksmur6x34562qp9iv1u3ksf-54hev,jonahadkins.cjksmqxdx33jj2wp90xd9x2md-4e5y2/{z}/{x}/{y}.vector.pbf?access_token=pk.eyJ1Ijoiam9uYWhhZGtpbnMiLCJhIjoiRlVVVkx3VSJ9.9sdVEK_B_VkEXPjssU5MqA' - }, { - name: 'Detroit Composite POIs', - src: 'composite-poi', - tooltip: 'Fire Inspections, Business Licenses, and other public location data collated from the City of Detroit.', - template: 'https://{switch:a,b,c,d}.tiles.mapbox.com/v4/jonahadkins.cjksmm6a02sli31myxhsr7zf3-2sw8h/{z}/{x}/{y}.vector.pbf?access_token=pk.eyJ1Ijoiam9uYWhhZGtpbnMiLCJhIjoiRlVVVkx3VSJ9.9sdVEK_B_VkEXPjssU5MqA' - }, { - name: 'Detroit All-The-Places POIs', - src: 'alltheplaces-poi', - tooltip: 'Public domain business location data created by web scrapers.', - template: 'https://{switch:a,b,c,d}.tiles.mapbox.com/v4/jonahadkins.cjksmswgk340g2vo06p1w9w0j-8fjjc/{z}/{x}/{y}.vector.pbf?access_token=pk.eyJ1Ijoiam9uYWhhZGtpbnMiLCJhIjoiRlVVVkx3VSJ9.9sdVEK_B_VkEXPjssU5MqA' - } - ]; - - // Only show this if the map is around Detroit.. - var detroit = geoExtent([-83.5, 42.1], [-82.8, 42.5]); - var showVectorItems = (context.map().zoom() > 9 && detroit.contains(context.map().center())); - - var container = selection.selectAll('.vectortile-container') - .data(showVectorItems ? [0] : []); - - container.exit() - .remove(); - - var containerEnter = container.enter() - .append('div') - .attr('class', 'vectortile-container'); - - containerEnter - .append('h4') - .attr('class', 'vectortile-header') - .text('Detroit Vector Tiles (Beta)'); - - containerEnter - .append('ul') - .attr('class', 'layer-list layer-list-vectortile'); - - containerEnter - .append('div') - .attr('class', 'vectortile-footer') - .append('a') - .attr('target', '_blank') - .attr('tabindex', -1) - .call(svgIcon('#iD-icon-out-link', 'inline')) - .attr('href', 'https://github.com/osmus/detroit-mapping-challenge') - .append('span') - .text('About these layers'); - - container = container - .merge(containerEnter); - - - var ul = container.selectAll('.layer-list-vectortile'); - - var li = ul.selectAll('.list-item') - .data(vtData); - - li.exit() - .remove(); - - var liEnter = li.enter() - .append('li') - .attr('class', function(d) { return 'list-item list-item-' + d.src; }); - - var labelEnter = liEnter - .append('label') - .each(function(d) { - d3_select(this).call( - tooltip().title(d.tooltip).placement('top') - ); - }); - - labelEnter - .append('input') - .attr('type', 'radio') - .attr('name', 'vectortile') - .on('change', selectVTLayer); - - labelEnter - .append('span') - .text(function(d) { return d.name; }); - - // Update - li - .merge(liEnter) - .classed('active', isVTLayerSelected) - .selectAll('input') - .property('checked', isVTLayerSelected); - - - function isVTLayerSelected(d) { - return dataLayer && dataLayer.template() === d.template; - } - - function selectVTLayer(d) { - context.storage('settings-custom-data-url', d.template); - if (dataLayer) { - dataLayer.template(d.template, d.src); - dataLayer.enabled(true); - } - } - } - - - function drawCustomDataItems(selection) { - var dataLayer = layers.layer('data'); - var hasData = dataLayer && dataLayer.hasData(); - var showsData = hasData && dataLayer.enabled(); - - var ul = selection - .selectAll('.layer-list-data') - .data(dataLayer ? [0] : []); - - // Exit - ul.exit() - .remove(); - - // Enter - var ulEnter = ul.enter() - .append('ul') - .attr('class', 'layer-list layer-list-data'); - - var liEnter = ulEnter - .append('li') - .attr('class', 'list-item-data'); - - var labelEnter = liEnter - .append('label') - .call(tooltip() - .title(t('map_data.layers.custom.tooltip')) - .placement('top') - ); - - labelEnter - .append('input') - .attr('type', 'checkbox') - .on('change', function() { toggleLayer('data'); }); - - labelEnter - .append('span') - .text(t('map_data.layers.custom.title')); - - liEnter - .append('button') - .call(tooltip() - .title(t('settings.custom_data.tooltip')) - .placement((textDirection === 'rtl') ? 'right' : 'left') - ) - .on('click', editCustom) - .call(svgIcon('#iD-icon-more')); - - liEnter - .append('button') - .call(tooltip() - .title(t('map_data.layers.custom.zoom')) - .placement((textDirection === 'rtl') ? 'right' : 'left') - ) - .on('click', function() { - d3_event.preventDefault(); - d3_event.stopPropagation(); - dataLayer.fitZoom(); - }) - .call(svgIcon('#iD-icon-search')); - - // Update - ul = ul - .merge(ulEnter); - - ul.selectAll('.list-item-data') - .classed('active', showsData) - .selectAll('label') - .classed('deemphasize', !hasData) - .selectAll('input') - .property('disabled', !hasData) - .property('checked', showsData); - } - - - function editCustom() { - d3_event.preventDefault(); - context.container() - .call(settingsCustomData); - } - - - function customChanged(d) { - var dataLayer = layers.layer('data'); - - if (d && d.url) { - dataLayer.url(d.url); - } else if (d && d.fileList) { - dataLayer.fileList(d.fileList); - } - } - - - 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') - .call(tooltip() - .html(true) - .title(function(d) { - var tip = t(name + '.' + d + '.tooltip'); - return uiTooltipHtml(tip); - }) - .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', false); - } - - - function renderDataLayers(selection) { - var container = selection.selectAll('.data-layer-container') - .data([0]); - - _dataLayerContainer = container.enter() - .append('div') - .attr('class', 'data-layer-container') - .merge(container); - - updateDataLayers(); - } - - function updateDataLayers() { - _dataLayerContainer - .call(drawOsmItems) - .call(drawQAItems) - .call(drawCustomDataItems) - .call(drawVectorItems); // Beta - Detroit mapping challenge - } - - function update() { - - if (!mapDataPane.selection().select('.disclosure-wrap-data_layers').classed('hide')) { - updateDataLayers(); - - _QAList - .call(drawListItems, ['keep-right'], 'checkbox', 'QA', function(d) { toggleLayer(d); }, showsQA); - } - - mapDataPane.selection().select('.map-data-photo-overlays') - .call(_photoOverlaysSection.render); - - mapDataPane.selection().select('.map-data-area-fills') - .call(_mapStyleOptionsSection.render); - - mapDataPane.selection().select('.map-data-feature-filters') - .call(_mapFeaturesSection.render); - } - - var mapDataPane = uiPane('map-data', context) - .key(t('map_data.key')) - .title(t('map_data.title')) - .description(t('map_data.description')) - .iconName('iD-icon-data'); - - mapDataPane.renderContent = function(content) { - - // data layers - content - .append('div') - .attr('class', 'map-data-data-layers') - .call(uiDisclosure(context, 'data_layers', true) - .title(t('map_data.data_layers')) - .content(renderDataLayers) - ); - - // photo overlays - content - .append('div') - .attr('class', 'map-data-photo-overlays'); - - // area fills - content - .append('div') - .attr('class', 'map-data-area-fills'); - - // feature filters - content - .append('div') - .attr('class', 'map-data-feature-filters'); - - update(); - }; context.keybinding() .on(t('area_fill.wireframe.key'), function toggleWireframe() { @@ -524,15 +20,38 @@ export function uiPaneMapData(context) { d3_event.stopPropagation(); context.map().toggleWireframe(); }) - .on(osmDataToggleKey, function() { + .on(uiCmd('⌥' + t('area_fill.wireframe.key')), function toggleOsmData() { d3_event.preventDefault(); d3_event.stopPropagation(); - toggleLayer('osm'); + + // Don't allow layer changes while drawing - #6584 + var mode = context.mode(); + if (mode && /^draw/.test(mode.id)) return; + + var layer = context.layers().layer('osm'); + if (layer) { + layer.enabled(!layer.enabled()); + if (!layer.enabled()) { + context.enter(modeBrowse(context)); + } + } }) .on(t('map_data.highlight_edits.key'), function toggleHighlightEdited() { d3_event.preventDefault(); context.map().toggleHighlightEdited(); }); + var mapDataPane = uiPane('map-data', context) + .key(t('map_data.key')) + .title(t('map_data.title')) + .description(t('map_data.description')) + .iconName('iD-icon-data') + .sections([ + uiSectionDataLayers(context), + uiSectionPhotoOverlays(context), + uiSectionMapStyleOptions(context), + uiSectionMapFeatures(context) + ]); + return mapDataPane; } diff --git a/modules/ui/sections/data_layers.js b/modules/ui/sections/data_layers.js new file mode 100644 index 000000000..db603c4e1 --- /dev/null +++ b/modules/ui/sections/data_layers.js @@ -0,0 +1,391 @@ +import _debounce from 'lodash-es/debounce'; +import { + event as d3_event, + select as d3_select +} from 'd3-selection'; + +import { t, textDirection } from '../../util/locale'; +import { tooltip } from '../../util/tooltip'; +import { svgIcon } from '../../svg/icon'; +import { geoExtent } from '../../geo'; +import { modeBrowse } from '../../modes/browse'; +import { uiCmd } from '../cmd'; +import { uiSection } from '../section'; +import { uiSettingsCustomData } from '../settings/custom_data'; +import { uiTooltipHtml } from '../tooltipHtml'; + +export function uiSectionDataLayers(context) { + + var settingsCustomData = uiSettingsCustomData(context) + .on('change', customChanged); + + var layers = context.layers(); + + var section = uiSection('data-layers', context) + .title(t('map_data.data_layers')); + + section.renderDisclosureContent = function(selection) { + var container = selection.selectAll('.data-layer-container') + .data([0]); + + container.enter() + .append('div') + .attr('class', 'data-layer-container') + .merge(container) + .call(drawOsmItems) + .call(drawQAItems) + .call(drawCustomDataItems) + .call(drawVectorItems); // Beta - Detroit mapping challenge + }; + + function showsLayer(which) { + var layer = layers.layer(which); + if (layer) { + return layer.enabled(); + } + return false; + } + + function setLayer(which, enabled) { + // Don't allow layer changes while drawing - #6584 + var mode = context.mode(); + if (mode && /^draw/.test(mode.id)) return; + + var layer = layers.layer(which); + if (layer) { + layer.enabled(enabled); + + if (!enabled && (which === 'osm' || which === 'notes')) { + context.enter(modeBrowse(context)); + } + } + } + + function toggleLayer(which) { + setLayer(which, !showsLayer(which)); + } + + function drawOsmItems(selection) { + var osmKeys = ['osm', 'notes']; + var osmLayers = layers.all().filter(function(obj) { return osmKeys.indexOf(obj.id) !== -1; }); + + var ul = selection + .selectAll('.layer-list-osm') + .data([0]); + + ul = ul.enter() + .append('ul') + .attr('class', 'layer-list layer-list-osm') + .merge(ul); + + var li = ul.selectAll('.list-item') + .data(osmLayers); + + li.exit() + .remove(); + + var liEnter = li.enter() + .append('li') + .attr('class', function(d) { return 'list-item list-item-' + d.id; }); + + var labelEnter = liEnter + .append('label') + .each(function(d) { + if (d.id === 'osm') { + d3_select(this) + .call(tooltip() + .html(true) + .title(uiTooltipHtml(t('map_data.layers.' + d.id + '.tooltip'), uiCmd('⌥' + t('area_fill.wireframe.key')))) + .placement('bottom') + ); + } else { + d3_select(this) + .call(tooltip() + .title(t('map_data.layers.' + d.id + '.tooltip')) + .placement('bottom') + ); + } + }); + + labelEnter + .append('input') + .attr('type', 'checkbox') + .on('change', function(d) { toggleLayer(d.id); }); + + labelEnter + .append('span') + .text(function(d) { return t('map_data.layers.' + d.id + '.title'); }); + + + // Update + li + .merge(liEnter) + .classed('active', function (d) { return d.layer.enabled(); }) + .selectAll('input') + .property('checked', function (d) { return d.layer.enabled(); }); + } + + function drawQAItems(selection) { + var qaKeys = ['keepRight', 'improveOSM', 'osmose']; + var qaLayers = layers.all().filter(function(obj) { return qaKeys.indexOf(obj.id) !== -1; }); + + var ul = selection + .selectAll('.layer-list-qa') + .data([0]); + + ul = ul.enter() + .append('ul') + .attr('class', 'layer-list layer-list-qa') + .merge(ul); + + var li = ul.selectAll('.list-item') + .data(qaLayers); + + li.exit() + .remove(); + + var liEnter = li.enter() + .append('li') + .attr('class', function(d) { return 'list-item list-item-' + d.id; }); + + var labelEnter = liEnter + .append('label') + .each(function(d) { + d3_select(this) + .call(tooltip() + .title(t('map_data.layers.' + d.id + '.tooltip')) + .placement('bottom') + ); + }); + + labelEnter + .append('input') + .attr('type', 'checkbox') + .on('change', function(d) { toggleLayer(d.id); }); + + labelEnter + .append('span') + .text(function(d) { return t('map_data.layers.' + d.id + '.title'); }); + + + // Update + li + .merge(liEnter) + .classed('active', function (d) { return d.layer.enabled(); }) + .selectAll('input') + .property('checked', function (d) { return d.layer.enabled(); }); + } + + // Beta feature - sample vector layers to support Detroit Mapping Challenge + // https://github.com/osmus/detroit-mapping-challenge + function drawVectorItems(selection) { + var dataLayer = layers.layer('data'); + var vtData = [ + { + name: 'Detroit Neighborhoods/Parks', + src: 'neighborhoods-parks', + tooltip: 'Neighborhood boundaries and parks as compiled by City of Detroit in concert with community groups.', + template: 'https://{switch:a,b,c,d}.tiles.mapbox.com/v4/jonahadkins.cjksmur6x34562qp9iv1u3ksf-54hev,jonahadkins.cjksmqxdx33jj2wp90xd9x2md-4e5y2/{z}/{x}/{y}.vector.pbf?access_token=pk.eyJ1Ijoiam9uYWhhZGtpbnMiLCJhIjoiRlVVVkx3VSJ9.9sdVEK_B_VkEXPjssU5MqA' + }, { + name: 'Detroit Composite POIs', + src: 'composite-poi', + tooltip: 'Fire Inspections, Business Licenses, and other public location data collated from the City of Detroit.', + template: 'https://{switch:a,b,c,d}.tiles.mapbox.com/v4/jonahadkins.cjksmm6a02sli31myxhsr7zf3-2sw8h/{z}/{x}/{y}.vector.pbf?access_token=pk.eyJ1Ijoiam9uYWhhZGtpbnMiLCJhIjoiRlVVVkx3VSJ9.9sdVEK_B_VkEXPjssU5MqA' + }, { + name: 'Detroit All-The-Places POIs', + src: 'alltheplaces-poi', + tooltip: 'Public domain business location data created by web scrapers.', + template: 'https://{switch:a,b,c,d}.tiles.mapbox.com/v4/jonahadkins.cjksmswgk340g2vo06p1w9w0j-8fjjc/{z}/{x}/{y}.vector.pbf?access_token=pk.eyJ1Ijoiam9uYWhhZGtpbnMiLCJhIjoiRlVVVkx3VSJ9.9sdVEK_B_VkEXPjssU5MqA' + } + ]; + + // Only show this if the map is around Detroit.. + var detroit = geoExtent([-83.5, 42.1], [-82.8, 42.5]); + var showVectorItems = (context.map().zoom() > 9 && detroit.contains(context.map().center())); + + var container = selection.selectAll('.vectortile-container') + .data(showVectorItems ? [0] : []); + + container.exit() + .remove(); + + var containerEnter = container.enter() + .append('div') + .attr('class', 'vectortile-container'); + + containerEnter + .append('h4') + .attr('class', 'vectortile-header') + .text('Detroit Vector Tiles (Beta)'); + + containerEnter + .append('ul') + .attr('class', 'layer-list layer-list-vectortile'); + + containerEnter + .append('div') + .attr('class', 'vectortile-footer') + .append('a') + .attr('target', '_blank') + .attr('tabindex', -1) + .call(svgIcon('#iD-icon-out-link', 'inline')) + .attr('href', 'https://github.com/osmus/detroit-mapping-challenge') + .append('span') + .text('About these layers'); + + container = container + .merge(containerEnter); + + + var ul = container.selectAll('.layer-list-vectortile'); + + var li = ul.selectAll('.list-item') + .data(vtData); + + li.exit() + .remove(); + + var liEnter = li.enter() + .append('li') + .attr('class', function(d) { return 'list-item list-item-' + d.src; }); + + var labelEnter = liEnter + .append('label') + .each(function(d) { + d3_select(this).call( + tooltip().title(d.tooltip).placement('top') + ); + }); + + labelEnter + .append('input') + .attr('type', 'radio') + .attr('name', 'vectortile') + .on('change', selectVTLayer); + + labelEnter + .append('span') + .text(function(d) { return d.name; }); + + // Update + li + .merge(liEnter) + .classed('active', isVTLayerSelected) + .selectAll('input') + .property('checked', isVTLayerSelected); + + + function isVTLayerSelected(d) { + return dataLayer && dataLayer.template() === d.template; + } + + function selectVTLayer(d) { + context.storage('settings-custom-data-url', d.template); + if (dataLayer) { + dataLayer.template(d.template, d.src); + dataLayer.enabled(true); + } + } + } + + function drawCustomDataItems(selection) { + var dataLayer = layers.layer('data'); + var hasData = dataLayer && dataLayer.hasData(); + var showsData = hasData && dataLayer.enabled(); + + var ul = selection + .selectAll('.layer-list-data') + .data(dataLayer ? [0] : []); + + // Exit + ul.exit() + .remove(); + + // Enter + var ulEnter = ul.enter() + .append('ul') + .attr('class', 'layer-list layer-list-data'); + + var liEnter = ulEnter + .append('li') + .attr('class', 'list-item-data'); + + var labelEnter = liEnter + .append('label') + .call(tooltip() + .title(t('map_data.layers.custom.tooltip')) + .placement('top') + ); + + labelEnter + .append('input') + .attr('type', 'checkbox') + .on('change', function() { toggleLayer('data'); }); + + labelEnter + .append('span') + .text(t('map_data.layers.custom.title')); + + liEnter + .append('button') + .call(tooltip() + .title(t('settings.custom_data.tooltip')) + .placement((textDirection === 'rtl') ? 'right' : 'left') + ) + .on('click', editCustom) + .call(svgIcon('#iD-icon-more')); + + liEnter + .append('button') + .call(tooltip() + .title(t('map_data.layers.custom.zoom')) + .placement((textDirection === 'rtl') ? 'right' : 'left') + ) + .on('click', function() { + d3_event.preventDefault(); + d3_event.stopPropagation(); + dataLayer.fitZoom(); + }) + .call(svgIcon('#iD-icon-search')); + + // Update + ul = ul + .merge(ulEnter); + + ul.selectAll('.list-item-data') + .classed('active', showsData) + .selectAll('label') + .classed('deemphasize', !hasData) + .selectAll('input') + .property('disabled', !hasData) + .property('checked', showsData); + } + + function editCustom() { + d3_event.preventDefault(); + context.container() + .call(settingsCustomData); + } + + function customChanged(d) { + var dataLayer = layers.layer('data'); + + if (d && d.url) { + dataLayer.url(d.url); + } else if (d && d.fileList) { + dataLayer.fileList(d.fileList); + } + } + + context.layers().on('change.uiSectionDataLayers', section.rerenderContent); + + context.map() + .on('move.uiSectionDataLayers', + _debounce(function() { + // Detroit layers may have moved in or out of view + window.requestIdleCallback(section.rerenderContent); + }, 1000) + ); + + return section; +}