Skip to content
Open
50 changes: 35 additions & 15 deletions visualize/static/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Comment on lines +53 to +62
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ko.applyBindings is only applied to #primary-content / #modal-container. On pages that include this app.js but don’t have #primary-content (e.g. visualize/templates/visualize/mobile-map.html), document.querySelector('#primary-content') will be null, ko.applyBindings will throw, and the catch will prevent any bindings from being applied at all (breaking all data-bind UI on that page). Suggestion: detect the presence of these containers and (a) skip binding/logging for #modal-container when it’s absent, and (b) fall back to applying bindings on an appropriate root (e.g. document.body or a page-specific container) when #primary-content isn’t present, so non-planner templates keep working.

Suggested change
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);
}
var primaryContent = document.querySelector('#primary-content');
var modalContainer = document.querySelector('#modal-container');
var primaryBindingRoot = primaryContent || document.body;
if (!primaryContent && primaryBindingRoot) {
console.warn('#primary-content not found; applying KO bindings to document.body instead.');
}
if (primaryBindingRoot) {
try {
ko.applyBindings(app.viewModel, primaryBindingRoot);
} catch (e) {
console.error('Error applying KO bindings to primary root:', e);
}
} else {
console.warn('No root element available for KO bindings.');
}
if (
modalContainer &&
modalContainer !== primaryBindingRoot &&
!primaryBindingRoot.contains(modalContainer)
) {
try {
ko.applyBindings(app.viewModel, modalContainer);
} catch (e) {
console.error('Error applying KO bindings to #modal-container:', e);
}
}

Copilot uses AI. Check for mistakes.
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');
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timeout warning message is misleading: the condition gating success checks both app.menus and app.menuModel.menuItems, but the warning always says only app.menus not found. This can send debugging in the wrong direction when menuModel is the missing piece. Suggest updating the warning to reflect both prerequisites (or log which one is still missing).

Suggested change
console.warn('app.menus not found after waiting:', maxWaitTime/1000, 'seconds');
var missingRequirements = [];
if (!(typeof app !== 'undefined' && app.hasOwnProperty('menus') && typeof app.menus === 'object')) {
missingRequirements.push('app.menus');
}
if (!(typeof app !== 'undefined' && app.hasOwnProperty('menuModel') && typeof app.menuModel !== 'undefined')) {
missingRequirements.push('app.menuModel');
} else if (!(app.menuModel.hasOwnProperty('menuItems') && typeof app.menuModel.menuItems === 'function')) {
missingRequirements.push('app.menuModel.menuItems');
}
console.warn('Required KO binding prerequisites not found after waiting:', maxWaitTime/1000, 'seconds. Missing:', missingRequirements.join(', '));

Copilot uses AI. Check for mistakes.
return false;
}
}

checkElement();
}

waitForMenusLoadToApplyKOBindings();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess Anna's work only removed Knockout from the data layer menu 😢 .


// app.viewModel.loadLayersFromServer().done(function() {
app.viewModel.initLeftNav().done(function () {
Expand Down
1 change: 0 additions & 1 deletion visualize/static/js/map.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
15 changes: 13 additions & 2 deletions visualize/static/js/models.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
Expand Down
117 changes: 91 additions & 26 deletions visualize/static/js/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -121,7 +120,6 @@ app.activateHashStateLayers = function() {
break;
}
}
}, 200);
}

app.updateHashStateLayers = function(id, status, visible) {
Expand All @@ -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) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

app._waitingForMapZoom is a great addition for improved performance!

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) {
Expand Down Expand Up @@ -258,41 +295,69 @@ 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) {
if ( $(window).width() < 768 || app.embeddedMap ) {
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' ) {
Expand Down
5 changes: 5 additions & 0 deletions visualize/templates/visualize/planner.html
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,12 @@

{% block outer_content %}

<div id="modal-container">
{% include "visualize/modals.html" %}
{% if user.is_authenticated %}
{% include "visualize/bookmark-modals.html" %}
{% endif %}
</div>

<div class="container-fluid" id="primary-content">
<div id="fullscreen">
Expand Down Expand Up @@ -394,6 +396,9 @@ <h4>
</div>
{% endblock %}

{% block footer %}
{% endblock %}

{% block extra_js %}
<script src="{% static 'amplify/lib/amplify.min.js' %}"></script>
<script src="{% static 'jquery-migrate/jquery-migrate.min.js' %}"></script>
Expand Down