PageSourceSearch

https://dmww.com/_assets_/js/scripts.js?v=

js dmww.com collected 2026-10-03 21:20:47 UTC 42,299 bytes, 1,269 lines download raw bytes

1/*-----------------------------------------------------------------------------------
2
3	Theme Name: Des Moines Water Works, IA
4	Front-end developer: Hayk Galoyan
5	Author Design: Nasryn Abou-Arabi
6	Author URI: http://www.revize.com/
7	Date: 3/12/2020
8
9-----------------------------------------------------------------------------------*/
10// $(function() {
11//   // test the full URL (handles http/https and optional www)
12//   if (/^https?:\/\/(www\.)?dmww\.com(\/|$)/i.test(window.location.href)) {
13//     $('body').addClass('remove-chatbot');
14//   } else {
15//     // optional: ensure class is removed on other hosts
16//     $('body').removeClass('remove-chatbot');
17//   }
18// });
19
20(function($) {
21
22	'use strict';
23
24	var $window = $(window),
25		$body = $('body');
26
27	/*!
28	 * IE10 viewport hack for Surface/desktop Windows 8 bug
29	 * Copyright 2014-2015 Twitter, Inc.
30	 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
31	 */
32
33	// See the Getting Started docs for more information:
34	// http://getbootstrap.com/getting-started/#support-ie10-width
35	if (navigator.userAgent.match(/IEMobile\/10\.0/)) {
36		var msViewportStyle = document.createElement('style');
37		msViewportStyle.appendChild(
38			document.createTextNode(
39			  '@-ms-viewport{width:auto!important}'
40			)
41		); document.querySelector('head').appendChild(msViewportStyle);
42	}
43
44	// RZ Class
45	if(typeof RZ !== "undefined"){
46		if(RZ.login){
47			$body.addClass("user-logged-in");
48		} else{
49			$body.addClass("user-not-logged-in");
50		}
51	}
52
53	// Keyboard Navigation: Nav, flyout
54	var isClick = false;
55	$("#nav li a, #flyout  li a, a, button, .toggle, .toggle2").on('focusin', function(e) {
56		if( isClick === false ) {
57			$(".focused").removeClass("focused");
58			$(e.currentTarget).parents("#nav li, #flyout li").addClass("focused");
59			$(".opened:not(.focused) ul:visible,.opened2:not(.focused) ul:visible").slideUp().parent().removeClass("opened opened2");
60		} else {
61			$(".focused").removeClass("focused");
62			isClick = false;
63		}
64	});
65
66	// prevent focused class changes on click - This way arrows wont pop when clicking nav links
67	$("#nav a,#flyout a").on('mousedown',function(){
68		isClick = true;
69	});
70
71	// Navigation Toggle
72	$("#nav-toggle").on("click", function(){
73		$("#nav").stop().slideToggle();
74		$(this).toggleClass("active");
75		$("#freeform-search").hide();
76
77		if ($(this).hasClass('active')) {
78			$(this).removeClass('fa-bars').addClass('fa-times');
79			$('#search-toggle').removeClass('active');
80		} else {
81			$(this).removeClass('fa-times').addClass('fa-bars');
82		}
83
84		$('#search-toggle').removeClass('fa-times').addClass('fa-search');
85	});
86
87	// Search Toggle
88	$("#search-toggle").on("click", function(){
89		$("#freeform-search").stop().slideToggle();
90		$(this).toggleClass("active");
91		$("#nav").hide();
92
93		if ($(this).hasClass('active')) {
94			$(this).removeClass('fa-search').addClass('fa-times');
95			$('#nav-toggle').removeClass('active');
96		} else {
97			$(this).removeClass('fa-times').addClass('fa-search');
98		}
99
100		$('#nav-toggle').removeClass('fa-times').addClass('fa-bars');
101	});
102
103	// Menu Arrows and Toggles
104	$("#nav >li>ul,#flyout >li>ul").addClass('first-level');
105	$("#nav  li ul ul").addClass('second-level');
106	$("#nav>li:has(ul)").each(function(){
107		$('<a href="#" class="fa fa-angle-down toggle" tabindex="0" aria-haspopup="true" aria-expanded="false" id="dropdown-toggle-'+$(this).index()+'" aria-label="Show Dropdown for '+ $(this).find('a:first').text() +'"></a>').insertAfter($(this).find('a:first'));
108		$(this).find('ul:first').attr('aria-labelledby', 'dropdown-toggle-'+$(this).index());
109	});
110	$('#nav li ul>li:has(ul)').each(function(index) {
111		$('<a href="#" class="fa fa-angle-down toggle2" tabindex="0" aria-haspopup="true" aria-expanded="false" id="sub-dropdown-toggle-'+index+'" aria-label="Show Dropdown for '+ $(this).find('a:first').text() +'"></a>').insertAfter($(this).find('a:first'));
112		$(this).find('ul:first').attr('aria-labelledby', 'sub-dropdown-toggle-'+index);
113	});
114	$('#flyout >li:has(ul)').each(function() {
115		$('<a href="#" class="fa fa-angle-down toggle" tabindex="0" aria-haspopup="true" aria-expanded="false" id="flyout-dropdown-toggle-'+$(this).index()+'" aria-label="Show Flyout for '+ $(this).find('a:first').text() +'"></a>').insertAfter($(this).find('a:first'));
116		$(this).find('ul:first').attr('aria-labelledby', 'flyout-dropdown-toggle-'+$(this).index());
117	});
118
119	function addNavClass() {
120		if ($window.width() < 992) {
121			// Insert needed for mobile
122		} else{
123			// Insert needed for desktop
124			$('#nav ul, #flyout ul').css('display', '');
125		}
126	}
127	addNavClass();
128	$window.resize(addNavClass);
129
130	$(".toggle").on("click keydown",function(e) {
131		if (e.keyCode === 13 || e.type === 'click') {
132			e.preventDefault();
133			var $parent = $(this).parent();
134			var $parentLi = $parent.parent();
135			$parent.toggleClass('opened');
136			if($parent.addClass('active') && $(this).next('.first-level').is(":visible")){
137				$(this).next('.first-level').slideUp();
138				$parent.removeClass('active');
139				$(this).attr({'aria-expanded': 'false'});
140			} else {
141				$(this).attr({'aria-expanded': 'true'});
142				$(".first-level").slideUp("slow");
143				$parent.addClass('active');
144				$(this).next('.first-level').slideToggle();
145			}
146		}
147	});
148
149	$(".toggle2").on("click keydown",function(e) {
150		if (e.keyCode === 13 || e.type === 'click') {
151			e.preventDefault();
152			var $parent = $(this).parent();
153			var $parentLi = $parent.parent();
154			$parent.toggleClass('opened2');
155			if($parent.addClass('active') && $(this).next('.second-level').is(":visible")){
156				$(this).next('.second-level').slideUp();
157				$parent.removeClass('active');
158				$(this).attr({'aria-expanded': 'false'});
159			} else {
160				$(this).attr({'aria-expanded': 'true'});
161				$(".second-level").slideUp("slow");
162				$parent.addClass('active');
163				$(this).next('.second-level').slideToggle();
164			}
165		}
166	});
167
168	// close nav if left
169	$(".desktop *").focus(function(e){
170		var $opened = $(".opened");
171		var $opened2 = $(".opened2");
172		if( $opened.length > 0 || $opened2.length > 0 ) {
173			if( $(".opened :focus").length < 1 ){
174				$opened.children("ul").slideUp();
175				$opened.removeClass("opened");
176				$(".opened2").removeClass("opened2");
177			}
178			if( $(".opened2 :focus").length < 1 ){
179				$opened2.children("ul").slideUp();
180				$opened2.removeClass("opened2");
181			}
182		}
183	});
184
185	// Flyout
186	var flyout = $('#flyout'),
187		flyoutwrap = $('#flyout-wrap');
188
189	if (flyout.text().length){
190		flyoutwrap.prepend('<div id="flyout-toggle" class="hidden-lg hidden-md"><i class="fa fa-bars"></i> Sub Menu</div>');
191	}
192
193	$("#flyout-toggle").on("click", function(){
194		flyout.slideToggle();
195		$(this).toggleClass("active");
196	});
197
198	/*
199	* E-Notify Auto submit
200	*/
201	$.urlParam=function(n){var e=new RegExp("[?&]"+n+"=([^]*)").exec(window.location.href);return null==e?null:e[1]||0};
202	var $enotify = $('iframe[src*="/revize/plugins/notify/notify.jsp"]');
203	if( $enotify.length > 0 ){
204		var emailStr = $.urlParam("email");
205		if( emailStr != null ){
206			$enotify.attr("src", $enotify.attr("src") + "&email=" + emailStr);
207		}
208	}
209
210	// Make sure all calendars have unique ids
211	$('iframe[name="calendar"]').each(function (index, calendar) {
212		calendar.id = 'calendar-' + index;
213	});
214
215	// Start Frame Resizer
216	function resizeIframe(height, frameElement) {
217		if ( frameElement ) {
218			frameElement.height = "";
219			frameElement.height = height;
220		}
221	}
222
223	var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
224	var eventHandler = window[eventMethod];
225	var messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message";
226	eventHandler(messageEvent, function (e) {
227		if ( Array.isArray(e.data) ) {
228			if( e.data[0] === "setCalHeight" || e.data[0] === "setNotifyHeight") {
229				var frames = document.querySelectorAll('iframe');
230				for( var i = 0; i < frames.length; i++ ) {
231					if( frames[i].contentWindow === e.source ){
232						resizeIframe(e.data[1], frames[i]);
233					}
234				}
235			}
236		}
237	});
238	// End Frame Resizer
239
240	// Tabs
241	$('#tabs li a').on('click keypress', function(e) {
242		if (e.keyCode === 13 || e.type === 'click') {
243			$('#tabs li, #tabs-content .current').removeClass('current').removeClass('fadeInLeft');
244			$(this).parent().addClass('current');
245
246			var currentTab = $(this).attr('href');
247
248			e.preventDefault();
249			$(currentTab).addClass('current animated fadeInLeft');
250			$(currentTab).find('h2').focus();
251		}
252  });
253
254  var $translateButton = $('#translate-button');
255  $translateButton.on('keydown click', function(e){
256	  if (e.keyCode === 13 || e.type === 'click') {
257		  $('#translation-links ul').stop().fadeToggle();
258		  $translateButton.attr('aria-expanded', !('true' === $translateButton.attr('aria-expanded')));
259	  }
260  });
261
262  $('#translation-links ul').on('mouseleave',function(){
263	  $(this).fadeOut();
264	  $translateButton.attr('aria-expanded', false);
265  });
266
267  function removeCookieString(value, domain) {
268	  domain = domain ? 'domain='+domain+'; ' : '';
269	  return value+'; expires=Thu, 01 Jan 1970 00:00:01 GMT; '+domain+'path=/';
270  }
271
272  function unsetGoogtransCookie() {
273	  for (var domains = window.location.hostname.split('.'); domains.length >= 2; domains.shift()) {
274		  var cookieString = removeCookieString('googtrans=unset', domains.join('.'));
275		  document.cookie=cookieString;
276		  if	(domains.length === 2) {
277			  cookieString = removeCookieString('googtrans=unset', '.'+domains.join('.'));
278			  document.cookie=cookieString;
279		  }
280	  }
281	  document.cookie=removeCookieString('googtrans=unset');
282  }
283
284  if (document.cookie.split(';').some(function(item) { return item.trim().startsWith('googtrans=/auto/en'); })) {
285	  unsetGoogtransCookie();
286  }
287
288  // Translate Script
289  var translateURL = "//translate.google.com/translate_a/element.js?cb=googleTranslateElementInit";
290  $.getScript(translateURL);
291  $('#translation-links a').click(function() {
292	  var lang = $(this).data('lang');
293	  var $frame = $('iframe.skiptranslate');
294	  if (lang === 'English') {
295		  $frame.contents().find('span.text:contains("English")').parent().parent().get(0).click();
296		  return;
297	  }
298	  if (!$frame.length) {
299		  return false;
300	  }
301	  var $el = $frame.contents().find('span.text:contains(' + lang + ')').parent().parent().get(0).click();
302	  console.log($el);
303	  if (!$el) {
304		  $frame.contents().find('span.text:contains("English")').parent().parent().get(0);
305	  }
306
307	  $el.click();
308	  return false;
309  });
310
311	// bxSlider
312	if(typeof $.fn.bxSlider !== "undefined"){
313		$('.bxslider').each(function(i, el){
314			$(el).bxSlider({
315				mode: 'fade',
316				auto: ($(el).children().length < 2) ? false : true,
317				pager: true,
318				pause: 8000
319			});
320		});
321	}
322
323	// Owl Slider
324	var newsCount = $("#news-slider .news").length;
325	function newsItems(cnt) {return newsCount > cnt ? cnt : newsCount;}
326	function newsNav(cnt) {return newsCount > cnt ? true : false;}
327
328	var freeformQuickLinksCount = $("#freeform-quick-links-slider .freeform-quick-link").length;
329	function freeformQuickLinksItems(cnt) {return freeformQuickLinksCount > cnt ? cnt : freeformQuickLinksCount;}
330	function freeformQuickLinksNav(cnt) {return freeformQuickLinksCount > cnt ? true : false;}
331
332	var btnCount = $("#btn-slider .btn-item").length;
333	function btnItems(cnt) {return btnCount > cnt ? cnt : btnCount;}
334	function btnNav(cnt) {return btnCount > cnt ? true : false;}
335
336	var featuredPostsCount = $("#featured-posts-slider .featured-post").length;
337	function featuredPostsItems(cnt) {return featuredPostsCount > cnt ? cnt : featuredPostsCount;}
338	function featuredPostsNav(cnt) {return featuredPostsCount > cnt ? true : false;}
339
340  // var recentPostsCount = $("#recent-posts-slider .recent-posts-slider-box").length;
341	var recentPostsCount = $("#recent-posts-slider .recent-post").length;
342	function recentPostsItems(cnt) {return recentPostsCount > cnt ? cnt : recentPostsCount;}
343	function recentPostsNav(cnt) {return recentPostsCount > cnt ? true : false;}
344
345	if(typeof $.fn.owlCarousel !== "undefined"){
346		$("#news-slider").owlCarousel({
347			navText: ["<i class='fa fa-chevron-circle-left'></i>","<i class='fa fa-chevron-circle-right'></i>"],
348			autoplay: true,
349			margin: 28,
350			stagePadding: 0,
351            responsive: {
352                0: {
353                    items: 1,
354					nav: newsNav(1),
355                },
356                650: {
357                    items: newsItems(2),
358                    nav: newsNav(2),
359                },
360                991: {
361					items: newsItems(3),
362					nav: newsNav(3),
363					stagePadding: 60,
364                }
365            }
366		});
367
368		$("#freeform-quick-links-slider").owlCarousel({
369			navText: ["<i class='fa fa-chevron-circle-left'></i>","<i class='fa fa-chevron-circle-right'></i>"],
370			autoplay: true,
371			margin:30,
372			responsive: {
373				0: {
374					items: 1,
375					nav: freeformQuickLinksNav(1),
376				},
377				500: {
378					items: freeformQuickLinksItems(2),
379					nav: freeformQuickLinksNav(2),
380				},
381				700: {
382					items: freeformQuickLinksItems(3),
383					nav: freeformQuickLinksNav(3),
384				},
385				900: {
386					items: freeformQuickLinksItems(4),
387					nav: freeformQuickLinksNav(4),
388				},
389			}
390		});
391
392		$("#btn-slider").owlCarousel({
393			navText: ["<i class='fa fa-chevron-circle-left'></i>","<i class='fa fa-chevron-circle-right'></i>"],
394			autoplay: true,
395			stagePadding: 0,
396			margin: 30,
397			autoplayTimeout: 10000,
398			responsive: {
399				0: {
400					items: 1,
401					nav: btnNav(1),
402					stagePadding: 0,
403				},
404				650: {
405					items: btnItems(2),
406					nav: btnNav(2),
407				},
408				991: {
409					items: btnItems(3),
410					nav: btnNav(3),
411					stagePadding: 60,
412				}
413			}
414		});
415
416		$("#featured-posts-slider").owlCarousel({
417			navText: ["<i class='fa fa-chevron-circle-left'></i>","<i class='fa fa-chevron-circle-right'></i>"],
418			autoplay: true,
419			margin: 28,
420			stagePadding: 0,
421			autoplayTimeout: 15000,
422            responsive: {
423                0: {
424                    items: 1,
425					nav: featuredPostsNav(1),
426                },
427                650: {
428                    items: featuredPostsItems(2),
429                    nav: featuredPostsNav(2),
430                },
431                991: {
432					items: featuredPostsItems(3),
433					nav: featuredPostsNav(3),
434					stagePadding: 60,
435                }
436            }
437		});
438
439		$("#recent-posts-slider").owlCarousel({
440			navText: ["<i class='fa fa-chevron-circle-left'></i>","<i class='fa fa-chevron-circle-right'></i>"],
441      margin: 30,
442      autoplay: $('.recent-post').length > 4,
443			autoplayTimeout: 10000,
444            responsive: {
445                0: {
446                    items: 1,
447					nav: recentPostsNav(1),
448                },
449                600: {
450                    items: recentPostsItems(2),
451					nav: recentPostsNav(2),
452                }
453            }
454		});
455  }
456
457  // $("#recent-posts-slider").owlCarousel();
458  $('.recent-posts-category').on('click', function(e){
459    e.preventDefault();
460    var get_class = $(this).text().trim().toLowerCase().split(" ").join("_");
461    var visible_items = 0;
462    if(get_class === "all"){
463      $('#recent-posts-slider .owl-item').show();
464      $('#recent-posts-slider .owl-item').each(function(item){
465        visible_items += 1;
466      });
467    } else {
468      $('#recent-posts-slider .owl-item').each(function(item){
469        var class_name = $(this)[0].childNodes[0].className;
470        var get_category = class_name.split(" ")[1] === undefined ? "null" : class_name.split(" ")[1].trim().toLowerCase().split(" ").join("_");
471        if(get_class === get_category){
472          $(this).show();
473          visible_items += 1;
474        } else {
475          $(this).hide();
476        }
477      })
478    }
479    $("#recent-posts-slider").trigger('to.owl.carousel', [0, 300]);
480    if(visible_items > 4){
481      setTimeout(function(){
482        $("#recent-posts-slider").trigger('play.owl.autoplay', [0, 3000]);
483      }, 3000)
484    } else {
485      $("#recent-posts-slider").trigger('stop.owl.autoplay');
486    }
487    //Returning false will prevent the anchor from going to a new page
488    return false;
489  });
490
491	// Responsive Tables
492	$('#post table:not(.layout-table):not(.not-responsive)').wrap('<div class="table-responsive"></div>');
493	$('.layout-table').attr('role', 'presentation');
494
495	$window.ready(function(){
496
497		// matchHeight
498		if(typeof $.fn.matchHeight !== "undefined"){
499			$('.equal').matchHeight({
500				//defaults
501				byRow: true,
502				property: 'height', // height or min-height
503				target: null,
504				remove: false
505			});
506
507			$('.news-title').matchHeight();
508			$('.featured-post-title').matchHeight();
509			$('.recent-post-title').matchHeight({byRow:0});
510		}
511
512		// Animations http://www.oxygenna.com/tutorials/scroll-animations-using-waypoints-js-animate-css
513		function onScrollInit( items, trigger ) {
514			items.each( function() {
515				var osElement = $(this),
516					osAnimationClass = osElement.data('os-animation'),
517					osAnimationDelay = osElement.data('os-animation-delay');
518
519				osElement.css({
520					'-moz-animation-delay':     osAnimationDelay,
521					'animation-delay':          osAnimationDelay,
522					'-webkit-animation-delay':  osAnimationDelay
523				});
524
525				var osTrigger = ( trigger ) ? trigger : osElement;
526
527				if(typeof $.fn.waypoint !== "undefined"){
528					osTrigger.waypoint(function() {
529						osElement.addClass('animated').addClass(osAnimationClass);
530					},{
531						triggerOnce: true,
532						offset: '100%'
533					});
534				}
535			});
536		}
537		onScrollInit($('.os-animation'));
538
539		//#Smooth Scrolling
540		$('a[href*="#"]').not('[href="#"]').not('[href*="#collapse"]').click(function(event) {
541			if (location.pathname.replace(/^\//,'') === this.pathname.replace(/^\//,'') && location.hostname === this.hostname) {
542				var target = $(this.hash);
543				target = target.length ? target : $('[name=' + this.hash.slice(1) +']');
544				if (target.length) {
545					event.preventDefault();
546					$('html,body').animate({
547						scrollTop: target.offset().top
548					}, 1000, function() {
549						var $target = $(target);
550						$target.focus();
551						if ($target.is(":focus")) {
552							return false;
553						} else {
554							$target.attr('tabindex','-1');
555							$target.focus();
556							$target.removeAttr('tabindex');
557						};
558					});
559				}
560			}
561		});
562
563		/*global jQuery */
564		/*!
565		* FlexVerticalCenter.js 1.0
566		*
567		* Copyright 2011, Paul Sprangers http://paulsprangers.com
568		* Released under the WTFPL license
569		* http://sam.zoy.org/wtfpl/
570		*
571		* Date: Fri Oct 28 19:12:00 2011 +0100
572		*/
573		$.fn.flexVerticalCenter = function( options ) {
574			var settings = $.extend({
575				cssAttribute:   'margin-top', // the attribute to apply the calculated value to
576				verticalOffset: 0,            // the number of pixels to offset the vertical alignment by
577				parentSelector: null,         // a selector representing the parent to vertically center this element within
578				debounceTimeout: 25,          // a default debounce timeout in milliseconds
579				deferTilWindowLoad: false     // if true, nothing will take effect until the $(window).load event
580			}, options || {});
581
582			return this.each(function(){
583				var $this   = $(this); // store the object
584				var debounce;
585
586				// recalculate the distance to the top of the element to keep it centered
587				var resizer = function () {
588
589					var parentHeight = (settings.parentSelector && $this.parents(settings.parentSelector).length) ?
590						$this.parents(settings.parentSelector).first().height() : $this.parent().height();
591
592					$this.css(
593						settings.cssAttribute, ( ( ( parentHeight - $this.height() ) / 2 ) + parseInt(settings.verticalOffset) )
594					);
595				};
596
597				// Call on resize. Opera debounces their resize by default.
598				$(window).resize(function () {
599					clearTimeout(debounce);
600					debounce = setTimeout(resizer, settings.debounceTimeout);
601				});
602
603				if (!settings.deferTilWindowLoad) {
604					// call it once, immediately.
605					resizer();
606				}
607
608				// Call again to set after window (frames, images, etc) loads.
609				$(window).load(function () {
610					resizer();
611				});
612
613			});
614
615		};
616		$('.v-align').flexVerticalCenter();
617
618		//
619		$('#translate-button span').text('Translate');
620		$('#translate-button span').css('display', 'inline-block');
621
622
623		// Water quality animation + JSON integration (start)
624
625		// Some category names to check ( waterQualitySiteName )
626		// Des Moines River
627		// Crystal Lake
628		// Fleur Treatment Plant-Gallery
629		// Fleur Treatment Plant
630
631		$(function () {
632
633			var waterQualityApiUrl = 'https://desmoineswater.revize.com/api/water-quality-data';
634			var waterQualitySiteName = 'Des Moines River';
635
636			function norm(s) { return $.trim(String(s || '')).toLowerCase(); }
637
638			function isValidNumberString(v) {
639				return /^-?(?:\d+\.?\d*|\.\d+)$/.test($.trim(String(v || '')));
640			}
641
642			function toNumberOrNull(v) {
643				v = $.trim(String(v == null ? '' : v));
644				return isValidNumberString(v) ? parseFloat(v) : null;
645			}
646
647			// Only set element text if currently empty.
648			// Treat "0" as empty placeholders too.
649			function setTextIfEmpty(selector, value, formatter) {
650				var el = $(selector).first();
651				if (!el[0]) {
652					//console.warn('[WQ] Missing element:', selector);
653					return false;
654				}
655
656				var current = $.trim(el.text());
657				var isEmpty = !current;
658
659				// Treat placeholder zeros as empty
660				if (current === '0' || current === '0.0' || current === '0.00') isEmpty = true;
661
662				if (!isEmpty) {
663					//console.log('[WQ] Kept existing value for', selector, '=>', current);
664					return false;
665				}
666
667				if (value == null || value === '') {
668					//console.log('[WQ] No JSON value available for', selector, '(value null/empty)');
669					return false;
670				}
671
672				var out = value;
673				if (typeof formatter === 'function') out = formatter(value);
674
675				el.text(out);
676				//console.log('[WQ] Set', selector, '=>', out);
677				return true;
678			}
679
680			// Ensure visible "start numbers" are not empty (prevents NaN start in animate)
681			function ensureStartNumber(selector, fallback) {
682				var el = $(selector).first();
683				if (!el[0]) return;
684				var t = $.trim(el.text());
685				if (!t) {
686					el.text(String(fallback));
687					//console.warn('[WQ] Start field was empty; set fallback for', selector, '=>', fallback);
688				}
689			}
690
691			function latestBucketForMetric(allRows, matchFn) {
692				var latestDate = '';
693				var i, r, d;
694
695				for (i = 0; i < allRows.length; i++) {
696					r = allRows[i];
697					d = r && r.SampleDate ? String(r.SampleDate) : '';
698					if (!d) continue;
699					if (matchFn(r) && d > latestDate) latestDate = d;
700				}
701
702				if (!latestDate) return { date: '', rows: [] };
703
704				var out = [];
705				for (i = 0; i < allRows.length; i++) {
706					r = allRows[i];
707					if (r && String(r.SampleDate) === latestDate && matchFn(r)) out.push(r);
708				}
709
710				return { date: latestDate, rows: out };
711			}
712
713			// For numeric metrics, pick ONE numeric value from the latest bucket (first numeric found)
714			function pickLatestSingleNumeric(siteRows, matchFn, label) {
715				var b = latestBucketForMetric(siteRows, matchFn);
716				for (var i = 0; i < b.rows.length; i++) {
717					var n = toNumberOrNull(b.rows[i] && b.rows[i].IPR_Result);
718					if (n != null) {
719						//console.log('[WQ] Picked', label, 'date:', b.date, 'value:', n, 'row:', b.rows[i]);
720						return { date: b.date, value: n };
721					}
722				}
723				//console.warn('[WQ] No numeric value found for', label, 'latestDate:', b.date || '(none)');
724				return { date: b.date || '', value: null };
725			}
726
727			// For bacteriological metrics, return up to 2 numeric values from latest bucket (unique)
728			function pickLatestBacteriaPair(siteRows, matchFn) {
729				var b = latestBucketForMetric(siteRows, matchFn);
730				var vals = [];
731
732				for (var i = 0; i < b.rows.length; i++) {
733					var n = toNumberOrNull(b.rows[i] && b.rows[i].IPR_Result);
734					if (n == null) continue;
735
736					var seen = false;
737					for (var j = 0; j < vals.length; j++) {
738						if (vals[j] === n) { seen = true; break; }
739					}
740					if (!seen) vals.push(n);
741				}
742
743				if (vals.length > 2) vals = [vals[0], vals[1]];
744
745				// Heuristic: larger number = negative, smaller = positive
746				if (vals.length === 2 && vals[0] < vals[1]) vals = [vals[1], vals[0]];
747
748				//console.log('[WQ] Picked Bacteria date:', b.date || '(none)', 'neg:', vals[0], 'pos:', vals[1], 'rows:', b.rows.length);
749
750				return {
751					date: b.date || '',
752					neg: (vals[0] != null ? vals[0] : null),
753					pos: (vals[1] != null ? vals[1] : null)
754				};
755			}
756
757			function runWaterQualityAnimation() {
758				if (typeof $.fn.waypoint === "undefined") {
759					//console.warn('[WQ] Waypoint plugin not found. Animation will not run.');
760					return;
761				}
762
763				// Ensure visible start values are not empty to avoid NaN
764				ensureStartNumber('#phnumber', 0);
765				ensureStartNumber('#residnum', 0);
766				ensureStartNumber('#bacterianumber', 0);
767				ensureStartNumber('#negative-sample-number', 0);
768				ensureStartNumber('#hardness-number', 0);
769				ensureStartNumber('#sodium-number', 0);
770				ensureStartNumber('#chloride-number', 0);
771				ensureStartNumber('#fluoride-number', 0);
772				ensureStartNumber('#alkalinity-number', 0);
773
774				// Start animation section
775				var phNumber = parseFloat($('#phnumber').text()) || 0;
776				var residualNum = parseFloat($('#residnum').text()) || 0;
777				var bacteriaNum = parseFloat($('#bacterianumber').text()) || 0;
778				var negativeSamples = parseFloat($('#bacterianumber').text()) || 0;
779				var hardnessNum = parseFloat($('#hardness-number').text()) || 0;
780				var sodiumNum = parseFloat($('#sodium-number').text()) || 0;
781				var chlorideNum = parseFloat($('#chloride-number').text()) || 0;
782				var fluorideNum = parseFloat($('#fluoride-number').text()) || 0;
783				var alkalinityNum = parseFloat($('#alkalinity-number').text()) || 0;
784
785				var residualAmount = parseFloat(residualNum) || 0;
786
787				var phBal = parseFloat($('#phBal').text()) || 0;
788				$('#phBal').hide();
789
790				var phCho = parseFloat($('#phCho').text()) || 0;
791				$('#phCho').hide();
792
793				var negSample = parseFloat($('#negSample').text()) || 0;
794				$('#negSample').hide();
795
796				var posSample = parseFloat($('#posSample').text()) || 0;
797				$('#posSample').hide();
798
799				var posPercent = parseFloat($('#posPercent').text()) || 0;
800				$('#posPercent').hide();
801
802				var hardNum = parseFloat($('#hardNum').text()) || 0;
803				$('#hardNum').hide();
804
805				var sodNum = parseFloat($('#sodNum').text()) || 0;
806				$('#sodNum').hide();
807
808				var choNum = parseFloat($('#choNum').text()) || 0;
809				$('#choNum').hide();
810
811				var fluNum = parseFloat($('#fluNum').text()) || 0;
812				$('#fluNum').hide();
813
814				var alkNum = parseFloat($('#alkNum').text()) || 0;
815				$('#alkNum').hide();
816
817				var waypoint = new Waypoint({
818					element: document.getElementById('lab-report'),
819					handler: function (direction) {
820						if (direction === 'down') {
821
822							$({ numberValue: phNumber }).animate({ numberValue: phBal }, {
823								duration: 3000,
824								easing: 'linear',
825								step: function (now) { $('#phnumber').text(now.toFixed(2)); }
826							});
827
828							$({ numberValue: residualNum }).animate({ numberValue: phCho }, {
829								duration: 3000,
830								easing: 'linear',
831								step: function (now) { $('#residnum').text(now.toFixed(2)); }
832							});
833
834							$({ numberValue: bacteriaNum }).animate({ numberValue: negSample + posSample }, {
835								duration: 3000,
836								easing: 'linear',
837								step: function (now) { $('#bacterianumber').text(Math.ceil(now)); }
838							});
839
840							$({ numberValue: bacteriaNum }).animate({ numberValue: negSample }, {
841								duration: 3000,
842								easing: 'linear',
843								step: function (now) { $('#negative-sample-number').text(Math.ceil(now)); }
844							});
845
846							$('#sample-positive-percent').text(posPercent);
847							$('.positive-sample .sample-num').text(posSample);
848
849							$({ numberValue: hardnessNum }).animate({ numberValue: hardNum }, {
850								duration: 3000,
851								easing: 'linear',
852								step: function (now) { $('#hardness-number').text(Math.ceil(now)); }
853							});
854
855							$({ numberValue: sodiumNum }).animate({ numberValue: sodNum }, {
856								duration: 3000,
857								easing: 'linear',
858								step: function (now) { $('#sodium-number').text(Math.ceil(now)); }
859							});
860
861							$({ numberValue: chlorideNum }).animate({ numberValue: choNum }, {
862								duration: 3000,
863								easing: 'linear',
864								step: function (now) { $('#chloride-number').text(Math.ceil(now)); }
865							});
866
867							$({ numberValue: fluorideNum }).animate({ numberValue: fluNum }, {
868								duration: 3000,
869								easing: 'linear',
870								step: function (now) { $('#fluoride-number').text(now.toFixed(1)); }
871							});
872
873							$({ numberValue: alkalinityNum }).animate({ numberValue: alkNum }, {
874								duration: 3000,
875								easing: 'linear',
876								step: function (now) { $('#alkalinity-number').text(Math.ceil(now)); }
877							});
878
879							var bar = new ProgressBar.Path('#hardness-path', { easing: 'linear', duration: 3000 });
880							bar.set(0);
881							//bar.animate((1 - hardNum / 180) / 2);
882							//var hardNeedleNum = Math.floor((hardNum / 180) * 260 - 38);
883							bar.animate((1 - hardNum / 400) / 2);
884							var hardNeedleNum = Math.floor((hardNum / 400) * 260 - 38);
885							$("#hardness-meter .needle").css("transform", "translate(-50%,-50%) rotate(" + hardNeedleNum + "deg)");
886
887							bar = new ProgressBar.Path('#sodium-path', { easing: 'linear', duration: 3000 });
888							bar.set(0);
889							//   bar.animate((1 - sodNum / 100) / 2);
890							//   var sodiumNeedleNum = Math.floor((sodNum / 100) * 260 - 38);
891							bar.animate((1 - sodNum / 200) / 2);
892							var sodiumNeedleNum = Math.floor((sodNum / 200) * 260 - 38);
893							$("#sodium-meter .needle").css("transform", "translate(-50%,-50%) rotate(" + sodiumNeedleNum + "deg)");
894
895							bar = new ProgressBar.Path('#chloride-path', { easing: 'linear', duration: 3000 });
896							bar.set(0);
897							//   bar.animate((1 - choNum / 250) / 2);
898							//   var chlorideNeedleNum = Math.floor((choNum / 250) * 260 - 38);
899							bar.animate((1 - choNum / 500) / 2);
900							var chlorideNeedleNum = Math.floor((choNum / 500) * 260 - 38);
901							$("#chloride-meter .needle").css("transform", "translate(-50%,-50%) rotate(" + chlorideNeedleNum + "deg)");
902
903							bar = new ProgressBar.Path('#fluoride-path', { easing: 'linear', duration: 3000 });
904							bar.set(0);
905							//   bar.animate((1 - fluNum / 4) / 2);
906							//   var fluorideNeedleNum = Math.floor((fluNum / 4) * 260 - 38);
907							bar.animate((1 - fluNum / 2) / 2);
908							var fluorideNeedleNum = Math.floor((fluNum / 2) * 260 - 38);
909							$("#fluoride-meter .needle").css("transform", "translate(-50%,-50%) rotate(" + fluorideNeedleNum + "deg)");
910
911							bar = new ProgressBar.Path('#alkalinity-path', { easing: 'linear', duration: 3000 });
912							bar.set(0);
913							//   bar.animate((1 - alkNum / 200) / 2);
914							//   var alkalinityNeedleNum = Math.floor((alkNum / 200) * 260 - 38);
915							bar.animate((1 - alkNum / 400) / 2);
916							var alkalinityNeedleNum = Math.floor((alkNum / 400) * 260 - 38);
917							$("#alkalinity-meter .needle").css("transform", "translate(-50%,-50%) rotate(" + alkalinityNeedleNum + "deg)");
918
919							// var phPercent = phBal / 14 * 93;
920							// $(".phbox").css("left", Math.ceil(phPercent) + "%");
921
922							// var choPercent = phCho / 4 * 93;
923							// $(".resdbox").css("left", Math.ceil(choPercent) + "%");
924
925							function clamp(n, min, max) {
926								n = parseFloat(n);
927								if (isNaN(n)) n = 0;
928								return Math.max(min, Math.min(max, n));
929							}
930
931							var phPercent = (clamp(phBal, 0, 14) / 14) * 93;
932							$(".phbox").css("left", Math.round(phPercent) + "%");
933
934							var choPercent = (clamp(phCho, 0, 4) / 4) * 93;
935							$(".resdbox").css("left", Math.round(choPercent) + "%");
936
937							this.destroy(); // destroy the waypoint so it runs only once.
938
939						}
940					}
941				});
942
943				var progressPercent = $('#progress-percent').text().replaceAll("%", "");
944				$('circle.progress-circle').css('stroke-dashoffset', 1000 - parseInt(progressPercent * 5, 10));
945			}
946
947			function integrateFromJsonThenRun() {
948				//console.log('[WQ] Starting JSON integration. SiteName:', waterQualitySiteName);
949
950				$.ajax({
951					url: waterQualityApiUrl,
952					method: 'GET',
953					dataType: 'text'
954				}).done(function (raw) {
955					//console.log('[WQ] AJAX success. Raw length:', raw && raw.length);
956
957					var rows;
958					try { rows = JSON.parse(raw); }
959					catch (e) {
960						//console.error('[WQ] JSON.parse failed:', e, raw && raw.slice ? raw.slice(0, 200) : raw);
961						runWaterQualityAnimation();
962						return;
963					}
964
965					//console.log('[WQ] Total rows:', rows.length);
966
967					// Filter to SiteName (trim + case-insensitive)
968					var siteKey = norm(waterQualitySiteName);
969					var siteRows = [];
970					for (var i = 0; i < rows.length; i++) {
971						if (rows[i] && norm(rows[i].SiteName) === siteKey) siteRows.push(rows[i]);
972					}
973
974					//console.log('[WQ] Rows for site:', siteRows.length);
975
976					// If site mismatch, show sample SiteNames
977					if (!siteRows.length) {
978						var sampleSites = {};
979						for (i = 0; i < rows.length && i < 2000; i++) {
980							var sn = rows[i] && rows[i].SiteName ? $.trim(String(rows[i].SiteName)) : '';
981							if (sn) sampleSites[sn] = true;
982						}
983						//console.warn('[WQ] No rows matched SiteName. Example SiteName values:', Object.keys(sampleSites).slice(0, 25));
984						runWaterQualityAnimation();
985						return;
986					}
987
988					// Pick metrics (latest-per-metric within site)
989					var ph = pickLatestSingleNumeric(siteRows, function (r) { return norm(r.T_FullName) === 'ph'; }, 'pH');
990					var chloramine = pickLatestSingleNumeric(siteRows, function (r) {
991						var t = norm(r.T_FullName);
992						return t === 'total chlorine residual' || t.indexOf('chloramine') !== -1;
993					}, 'Total Residual Chloramine / Total Chlorine Residual');
994
995					var hardness = pickLatestSingleNumeric(siteRows, function (r) { return norm(r.T_FullName).indexOf('hardness') !== -1; }, 'Hardness');
996					var sodium = pickLatestSingleNumeric(siteRows, function (r) { return norm(r.T_FullName) === 'sodium'; }, 'Sodium');
997					var chloride = pickLatestSingleNumeric(siteRows, function (r) { return norm(r.T_FullName) === 'chloride'; }, 'Chloride');
998					var fluoride = pickLatestSingleNumeric(siteRows, function (r) { return norm(r.T_FullName) === 'fluoride'; }, 'Fluoride');
999					var alkalinity = pickLatestSingleNumeric(siteRows, function (r) { return norm(r.T_FullName) === 'total alkalinity'; }, 'Alkalinity');
1000
1001					var bacteria = pickLatestBacteriaPair(siteRows, function (r) {
1002						var t = norm(r.T_FullName);
1003						return (t.indexOf('coli') !== -1) || (t.indexOf('coliform') !== -1);
1004					});
1005
1006					// Populate hidden target fields only if empty
1007					setTextIfEmpty('#phBal', ph.value, function (v) { return v.toFixed(2); });
1008					setTextIfEmpty('#phCho', chloramine.value, function (v) { return v.toFixed(2); });
1009
1010					setTextIfEmpty('#hardNum', hardness.value, function (v) { return Math.round(v); });
1011					setTextIfEmpty('#sodNum', sodium.value, function (v) { return Math.round(v); });
1012					setTextIfEmpty('#choNum', chloride.value, function (v) { return Math.round(v); });
1013					setTextIfEmpty('#fluNum', fluoride.value, function (v) { return v.toFixed(1); });
1014					setTextIfEmpty('#alkNum', alkalinity.value, function (v) { return Math.round(v); });
1015
1016					setTextIfEmpty('#negSample', bacteria.neg, function (v) { return Math.round(v); });
1017					setTextIfEmpty('#posSample', bacteria.pos, function (v) { return Math.round(v); });
1018
1019					// Compute posPercent if empty
1020					var negNow = toNumberOrNull($('#negSample').text());
1021					var posNow = toNumberOrNull($('#posSample').text());
1022					if (negNow != null && posNow != null) {
1023						var total = negNow + posNow;
1024						if (total > 0) {
1025							var pct = (posNow / total) * 100;
1026							setTextIfEmpty('#posPercent', pct, function (v) { return Math.round(v); });
1027						}
1028					}
1029
1030					//console.log('[WQ] Integration complete. Running animation.');
1031					runWaterQualityAnimation();
1032
1033				}).fail(function (xhr, status) {
1034					//console.error('[WQ] AJAX failed:', status, 'HTTP:', xhr && xhr.status);
1035					runWaterQualityAnimation();
1036				});
1037			}
1038
1039			if ($('#phnumber').length) {
1040				integrateFromJsonThenRun();
1041			}
1042
1043		});
1044		// Water quality animation + JSON integration (end)
1045
1046	}); // Ready
1047
1048	// Control navs on resize
1049	var matchMediaQuery = window.matchMedia("(max-width: 991px)");
1050
1051	function controlNavs(matchMediaQuery) {
1052		if (matchMediaQuery.matches) {
1053			$('#freeform-search').fadeOut();
1054			$('#search-toggle').removeClass('fa-times active').addClass('fa-search');
1055			$('#nav').fadeOut();
1056			$('#nav-toggle').removeClass('fa-times active').addClass('fa-bars');
1057		}
1058	}
1059
1060	controlNavs(matchMediaQuery);
1061	matchMediaQuery.addListener(controlNavs);
1062
1063	// Scroll button
1064	$('#app-scroll').click(function(e){
1065		var jump = $(this).attr('href');
1066		var new_position = $(jump).offset();
1067		$('html, body').stop().animate({ scrollTop: new_position.top }, 800);
1068		e.preventDefault();
1069	});
1070
1071	// function quickLinksMove() {
1072	// 	if ($window.width() < 1550) {
1073	// 		$('#quick-links').prependTo('#main');
1074	// 	} else{
1075	// 		$('#quick-links').appendTo('#slider');
1076	// 	}
1077	// }
1078	// quickLinksMove();
1079	// $window.resize(quickLinksMove);
1080
1081	/* quick-links position */
1082	// window.onscroll = function() {
1083	// 	if (document.body.scrollTop > 0 || document.documentElement.scrollTop > 0) {
1084	// 		$('#quick-links').css({
1085	// 			'top': '50%',
1086	// 			'transform': 'translateY(-50%)',
1087	// 		});
1088	// 	} else {
1089	// 		$('#quick-links').css({
1090	// 			'top': '433px',
1091	// 			'transform': 'none',
1092	// 		});
1093	// 	}
1094	// };
1095
1096	/* quick-links slider items count */
1097	var slides = $( "#quick-links-slider .quick-link" );
1098
1099	function chunkArray(myArray, chunk_size) {
1100		var results = [];
1101
1102		while (myArray.length) {
1103			results.push(myArray.splice(0, chunk_size))
1104		}
1105
1106		return results;
1107	}
1108
1109	var slidersGroup = chunkArray(slides, 4);
1110
1111	// quick-links slider
1112	var matchMediaQuery = window.matchMedia("(min-width: 1550px)");
1113
1114	function manageCarousel(matchMediaQuery) {
1115		var slides = $( "#quick-links-slider .quick-link" );
1116
1117		if (matchMediaQuery.matches) {
1118
1119			for(var j = 0; j < slidersGroup.length; j++) {
1120				if (! $(slidersGroup[j]).parent().is( "div.quick-links-box" ) ) {
1121					$(slidersGroup[j]).wrapAll( "<div class='quick-links-box'></div>");
1122				}
1123			}
1124
1125			// destroy slider after resizing
1126			$('#quick-links-slider').trigger('destroy.owl.carousel').removeClass('owl-carousel owl-loaded');
1127			$('#quick-links-slider').find('.owl-stage-outer').children().unwrap();
1128			$('#quick-links-slider .owl-item').remove();
1129
1130			var quickLinksBoxCount = $("#quick-links-slider .quick-links-box").length;
1131			function quickLinksNav(cnt) {return quickLinksBoxCount > cnt ? true : false;}
1132
1133			//creates the slider again
1134			//item consists of a group of 5 elements
1135			if(typeof $.fn.owlCarousel !== "undefined"){
1136				$("#quick-links-slider").owlCarousel({
1137					navText : ["<img src='_assets_/images/quick-links-prev.png'>","<img src='_assets_/images/quick-links-next.png'>"],
1138					navContainer:'.quick-links-arrows',
1139					autoplay: true,
1140					autoplayTimeout: 8000,
1141					loop: true,
1142					smartSpeed: 600,
1143					margin:0,
1144					responsive: {
1145						0: {
1146							items: 1,
1147							nav: quickLinksNav(1),
1148						}
1149					}
1150				});
1151			}
1152		} else {
1153			if ( slides.parent().is( "div.quick-links-box" ) ) {
1154				slides.unwrap();
1155
1156				// destroy slider after resizing
1157				$('#quick-links-slider').trigger('destroy.owl.carousel').removeClass('owl-carousel owl-loaded');
1158				$('#quick-links-slider').find('.owl-stage-outer').children().unwrap();
1159				$('#quick-links-slider .owl-item').remove();
1160
1161				var quickLinksCount = $("#quick-links-slider .quick-link").length;
1162				function quickLinksItems(cnt) {return quickLinksCount > cnt ? cnt : quickLinksCount;}
1163				function quickLinksNav(cnt) {return quickLinksCount > cnt ? true : false;}
1164
1165				//creates a slider again
1166				if(typeof $.fn.owlCarousel !== "undefined"){
1167
1168					$("#quick-links-slider").owlCarousel({
1169						navText : ["<img src='_assets_/images/quick-links-prev.png'>","<img src='_assets_/images/quick-links-next.png'>"],
1170						navContainer:'.quick-links-arrows',
1171						autoplay: true,
1172						autoplayTimeout: 10000,
1173						smartSpeed: 600,
1174						margin:0,
1175						responsive: {
1176							0: {
1177								items: 1,
1178								nav: quickLinksNav(1),
1179							},
1180							400: {
1181								items: quickLinksItems(2),
1182								nav: quickLinksNav(2),
1183							},
1184							650: {
1185								items: quickLinksItems(3),
1186								nav: quickLinksNav(3),
1187							},
1188							1200: {
1189								items: quickLinksItems(4),
1190								nav: quickLinksNav(4),
1191							}
1192						}
1193					});
1194				}
1195			}
1196		}
1197	}
1198
1199	manageCarousel(matchMediaQuery);
1200	matchMediaQuery.addListener(manageCarousel);
1201
1202	// Search placeholder text
1203	function searchPlaceholder(){
1204		if (window.innerWidth > 767) {
1205			$('#homepage #search input').attr('placeholder', 'Search Des Moines Water Works...');
1206		} else {
1207			$('#homepage #search input').attr('placeholder', 'Search...');
1208		}
1209
1210		if (window.innerWidth > 991 && window.innerWidth < 1300) {
1211			$('#freeform-search input').attr('placeholder', 'Search...');
1212		} else {
1213			$('#freeform-search input').attr('placeholder', 'Search here...');
1214		}
1215	}
1216
1217	searchPlaceholder();
1218	$(window).resize(searchPlaceholder);
1219
1220	/* Freeform search focus */
1221	$('#freeform-search input').focus(function(){
1222		$(this).css({
1223			'padding-left': 10,
1224			'background': '#f0f4fa'
1225		});
1226	});
1227
1228	$('#freeform-search input').blur(function(){
1229		$(this).css('padding-left', 42);
1230		$(this).css({
1231			'padding-left': 42,
1232			'background': '#f0f4fa url(_assets_/images/search-icon2.png) 16px 17px/15px no-repeat'
1233		});
1234	});
1235
1236	$("#translate-button:contains('EN')").html(function(_, html) {
1237		return html.replace(/(EN)/g, '<span class="translate-en">$1</span>');
1238	});
1239
1240	$("#translate-button").bind("DOMSubtreeModified", function() {
1241		var translateEN = $.trim($(this).text());
1242		if (translateEN == 'IN' || translateEN == 'INEN'){
1243			$('.translate-en').text('EN');
1244		}
1245	});
1246
1247})(jQuery);
1248
1249$(window).on('load', function() {
1250	// .first-level position and hover
1251	function navDropdown(){
1252		$('#nav ul.first-level').each(function(){
1253			if (window.innerWidth > 991) {
1254				var containerHeight = $('.header-bottom-wrap').height();
1255				var parentHeight = $(this).parent().height();
1256				var parentOffset = $(this).parent().position().top;
1257				var remain = containerHeight - (parentHeight + parentOffset);
1258				var topPosition = parentHeight + remain;
1259				$(this).css('top', topPosition);
1260			}
1261		});
1262	}
1263
1264	navDropdown();
1265	$(window).resize(navDropdown);
1266
1267});
1268
1269

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.