1/* 2* filename: cameraViewer.js 3* purpose: interface for loading modal 4*/ 5 6var CameraViewer = function() { 7 // state --------------------------------------------------------------------------------------- 8 var config = { 9 rwisOnly: true 10 }; // static config, or passed in through init function 11 var state = { 12 camId: null, 13 cam: null, 14 camNumber: null, 15 isFullscreen: null 16 }; // state 17 var elements = {}; // html elements, js objects 18 var components = {}; // component modules 19 var reactions = {}; 20 21 const TIME_ZONE = 'America/Chicago'; 22 // mount component ----------------------------------------------------------------------------- 23 function init() { 24 //console.log('ModalLoader -> init() called'); 25 // create or capture container 26 config.container = document.querySelector('#cameraViewer'); 27 //console.log('container = ', config.container); 28 29 // load bootstrap styles if needed 30 31 render(); 32 captureElements(); 33 addEventListeners(); 34 addComponents(); 35 } 36 37 // render method ------------------------------------------------------------------------------- 38 function render () { 39 // #cameraViewer div is rendered by backend 40 } 41 42 // capture elements ---------------------------------------------------------------------------- 43 function captureElements() { 44 // DOM handles 45 elements.container = config.container; 46 47 if (!elements.container) { 48 console.error('CameraViewer: container element not found'); 49 return; 50 } 51 52 elements.modalDialog = elements.container.querySelector('.modal-dialog'); 53 elements.modalContent = elements.container.querySelector('.modal-content'); 54 elements.modalBody = elements.container.querySelector('.modal-body'); 55 elements.cameraImagesCarousel = elements.container.querySelector('#camImagesCarousel'); 56 elements.camViewerFavIcon = elements.container.querySelector('#camViewerFavIcon'); 57 elements.camDescription = elements.container.querySelector('#camDescription'); 58 elements.camUpdateSpan = elements.container.querySelector('p.imageUpdated span'); 59 elements.camOrder = elements.container.querySelector('#camOrder'); 60 elements.camSelect = elements.container.querySelector('#camViewerCamSelect'); 61 // elements.locSelect = elements.container.querySelector('#camViewerLocationSelect'); 62 // elements.locSelectLabel = elements.container.querySelector('label[for=camViewerLocationSelect]'); 63 elements.nextIcon = elements.container.querySelector('a.carousel-control-next'); 64 elements.prevIcon = elements.container.querySelector('a.carousel-control-prev'); 65 elements.carouselIndicators = elements.container.querySelector('ol.carousel-indicators'); 66 elements.closeCamViewerBtn = elements.container.querySelector('#closeCamViewerBtn'); 67 68 // jQuery handles 69 elements.$container = $('#' + config.container.id); 70 elements.$camImagesCarousel = $('#'+config.container.id+ ' #camImagesCarousel'); 71 72 // Log warnings for missing critical elements 73 var criticalElements = ['camDescription', 'camViewerFavIcon', 'camOrder', 'camSelect']; 74 criticalElements.forEach(function(elementName) { 75 if (!elements[elementName]) { 76 console.warn('CameraViewer: Critical element not found:', elementName); 77 } 78 }); 79 } 80 81 // add components ------------------------------------------------------------------------------ 82 function addComponents() { 83 // bootstrap components 84 elements.$camImagesCarousel.carousel({ 85 interval: false 86 }) 87 } 88 89 // external API -------------------------------------------------------------------------------- 90 function show(id) { 91 var cam = InfrastructureServices.getCameraById(id), 92 camsOnRoute = InfrastructureServices.getCamerasOnSameRoute(id, {rwisOnly: true}); 93 //console.error('Attempting to show cam ', cam, ' camsOnR
93oute = ', camsOnRoute); 94 95 // Sync favorite status with currentCCTVFeatures (authoritative source used by table) 96 console.log('CameraViewer: Opening camera', id, 'initial favorite status:', !!cam.isFavorite); 97 98 var favoriteStatusSynced = false; 99 if (typeof currentCCTVFeatures !== 'undefined' && currentCCTVFeatures.length > 0) { 100 // Try multiple ID matching strategies to ensure we find the camera 101 for (var i = 0; i < currentCCTVFeatures.length; i++) { 102 var tableCamera = currentCCTVFeatures[i]; 103 // Match by ID (try both loose and strict comparison for different data types) 104 if (tableCamera.id == id || tableCamera.id === id || 105 String(tableCamera.id) === String(id)) { 106 console.log('CameraViewer: Found matching camera in table data'); 107 console.log('CameraViewer: Table camera favorite status:', !!tableCamera.properties.favorite); 108 console.log('CameraViewer: Camera object before sync:', !!cam.isFavorite); 109 110 // Sync the favorite status from table to camera object 111 cam.isFavorite = !!tableCamera.properties.favorite; 112 favoriteStatusSynced = true; 113 114 console.log('CameraViewer: Camera object after sync:', !!cam.isFavorite); 115 break; 116 } 117 } 118 } 119 120 if (!favoriteStatusSynced) { 121 console.warn('CameraViewer: Could not sync favorite status from table data for camera', id); 122 console.log('CameraViewer: currentCCTVFeatures available:', typeof currentCCTVFeatures !== 'undefined'); 123 console.log('CameraViewer: currentCCTVFeatures length:', typeof currentCCTVFeatures !== 'undefined' ? currentCCTVFeatures.length : 'N/A'); 124 } 125 126 state.camId = id; 127 state.cam = cam; 128 state.camNumber = 0; 129 state.camCount = cam.cameras? cam.cameras.length : 0; 130 state.isFullscreen = false; 131 132 updateCameraDescription(cam); 133 updateImageTime(id); 134 updateCarouselImages(cam.cameras); 135 updateFavoriteStatus(cam); 136 updateCameraOrder(); 137 updateCamSelect(); 138 // updateLocationSelect(camsOnRoute, id); 139 hideCarouselControlsOnSlide(); 140 141 elements.cameraImagesCarousel.style.minHeight = calculateMinCarouselHeight(); 142 elements.$container.modal('show'); 143 if (document.querySelector('.mapboxgl-popup') != null) { 144 document.querySelector('.mapboxgl-popup').remove(); 145 } 146 elements.closeCamViewerBtn.focus(); 147 } 148 149 function hide() { 150 state.camId = null; 151 state.cam = null; 152 state.camNumber = null; 153 state.camCount = 0; 154 155 elements.$container.modal('hide'); 156 elements.cameraImagesCarousel.style.minHeight = 0; 157 if (isFullscreen()) { 158 toggleFullscreenMode(false); 159 state.isFullscreen = null; 160 } 161 } 162 163 function getCameraById(id) { 164 // will return instance of Camera class 165 var camera = camArray.find(function(cam) { 166 return cam.id+'' == id+''; 167 }); 168 return camera; 169 } 170 171 172 173 function isOpen() { 174 return state.cam !== null; 175 } 176 177 function isFullscreen() { 178 return state.isFullscreen; 179 } 180 181 // event handlers i.e. internal API ------------------------------------------------------------ 182 function handleNextIconClick() { 183 //console.log('CameraViewer->handleNexticonClick called'); 184 if (state.camNumber+1 < state.camCount) { 185 state.camNumber++; 186 updateCameraOrder(); 187 updateCamSelect(); 188 updateViewerImage(state.camNumber); 189 elements.$camImagesCarousel.carousel('next'); 190 hideCarouselControlsOnSlide(); 191 } 192 193 } 194 195 function handlePrevIconClick() { 196 //console.log('CameraViewer->handlePrevIconClick called'); 197 if (state.camNumber > 0) { 198 state.camNumber--; 199 updateCameraOrder(); 200 updateCamSelect(); 201 updateViewerImage(state.camNumber); 202 elements.$camImagesCarousel.carousel('prev'); 203 hideCarouselControlsOnSlide(); 204 } 205 } 206 207 function handleDoubleClick() { 208 elements.$camImagesCarousel.carousel(state.camNumber); 209 updateCameraOrder(); 210 } 211 212 function handleCarouselIndicatorClick(e) { 213 //console.log('CameraViewer->handleCarouselIndicatorClick called'); 214 var slideToIndex = parseInt(e.target.dataset.slideTo); 215 state.camNumber = slideToIndex; 216 updateCameraOrder(); 217 updateCamSelect(); 218 updateViewerImage(state.camNumber); 219 elements.$camImagesCarousel.carousel(slideToIndex); 220 hideCarouselControlsOnSlide(); 221 } 222 223 function handleCamerasSelect(e) { 224 //console.log('CameraViewer->handleCamerasSelect called'); 225 var slideToIndex = parseInt(e.target.value); 226 state.camNumber = slideToIndex; 227 updateCameraOrder(); 228 updateCamSelect(); 229 updateViewerImage(state.camNumber); 230 elements.$camImagesCarousel.carousel(slideToIndex); 231 hideCarouselControlsOnSlide(); 232 } 233 234 function handleLocationSelect(e) { 235 //console.log('CameraViewer->handleLocationSelect called'); 236 var newCamId = parseInt(e.target.value),
237 newCam = InfrastructureServices.getCameraById(newCamId), 238 camsOnRoute = InfrastructureServices.getCamerasOnSameRoute(newCamId, {rwisOnly: true}); 239 //console.log('showing cam ', newCam, ', camsOnRoute = ', camsOnRoute); 240 state.camId = newCamId; 241 state.cam = newCam; 242 state.camNumber = 0; 243 state.camCount = newCam.cameras? newCam.cameras.length : 0; 244 245 updateCameraDescription(newCam); 246 updateImageTime(newCamId); 247 updateCarouselImages(newCam.cameras); 248 updateFavoriteStatus(newCam); 249 updateCameraOrder(); 250 updateCamSelect(); 251 updateLocationSelect(camsOnRoute, newCamId); 252 hideCarouselControlsOnSlide(); 253 254 // TODO: add map pan to 255 // MapCanvas.flyTo(state.cam.lat, state.cam.lng); 256 map.flyTo({center: [state.cam.lng, state.cam.lat], zoom: 8}); 257 } 258 259 function handleFavoriteIconClick() { 260 //console.log('CameraViewer->handleFavoriteIconClick called'); 261 262 // Use the shared toggle_favorite_status function to ensure synchronization with camera table 263 if (typeof toggle_favorite_status === 'function') { 264 console.log('Camera viewer using shared toggle_favorite_status for camera:', state.cam.id); 265 266 // Update the viewer icon first to provide immediate feedback 267 var favIcon = elements.camViewerFavIcon; 268 if (!state.cam.isFavorite) { 269 // Switching to favorite 270 favIcon.classList.add('favorite-heart', 'fas', 'text-danger'); 271 favIcon.classList.remove('non-favorite-heart', 'far', 'text-muted'); 272 state.cam.isFavorite = true; 273 } else { 274 // Switching to non-favorite 275 favIcon.classList.add('non-favorite-heart', 'far', 'text-muted'); 276 favIcon.classList.remove('favorite-heart', 'fas', 'text-danger'); 277 state.cam.isFavorite = false; 278 } 279 280 // Call shared function to update data and sync with table 281 toggle_favorite_status(state.cam.id, true); 282 283 } else { 284 // Fallback to original UserServices method if shared function not available 285 console.log('Camera viewer using fallback UserServices method for camera:', state.cam.id); 286 if (!state.cam.isFavorite) { 287 UserServices.saveIdToFavoriteCameras(state.cam.id) 288 .then(function() {updateFavoriteStatus(state.cam)}) 289 .catch(function() {console.log('fail 1')}); 290 } else { 291 UserServices.deleteIdInFavoriteCameras(state.cam.id) 292 .then(function() {updateFavoriteStatus(state.cam)}) 293 .catch(function() {console.log('fail 2')}); 294 } 295 } 296 } 297 298 function handleImageClick(e) { 299 if (e.target.tagName == 'IMG' && e.target.classList.contains('camImage')) { 300 //console.log('CameraViewer->handleImageClick called'); 301 // show full screen 302 if (isFullscreen()) { 303 toggleFullscreenMode(false); 304 state.isFullscreen = false; 305 } else { 306 toggleFullscreenMode(true); 307 state.isFullscreen = true; 308 } 309 } 310 } 311 312 function hideCarouselControlsOnSlide(e) { 313 // hide first and last carousel items when appropriate 314 elements.nextIcon.classList.remove('d-none'); 315 elements.prevIcon.classList.remove('d-none'); 316 317 if (state.camNumber == 0) { 318 elements.prevIcon.classList.add('d-none'); 319 } 320 if (state.camNumber+1 == state.camCount) { 321 elements.nextIcon.classList.add('d-none'); 322 } 323 } 324 325 function handleCamViewerHide(e) { 326 //console.log('CameraViewer->handleCamViewerHide called'); 327 328 state.camId = null; 329 state.cam = null; 330 state.camNumber = null; 331 state.camCount = 0; 332 elements.cameraImagesCarousel.style.minHeight = 0; 333 setTimeout(function() { 334 if (isFullscreen()) { 335 toggleFullscreenMode(false); 336 state.isFullscreen = null; 337 } 338 }, 300); 339 340 } 341 342 // add event listeners ------------------------------------------------------------------------- 343 function addEventListeners() { 344 elements.nextIcon.addEventListener('click', handleNextIconClick); 345 elements.prevIcon.addEventListener('click', handlePrevIconClick); 346 elements.nextIcon.addEventListener('dblclick', handleDoubleClick); 347 elements.prevIcon.addEventListener('dblclick', handleDoubleClick); 348 elements.camViewerFavIcon.addEventListener('click', handleFavoriteIconClick); 349 elements.camSelect.addEventListener('change', handleCamerasSelect); 350 // elements.locSelect.addEventListener('change', handleLocationSelect); 351 elements.carouselIndicators.addEventListener('click', handleCarouselIndicatorClick); 352 elements.cameraImagesCarousel.addEventListener('click', handleImageClick); 353 elements.$camImagesCarousel.on('slide.bs.carousel', hideCarouselControlsOnSlide); 354 elements.$container.on('hide.bs.modal', handleCamViewerHide); 355 } 356 357 function removeEventListeners() { 358 elements.nextIcon.removeEventListener('click', handleNextIconClick); 359 elements.prevIcon.removeEventListener('click', handlePrevIconClick); 360 elements.nextIcon.removeEventListener('dblclick', handleDoubleClick); 361 elements.prevIcon.removeEventListener('dblclick', handleDoubleClick); 362 elements.camViewerFavIcon.removeEventListener('click', handleFavoriteIconClick); 363 elements.camSelect.removeEventListener('change', handleCamerasSelect); 364 // elements.locSelect.removeEventListener('change', handleLocationSelect); 365 elements.carouselIndicators.removeEventListener('click', handleCarouselIndicatorClick); 366 elements.cameraImagesCarousel.removeEventListener('click', handleImageClick); 367 } 368 // unmount ------------------------------------------------------------------------------------- 369 function unmount() { 370 removeEventListeners(); 371 } 372 373 374 // helpers ------------------------------------------------------------------------------------- 375 function toggleFullscreenMode(isFullscreen) { 376 if (isFullscreen) { 377 elements.cameraImagesCarousel.classList.add('mb-auto');
378 elements.cameraImagesCarousel.classList.add('mt-auto'); 379 elements.modalDialog.classList.add('m-0'); 380 elements.modalDialog.classList.add('max-w-100'); 381 elements.modalDialog.classList.add('h-100'); 382 elements.modalContent.classList.add('w-100'); 383 elements.modalContent.classList.add('min-h-100vh'); 384 if (EnvUtils.isPortrait()) { 385 elements.modalContent.classList.add('h-100'); 386 } else { 387 elements.cameraImagesCarousel.style.minHeight = '54vw'; 388 } 389 } else { 390 elements.cameraImagesCarousel.classList.remove('mb-auto'); 391 elements.cameraImagesCarousel.classList.remove('mt-auto'); 392 elements.modalDialog.classList.remove('m-0'); 393 elements.modalDialog.classList.remove('max-w-100'); 394 elements.modalDialog.classList.remove('h-100'); 395 elements.modalContent.classList.remove('w-100'); 396 elements.modalContent.classList.remove('min-h-100vh'); 397 if (elements.modalContent.classList.contains('h-100')) { 398 elements.modalContent.classList.remove('h-100'); 399 } 400 elements.cameraImagesCarousel.style.minHeight = calculateMinCarouselHeight(); 401 } 402 } 403 404 function updateCameraDescription(cam) { 405 var camDescriptionDiv = elements.camDescription, 406 camDescription = cam.generateDescription(); 407 408 if (camDescriptionDiv) { 409 camDescriptionDiv.innerHTML = camDescription; 410 } else { 411 console.warn('CameraViewer: camDescription element not found. Attempting to re-capture elements.'); 412 // Try to re-capture the element 413 elements.camDescription = elements.container.querySelector('#camDescription'); 414 if (elements.camDescription) { 415 elements.camDescription.innerHTML = camDescription; 416 } else { 417 console.error('CameraViewer: #camDescription element still not found in DOM'); 418 } 419 } 420 } 421 422 function updateInstanceTime(time) { 423 var camUpdateSpan = elements.camUpdateSpan; 424 var lastUpdate; 425 // Convert Unix timestamp (seconds) to milliseconds 426 var camUpdateTime = luxon.DateTime.fromMillis(time * 1000).setZone(TIME_ZONE); 427 var messageTime = luxon.DateTime.local().minus({ hours: 4 }).toSeconds(); 428 if (time < messageTime) { 429 lastUpdate = "Image is more than 4 hours old"; 430 camUpdateSpan.classList.add('text-danger'); 431 camUpdateSpan.innerHTML = lastUpdate; 432 } else { 433 lastUpdate = "Image Updated Today at " + camUpdateTime.toFormat('h:mm a'); 434 camUpdateSpan.classList.remove('text-danger'); 435 camUpdateSpan.innerHTML = lastUpdate; 436 } 437 } 438 439 function updateImageTime(camId) { 440 var camUpdateSpan = elements.camUpdateSpan; 441 442 if (!camUpdateSpan) { 443 console.warn('CameraViewer: camUpdateSpan element not found'); 444 return; 445 } 446 447 var cam = InfrastructureServices.getCameraById(camId); 448 var camUpdateTime = cam.generateUpdateTime(); 449 if (camUpdateTime.class != null) { 450 camUpdateSpan.classList.add(camUpdateTime.class); 451 camUpdateSpan.innerHTML = camUpdateTime.updateTime; 452 } else { 453 camUpdateSpan.classList.remove('text-danger'); 454 camUpdateSpan.innerHTML = camUpdateTime.updateTime; 455 } 456 } 457 458 function updateFavoriteStatus(cam) { 459 var favIcon = elements.camViewerFavIcon; 460 461 if (!favIcon) { 462 console.warn('CameraViewer: camViewerFavIcon element not found'); 463 return; 464 } 465 466 console.log('CameraViewer: Updating favorite icon for camera', cam.id, 'isFavorite:', !!cam.isFavorite); 467 468 if (cam.isFavorite) { 469 console.log('CameraViewer: Setting icon to favorited state'); 470 favIcon.classList.add('favorite-heart'); 471 favIcon.classList.add('fas'); 472 favIcon.classList.add('text-danger'); 473 favIcon.classList.remove('non-favorite-heart'); 474 favIcon.classList.remove('far'); 475 favIcon.classList.remove('text-muted'); 476 } else { 477 console.log('CameraViewer: Setting icon to non-favorited state'); 478 favIcon.classList.add('non-favorite-heart'); 479 favIcon.classList.add('far'); 480 favIcon.classList.add('text-muted'); 481 favIcon.classList.remove('favorite-heart'); 482 favIcon.classList.remove('fas'); 483 favIcon.classList.remove('text-danger'); 484 } 485 486 console.log('CameraViewer: Favorite icon classes after update:', favIcon.className); 487 } 488 489 function updateCameraOrder() { 490 var camOrderDiv = elements.camOrder; 491 492 if (!camOrderDiv) { 493 console.warn('CameraViewer: camOrder element not found'); 494 return; 495 } 496 497 camOrderDiv.innerHTML = 'Camera '+(state.camNumber+1)+' of '+state.camCount; 498 } 499 500 function updateCarouselImages(camArray) { 501 var carouselContainer = elements.cameraImagesCarousel, 502 carouselIndicators = carouselContainer.querySelector('.carousel-indicators'), 503 carouselInner = carouselContainer.querySelector('.carousel-inner'), 504 carouselInnerHtml = '', 505 carouselIndicatorsHtml = '', 506 carouselItemTemplate = 507 '<div class="carousel-item IS_ACTIVE">' 508 + '<img class="camImage d-block w-100" src="SRC" alt="DESC" style="cursor: pointer;">' 509 + '</div>', 510 carouselIndicatorTemplate = '<li data-target="#carouselExampleIndicators" data-sli
510de-to="INDEX" class="IS_ACTIVE"></li>'; 511 512 for (var i = 0; i < camArray.length; i++) { 513 var cam = camArray[i]; 514 515 var processedInnerTemplate = carouselItemTemplate.replace('SRC', cam.image); 516 processedInnerTemplate = processedInnerTemplate.replace('IS_ACTIVE', i == state.camNumber? 'active' : ''); 517 processedInnerTemplate = processedInnerTemplate.replace('DESC',cam.name); 518 carouselInnerHtml += processedInnerTemplate; 519 520 var processedIndicatorTemplate = carouselIndicatorTemplate.replace('INDEX', i); 521 processedIndicatorTemplate = processedIndicatorTemplate.replace('IS_ACTIVE', i == 0 ? 'active' : ''); 522 carouselIndicatorsHtml += processedIndicatorTemplate; 523 } 524 carouselInner.innerHTML = carouselInnerHtml; 525 carouselIndicators.innerHTML = carouselIndicatorsHtml; 526 527 if (state.camNumber == 0) { 528 elements.prevIcon.classList.add('d-none'); 529 } 530 if (state.camNumber+1 == state.camCount) { 531 elements.nextIcon.classList.add('d-none'); 532 } 533 534 } 535 536 function updateCamSelect() { 537 var camArray = state.cam.cameras, 538 camSelect = elements.camSelect, 539 optionsHtml = ''; 540 541 if (!camSelect) { 542 console.warn('CameraViewer: camSelect element not found'); 543 return; 544 } 545 546 for (var i = 0; i < camArray.length; i++) { 547 var cam = camArray[i], 548 optionHtml = '<option value="'+i+'" '+ (i==state.camNumber? 'selected' : '') +'>'+cam.name+'</option>'; 549 optionsHtml += optionHtml; 550 } 551 camSelect.innerHTML = optionsHtml; 552 // gtag('event','Camera Selected', { 553 // "Camera ": camArray[state.camNumber].description 554 // }); 555 window.dataLayer.push({ 556 'event':'Camera Selected', 557 "Camera ": camArray[state.camNumber].description 558 }); 559 ////_paq.push(['trackEvent', "Camera Selected", "Camera", camArray[state.camNumber].description]); 560 updateInstanceTime(camArray[state.camNumber].updateTime); 561 } 562 563 // function updateLocationSelect(camsForRoute, selectedCamId) { 564 // var locSelect = elements.locSelect, 565 // optionsHtml = ''; 566 // camsForRoute.forEach(cam => { 567 // let description = cam.name + (cam.route? ' - '+cam.route : '') + (cam.mrm?' MM '+cam.mrm : ''), 568 // isSelected = cam.id == selectedCamId? 'selected' : '', 569 // optionHtml = '<option value="'+cam.id+'" '+isSelected+'>'+description+'</option>'; 570 // optionsHtml += optionHtml; 571 // }); 572 // locSelect.innerHTML = optionsHtml; 573 574 // var selectedCam = InfrastructureServices.getCameraById(selectedCamId); 575 // // gtag('event','Camera Location Selected', { 576 // // "Location ": selectedCam.description 577 // // }); 578 // window.dataLayer.push({ 579 // 'event':'Camera Location Selected', 580 // "Location ": selectedCam.description 581 // }); 582 // //_paq.push(['trackEvent', "Camera Location Selected", "Location", selectedCam.description]); 583 // if (config.rwisOnly && !selectedCam.isRwis()) { 584 // locSelect.classList.add('d-none'); 585 // elements.locSelectLabel.classList.add('d-none'); 586 // } else { 587 // locSelect.classList.remove('d-none'); 588 // elements.locSelectLabel.classList.remove('d-none'); 589 // } 590 // } 591 592 function updateViewerImage(indx) { 593 $('.carousel-item').removeClass('active'); 594 $('[data-target = "#carouselExampleIndicators"]').removeClass('active') 595 $($('.carousel-item')[indx]).addClass('active'); 596 $($('[data-target = "#carouselExampleIndicators"]')[indx]).addClass('active') 597 }; 598 599 function calculateMinCarouselHeight() { 600 var minHeight; 601 if (window.innerWidth > 500) { 602 minHeight = '258px'; 603 } else { 604 minHeight = (window.innerWidth - 16*3) * (0.5); 605 minHeight += 'px'; 606 } 607 return minHeight; 608 } 609 610 // interface ----------------------------------------------------------------------------------- 611 var interface = { 612 state: state, 613 elements: elements, 614 components: components, 615 init: init, 616 unmount: unmount, 617 show: show, 618 hide: hide, 619 toggleFullscreenMode: toggleFullscreenMode, 620 updateFavoriteStatus: updateFavoriteStatus 621 }; 622 return interface; 623}(); 624 625window.addEventListener('load', CameraViewer.init); 626window.addEventListener('unload', CameraViewer.unmount);
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.