diff --git a/visualize/static/js/app.js b/visualize/static/js/app.js index d9f3dc61..7df32d64 100644 --- a/visualize/static/js/app.js +++ b/visualize/static/js/app.js @@ -39,21 +39,41 @@ let postKOBindingCleanup = function() { $('#user-content-notice').css('visibility','unset'); } -// RDH 2022-10-20: Async Issue -- sometimes only part of one theme would load, then upon hitting a call to 'app.map.zoom()' the function -// could not be found and numerous bindings would fail. Delaying applying bindings by a second seems to resolve the issue. Attempts at -// shorter timeouts (~800ms) didn't consistently fix the issue. -/** - * DLP 2025-06-11: Potential solution to replace the window.setTimeout is to use ko.cleanNode(document.body) before ko.applyBindings(app.viewModel) - * i.e., - * ko.cleanNode(document.body); - * ko.applyBindings(app.viewModel); - * postKOBindingCleanup(); - */ - -window.setTimeout(function() { - ko.applyBindings(app.viewModel); - postKOBindingCleanup(); -}, 1000) +// Helper function to wait for element to exist, then activate layers +// replaces clunky setTimeout call; we need app.menus and app.menuModel.menuItems to exist before we can activate layers. +function waitForMenusLoadToApplyKOBindings(maxWaitTime) { + maxWaitTime = maxWaitTime || 20000; // Default 20 second max wait + var startTime = Date.now(); + + function checkElement() { + if ( + (typeof app !== 'undefined' && app.hasOwnProperty('menus') && typeof app.menus === 'object') + && (app.hasOwnProperty('menuModel') && typeof app.menuModel !== 'undefined' && app.menuModel.hasOwnProperty('menuItems') && typeof app.menuModel.menuItems === 'function') + ) { + try { + ko.applyBindings(app.viewModel, document.querySelector('#primary-content')); + } catch (e) { + console.error('Error applying KO bindings:', e); + } + try { + ko.applyBindings(app.viewModel, document.querySelector('#modal-container')); + } catch (e) { + console.error('Error applying KO bindings:', e); + } + postKOBindingCleanup(); + } else if (Date.now() - startTime < maxWaitTime) { + setTimeout(checkElement, 50); // Check every 50ms + return false; + } else { + console.warn('app.menus not found after waiting:', maxWaitTime/1000, 'seconds'); + return false; + } + } + + checkElement(); +} + +waitForMenusLoadToApplyKOBindings(); // app.viewModel.loadLayersFromServer().done(function() { app.viewModel.initLeftNav().done(function () { diff --git a/visualize/static/js/map.js b/visualize/static/js/map.js index 489852ed..ef6dfbef 100644 --- a/visualize/static/js/map.js +++ b/visualize/static/js/map.js @@ -501,7 +501,6 @@ app.init = function () { // manually bind up the context menu here, otherwise ko will complain // that we're binding the same element twice (MP's viewmodel applies // to the entire page - //ContextualMenu.Init(app.menus, document.querySelector('#context-menu')) app.menuModel = new ContextualMenu.Model(app.menus, document.querySelector('#context-menu')); // fix for top nav's negative margin app.menuModel.setCorrectionOffset(0, 0); diff --git a/visualize/static/js/models.js b/visualize/static/js/models.js index df13a9c9..12345f1a 100644 --- a/visualize/static/js/models.js +++ b/visualize/static/js/models.js @@ -3472,14 +3472,25 @@ function viewModel() { $.each(self.activeLayers(), function(i, layer) { if (layer instanceof layerModel && layer.is_multilayer_parent()) { - if ($('#'+ layer.id + '_' + layer.dimensions[0].label + '_multilayerslider').length == 0 || $('#'+ layer.id + '_' + layer.dimensions[0].label + '_multilayerslider').html() == "") { + var sliderId = '#'+ layer.id + '_' + layer.dimensions[0].label + '_multilayerslider'; + var sliderElement = $(sliderId); + + // Check if slider exists but hasn't been built yet, and hasn't been flagged as processing + if ((sliderElement.length == 0 || sliderElement.html() == "") && !layer._sliderBuilding) { + layer._sliderBuilding = true; // Flag to prevent duplicate calls try { setTimeout(function() { - layer.buildMultilayerValueLookup(); + try { + layer.buildMultilayerValueLookup(); + } + finally { + layer._sliderBuilding = false; // Reset flag after completion or failure + } }, 30) } catch (err) { console.log('pass: ' + layer ); + layer._sliderBuilding = false; // Reset flag if scheduling the timeout fails } } } diff --git a/visualize/static/js/state.js b/visualize/static/js/state.js index 9be68d39..5b4b4c2c 100644 --- a/visualize/static/js/state.js +++ b/visualize/static/js/state.js @@ -105,7 +105,6 @@ app.establishLayerLoadState = function () { * regardless of what order they come back from the AJAX calls. */ app.activateHashStateLayers = function() { - window.setTimeout(function() { for (var i = 0; i < app.hashStateLayers.length; i++) { var layerStatus = app.hashStateLayers[i].status if (layerStatus instanceof layerModel) { @@ -121,7 +120,6 @@ app.activateHashStateLayers = function() { break; } } - }, 200); } app.updateHashStateLayers = function(id, status, visible) { @@ -145,8 +143,47 @@ app.updateHashStateLayers = function(id, status, visible) { }); } - app.activateHashStateLayers(); + // Wait for app.map.zoom to exist before activating layers, but ensure + // only one polling loop is active at a time during state restoration. + var maxWaitTime = 20000; // Default 20 second max wait + var startTime = Date.now(); + + function isMapZoomReady() { + return typeof app !== 'undefined' && + app.hasOwnProperty('map') && + typeof app.map !== 'undefined' && + app.map.hasOwnProperty('zoom') && + typeof app.map.zoom === 'function'; + } + + if (isMapZoomReady()) { + app.activateHashStateLayers(); + return; + } + + if (app._waitingForMapZoom) { + return; + } + + app._waitingForMapZoom = true; + + function checkElement() { + if (isMapZoomReady()) { + app._waitingForMapZoom = false; + app._mapZoomWaitTimer = null; + app.activateHashStateLayers(); + } else if (Date.now() - startTime < maxWaitTime) { + app._mapZoomWaitTimer = setTimeout(checkElement, 50); // Check every 50ms + return false; + } else { + app._waitingForMapZoom = false; + app._mapZoomWaitTimer = null; + console.warn('map.zoom not found after waiting:', maxWaitTime/1000, 'seconds'); + return false; + } + } + checkElement(); } app.addKnownLayerFromState = function(id, opacity, isVisible, unloadedDesigns) { @@ -258,21 +295,17 @@ app.loadCompressedState = function(state) { app.establishLayerLoadState(); // data tab and open themes if (state.themes) { - //$('#dataTab').tab('show'); - if (state.themes) { - $.each(app.viewModel.themes(), function (i, theme) { - if ( $.inArray(theme.id, state.themes.ids) !== -1 || $.inArray(theme.id.toString(), state.themes.ids) !== -1 ) { - if ( app.viewModel.openThemes.indexOf(theme) === -1 ) { - //app.viewModel.openThemes.push(theme); - theme.setOpenTheme(); - } - } else { - if ( app.viewModel.openThemes.indexOf(theme) !== -1 ) { - app.viewModel.openThemes.remove(theme); - } + $.each(app.viewModel.themes(), function (i, theme) { + if ( $.inArray(theme.id, state.themes.ids) !== -1 || $.inArray(theme.id.toString(), state.themes.ids) !== -1 ) { + if ( app.viewModel.openThemes.indexOf(theme) === -1 ) { + theme.setOpenTheme(); } - }); - } + } else { + if ( app.viewModel.openThemes.indexOf(theme) !== -1 ) { + app.viewModel.openThemes.remove(theme); + } + } + }); } //if (app.embeddedMap) { @@ -280,19 +313,51 @@ app.loadCompressedState = function(state) { state.tab = "data"; } - // active tab -- the following prevents theme and data layers from loading in either tab (not sure why...disbling for now) - // it appears the dataTab show in state.themes above was causing the problem...? - // timeout worked, but then realized that removing datatab show from above worked as well... - // now reinstating the timeout which seems to fix the toggling between tours issue (toggling to ActiveTour while already in ActiveTab) + // Helper function to wait for element to exist, then show tab + function waitForElementAndShowTab(selector, maxWaitTime) { + maxWaitTime = maxWaitTime || 5000; // Default 5 second max wait + var startTime = Date.now(); + + function checkElement() { + var element = $(selector); + if (element.length > 0 && typeof element.tab === 'function') { + try { + element.tab('show'); + return true; + } catch (e) { + console.warn('Error showing tab:', selector, e); + return false; + } + } else if (element.length > 0 && typeof element.tab !== 'function') { + // Element exists but tab functionality not available yet + if (Date.now() - startTime < maxWaitTime) { + setTimeout(checkElement, 50); // Check every 50ms + return false; + } else { + console.warn('Tab element found but tab functionality not available:', selector); + return false; + } + } else if (Date.now() - startTime < maxWaitTime) { + setTimeout(checkElement, 50); // Check every 50ms + return false; + } else { + console.warn('Tab element not found after waiting:', selector); + return false; + } + } + + checkElement(); + } + + // active tab -- wait for element to exist before showing if (state.tab && state.tab === "active") { - //$('#activeTab').tab('show'); - setTimeout( function() { $('#activeTab').tab('show'); }, 200 ); + waitForElementAndShowTab('#activeTab'); } else if (state.tab && state.tab === "designs") { - setTimeout( function() { $('#designsTab').tab('show'); }, 200 ); + waitForElementAndShowTab('#designsTab'); } else if (state.tab && state.tab === "legend") { - setTimeout( function() { $('#legendTab').tab('show'); }, 200 ); + waitForElementAndShowTab('#legendTab'); } else { - setTimeout( function() { $('#dataTab').tab('show'); }, 200 ); + waitForElementAndShowTab('#dataTab'); } if ( state.legends && state.legends === 'true' ) { diff --git a/visualize/templates/visualize/planner.html b/visualize/templates/visualize/planner.html index 0afc3fa5..0e6f3dc9 100644 --- a/visualize/templates/visualize/planner.html +++ b/visualize/templates/visualize/planner.html @@ -72,10 +72,12 @@ {% block outer_content %} +
@@ -394,6 +396,9 @@

{% endblock %} +{% block footer %} +{% endblock %} + {% block extra_js %}