From d87a8cbacdd961a301fa47564e7655fa603c8f6f Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Mon, 28 May 2018 16:13:50 -0400 Subject: [PATCH] Optimize layer menu to better handle many layers --- src/dataset/mapshaper-catalog.js | 6 +- src/gui/mapshaper-layer-control.js | 201 ++++++++++++++++++----------- 2 files changed, 130 insertions(+), 77 deletions(-) diff --git a/src/dataset/mapshaper-catalog.js b/src/dataset/mapshaper-catalog.js index f96d10299..9415970f5 100644 --- a/src/dataset/mapshaper-catalog.js +++ b/src/dataset/mapshaper-catalog.js @@ -38,10 +38,12 @@ function Catalog() { } }; - this.findLayer = function(target) { + // @arg: a layer object or a test function + this.findLayer = function(arg) { + var test = typeof arg == 'function' ? arg : null; var found = null; this.forEachLayer(function(lyr, dataset) { - if (lyr == target) { + if (test ? test(lyr, dataset) : lyr == arg) { found = layerObject(lyr, dataset); } }); diff --git a/src/gui/mapshaper-layer-control.js b/src/gui/mapshaper-layer-control.js index e91fd66e7..20bd56c77 100644 --- a/src/gui/mapshaper-layer-control.js +++ b/src/gui/mapshaper-layer-control.js @@ -4,14 +4,30 @@ function LayerControl(model, map) { var el = El("#layer-control").on('click', gui.handleDirectEvent(gui.clearMode)); var buttonLabel = El('#layer-control-btn .layer-name'); var isOpen = false; + var renderCache = {}; + var idCount = 0; // layer counter for creating unique layer ids new ModeButton('#layer-control-btn .header-btn', 'layer_menu'); gui.addMode('layer_menu', turnOn, turnOff); model.on('update', function(e) { - updateBtn(); + updateMenuBtn(); if (isOpen) render(); }); + function findLayerById(id) { + return model.findLayer(function(lyr, dataset) { + return lyr.menu_id == id; + }); + } + + function layerIsPinned(lyr) { + return lyr == map.getReferenceLayer(); + } + + function layerIsSelected(lyr) { + return lyr == model.getActiveLayer().layer; + } + function turnOn() { isOpen = true; // set max layer menu height @@ -25,27 +41,124 @@ function LayerControl(model, map) { el.hide(); } - function updateBtn() { + function updateMenuBtn() { var name = model.getActiveLayer().layer.name || "[unnamed layer]"; buttonLabel.html(name + "  ▼"); } function render() { var list = El('#layer-control .layer-list'); - var pinnable = 0; - if (isOpen) { - list.hide().empty(); - model.forEachLayer(function(lyr, dataset) { - if (isPinnable(lyr)) pinnable++; - }); - if (pinnable === 0 && map.getReferenceLayer()) { - clearPin(); // a layer has been deleted... + var uniqIds = {}; + var pinnableCount = 0; + var oldCache = renderCache; + if (!isOpen) return; + renderCache = {}; + list.empty(); + model.forEachLayer(function(lyr, dataset) { + if (isPinnable(lyr)) pinnableCount++; + }); + if (pinnableCount === 0 && map.getReferenceLayer()) { + clearPin(); // a layer has been deleted... + } + model.forEachLayer(function(lyr, dataset) { + var pinnable = pinnableCount > 1 && isPinnable(lyr); + var html, element; + // Assign a unique id to each layer, so html strings + // can be used as unique identifiers for caching rendered HTML, and as + // an id for layer menu event handlers + if (!lyr.menu_id || uniqIds[lyr.menu_id]) { + lyr.menu_id = ++idCount; + } + uniqIds[lyr.menu_id] = true; + html = renderLayer(lyr, dataset, pinnable); + if (html in oldCache) { + element = oldCache[html]; + } else { + element = El('div').html(html).firstChild(); + initMouseEvents(element, lyr.menu_id, pinnable); + } + renderCache[html] = element; + list.appendChild(element); + }); + } + + function renderLayer(lyr, dataset, pinnable) { + var warnings = getWarnings(lyr, dataset); + var classes = 'layer-item'; + var entry, html; + + if (layerIsSelected(lyr)) classes += ' active'; + if (layerIsPinned(lyr)) classes += ' pinned'; + + html = '
'; + html += rowHTML('name', '' + getDisplayName(lyr.name) + '', 'row1'); + html += rowHTML('source file', describeSrc(lyr, dataset) || 'n/a'); + html += rowHTML('contents', describeLyr(lyr)); + if (warnings) { + html += rowHTML('problems', warnings, 'layer-problems'); + } + html += ''; + if (pinnable) { + html += ''; + html += ''; + } + html += '
'; + return html; + } + + function initMouseEvents(entry, id, pinnable) { + entry.on('mouseover', init); + function init() { + entry.removeEventListener('mouseover', init); + initMouseEvents2(entry, id, pinnable); + } + } + + function initMouseEvents2(entry, id, pinnable) { + // init delete button + entry.findChild('img.close-btn').on('mouseup', function(e) { + var target = findLayerById(id); + e.stopPropagation(); + if (layerIsPinned(target.layer)) { + clearPin(); } - model.forEachLayer(function(lyr, dataset) { - list.appendChild(renderLayer(lyr, dataset, pinnable > 1 && isPinnable(lyr))); + model.deleteLayer(target.layer, target.dataset); + }); + + if (pinnable) { + // init pin button + entry.findChild('img.pinned').on('mouseup', function(e) { + var target = findLayerById(id); + e.stopPropagation(); + if (layerIsPinned(target.layer)) { + clearPin(); + } else { + setPin(target.layer, target.dataset); + entry.addClass('pinned'); + } }); - list.show(); } + + // init name editor + new ClickText2(entry.findChild('.layer-name')) + .on('change', function(e) { + var target = findLayerById(id); + var str = cleanLayerName(this.value()); + this.value(getDisplayName(str)); + target.layer.name = str; + updateMenuBtn(); + }); + + // init click-to-select + gui.onClick(entry, function() { + var target = findLayerById(id); + if (!gui.getInputElement()) { // don't select if user is typing + gui.clearMode(); + if (!layerIsSelected(target.layer)) { + model.updated({select: true}, target.layer, target.dataset); + } + } + }); } function describeLyr(lyr) { @@ -117,68 +230,6 @@ function LayerControl(model, map) { return internal.layerHasGeometry(lyr); } - function renderLayer(lyr, dataset, pinnable) { - var editLyr = model.getActiveLayer().layer; - var entry = El('div').addClass('layer-item').classed('active', lyr == editLyr); - var html = rowHTML('name', '' + getDisplayName(lyr.name) + '', 'row1'); - var warn = getWarnings(lyr, dataset); - html += rowHTML('source file', describeSrc(lyr, dataset) || 'n/a'); - html += rowHTML('contents', describeLyr(lyr)); - if (warn) { - html += rowHTML('problems', warn, 'layer-problems'); - } - html += ''; - if (pinnable) { - html += ''; - html += ''; - } - entry.html(html); - - // init delete button - entry.findChild('img.close-btn').on('mouseup', function(e) { - e.stopPropagation(); - if (lyr == map.getReferenceLayer()) { - clearPin(); - } - model.deleteLayer(lyr, dataset); - }); - - if (pinnable) { - if (map.getReferenceLayer() == lyr) { - entry.addClass('pinned'); - } - - // init pin button - entry.findChild('img.pinned').on('mouseup', function(e) { - e.stopPropagation(); - if (lyr == map.getReferenceLayer()) { - clearPin(); - } else { - setPin(lyr, dataset); - entry.addClass('pinned'); - } - }); - } - - // init name editor - new ClickText2(entry.findChild('.layer-name')) - .on('change', function(e) { - var str = cleanLayerName(this.value()); - this.value(getDisplayName(str)); - lyr.name = str; - updateBtn(); - }); - // init click-to-select - gui.onClick(entry, function() { - if (!gui.getInputElement()) { // don't select if user is typing - gui.clearMode(); - if (lyr != editLyr) { - model.updated({select: true}, lyr, dataset); - } - } - }); - return entry; - } function cleanLayerName(raw) { return raw.replace(/[\n\t/\\]/g, '')