PageSourceSearch

https://www.sd511.org/atis-static.iteriscdn.com/WebApps/SD/SafeTra…/js/components/cameraViewer.js?v=1.9.16

js sd511.org collected 2026-09-24 12:09:22 UTC 26,805 bytes, 626 lines download raw bytes

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.