PageSourceSearch

https://commonwealth-connection.com/static/web_portal/js/broadband_coverage.js

js commonwealth-connection.com collected 2026-10-04 05:26:59 UTC 17,629 bytes, 558 lines download raw bytes

1/**
2 * Definition import and declaration to support IntelliSense completion for development
3 * @typedef {import('/static/notifications/js/notifications.mjs').Notifications} Notifications
4 * @type Notifications
5 */
6var notifications
7
8const bbcov_layer_structure = {	
9
10	'root':'bbcov',
11	'source': {
12		'va': 'vacoll',
13		'fcc': 'fccbdc',
14	},
15	'coverage': {
16		'0': '0p',
17		'80': '80p',
18		'85': '85p',
19		'95': '95p',
20	},
21	'speed': {
22		'25_3': '25d3u',
23		'100_20': '100d20u',
24		'100': '100d',
25		'250': '250d',
26		'1000': '1000d',
27	},
28	'percent': {
29		'0': '0p',
30		'80': '80p',
31		'85': '85p',
32		'95': '95p',
33	},
34	'tech': {
35		'any': 'any',
36		'cable': 'cable',
37		'dsl': 'dsl',
38		'fiber': 'fiber',
39		'fixed': 'fixedwireless',
40	},
41};
42
43const bbcov_layer_structure_cql = {	
44		
45	'speed': {
46		// '25d3u': '25_3',
47		'100d20u': '100_20',
48		'100d': '100',
49		'250d': '250',
50		'1000d': '1000',
51	},
52};
53
54var bbcov_layer_options = {
55	'speed': bbcov_layer_structure['speed']['100_20'],
56	'tech': bbcov_layer_structure['tech']['any'],
57	'percent': bbcov_layer_structure['percent']['80'],
58}
59
60var layer_template_bbcov = {
61		"displayName": "",
62		"legendName": "",
63		"date": "",
64		"url": "https://cgit06.cc.vt.edu/geoserver/broadband/wms",
65		"opacity": 0.50,
66		"options": {
67			"service": "WMS",
68			"version": "1.1.1",
69			"request": "GetMap",
70			"srs": "EPSG:3857",
71			"layers": "",
72			"transparent": true,
73			"tiled": true,
74			"format": "image/png",
75			"styles": "",
76			"width": "256",
77			"height": "256",
78		},
79		"color":"",
80		"zIndex": 2
81};
82
83var useCQL = false;
84
85var bbcov_map_layers = {};
86
87// Multi-Layer System Variables
88var activeCoverageLayers = [];
89const availableColors = new Set(['ff2626', '3232fe', '00ff00']); // old ones: ['fb6c05', 'ec4ffd', '8105fb']
90var maxLayers = 3;
91
92// Multi-Layer Helper Functions
93/**
94 * Return the next available color from the set of available colors
95 * @returns {String|undefined}	Either a color string or undefined if none is available
96 */
97function getNextAvailableColor() {
98    // Build a set from the colors used in the active coverage layers, diff the color list against it, and return the first unused color
99    return [...availableColors.difference(new Set(activeCoverageLayers.map(l => l.color)))][0]
100}
101
102function addTestLayer(speed, tech, coverage, version, color, layerId) {
103    if (activeCoverageLayers.length >= maxLayers) {
104        notifications.warn('Maximum 3 layers allowed');
105        return false;
106    }
107        
108    activeCoverageLayers.push({
109        id: layerId, speed, tech, coverage, version, color,
110        displayName: buildLayerDisplayName(speed, tech, coverage, version)
111    });
112    return true;
113}
114
115function removeTestLayer(layerId) {
116    activeCoverageLayers = activeCoverageLayers.filter(layer => layer.id !== layerId);    
117    removeCustomLayerFromLegend(layerId);
118    updateActiveLayersDisplay();
119}
120
121$( document ).ready(function() {
122	
123	bbcovLayerRefresh();
124});
125
126function bbcovLayerRefresh(){
127	
128	bbcov_map_layers = {
129		'va_collected': {
130			'id': `${bbcov_layer_structure['root']}_${bbcov_layer_structure['source']['va']}_${bbcov_layer_options['speed']}_${bbcov_layer_options['percent']}_${bbcov_layer_options['tech']}`,
131			"displayName": "VA Collected",
132			"legendName": "VA Collected",
133			"date": "20240308",
134			"opacity": 0.5,
135			"options": {
136                "layers": "broadband:bbcoverage_vacolleced",
137				"styles": `${bbcov_layer_structure['root']}_${bbcov_layer_structure['source']['va']}_${bbcov_layer_options['speed']}_${bbcov_layer_options['percent']}_${bbcov_layer_options['tech']}`,
138			},
139			"color": "#7cb5db",
140			"zIndex": 4
141		},
142		'fcc_any': {
143			'id': `${bbcov_layer_structure['root']}_${bbcov_layer_structure['source']['fcc']}_${bbcov_layer_options['speed']}_${bbcov_layer_options['percent']}_${bbcov_layer_options['tech']}`,	
144			"displayName": "FCC BDC",
145			"legendName": "FCC BDC",
146			"date": "20241231",
147			"opacity": 0.65,
148			"options": {
149				"layers": "broadband:bbcoverage_fccbdc",
150				"styles": `${bbcov_layer_structure['root']}_${bbcov_layer_structure['source']['fcc']}_${bbcov_layer_options['speed']}_${bbcov_layer_options['percent']}_${bbcov_layer_options['tech']}`,
151			},
152			"color": "#ec4ffd",
153			"zIndex": 2
154		}
155	}
156}
157
158function getCQLfilter(layerKey){
159	
160	var returnDict = {						
161		"styles": '',
162		"CQL_FILTER": "",
163	}
164	
165	var coverage = 0;
166	var speed = bbcov_layer_structure_cql['speed'][bbcov_layer_options['speed']];
167	var operand = '>';
168	
169	switch(layerKey){
170        case 'va_collected':
171			returnDict["styles"] = "bbcov_vacollected_80p";
172			coverage = 80;
173			operand = '>=';
174			break;
175		case 'va_any':
176			returnDict["styles"] = "bbcov_vacollected_0p";
177			coverage = 0;
178			operand = '>';
179			break;
180		case 'va_80':
181			returnDict["styles"] = "bbcov_vacollected_80p";
182			coverage = 80;
183			operand = '>=';
184			break;
185		case 'va_85':
186			returnDict["styles"] = "bbcov_vacollected_85p";
187			coverage = 85;
188			operand = '>=';
189			break;
190		case 'va_95':
191			returnDict["styles"] = "bbcov_vacollected_95p";
192			coverage = 95;
193			operand = '>=';
194			break;
195		case 'fcc_any':
196			returnDict["styles"] = "bbcov_fcc_0p";
197			coverage = 0;
198			operand = '>';
199			break;
200	}
201	
202	switch(bbcov_layer_options['tech']){
203		case 'any': 
204			returnDict["CQL_FILTER"] = `cable_${speed}${operand}${coverage} OR dsl_${speed}${operand}${coverage} OR fiber_${speed}${operand}${coverage} OR fixedwireless_${speed}${operand}${coverage}`;
205			break;
206		case 'cable': 
207			returnDict["CQL_FILTER"] = `cable_${speed}${operand}${coverage}`;
208			break;
209		case 'dsl': 
210			returnDict["CQL_FILTER"] = `dsl_${speed}${operand}${coverage}`;
211			break;
212		case 'fiber': 
213			returnDict["CQL_FILTER"] = `fiber_${speed}${operand}${coverage}`;
214			break;
215		case 'fixedwireless': 
216			returnDict["CQL_FILTER"] = `fixedwireless_${speed}${operand}${coverage}`;
217			break;
218	}
219	
220	return returnDict;
221}
222
223function bbcovSetLayerOption(option,value){
224	
225	var layer_name;
226	
227	bbcov_layer_options[option] = bbcov_layer_structure[option][value];
228	
229	if(option == 'speed'){
230		// Updating values in legend
231		let upDown = value.split("_");
232		$("#lg-bb-down").html(upDown[0] + " mbps");
233
234		if(upDown.length == 1) {
235			$("#lg-bb-up").html("Any");
236		} else {
237			$("#lg-bb-up").html(upDown[1] + " mbps");
238		}
239	} else if ( option == 'tech') {
240		let mapTechToDisplayName = {
241			'any':'All Technologies',
242			'cable':'Cable',
243			'dsl':'DSL/Copper',
244			'fiber':'Fiber',
245			'fixedwireless':'Fixed Wireless'
246		}
247		$('#lg-bb-tech').html(mapTechToDisplayName[value])
248	} else if (option=='percent'){
249		let mapPercentToDisplayName = {
250            '0':'At least One Address',
251			'80':'80%',
252			'85':'85%',
253			'95':'95%'
254		}
255		$('#lg-bb-percent').html(mapPercentToDisplayName[value])
256	}
257
258	$('.bbcov-pick-layer').each(function () {
259		
260		if ($(this).is(':checked')){
261			layer_name = bbcov_map_layers[$(this).val()]['id'];
262			bbMAP.removeLayer(layer_name);
263		}
264	});
265	
266	bbcovLayerRefresh();
267	
268	$('.bbcov-pick-layer').each(function () {
269		
270		if ($(this).is(':checked')){
271			layer_name = bbcov_map_layers[$(this).val()]['id'];
272			bbcovAddLayer($(this).val(),layer_name);
273		}
274	});
275
276    fabricLayerRefresh();
277}
278
279$('.bbcov-pick-layer').change(function(event) {
280	
281	var layer_name = bbcov_map_layers[$(this).val()]['id'];
282	if ($(this).is(':checked')){
283		bbcovAddLayer($(this).val(),layer_name);
284	}
285	else {
286		bbMAP.removeLayer(layer_name);
287	}
288});
289
290function bbcovAddLayer(layerKey,LayerName){
291	
292	var layerDict = {};
293	var layerDictOptions = {};
294	
295	Object.assign(layerDict, layer_template_bbcov);	
296	Object.assign(layerDictOptions, layer_template_bbcov['options']);
297	
298	for (const [key, value] of Object.entries(bbcov_map_layers[layerKey])) {		
299		layerDict[key] = value;
300	}
301	
302	for (const [key, value] of Object.entries(bbcov_map_layers[layerKey]['options'])) {		
303		layerDictOptions[key] = value;
304	}
305	
306	layerDict['options'] = layerDictOptions;
307	
308	if (useCQL){
309		var cqlDict = getCQLfilter(layerKey);
310		layerDict['options']['styles'] = cqlDict['styles'];
311		layerDict['options']['CQL_FILTER'] = cqlDict['CQL_FILTER'];
312	}
313	
314	bbcovLayerRefresh();
315	
316	bbMAP.addLayer(LayerName,bbLAYERS.createLayer(LayerName,layerDict),layerDict['legendName']);
317}
318
319function loadMultiLayer(layerId) {
320    const layer = activeCoverageLayers.find(l => l.id === layerId);
321    if (!layer) return;
322
323    const layerIndex = activeCoverageLayers.indexOf(layer);
324    const layerDict = {
325        ...layer_template_bbcov,
326        displayName: layer.displayName,
327        legendName: layer.displayName,
328        color: layer.color,
329    	opacity: 0.40 - (layerIndex * 0.05),
330    };
331    layerDict.options = {
332    	...layerDict.options, 
333		layers: 'broadband:bbcoverage_vacollected_geoview',
334		styles: layer.color,
335		viewparams: buildViewParams(layer),
336    }
337    
338    bbMAP.addLayer(layerId, bbLAYERS.createLayer(layerId, layerDict), layer.displayName);
339    
340    addCustomLayerToLegend(layerId);
341}
342
343function buildViewParams(layer) {    
344    const speedParam = layer.speed;
345    const techParam = layer.tech; 
346    const coverageParam = layer.coverage;
347	const coverageVersion = layer.version
348    return `tech_speed:${techParam}_${speedParam};percent:${coverageParam};version_id:${coverageVersion}`;
349}
350
351function addCustomLayerToLegend(layerId) {
352    const layer = activeCoverageLayers.find(l => l.id === layerId);
353    if (!layer) return;
354
355    document.getElementById('lg-bb-custom-section').style.display = ''
356    
357    const layerIndex = activeCoverageLayers.indexOf(layer);
358
359    const legendHTML = `
360        <div id="lg-custom-${layerId}" class="horizontal-flex" style="margin-bottom: 5px;">
361            <div class="circle-legend legend-override-${layer.color}">&nbsp;</div>
362            <div style="font-size: 0.9em; margin-left: 8px;">
363                <div>
363${layer.displayName}</div>
364            </div>
365            <!--<div>layer Index: ${layerIndex}</div>-->
366        </div>`;
367    
368    document.getElementById('lg-bb-custom-layers').insertAdjacentHTML('afterbegin', legendHTML);
369}
370
371function removeCustomLayerFromLegend(layerId) {
372    document.getElementById(`lg-custom-${layerId}`)?.remove();
373    
374    if (document.getElementById('lg-bb-custom-layers').children.length === 0) {
375        document.getElementById('lg-bb-custom-section').style.display = 'none'
376    }
377}
378
379function addCustomLayerFromModalSettings(speed, tech, coverage, version) {
380    // Check max layers
381    if (activeCoverageLayers.length >= maxLayers) {
382        notifications.warn(`Maximum ${maxLayers} custom layers allowed. Please remove a layer first.`);
383        return false;
384    }
385    // Check duplicate
386    if (layerAlreadyExists(speed, tech, coverage, version)) {
387        notifications.warn('A layer with these exact settings already exists.');
388        return false;
389    }
390    // Check available color
391    const color = getNextAvailableColor();
392    if (!color) {
393        notifications.warn('No colors available.');
394        return false;
395    }
396    
397    const layerId = `custom-layer-${Date.now()}-${Math.random().toString(36).slice(2, 5)}`;
398    
399    // Create the layer
400    const success = addTestLayer(speed, tech, coverage, version, color, layerId);
401    
402    if (success) {
403        loadMultiLayer(layerId);
404        updateActiveLayersDisplay();
405        return true;
406    }
407    
408    return false;
409}
410
411function layerAlreadyExists(speed, tech, coverage, version) {
412    return activeCoverageLayers.some(layer => 
413        layer.speed === speed && 
414        layer.tech === tech && 
415        layer.coverage === coverage && 
416        layer.version === version
417    );
418}
419
420const techCodeLabels = {
421    'any': 'Any Tech',
422    'cable': 'Cable',
423    'dsl': 'DSL',
424    'fiber': 'Fiber',
425    'fixedwireless': 'Fixed Wireless'
426}
427function resolveTech(tech){
428    return techCodeLabels[tech] || tech
429}
430
431const coverageLabels = {'0': 'Any Coverage'}
432function resolveCoverageLabel(coverage){
433    return coverageLabels[coverage]	|| coverage + '%'
434}
435
436function buildLayerDisplayName(speed, tech, coverage, version) {
437    const speedDisplay = speed.replace('_', '/');
438    const techDisplay = resolveTech(tech);
439    
440    const coverageDisplay = resolveCoverageLabel(coverage);
441    const coverageVersion = globalThis.coverageVersions.find(entry => entry.id === version)?.name // TODO: Change this to use data's coverageVersions()
442    
443    return `${speedDisplay} Mbps ${techDisplay} ${coverageDisplay} - ${coverageVersion}`;
444}
445
446function updateActiveLayersDisplay() {
447    const container = document.getElementById('active-custom-layers');
448    const addButton = document.getElementById('add-custom-layer-btn');
449
450    container.replaceChildren();
451
452    activeCoverageLayers.forEach((layer, index) => {
453        const layerNumber = index + 1;
454        
455        // Parse speed values
456        const speedParts = layer.speed.split('_');
457        const downloadSpeed = speedParts[0] || '100';
458        // If no underscore, it's download-only, so upload is "Any"
459        const uploadSpeed = speedParts.length > 1 ? speedParts[1] : 'Any';
460        
461        // Format technology display
462        const techDisplay = resolveTech(layer.tech);
463        
464        // Format coverage display
465        const coverageDisplay = resolveCoverageLabel(layer.coverage);
466
467		const coverageVersion = globalThis.coverageVersions.find(entry => entry.id === layer.version)?.name // TODO: Change this to use data's coverageVersions()
468        
469        const layerHTML = `
470            <div class="custom-layer-item layer-sidebar-override override-${layer.color}">
471                <!-- Colored square box in top-left corner -->
472                <div class="custom-layer-colored-box" style="background-color: #${layer.color};">
473                    <!-- Checkbox with tick inside the colored box -->
474                    <div class="custom-layer-checkbox" style="background-color: #${layer.color};">
475                        <i class="fa-solid fa-check"></i>
476                    </div>
477                </div>
478
479                <!-- Header section with fixed positioning and grey background -->
480                <div class="custom-layer-header">
481					<div class="toggle-custom-layer-btn toggle-on" 
482                            data-layer-id="${layer.id}" 
483                            title="Toggle visibility">
484                        <i class="fa-duotone fa-solid fa-toggle-large-on fa-rotate-180" data-icon-on></i>
485                        <i class="fa-duotone fa-solid fa-toggle-large-off fa-rotate-180" data-icon-off></i>
486                    </div>
487                    <div class="custom-layer-title">
488                        Coverage Layer #${layerNumber}
489                    </div>
490					<div class="remove-custom-layer-btn" 
491                            data-layer-id="${layer.id}" 
492                            title="Remove this layer">
493                        <i class="fa-solid fa-xmark"></i>
494                    </div>
495                </div>
496                
497                <div class="custom-layer-content">
498                    <!-- Speed -->
499                    <div class="custom-layer-parameter">
500                        <strong class="custom-layer-parameter-label">
501                            <i class="fa-solid fa-gauge-high"></i>
502                            Speed:
503                        </strong>
504                        <span class="custom-layer-parameter-value">
505                            <i class="fa-regular fa-down-long custom-layer-speed-icon"></i>
506                            <span>${downloadSpeed} <span class="custom-layer-speed-value-small">mbps</span></span>
507                            <span class="custom-layer-speed-value">
508                                <i class="fa-regular fa-up-long custom-layer-speed-icon"></i>
509                                ${uploadSpeed === 'Any' 
510                                    ? '<span>Any</span>' 
511                                    : `<span>${uploadSpeed} <span class="custom-layer-speed-value-small">mbps</span></span>`}
512                            </span>
513                        </span>
514                    </div>
515                    
516                    <!-- Technology -->
517                    <div class="custom-layer-parameter">
518                        <strong class="custom-layer-parameter-label">
519                            <i class="fa-solid fa-ethernet"></i>
520                            Technology:
521                        </strong>
522                        <span class="custom-layer-parameter-value">
523                            ${techDisplay}
524                        </span>
525                    </div>
526                    
527                    <!-- Coverage -->
528                    <div class="custom-layer-parameter">
529                        <strong class="custom-layer-parameter-label">
530                            <i class="fa-solid fa-percent"></i>
531                            Coverage:
532                        </strong>
533                        <span class="custom-layer-parameter-value">
534                            ${coverageDisplay}
535                        </span>
536                    </div>
537                    
538                    <!-- Version -->
539                    <div class="custom-layer-parameter">
540                        <strong class="custom-layer-parameter-label">
541                            <i class="fa-regular fa-globe-pointer"></i>
542                            Version:
543                        </strong>
544                        <span class="custom-layer-parameter-value">
545                            ${coverageVersion}
546                        </span>
547                    </div>
548                </div>
549            </div>`;
550        container.insertAdjacentHTML('afterbegin', layerHTML);
551    });
552
553    if (maxLayers > activeCoverageLayers.length) {
554        addButton.style.display = ''
555    } else {
556        addButton.style.display = 'none'
557    }
558}

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.