From a996a7673a7190cbce5c9ea420384fb08db95ba1 Mon Sep 17 00:00:00 2001 From: Ryan Hodges Date: Thu, 23 Apr 2026 12:04:08 -0700 Subject: [PATCH 1/8] removing some setTimeout logic to fix inconsistent slider-from-bookmark loading --- visualize/static/js/app.js | 41 +- visualize/static/js/map.js | 1 - visualize/static/js/models.js | 9 +- visualize/static/js/state.js | 101 ++- .../js/wrappers/ol6/ols_ocean_labels_style.js | 719 ++++++++++++++++++ 5 files changed, 828 insertions(+), 43 deletions(-) create mode 100644 visualize/static/js/wrappers/ol6/ols_ocean_labels_style.js diff --git a/visualize/static/js/app.js b/visualize/static/js/app.js index d9f3dc61..1666c471 100644 --- a/visualize/static/js/app.js +++ b/visualize/static/js/app.js @@ -39,21 +39,32 @@ 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.map.zoom to exist before we can activate layers. +function waitForMenusLoadToApplyKOBindings(maxWaitTime) { + maxWaitTime = maxWaitTime || 5000; // Default 5 second max wait + var startTime = Date.now(); + + function checkElement() { + if ( + (typeof app !== 'undefined' && app.hasOwnProperty('menus') && typeof app.menus === 'object') + ) { + console.log('app.js - applying KO bindings to app.viewModel'); + ko.applyBindings(app.viewModel); + 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..0df96cd6 100644 --- a/visualize/static/js/models.js +++ b/visualize/static/js/models.js @@ -3472,14 +3472,21 @@ 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(); + layer._sliderBuilding = false; // Reset flag after completion }, 30) } catch (err) { console.log('pass: ' + layer ); + layer._sliderBuilding = false; // Reset flag on error too } } } diff --git a/visualize/static/js/state.js b/visualize/static/js/state.js index 9be68d39..2ea3d303 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,7 +143,30 @@ app.updateHashStateLayers = function(id, status, visible) { }); } - app.activateHashStateLayers(); + // Helper function to wait for element to exist, then activate layers + // replaces clunky setTimeout call; we need app.map.zoom to exist before we can activate layers. + function waitForMapLoad(maxWaitTime) { + maxWaitTime = maxWaitTime || 5000; // Default 5 second max wait + var startTime = Date.now(); + + function checkElement() { + if (typeof app !== 'undefined' && app.hasOwnProperty('map') && app.map.hasOwnProperty('zoom') && typeof app.map.zoom === 'function') { + app.activateHashStateLayers(); + } else if (Date.now() - startTime < maxWaitTime) { + setTimeout(checkElement, 50); // Check every 50ms + return false; + } else { + console.warn('map.zoom not found after waiting:', maxWaitTime/1000, 'seconds'); + return false; + } + } + + checkElement(); + } + + waitForMapLoad(); + + } @@ -258,21 +279,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 +297,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/static/js/wrappers/ol6/ols_ocean_labels_style.js b/visualize/static/js/wrappers/ol6/ols_ocean_labels_style.js new file mode 100644 index 00000000..e3390ab6 --- /dev/null +++ b/visualize/static/js/wrappers/ol6/ols_ocean_labels_style.js @@ -0,0 +1,719 @@ +let default_land_style = { + color: 'black', + outline_width: 2, + outline_color: 'white', + size: '12px', + font: '"Verdana"', + weight: 'normal', + wrap: false, +}; +let default_water_style = { + color: 'blue', + outline_width: 2, + outline_color: 'white', + size: '12px', + font: '"Open Sans"', + weight: 'normal', + wrap: false, +}; +let label_layers = { + 'Admin0 point': { // Country + color: 'rgba(150,150,150,0.7)', + outline_width: 0, + outline_color: 'rgba(0,0,0,0)', + size: '14px', + font: '"Open Sans"', + weight: 'bold', + wrap: false, + }, + 'Admin1 area/label': { //State + color: 'rgba(150,150,150,0.5)', + outline_width: 0, + outline_color: 'rgba(0,0,0,0)', + size: '14px', + font: '"Open Sans"', + weight: 'bold', + wrap: false, + }, + 'Admin1 forest or park/label': default_land_style, + 'Admin2 area/label': { // district/region + color: 'rgba(100,100,100,0.7)', + outline_width: 0, + outline_color: 'rgba(0,0,0,0)', + size: '14px', + font: '"Open Sans"', + weight: 'bold italic', + wrap: true, + special: [ + { + 'condition': ['resolution_below', 152.87], + 'change': [ + ['size', '18px'], + ['color', 'rgba(100,100,100,0.5)'] + ] + }, + ], + }, + 'Beach/label':{ // district/region + color: 'rgba(100,100,100,0.5)', + outline_width: 0, + outline_color: 'rgba(0,0,0,0)', + size: '14px', + font: '"Open Sans"', + weight: 'bold italic', + wrap: true, + }, + 'Continent': { //Continent + color: 'rgba(150,150,150,0.7)', + outline_width: 0, + outline_color: 'rgba(0,0,0,0)', + size: '16px', + font: '"Open Sans"', + weight: 'bold', + wrap: true, + }, + 'City large scale': { // Small City + color: 'rgba(80,80,80,0.5)', + outline_width: 0, + outline_color: 'rgba(0,0,0,0)', + size: '10px', + font: '"Open Sans"', + weight: 'normal', + wrap: false, + }, + 'City small scale': { // City + color: 'rgba(80,80,80,0.7)', + outline_width: 0, + outline_color: 'rgba(0,0,0,0)', + size: '10px', + font: '"Open Sans"', + weight: 'normal', + wrap: false, + }, + 'Disputed label point': default_land_style, + 'Indigenous/label':default_land_style, + 'land': default_land_style, + 'Landform/label': { //Landform + color: 'rgba(120,120,120,0.5)', + outline_width: 0, + outline_color: 'rgba(0,0,0,0)', + size: '16px', + font: '"Open Sans"', + weight: 'bold italic', + wrap: true, + }, + 'Marine area/label': { //Ocean or large sea + color: 'rgba(00,90,200,0.7)', + outline_width: 0, + outline_color: 'rgba(0,0,0,0)', + size: '12px', + font: 'Verdana', + weight: 'italic', + wrap: true, + special: [ + { + 'condition':['label', [ + 'basin', ] + ], + 'change': [ + ['color', 'rgba(100,100,100,0.7)'], + ['weight', 'italic'], + ['font', '"Open Sans"'] + ] + } + ], + }, + 'Marine waterbody/label': { //Ocean or large sea + color: 'rgba(00,90,200,0.7)', + outline_width: 0, + outline_color: 'rgba(0,0,0,0)', + size: '12px', + font: 'Verdana', + weight: 'italic', + wrap: true, + special: [ + { + 'condition': ['label', ['ocean',]], + 'change': [['size', '17px'],] + }, + ], + }, + 'Ocean area/label': { //Sea, basin, strait, or ridge + color: 'rgba(40,100,200,0.7)', + outline_width: 0, + outline_color: 'rgba(0,0,0,0)', + size: '11px', + font: 'Verdana', + weight: 'italic', + wrap: true, + special: [ + { + 'condition':['label', [ + 'basin', 'ridge', 'plain', + 'rise', 'trench', 'escarpment', + 'plateau', 'seamount', 'bank', + 'banque', 'canyon', 'shoal', + 'fracture zone', 'island', 'reef', + 'ledge'] + ], + 'change': [ + ['color', 'rgba(80,80,80,0.7)'], + ['weight', 'italic'], + ['font', '"Open Sans"'] + ] + }, + { + 'condition': ['resolution_below', 4892], //resolutions[5].min_resolution + 'change': [ + ['size', '16px'] + ] + } + ], + }, + 'Ocean point': { //Bathy feature/depth + color: 'rgba(0,0,0,0.5)', + outline_width: 0, + outline_color: 'rgba(0,0,0,0)', + size: '10px', + font: 'Verdana', + weight: 'normal', + wrap: false, + }, + 'Outdoors place': default_land_style, + 'Water area/label': { //water body + color: 'rgba(40,150,200,0.7)', + outline_width: 0, + outline_color: 'rgba(0,0,0,0)', + size: '12px', + font: 'Verdana', + weight: 'italic', + wrap: false, + special: [ + { + 'condition':['label', [ + 'basin', 'ridge', 'plain', + 'rise', 'trench', 'escarpment', + 'plateau', 'seamount', 'bank', + 'banque', 'canyon', 'shoal', + 'fracture zone', 'island', 'wetland', + ' rock', ' marsh'] + ], + 'change': [ + ['color', 'rgba(100,100,100,0.6)'], + ['weight', 'italic'], + ['font', '"Open Sans"'] + ] + }, + ], + }, + 'Water area large scale': { //small lake + color: 'rgba(40,150,200,0.7)', + outline_width: 0, + outline_color: 'rgba(0,0,0,0)', + size: '11px', + font: 'Verdana', + weight: 'italic', + wrap: false, + }, + 'Water area large scale/label': { //small lake + color: 'rgba(40,100,200,0.7)', + outline_width: 0, + outline_color: 'rgba(0,0,0,0)', + size: '12px', + font: 'Verdana', + weight: 'italic', + wrap: false, + }, + 'Water area small scale/label': { //large lake + color: 'rgba(40,150,200,0.7)', + outline_width: 0, + outline_color: 'rgba(0,0,0,0)', + size: '11px', + font: 'Verdana', + weight: 'italic', + wrap: false, + }, + 'Water line': { //river + color: 'rgba(40,150,200,0.5)', + outline_width: 0, + outline_color: 'rgba(0,0,0,0)', + size: '10px', + font: 'Verdana', + weight: 'italic', + wrap: false, + }, + 'Water line/label': { //river + color: 'rgba(40,150,200,0.5)', + outline_width: 0, + outline_color: 'rgba(0,0,0,0)', + size: '10px', + font: 'Verdana', + weight: 'italic', + wrap: false, + }, + 'Water line large scale/label': { //small river + color: 'rgba(40,150,200,0.7)', + outline_width: 0, + outline_color: 'rgba(0,0,0,0)', + size: '11px', + font: 'Verdana', + weight: 'italic', + wrap: false, + }, + 'Water line medium scale/label': { //large river + color: 'rgba(40,150,200,0.7)', + outline_width: 0, + outline_color: 'rgba(0,0,0,0)', + size: '11px', + font: 'Verdana', + weight: 'italic', + wrap: false, + }, + 'Water line small scale/label': default_water_style, + 'Water point/Sea or ocean': default_water_style, +} + +let resolutions = [ + { + zoom: 0, + min_resolution: 88270.96, + max_resolution: Infinity, + layers: [ + 'Continent', + 'Marine waterbody/label' + ] + }, + { + zoom: 1, + min_resolution: 44135.48, + max_resolution: 88270.96, + layers: [ + 'Continent', + 'Marine waterbody/label' + ] + }, + { + zoom: 2, + min_resolution: 39135.75, + max_resolution: 44135.48, + layers: [ + 'Continent', + 'Marine waterbody/label' + ] + }, + { + zoom: 3, + min_resolution: 19567.87, + max_resolution: 39135.75, + layers: [ + 'Continent', //Continent + 'Marine waterbody/label', //Ocean + 'Admin0 point', //Country + 'Ocean point', //bathy feature/depth + 'Ocean area/label', //Sea + ] + }, + { + zoom: 4, + min_resolution: 9783.93, + max_resolution: 19567.87, + layers: [ + 'Marine waterbody/label', //Ocean + 'Admin0 point', //Country + 'Ocean point', //bathy feature/depth + 'Ocean area/label', //Sea + 'City small scale', //City + ] + }, + { + zoom: 5, + min_resolution: 4891.96, + max_resolution: 9783.93, + layers: [ + 'Marine waterbody/label', //Ocean + 'Admin0 point', //Country + 'Ocean point', //bathy feature/depth + 'Ocean area/label', //Sea + 'City small scale', //City + 'Water area small scale/label', //large lakes + ] + }, + { + zoom: 6, + min_resolution: 2445.98, + max_resolution: 4891.96, + layers: [ + 'Marine waterbody/label', //Ocean + 'Admin0 point', //Country + 'Ocean point', //bathy feature/depth + 'Ocean area/label', //Sea + 'City small scale', //City + 'Water area small scale/label', //large lakes + ] + }, + { + zoom: 7, + min_resolution: 1222.99, + max_resolution: 2445.98, + layers: [ + 'Marine waterbody/label', //Ocean + 'Admin0 point', //Country + 'Ocean point', //bathy feature/depth + 'Ocean area/label', //Sea + 'City small scale', //City + 'Water area small scale/label', //large lakes + ] + }, + { + zoom: 8, + min_resolution: 611.49, + max_resolution: 1222.99, + layers: [ + 'Marine waterbody/label', //Ocean + 'Admin0 point', //Country + 'Ocean point', //bathy feature/depth + 'Ocean area/label', //Sea + 'City small scale', //City + 'Water area small scale/label', //large lakes + 'Admin1 area/label', //State + ] + }, + { + zoom: 9, + min_resolution: 305.74, + max_resolution: 611.49, + layers: [ + 'Marine waterbody/label', //Ocean + 'Admin0 point', //Country + 'Ocean point', //bathy feature/depth + 'Ocean area/label', //Sea + 'City small scale', //City + 'Water area small scale/label', //large lakes + 'Water line medium scale/label', //large rivers + 'Admin1 area/label', //State + 'Marine area/label', //Sounds + ] + }, + { + zoom: 10, + min_resolution: 152.87, + max_resolution: 305.74, + layers: [ + 'Marine waterbody/label', //Ocean + 'Admin0 point', //Country + 'Ocean point', //bathy feature/depth + 'Ocean area/label', //Sea + 'City small scale', //City + 'Water area small scale/label', //large lakes + 'Water line medium scale/label', //large rivers + 'Admin1 area/label', //State + 'Marine area/label', //Sounds + 'Water area large scale/label', //Small Lakes + ] + }, + { + zoom: 11, + min_resolution: 76.43, + max_resolution: 152.87, + layers: [ + 'Marine waterbody/label', //Ocean + 'Admin0 point', //Country + 'Ocean point', //bathy feature/depth + 'Ocean area/label', //Sea + 'City small scale', //City + 'City large scale', //small city + 'Water area small scale/label', //large lakes + 'Water line medium scale/label', //large rivers + 'Admin1 area/label', //State + 'Admin2 area/label', //districts/regions + 'Landform/label', //landforms + 'Marine area/label', //Sounds + 'Water area large scale/label', //Small Lakes + ] + }, + { + zoom: 12, + min_resolution: 38.21, + max_resolution: 76.43, + layers: [ + 'Marine waterbody/label', //Ocean + 'Admin0 point', //Country + 'Ocean point', //bathy feature/depth + 'Ocean area/label', //Sea + 'City large scale', //small city + 'Admin1 area/label', //State + 'Admin2 area/label', //districts/regions + 'Landform/label', //landforms + 'Marine area/label', //Sounds + 'Water area/label', // Water body + 'Water line/label', // River + 'Water area large scale/label', //Small Lakes + ] + }, + { + zoom: 13, + min_resolution: 19.10, + max_resolution: 38.21, + layers: [ + 'Marine waterbody/label', //Ocean + 'Admin0 point', //Country + 'Ocean point', //bathy feature/depth + 'Ocean area/label', //Sea + 'Admin1 area/label', //State + 'Admin2 area/label', //districts/regions + 'Landform/label', //landforms + 'Marine area/label', //Sounds + 'Water area/label', // Water body + 'Water line/label', // River + 'City large scale', //small city + 'Water area large scale/label', //Small Lakes + ] + }, + { + zoom: 14, + min_resolution: 9.55, + max_resolution: 19.10, + layers: [ + 'Marine waterbody/label', //Ocean + 'Admin0 point', //Country + 'Ocean point', //bathy feature/depth + 'Ocean area/label', //Sea + 'Admin1 area/label', //State + 'Admin2 area/label', //districts/regions + 'Landform/label', //landforms + 'Marine area/label', //Sounds + 'Water area/label', // Water body + 'Water line/label', // River + 'City large scale', //small city + 'Water area large scale/label', //Small Lakes + ] + }, + { + zoom: 15, + min_resolution: 4.777, + max_resolution: 9.55, + layers: [ + 'Marine waterbody/label', //Ocean + 'Admin0 point', //Country + 'Ocean point', //bathy feature/depth + 'Ocean area/label', //Sea + 'Admin1 area/label', //State + 'Admin2 area/label', //districts/regions + 'Landform/label', //landforms + 'Marine area/label', //Sounds + 'Water area/label', // Water body + 'Water line/label', // River + 'City large scale', //small city + 'Beach/label', //Beach + 'Water area large scale/label', //Small Lakes + ] + }, + { + zoom: 16, + min_resolution: 2.022, + max_resolution: 4.777, + layers: [ + 'Marine waterbody/label', //Ocean + 'Admin0 point', //Country + 'Ocean point', //bathy feature/depth + 'Ocean area/label', //Sea + 'Admin1 area/label', //State + 'Admin2 area/label', //districts/regions + 'Landform/label', //landforms + 'Marine area/label', //Sounds + 'Water area/label', // Water body + 'Water line/label', // River + 'City large scale', //small city + 'Beach/label', //Beach + 'Water area large scale/label', //Small Lakes + ] + }, + { + zoom: 17, + min_resolution: 1.194, + max_resolution: 2.388, // 1.55422975 + layers: [] + }, + { + zoom: 18, + min_resolution: 0.597, + max_resolution: 1.194, + layers: [] + }, + { + zoom: 19, + min_resolution: 0, + max_resolution: 0.597, + layers: [] + }, +] + +/* + showDepth: Determine if a feature's "minzoom" is allowed to be displayed + at the curent resolution +*/ +let showDepth = function(resolution, minzoom) { + if (resolution < 15) { + return true; + } else if (resolution <= 20) { + return (minzoom <= 140 ? true : false); + } else if (resolution <= 25) { + return (minzoom <= 130 ? true : false); + } else if (resolution <= 50) { + return (minzoom <= 120 ? true : false); + } else if (resolution <= 150) { + return (minzoom <= 110 ? true : false); + } else if (resolution <= 350) { + return (minzoom <= 100 ? true : false); + } else if (resolution <= 750) { + return (minzoom <= 90 ? true : false); + } else if (resolution <= 1400) { + return (minzoom <= 80 ? true : false); + } else if (resolution <= 1500) { + return (minzoom <= 70 ? true : false); + } else if (resolution <= 3000) { + return (minzoom <= 60 ? true : false); + } else if (resolution <= 5000) { + return (minzoom <= 50 ? true : false); + } + return true; +} + +/* + getFeatureName: Attempt to get an appropriate label from a feature +*/ +let getFeatureName = function(feature) { + let name = feature.get('_name'); + if (!name || name === undefined) { + name = feature.get('_name_en'); + } + if (!name || name === undefined) { + name = feature.get('_name_global'); + } + return name; +} + +/* + getResolutionLayers: Loop through the resolutions list to find which + layers should be associated with the current resolution and + return them. +*/ +let getResolutionLayers = function(resolution) { + for (let x of resolutions) { + if (resolution < x.max_resolution && resolution >= x.min_resolution) { + return x.layers; + } + } + return []; +} + +/* + applySpecialStyle: If a style has some dependencies, read the + definitions and apply as needed. +*/ +let applySpecialStyle = function(label, resolution, style, rule) { + let active = false; + switch(rule.condition[0]) { + case 'label': + for (match of rule.condition[1]){ + if (label.toLowerCase().indexOf(match.toLowerCase()) >= 0){ + active = true; + } + } + break; + case 'resolution_below': + if (resolution < rule.condition[1]){ + active = true; + } + break; + default: + active = false; + } + if (active){ + for (change of rule.change) { + style[change[0]] = change[1]; + } + } + return style; +}; + +/* + buildOceanLabelStyle: Given a feature and the current map resolution, + determine whether it should be displayed, apply a style + and add it to the map. +*/ +let buildOceanLabelStyle = function(feature, resolution) { + let layer = feature.get('layer'); + let label = getFeatureName(feature); + let minzoom = feature.get('_minzoom'); + let label_class = feature.get('_label_class'); + let visible_layers = getResolutionLayers(resolution); + if (visible_layers.indexOf(layer) >= 0) { + let style = structuredClone(label_layers[layer]); + if (style.special) { + for (rule of style.special) { + style = applySpecialStyle(label, resolution, style, rule); + } + } + if (style.wrap) { + label = stringDivider(label, 12, '\n'); + } + if(!showDepth(resolution, minzoom)) { + // console.log('[BLOCKED] ' + layer + ' - ' + label + ' - ' + resolution + ' - mz: ' + minzoom); + return null; + } + // console.log('[VISIBLE] ' + layer + ' - ' + label + ' - ' + resolution + ' - mz: ' + minzoom + '; cls: ' + label_class); + return new ol.style.Text({ + text: label, + font: style.weight + ' ' + style.size + ' ' + style.font, + fill: new ol.style.Fill({ + color: style.color + }), + stroke: new ol.style.Stroke({ + color: style.outline_color, + width: style.outline_width, + }), + }); + } + return null; +} + +/* + oceanLabelStyleFunction: Given a feature and a resolution, + make all features invisible, then pass details on to build + styles for the text labels +*/ +let oceanLabelStyleFunction = function(feature, resolution) { + let label_style = new ol.style.Style({ + stroke: new ol.style.Stroke({ + width: 1, + color: 'rgba(255,0,0,0)' + }), + fill: new ol.style.Fill({ + color: 'rgba(0,255,0,0)' + }), + text: buildOceanLabelStyle(feature, resolution) + }); + return label_style; +} + +// https://stackoverflow.com/questions/14484787/wrap-text-in-javascript +function stringDivider(str, width, spaceReplacer) { + if (str.length > width) { + let p = width; + while (p > 0 && str[p] != ' ' && str[p] != '-') { + p--; + } + if (p > 0) { + let left; + if (str.substring(p, p + 1) == '-') { + left = str.substring(0, p + 1); + } else { + left = str.substring(0, p); + } + const right = str.substring(p + 1); + return left + spaceReplacer + stringDivider(right, width, spaceReplacer); + } + } + return str; + } \ No newline at end of file From 565dc5d0afb07696b70f83180d039fc3d3a45854 Mon Sep 17 00:00:00 2001 From: Ryan Hodges Date: Thu, 23 Apr 2026 16:32:42 -0700 Subject: [PATCH 2/8] knockout binding wait for menuItems, longer allowed wait time --- visualize/static/js/app.js | 11 ++++++++--- visualize/static/js/state.js | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/visualize/static/js/app.js b/visualize/static/js/app.js index 1666c471..43ea0ff2 100644 --- a/visualize/static/js/app.js +++ b/visualize/static/js/app.js @@ -48,10 +48,15 @@ function waitForMenusLoadToApplyKOBindings(maxWaitTime) { function checkElement() { if ( (typeof app !== 'undefined' && app.hasOwnProperty('menus') && typeof app.menus === 'object') + && (app.hasOwnProperty('menuModel') && app.menuModel.hasOwnProperty('menuItems') && typeof app.menuModel.menuItems === 'function') ) { - console.log('app.js - applying KO bindings to app.viewModel'); - ko.applyBindings(app.viewModel); - postKOBindingCleanup(); + try { + ko.applyBindings(app.viewModel); + postKOBindingCleanup(); + } catch (e) { + console.warn('Error applying KO bindings:', e); + console.warn('if this is about "cannot apply bindings multiple times to the same element", this is expected. menuModel should already have been applied to the context menu'); + } } else if (Date.now() - startTime < maxWaitTime) { setTimeout(checkElement, 50); // Check every 50ms return false; diff --git a/visualize/static/js/state.js b/visualize/static/js/state.js index 2ea3d303..01b09b77 100644 --- a/visualize/static/js/state.js +++ b/visualize/static/js/state.js @@ -146,7 +146,7 @@ app.updateHashStateLayers = function(id, status, visible) { // Helper function to wait for element to exist, then activate layers // replaces clunky setTimeout call; we need app.map.zoom to exist before we can activate layers. function waitForMapLoad(maxWaitTime) { - maxWaitTime = maxWaitTime || 5000; // Default 5 second max wait + maxWaitTime = maxWaitTime || 20000; // Default 20 second max wait var startTime = Date.now(); function checkElement() { From 8d8eb63365e1eaeeb8440bc2951cf77997a77edb Mon Sep 17 00:00:00 2001 From: Ryan Hodges Date: Thu, 23 Apr 2026 16:36:38 -0700 Subject: [PATCH 3/8] upping app load wait allowance to 20 seconds for slow-butt Chrome --- visualize/static/js/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/visualize/static/js/app.js b/visualize/static/js/app.js index 43ea0ff2..62925b9b 100644 --- a/visualize/static/js/app.js +++ b/visualize/static/js/app.js @@ -42,7 +42,7 @@ let postKOBindingCleanup = function() { // Helper function to wait for element to exist, then activate layers // replaces clunky setTimeout call; we need app.map.zoom to exist before we can activate layers. function waitForMenusLoadToApplyKOBindings(maxWaitTime) { - maxWaitTime = maxWaitTime || 5000; // Default 5 second max wait + maxWaitTime = maxWaitTime || 20000; // Default 20 second max wait var startTime = Date.now(); function checkElement() { From 2f510719eda1ecb54433d4501e26eae84b233819 Mon Sep 17 00:00:00 2001 From: Ryan Hodges Date: Thu, 23 Apr 2026 16:51:17 -0700 Subject: [PATCH 4/8] small change to ensure binding cleanup runs --- visualize/static/js/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/visualize/static/js/app.js b/visualize/static/js/app.js index 62925b9b..a3b30266 100644 --- a/visualize/static/js/app.js +++ b/visualize/static/js/app.js @@ -52,11 +52,11 @@ function waitForMenusLoadToApplyKOBindings(maxWaitTime) { ) { try { ko.applyBindings(app.viewModel); - postKOBindingCleanup(); } catch (e) { console.warn('Error applying KO bindings:', e); console.warn('if this is about "cannot apply bindings multiple times to the same element", this is expected. menuModel should already have been applied to the context menu'); } + postKOBindingCleanup(); } else if (Date.now() - startTime < maxWaitTime) { setTimeout(checkElement, 50); // Check every 50ms return false; From 518a5c264e42221aeecb320523ede2ca7fde0198 Mon Sep 17 00:00:00 2001 From: Ryan Hodges Date: Fri, 24 Apr 2026 08:42:26 -0700 Subject: [PATCH 5/8] removing accidentally included, benign file from branch --- .../js/wrappers/ol6/ols_ocean_labels_style.js | 719 ------------------ 1 file changed, 719 deletions(-) delete mode 100644 visualize/static/js/wrappers/ol6/ols_ocean_labels_style.js diff --git a/visualize/static/js/wrappers/ol6/ols_ocean_labels_style.js b/visualize/static/js/wrappers/ol6/ols_ocean_labels_style.js deleted file mode 100644 index e3390ab6..00000000 --- a/visualize/static/js/wrappers/ol6/ols_ocean_labels_style.js +++ /dev/null @@ -1,719 +0,0 @@ -let default_land_style = { - color: 'black', - outline_width: 2, - outline_color: 'white', - size: '12px', - font: '"Verdana"', - weight: 'normal', - wrap: false, -}; -let default_water_style = { - color: 'blue', - outline_width: 2, - outline_color: 'white', - size: '12px', - font: '"Open Sans"', - weight: 'normal', - wrap: false, -}; -let label_layers = { - 'Admin0 point': { // Country - color: 'rgba(150,150,150,0.7)', - outline_width: 0, - outline_color: 'rgba(0,0,0,0)', - size: '14px', - font: '"Open Sans"', - weight: 'bold', - wrap: false, - }, - 'Admin1 area/label': { //State - color: 'rgba(150,150,150,0.5)', - outline_width: 0, - outline_color: 'rgba(0,0,0,0)', - size: '14px', - font: '"Open Sans"', - weight: 'bold', - wrap: false, - }, - 'Admin1 forest or park/label': default_land_style, - 'Admin2 area/label': { // district/region - color: 'rgba(100,100,100,0.7)', - outline_width: 0, - outline_color: 'rgba(0,0,0,0)', - size: '14px', - font: '"Open Sans"', - weight: 'bold italic', - wrap: true, - special: [ - { - 'condition': ['resolution_below', 152.87], - 'change': [ - ['size', '18px'], - ['color', 'rgba(100,100,100,0.5)'] - ] - }, - ], - }, - 'Beach/label':{ // district/region - color: 'rgba(100,100,100,0.5)', - outline_width: 0, - outline_color: 'rgba(0,0,0,0)', - size: '14px', - font: '"Open Sans"', - weight: 'bold italic', - wrap: true, - }, - 'Continent': { //Continent - color: 'rgba(150,150,150,0.7)', - outline_width: 0, - outline_color: 'rgba(0,0,0,0)', - size: '16px', - font: '"Open Sans"', - weight: 'bold', - wrap: true, - }, - 'City large scale': { // Small City - color: 'rgba(80,80,80,0.5)', - outline_width: 0, - outline_color: 'rgba(0,0,0,0)', - size: '10px', - font: '"Open Sans"', - weight: 'normal', - wrap: false, - }, - 'City small scale': { // City - color: 'rgba(80,80,80,0.7)', - outline_width: 0, - outline_color: 'rgba(0,0,0,0)', - size: '10px', - font: '"Open Sans"', - weight: 'normal', - wrap: false, - }, - 'Disputed label point': default_land_style, - 'Indigenous/label':default_land_style, - 'land': default_land_style, - 'Landform/label': { //Landform - color: 'rgba(120,120,120,0.5)', - outline_width: 0, - outline_color: 'rgba(0,0,0,0)', - size: '16px', - font: '"Open Sans"', - weight: 'bold italic', - wrap: true, - }, - 'Marine area/label': { //Ocean or large sea - color: 'rgba(00,90,200,0.7)', - outline_width: 0, - outline_color: 'rgba(0,0,0,0)', - size: '12px', - font: 'Verdana', - weight: 'italic', - wrap: true, - special: [ - { - 'condition':['label', [ - 'basin', ] - ], - 'change': [ - ['color', 'rgba(100,100,100,0.7)'], - ['weight', 'italic'], - ['font', '"Open Sans"'] - ] - } - ], - }, - 'Marine waterbody/label': { //Ocean or large sea - color: 'rgba(00,90,200,0.7)', - outline_width: 0, - outline_color: 'rgba(0,0,0,0)', - size: '12px', - font: 'Verdana', - weight: 'italic', - wrap: true, - special: [ - { - 'condition': ['label', ['ocean',]], - 'change': [['size', '17px'],] - }, - ], - }, - 'Ocean area/label': { //Sea, basin, strait, or ridge - color: 'rgba(40,100,200,0.7)', - outline_width: 0, - outline_color: 'rgba(0,0,0,0)', - size: '11px', - font: 'Verdana', - weight: 'italic', - wrap: true, - special: [ - { - 'condition':['label', [ - 'basin', 'ridge', 'plain', - 'rise', 'trench', 'escarpment', - 'plateau', 'seamount', 'bank', - 'banque', 'canyon', 'shoal', - 'fracture zone', 'island', 'reef', - 'ledge'] - ], - 'change': [ - ['color', 'rgba(80,80,80,0.7)'], - ['weight', 'italic'], - ['font', '"Open Sans"'] - ] - }, - { - 'condition': ['resolution_below', 4892], //resolutions[5].min_resolution - 'change': [ - ['size', '16px'] - ] - } - ], - }, - 'Ocean point': { //Bathy feature/depth - color: 'rgba(0,0,0,0.5)', - outline_width: 0, - outline_color: 'rgba(0,0,0,0)', - size: '10px', - font: 'Verdana', - weight: 'normal', - wrap: false, - }, - 'Outdoors place': default_land_style, - 'Water area/label': { //water body - color: 'rgba(40,150,200,0.7)', - outline_width: 0, - outline_color: 'rgba(0,0,0,0)', - size: '12px', - font: 'Verdana', - weight: 'italic', - wrap: false, - special: [ - { - 'condition':['label', [ - 'basin', 'ridge', 'plain', - 'rise', 'trench', 'escarpment', - 'plateau', 'seamount', 'bank', - 'banque', 'canyon', 'shoal', - 'fracture zone', 'island', 'wetland', - ' rock', ' marsh'] - ], - 'change': [ - ['color', 'rgba(100,100,100,0.6)'], - ['weight', 'italic'], - ['font', '"Open Sans"'] - ] - }, - ], - }, - 'Water area large scale': { //small lake - color: 'rgba(40,150,200,0.7)', - outline_width: 0, - outline_color: 'rgba(0,0,0,0)', - size: '11px', - font: 'Verdana', - weight: 'italic', - wrap: false, - }, - 'Water area large scale/label': { //small lake - color: 'rgba(40,100,200,0.7)', - outline_width: 0, - outline_color: 'rgba(0,0,0,0)', - size: '12px', - font: 'Verdana', - weight: 'italic', - wrap: false, - }, - 'Water area small scale/label': { //large lake - color: 'rgba(40,150,200,0.7)', - outline_width: 0, - outline_color: 'rgba(0,0,0,0)', - size: '11px', - font: 'Verdana', - weight: 'italic', - wrap: false, - }, - 'Water line': { //river - color: 'rgba(40,150,200,0.5)', - outline_width: 0, - outline_color: 'rgba(0,0,0,0)', - size: '10px', - font: 'Verdana', - weight: 'italic', - wrap: false, - }, - 'Water line/label': { //river - color: 'rgba(40,150,200,0.5)', - outline_width: 0, - outline_color: 'rgba(0,0,0,0)', - size: '10px', - font: 'Verdana', - weight: 'italic', - wrap: false, - }, - 'Water line large scale/label': { //small river - color: 'rgba(40,150,200,0.7)', - outline_width: 0, - outline_color: 'rgba(0,0,0,0)', - size: '11px', - font: 'Verdana', - weight: 'italic', - wrap: false, - }, - 'Water line medium scale/label': { //large river - color: 'rgba(40,150,200,0.7)', - outline_width: 0, - outline_color: 'rgba(0,0,0,0)', - size: '11px', - font: 'Verdana', - weight: 'italic', - wrap: false, - }, - 'Water line small scale/label': default_water_style, - 'Water point/Sea or ocean': default_water_style, -} - -let resolutions = [ - { - zoom: 0, - min_resolution: 88270.96, - max_resolution: Infinity, - layers: [ - 'Continent', - 'Marine waterbody/label' - ] - }, - { - zoom: 1, - min_resolution: 44135.48, - max_resolution: 88270.96, - layers: [ - 'Continent', - 'Marine waterbody/label' - ] - }, - { - zoom: 2, - min_resolution: 39135.75, - max_resolution: 44135.48, - layers: [ - 'Continent', - 'Marine waterbody/label' - ] - }, - { - zoom: 3, - min_resolution: 19567.87, - max_resolution: 39135.75, - layers: [ - 'Continent', //Continent - 'Marine waterbody/label', //Ocean - 'Admin0 point', //Country - 'Ocean point', //bathy feature/depth - 'Ocean area/label', //Sea - ] - }, - { - zoom: 4, - min_resolution: 9783.93, - max_resolution: 19567.87, - layers: [ - 'Marine waterbody/label', //Ocean - 'Admin0 point', //Country - 'Ocean point', //bathy feature/depth - 'Ocean area/label', //Sea - 'City small scale', //City - ] - }, - { - zoom: 5, - min_resolution: 4891.96, - max_resolution: 9783.93, - layers: [ - 'Marine waterbody/label', //Ocean - 'Admin0 point', //Country - 'Ocean point', //bathy feature/depth - 'Ocean area/label', //Sea - 'City small scale', //City - 'Water area small scale/label', //large lakes - ] - }, - { - zoom: 6, - min_resolution: 2445.98, - max_resolution: 4891.96, - layers: [ - 'Marine waterbody/label', //Ocean - 'Admin0 point', //Country - 'Ocean point', //bathy feature/depth - 'Ocean area/label', //Sea - 'City small scale', //City - 'Water area small scale/label', //large lakes - ] - }, - { - zoom: 7, - min_resolution: 1222.99, - max_resolution: 2445.98, - layers: [ - 'Marine waterbody/label', //Ocean - 'Admin0 point', //Country - 'Ocean point', //bathy feature/depth - 'Ocean area/label', //Sea - 'City small scale', //City - 'Water area small scale/label', //large lakes - ] - }, - { - zoom: 8, - min_resolution: 611.49, - max_resolution: 1222.99, - layers: [ - 'Marine waterbody/label', //Ocean - 'Admin0 point', //Country - 'Ocean point', //bathy feature/depth - 'Ocean area/label', //Sea - 'City small scale', //City - 'Water area small scale/label', //large lakes - 'Admin1 area/label', //State - ] - }, - { - zoom: 9, - min_resolution: 305.74, - max_resolution: 611.49, - layers: [ - 'Marine waterbody/label', //Ocean - 'Admin0 point', //Country - 'Ocean point', //bathy feature/depth - 'Ocean area/label', //Sea - 'City small scale', //City - 'Water area small scale/label', //large lakes - 'Water line medium scale/label', //large rivers - 'Admin1 area/label', //State - 'Marine area/label', //Sounds - ] - }, - { - zoom: 10, - min_resolution: 152.87, - max_resolution: 305.74, - layers: [ - 'Marine waterbody/label', //Ocean - 'Admin0 point', //Country - 'Ocean point', //bathy feature/depth - 'Ocean area/label', //Sea - 'City small scale', //City - 'Water area small scale/label', //large lakes - 'Water line medium scale/label', //large rivers - 'Admin1 area/label', //State - 'Marine area/label', //Sounds - 'Water area large scale/label', //Small Lakes - ] - }, - { - zoom: 11, - min_resolution: 76.43, - max_resolution: 152.87, - layers: [ - 'Marine waterbody/label', //Ocean - 'Admin0 point', //Country - 'Ocean point', //bathy feature/depth - 'Ocean area/label', //Sea - 'City small scale', //City - 'City large scale', //small city - 'Water area small scale/label', //large lakes - 'Water line medium scale/label', //large rivers - 'Admin1 area/label', //State - 'Admin2 area/label', //districts/regions - 'Landform/label', //landforms - 'Marine area/label', //Sounds - 'Water area large scale/label', //Small Lakes - ] - }, - { - zoom: 12, - min_resolution: 38.21, - max_resolution: 76.43, - layers: [ - 'Marine waterbody/label', //Ocean - 'Admin0 point', //Country - 'Ocean point', //bathy feature/depth - 'Ocean area/label', //Sea - 'City large scale', //small city - 'Admin1 area/label', //State - 'Admin2 area/label', //districts/regions - 'Landform/label', //landforms - 'Marine area/label', //Sounds - 'Water area/label', // Water body - 'Water line/label', // River - 'Water area large scale/label', //Small Lakes - ] - }, - { - zoom: 13, - min_resolution: 19.10, - max_resolution: 38.21, - layers: [ - 'Marine waterbody/label', //Ocean - 'Admin0 point', //Country - 'Ocean point', //bathy feature/depth - 'Ocean area/label', //Sea - 'Admin1 area/label', //State - 'Admin2 area/label', //districts/regions - 'Landform/label', //landforms - 'Marine area/label', //Sounds - 'Water area/label', // Water body - 'Water line/label', // River - 'City large scale', //small city - 'Water area large scale/label', //Small Lakes - ] - }, - { - zoom: 14, - min_resolution: 9.55, - max_resolution: 19.10, - layers: [ - 'Marine waterbody/label', //Ocean - 'Admin0 point', //Country - 'Ocean point', //bathy feature/depth - 'Ocean area/label', //Sea - 'Admin1 area/label', //State - 'Admin2 area/label', //districts/regions - 'Landform/label', //landforms - 'Marine area/label', //Sounds - 'Water area/label', // Water body - 'Water line/label', // River - 'City large scale', //small city - 'Water area large scale/label', //Small Lakes - ] - }, - { - zoom: 15, - min_resolution: 4.777, - max_resolution: 9.55, - layers: [ - 'Marine waterbody/label', //Ocean - 'Admin0 point', //Country - 'Ocean point', //bathy feature/depth - 'Ocean area/label', //Sea - 'Admin1 area/label', //State - 'Admin2 area/label', //districts/regions - 'Landform/label', //landforms - 'Marine area/label', //Sounds - 'Water area/label', // Water body - 'Water line/label', // River - 'City large scale', //small city - 'Beach/label', //Beach - 'Water area large scale/label', //Small Lakes - ] - }, - { - zoom: 16, - min_resolution: 2.022, - max_resolution: 4.777, - layers: [ - 'Marine waterbody/label', //Ocean - 'Admin0 point', //Country - 'Ocean point', //bathy feature/depth - 'Ocean area/label', //Sea - 'Admin1 area/label', //State - 'Admin2 area/label', //districts/regions - 'Landform/label', //landforms - 'Marine area/label', //Sounds - 'Water area/label', // Water body - 'Water line/label', // River - 'City large scale', //small city - 'Beach/label', //Beach - 'Water area large scale/label', //Small Lakes - ] - }, - { - zoom: 17, - min_resolution: 1.194, - max_resolution: 2.388, // 1.55422975 - layers: [] - }, - { - zoom: 18, - min_resolution: 0.597, - max_resolution: 1.194, - layers: [] - }, - { - zoom: 19, - min_resolution: 0, - max_resolution: 0.597, - layers: [] - }, -] - -/* - showDepth: Determine if a feature's "minzoom" is allowed to be displayed - at the curent resolution -*/ -let showDepth = function(resolution, minzoom) { - if (resolution < 15) { - return true; - } else if (resolution <= 20) { - return (minzoom <= 140 ? true : false); - } else if (resolution <= 25) { - return (minzoom <= 130 ? true : false); - } else if (resolution <= 50) { - return (minzoom <= 120 ? true : false); - } else if (resolution <= 150) { - return (minzoom <= 110 ? true : false); - } else if (resolution <= 350) { - return (minzoom <= 100 ? true : false); - } else if (resolution <= 750) { - return (minzoom <= 90 ? true : false); - } else if (resolution <= 1400) { - return (minzoom <= 80 ? true : false); - } else if (resolution <= 1500) { - return (minzoom <= 70 ? true : false); - } else if (resolution <= 3000) { - return (minzoom <= 60 ? true : false); - } else if (resolution <= 5000) { - return (minzoom <= 50 ? true : false); - } - return true; -} - -/* - getFeatureName: Attempt to get an appropriate label from a feature -*/ -let getFeatureName = function(feature) { - let name = feature.get('_name'); - if (!name || name === undefined) { - name = feature.get('_name_en'); - } - if (!name || name === undefined) { - name = feature.get('_name_global'); - } - return name; -} - -/* - getResolutionLayers: Loop through the resolutions list to find which - layers should be associated with the current resolution and - return them. -*/ -let getResolutionLayers = function(resolution) { - for (let x of resolutions) { - if (resolution < x.max_resolution && resolution >= x.min_resolution) { - return x.layers; - } - } - return []; -} - -/* - applySpecialStyle: If a style has some dependencies, read the - definitions and apply as needed. -*/ -let applySpecialStyle = function(label, resolution, style, rule) { - let active = false; - switch(rule.condition[0]) { - case 'label': - for (match of rule.condition[1]){ - if (label.toLowerCase().indexOf(match.toLowerCase()) >= 0){ - active = true; - } - } - break; - case 'resolution_below': - if (resolution < rule.condition[1]){ - active = true; - } - break; - default: - active = false; - } - if (active){ - for (change of rule.change) { - style[change[0]] = change[1]; - } - } - return style; -}; - -/* - buildOceanLabelStyle: Given a feature and the current map resolution, - determine whether it should be displayed, apply a style - and add it to the map. -*/ -let buildOceanLabelStyle = function(feature, resolution) { - let layer = feature.get('layer'); - let label = getFeatureName(feature); - let minzoom = feature.get('_minzoom'); - let label_class = feature.get('_label_class'); - let visible_layers = getResolutionLayers(resolution); - if (visible_layers.indexOf(layer) >= 0) { - let style = structuredClone(label_layers[layer]); - if (style.special) { - for (rule of style.special) { - style = applySpecialStyle(label, resolution, style, rule); - } - } - if (style.wrap) { - label = stringDivider(label, 12, '\n'); - } - if(!showDepth(resolution, minzoom)) { - // console.log('[BLOCKED] ' + layer + ' - ' + label + ' - ' + resolution + ' - mz: ' + minzoom); - return null; - } - // console.log('[VISIBLE] ' + layer + ' - ' + label + ' - ' + resolution + ' - mz: ' + minzoom + '; cls: ' + label_class); - return new ol.style.Text({ - text: label, - font: style.weight + ' ' + style.size + ' ' + style.font, - fill: new ol.style.Fill({ - color: style.color - }), - stroke: new ol.style.Stroke({ - color: style.outline_color, - width: style.outline_width, - }), - }); - } - return null; -} - -/* - oceanLabelStyleFunction: Given a feature and a resolution, - make all features invisible, then pass details on to build - styles for the text labels -*/ -let oceanLabelStyleFunction = function(feature, resolution) { - let label_style = new ol.style.Style({ - stroke: new ol.style.Stroke({ - width: 1, - color: 'rgba(255,0,0,0)' - }), - fill: new ol.style.Fill({ - color: 'rgba(0,255,0,0)' - }), - text: buildOceanLabelStyle(feature, resolution) - }); - return label_style; -} - -// https://stackoverflow.com/questions/14484787/wrap-text-in-javascript -function stringDivider(str, width, spaceReplacer) { - if (str.length > width) { - let p = width; - while (p > 0 && str[p] != ' ' && str[p] != '-') { - p--; - } - if (p > 0) { - let left; - if (str.substring(p, p + 1) == '-') { - left = str.substring(0, p + 1); - } else { - left = str.substring(0, p); - } - const right = str.substring(p + 1); - return left + spaceReplacer + stringDivider(right, width, spaceReplacer); - } - } - return str; - } \ No newline at end of file From e034b945feb20ed1f08954af4cc126e9a62c41c8 Mon Sep 17 00:00:00 2001 From: Ryan Hodges Date: Fri, 24 Apr 2026 15:25:19 -0700 Subject: [PATCH 6/8] implement several suggested fixes from copilot --- visualize/static/js/app.js | 4 ++-- visualize/static/js/models.js | 10 +++++++--- visualize/static/js/state.js | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/visualize/static/js/app.js b/visualize/static/js/app.js index a3b30266..6ee528df 100644 --- a/visualize/static/js/app.js +++ b/visualize/static/js/app.js @@ -40,7 +40,7 @@ let postKOBindingCleanup = function() { } // Helper function to wait for element to exist, then activate layers -// replaces clunky setTimeout call; we need app.map.zoom to exist before we can 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(); @@ -48,7 +48,7 @@ function waitForMenusLoadToApplyKOBindings(maxWaitTime) { function checkElement() { if ( (typeof app !== 'undefined' && app.hasOwnProperty('menus') && typeof app.menus === 'object') - && (app.hasOwnProperty('menuModel') && app.menuModel.hasOwnProperty('menuItems') && typeof app.menuModel.menuItems === 'function') + && (app.hasOwnProperty('menuModel') && typeof app.menuModel !== 'undefined' && app.menuModel.hasOwnProperty('menuItems') && typeof app.menuModel.menuItems === 'function') ) { try { ko.applyBindings(app.viewModel); diff --git a/visualize/static/js/models.js b/visualize/static/js/models.js index 0df96cd6..12345f1a 100644 --- a/visualize/static/js/models.js +++ b/visualize/static/js/models.js @@ -3480,13 +3480,17 @@ function viewModel() { layer._sliderBuilding = true; // Flag to prevent duplicate calls try { setTimeout(function() { - layer.buildMultilayerValueLookup(); - layer._sliderBuilding = false; // Reset flag after completion + 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 on error too + 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 01b09b77..26a96f55 100644 --- a/visualize/static/js/state.js +++ b/visualize/static/js/state.js @@ -150,7 +150,7 @@ app.updateHashStateLayers = function(id, status, visible) { var startTime = Date.now(); function checkElement() { - if (typeof app !== 'undefined' && app.hasOwnProperty('map') && app.map.hasOwnProperty('zoom') && typeof app.map.zoom === 'function') { + if (typeof app !== 'undefined' && app.hasOwnProperty('map') && typeof app.map !== 'undefined' && app.map.hasOwnProperty('zoom') && typeof app.map.zoom === 'function') { app.activateHashStateLayers(); } else if (Date.now() - startTime < maxWaitTime) { setTimeout(checkElement, 50); // Check every 50ms From d9124e264c3e4fca32eeb053e9eb3daa25ea1d3d Mon Sep 17 00:00:00 2001 From: Ryan Hodges Date: Fri, 24 Apr 2026 15:51:18 -0700 Subject: [PATCH 7/8] Copilot: prevent buildup of layers waiting for timeout on load Verbose, but nice for these edge cases! Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- visualize/static/js/state.js | 58 +++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/visualize/static/js/state.js b/visualize/static/js/state.js index 26a96f55..5b4b4c2c 100644 --- a/visualize/static/js/state.js +++ b/visualize/static/js/state.js @@ -143,31 +143,47 @@ app.updateHashStateLayers = function(id, status, visible) { }); } - // Helper function to wait for element to exist, then activate layers - // replaces clunky setTimeout call; we need app.map.zoom to exist before we can activate layers. - function waitForMapLoad(maxWaitTime) { - maxWaitTime = maxWaitTime || 20000; // Default 20 second max wait - var startTime = Date.now(); - - function checkElement() { - if (typeof app !== 'undefined' && app.hasOwnProperty('map') && typeof app.map !== 'undefined' && app.map.hasOwnProperty('zoom') && typeof app.map.zoom === 'function') { - app.activateHashStateLayers(); - } else if (Date.now() - startTime < maxWaitTime) { - setTimeout(checkElement, 50); // Check every 50ms - return false; - } else { - console.warn('map.zoom not found after waiting:', maxWaitTime/1000, 'seconds'); - return false; - } - } - - checkElement(); + // 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; } - waitForMapLoad(); + 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) { From fa2d09f5c92ab0b8508424164ea160e74c28933d Mon Sep 17 00:00:00 2001 From: Ryan Hodges Date: Fri, 24 Apr 2026 16:22:31 -0700 Subject: [PATCH 8/8] FINALLY resolving knockout binding overlap fixing missing sharing groups issue; removing unused footer --- visualize/static/js/app.js | 10 +++++++--- visualize/templates/visualize/planner.html | 5 +++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/visualize/static/js/app.js b/visualize/static/js/app.js index 6ee528df..7df32d64 100644 --- a/visualize/static/js/app.js +++ b/visualize/static/js/app.js @@ -51,10 +51,14 @@ function waitForMenusLoadToApplyKOBindings(maxWaitTime) { && (app.hasOwnProperty('menuModel') && typeof app.menuModel !== 'undefined' && app.menuModel.hasOwnProperty('menuItems') && typeof app.menuModel.menuItems === 'function') ) { try { - ko.applyBindings(app.viewModel); + ko.applyBindings(app.viewModel, document.querySelector('#primary-content')); } catch (e) { - console.warn('Error applying KO bindings:', e); - console.warn('if this is about "cannot apply bindings multiple times to the same element", this is expected. menuModel should already have been applied to the context menu'); + 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) { 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 %}