3a52ad30839586e54377927d8ee87a65d4806a7f
galt
  Thu Oct 23 21:21:22 2025 -0700
hubCheck can now scan for and report with text valid values for VIEWLIMIT VIEWLIMITMAX and DEFAULTVIEWLIMIT. fixes #12098

diff --git src/hg/utils/hubCheck/hubCheck.c src/hg/utils/hubCheck/hubCheck.c
index 5d66be2cf4f..fb0d06cdea2 100644
--- src/hg/utils/hubCheck/hubCheck.c
+++ src/hg/utils/hubCheck/hubCheck.c
@@ -158,30 +158,31 @@
     int score = scoreWordMatch(setting, suggest->name, ss);
     if (score < bestScore)
         continue;
     if (score > bestScore)
         {
         best = suggest->name;
         bestScore = score;
         bestCount = 1;
         }
     else
         {
         // same score
         bestCount++;
         }
     }
+    
 if (bestCount == 1 && bestScore > 15)
     {
     verbose(3, "suggest %s score: %d\n", best, bestScore);
     return best;
     }
 return NULL;
 }
 
 struct htmlPage *trackHubVersionSpecMustGet(char *specHost, char *version)
 /* Open the trackDbHub.html file and attach html reader. Use default version if NULL */
 {
 char *specUrl;
 char buf[256];
 if (version != NULL && startsWith("http", version))
     specUrl = version;
@@ -821,30 +822,104 @@
             {
             errAbort("error in type line \"%s\" for track \"%s\". "
                 "Only \"+\" or \".\" is allowed after bigBed numFields setting.", type, tdb->track);
             }
         }
     }
 errCatchEnd(errCatch);
 if (errCatch->gotError)
     {
     trackDbErr(errors, errCatch->message->string, genome, tdb, options->htmlOut);
     retVal = TRUE;
     }
 return retVal;
 }
 
+boolean isValidDouble(char *s, double *result)
+/* Convert string to a double.  Assumes all of string is number
+ * and returns FALSE if double is invalid.
+ * Does not errAbort. Avoids sqlDouble which does abort. */
+{
+char* end;
+double val = strtod(s, &end);
+
+if ((end == s) || (*end != '\0')
+ || *s == ' '
+ )
+    return FALSE;
+*result = val;
+return TRUE;
+}
+
+
+static void parseColonRange(struct trackDb *tdb, char *settingName, char *setting)
+/* Parse setting's two colon-separated numbers and detects errors in their value. 
+ * Helpfully shows track and setting name as well as setting value in the error message. 
+ * Does not swap or return min max range values.
+ * trackDbDoc syntax
+    viewLimits <lower:upper>
+ */
+{
+char tmp[64];
+safecpy(tmp, sizeof(tmp), setting);
+char *words[3];
+if (chopByChar(tmp, ':', words, ArraySize(words)) == 2)
+    {
+    char *lower = words[0];
+    double lowerVal = 0;
+    boolean validLower = isValidDouble(lower, &lowerVal);
+    if (!validLower)
+	errAbort("Invalid double range lower value '%s' in range '%s' in setting %s track %s", lower, setting, settingName, tdb->track);
+
+    char *upper = words[1];
+    double upperVal = 0;
+    boolean validUpper = isValidDouble(upper, &upperVal);
+    if (!validUpper)
+	errAbort("Invalid double range upper value '%s' in range '%s' in setting %s track %s", upper, setting, settingName, tdb->track);
+
+    if (validUpper && validLower)
+	{
+	if (upperVal < lowerVal)
+	    warn("upper < lower. Should swap lower and upper range values in %s' in setting %s track %s", setting, settingName, tdb->track);
+	}
+    }
+else
+    errAbort("Missing a colon in range value '%s' in setting %s track %s", setting, settingName, tdb->track);
+}
+
+void checkViewLimitsSettings(struct trackDb *tdb)
+/* check viewLimits and viewLimitsMax and defaultViewLimits setting values */
+{
+int i;
+for(i = 0; i < 3; i++)
+    {
+    char *settingName;
+    if (i == 0)
+	settingName = VIEWLIMITS;
+    if (i == 1)
+	settingName = VIEWLIMITSMAX;
+    if (i == 2)
+	settingName = DEFAULTVIEWLIMITS;
+
+    char *setting = trackDbSetting(tdb, settingName);
+    if (setting)
+        {
+        parseColonRange(tdb, settingName, setting);
+        }
+    }
+}
+
 int hubCheckTrack(struct trackHub *hub, struct trackHubGenome *genome, struct trackDb *tdb,
                         struct trackHubCheckOptions *options, struct dyString *errors)
 /* Check track settings and optionally, files */
 {
 int retVal = 0;
 int trackDbErrorCount = 0;
 
 if (options->checkSettings && options->settings)
     {
     //verbose(3, "Found %d settings to check to spec\n", slCount(settings));
     verbose(3, "Checking track: %s\n", tdb->shortLabel);
     verbose(3, "Found %d settings to check to spec\n", hashNumEntries(tdb->settingsHash));
     struct hashEl *hel;
     struct hashCookie cookie = hashFirst(tdb->settingsHash);
     while ((hel = hashNext(&cookie)) != NULL)
@@ -901,30 +976,33 @@
 
     if (tdbIsSubtrack(tdb))
         retVal |= hubCheckSubtrackSettings(genome, tdb, errors, options);
 
     // check that type line is syntactically correct regardless of
     // if we actually want to check the data file itself
     boolean foundTypeError = checkTypeLine(genome, tdb, errors, options);
 
     // No point in checking the data file if the type setting is incorrect,
     // since hubCheckBigDataUrl will error out early with a less clear message
     // if the type line is messed up. This has the added benefit of providing
     // consistent messaging on command line interface vs web interface
     if (!foundTypeError && options->checkFiles)
         hubCheckBigDataUrl(hub, genome, tdb);
 
+
+    checkViewLimitsSettings(tdb);
+
     if (!sameString(tdb->track, "cytoBandIdeo"))
         {
         trackHubAddDescription(genome->trackDbFile, tdb);
         if (!tdb->html)
             warn("warning: missing description page for track. Add 'html %s.html' line to the '%s' track stanza. ",
                  tdb->track, tdb->track);
         }
 
     if (!trackIsContainer && sameString(trackDbRequiredSetting(tdb, "type"), "bigWig"))
         {
         char *autoScaleSetting = trackDbLocalSetting(tdb, "autoScale");
         if (autoScaleSetting && !sameString(autoScaleSetting, "off") && !sameString(autoScaleSetting, "on"))
             {
             errAbort("track \"%s\" has value \"%s\" for autoScale setting, "
                     "valid autoScale values for individual bigWig tracks are \"off\" or \"on\" only. "