let subnetMap = {}; let subnetNotes = {}; let maxNetSize = 0; let infoColumnCount = 5 // NORMAL mode: // - Smallest subnet: /32 // - Two reserved addresses per subnet of size <= 30: // - Net+0 = Network Address // - Last = Broadcast Address // AWS mode: // - Smallest subnet: /28 // - Two reserved addresses per subnet: // - Net+0 = Network Address // - Net+1 = AWS Reserved - VPC Router // - Net+2 = AWS Reserved - VPC DNS // - Net+3 = AWS Reserved - Future Use // - Last = Broadcast Address // Azure mode: // - Smallest subnet: /29 // - Two reserved addresses per subnet: // - Net+0 = Network Address // - Net+1 = Reserved - Default Gateway // - Net+2 = Reserved - DNS Mapping // - Net+3 = Reserved - DNS Mapping // - Last = Broadcast Address let noteTimeout; let operatingMode = 'Standard' let previousOperatingMode = 'Standard' let inflightColor = 'NONE' let urlVersion = '1' let configVersion = '2' const netsizePatterns = { Standard: '^([12]?[0-9]|3[0-2])$', AZURE: '^([12]?[0-9])$', AWS: '^(1?[0-9]|2[0-8])$', }; const minSubnetSizes = { Standard: 32, AZURE: 29, AWS: 28, }; $('input#network').on('paste', function (e) { let pastedData = window.event.clipboardData.getData('text') if (pastedData.includes('/')) { let [network, netSize] = pastedData.split('/') $('#network').val(network) $('#netsize').val(netSize) } e.preventDefault() }); $("input#network").on('keydown', function (e) { if (e.key === '/') { e.preventDefault() $('input#netsize').focus().select() } }); $('input#network,input#netsize').on('input', function() { $('#input_form')[0].classList.add('was-validated'); }) $('#color_palette div').on('click', function() { // We don't really NEED to convert this to hex, but it's really low overhead to do the // conversion here and saves us space in the export/save inflightColor = rgba2hex($(this).css('background-color')) }) $('#calcbody').on('click', '.row_address, .row_range, .row_usable, .row_hosts, .note, input', function(event) { if (inflightColor !== 'NONE') { mutate_subnet_map('color', this.dataset.subnet, '', inflightColor) // We could re-render here, but there is really no point, keep performant and just change the background color now //renderTable(); $(this).closest('tr').css('background-color', inflightColor) } }) $('#btn_go').on('click', function() { $('#input_form').removeClass('was-validated'); $('#input_form').validate(); if ($('#input_form').valid()) { $('#input_form')[0].classList.add('was-validated'); reset(); // Additional actions upon validation can be added here } else { show_warning_modal('
Please correct the errors in the form!
'); } }) $('#dropdown_standard').click(function() { previousOperatingMode = operatingMode; operatingMode = 'Standard'; if(!switchMode(operatingMode)) { operatingMode = previousOperatingMode; $('#dropdown_'+ operatingMode.toLowerCase()).addClass('active'); } }); $('#dropdown_azure').click(function() { previousOperatingMode = operatingMode; operatingMode = 'AZURE'; if(!switchMode(operatingMode)) { operatingMode = previousOperatingMode; $('#dropdown_'+ operatingMode.toLowerCase()).addClass('active'); } }); $('#dropdown_aws').click(function() { previousOperatingMode = operatingMode; operatingMode = 'AWS'; if(!switchMode(operatingMode)) { operatingMode = previousOperatingMode; $('#dropdown_'+ operatingMode.toLowerCase()).addClass('active'); } }); $('#importBtn').on('click', function() { importConfig(JSON.parse($('#importExportArea').val())) }) $('#bottom_nav #colors_word_open').on('click', function() { $('#bottom_nav #color_palette').removeClass('d-none'); $('#bottom_nav #colors_word_close').removeClass('d-none'); $('#bottom_nav #colors_word_open').addClass('d-none'); }) $('#bottom_nav #colors_word_close').on('click', function() { $('#bottom_nav #color_palette').addClass('d-none'); $('#bottom_nav #colors_word_close').addClass('d-none'); $('#bottom_nav #colors_word_open').removeClass('d-none'); inflightColor = 'NONE' }) $('#bottom_nav #copy_url').on('click', function() { // TODO: Provide a warning here if the URL is longer than 2000 characters, probably using a modal. let url = window.location.origin + getConfigUrl() navigator.clipboard.writeText(url); $('#bottom_nav #copy_url span').text('Copied!') // Swap the text back after 3sec setTimeout(function(){ $('#bottom_nav #copy_url span').text('Copy Shareable URL') }, 2000) }) $('#btn_import_export').on('click', function() { $('#importExportArea').val(JSON.stringify(exportConfig(false), null, 2)) }) function reset() { set_usable_ips_title(operatingMode); let cidrInput = $('#network').val() + '/' + $('#netsize').val() let rootNetwork = get_network($('#network').val(), $('#netsize').val()) let rootCidr = rootNetwork + '/' + $('#netsize').val() if (cidrInput !== rootCidr) { show_warning_modal('
Your network input is not on a network boundary for this network size. It has been automatically changed:
' + $('#network').val() + ' -> ' + rootNetwork + '
') $('#network').val(rootNetwork) cidrInput = $('#network').val() + '/' + $('#netsize').val() } if (Object.keys(subnetMap).length > 0) { // This page already has data imported, so lets see if we can just change the range if (isMatchingSize(Object.keys(subnetMap)[0], cidrInput)) { subnetMap = changeBaseNetwork(cidrInput) } else { // This is a page with existing data of a different subnet size, so make it blank // Could be an opportunity here to do the following: // - Prompt the user to confirm they want to clear the existing data // - Resize the existing data anyway by making the existing network a subnetwork of their new input (if it // is a larger network), or by just trimming the network to the new size (if it is a smaller network), // or even resizing all of the containing networks by change in size of the base network. For example a // base network going from /16 -> /18 would be all containing networks would be resized smaller (/+2), // or bigger (/-2) if going from /18 -> /16. subnetMap = {} subnetMap[rootCidr] = {} } } else { // This is a fresh page load with no existing data subnetMap[rootCidr] = {} } maxNetSize = parseInt($('#netsize').val()) renderTable(operatingMode); } function changeBaseNetwork(newBaseNetwork) { // Minifiy it, to make all the keys in the subnetMap relative to their original base network // Then expand it, but with the new CIDR as the base network, effectively converting from old to new. let miniSubnetMap = {} minifySubnetMap(miniSubnetMap, subnetMap, Object.keys(subnetMap)[0]) let newSubnetMap = {} expandSubnetMap(newSubnetMap, miniSubnetMap, newBaseNetwork) return newSubnetMap } function isMatchingSize(subnet1, subnet2) { return subnet1.split('/')[1] === subnet2.split('/')[1]; } $('#calcbody').on('click', 'td.split,td.join', function(event) { // HTML DOM Data elements! Yay! See the `data-*` attributes of the HTML tags mutate_subnet_map(this.dataset.mutateVerb, this.dataset.subnet, '') renderTable(operatingMode); }) $('#calcbody').on('keyup', 'td.note input', function(event) { // HTML DOM Data elements! Yay! See the `data-*` attributes of the HTML tags let delay = 1000; clearTimeout(noteTimeout); noteTimeout = setTimeout(function(element) { mutate_subnet_map('note', element.dataset.subnet, '', element.value) }, delay, this); }) $('#calcbody').on('focusout', 'td.note input', function(event) { // HTML DOM Data elements! Yay! See the `data-*` attributes of the HTML tags clearTimeout(noteTimeout); mutate_subnet_map('note', this.dataset.subnet, '', this.value) }) function renderTable(operatingMode) { // TODO: Validation Code $('#calcbody').empty(); let maxDepth = get_dict_max_depth(subnetMap, 0) addRowTree(subnetMap, 0, maxDepth, operatingMode) } function addRowTree(subnetTree, depth, maxDepth,operatingMode) { for (let mapKey in subnetTree) { if (mapKey.startsWith('_')) { continue; } if (has_network_sub_keys(subnetTree[mapKey])) { addRowTree(subnetTree[mapKey], depth + 1, maxDepth,operatingMode) } else { let subnet_split = mapKey.split('/') let notesWidth = '30%'; if ((maxDepth > 5) && (maxDepth <= 10)) { notesWidth = '25%'; } else if ((maxDepth > 10) && (maxDepth <= 15)) { notesWidth = '20%'; } else if ((maxDepth > 15) && (maxDepth <= 20)) { notesWidth = '15%'; } else if (maxDepth > 20) { notesWidth = '10%'; } addRow(subnet_split[0], parseInt(subnet_split[1]), (infoColumnCount + maxDepth - depth), (subnetTree[mapKey]['_note'] || ''), notesWidth, (subnetTree[mapKey]['_color'] || ''),operatingMode) } } } function addRow(network, netSize, colspan, note, notesWidth, color, operatingMode) { let addressFirst = ip2int(network) let addressLast = subnet_last_address(addressFirst, netSize) let usableFirst = subnet_usable_first(addressFirst, netSize, operatingMode) let usableLast = subnet_usable_last(addressFirst, netSize) let hostCount = 1 + usableLast - usableFirst let styleTag = '' if (color !== '') { styleTag = ' style="background-color: ' + color + '"' } let rangeCol, usableCol; if (netSize < 32) { rangeCol = int2ip(addressFirst) + ' - ' + int2ip(addressLast); usableCol = int2ip(usableFirst) + ' - ' + int2ip(usableLast); } else { rangeCol = int2ip(addressFirst); usableCol = int2ip(usableFirst); } let rowId = 'row_' + network.replace('.', '-') + '_' + netSize let rowCIDR = network + '/' + netSize let newRow = ' \n' + ' ' + rowCIDR + '\n' + ' ' + rangeCol + '\n' + ' ' + usableCol + '\n' + ' ' + hostCount + '\n' + ' \n' + ' /' + netSize + '\n' if (netSize > maxNetSize) { // This is wrong. Need to figure out a way to get the number of children so you can set rowspan and the number // of ancestors so you can set colspan. // DONE: If the subnet address (without the mask) matches a larger subnet address // in the heirarchy that is a signal to add more join buttons to that row, since they start at the top row and // via rowspan extend downward. let matchingNetworkList = get_matching_network_list(network, subnetMap).slice(1) for (const i in matchingNetworkList) { let matchingNetwork = matchingNetworkList[i] let networkChildrenCount = count_network_children(matchingNetwork, subnetMap, []) newRow += ' /' + matchingNetwork.split('/')[1] + '\n' } } newRow += ' '; $('#calcbody').append(newRow) } // Helper Functions function ip2int(ip) { return ip.split('.').reduce(function(ipInt, octet) { return (ipInt<<8) + parseInt(octet, 10)}, 0) >>> 0; } function int2ip (ipInt) { return ((ipInt>>>24) + '.' + (ipInt>>16 & 255) + '.' + (ipInt>>8 & 255) + '.' + (ipInt & 255)); } function toBase36(num) { return num.toString(36); } function fromBase36(str) { return parseInt(str, 36); } /** * Coordinate System for Subnet Representation * * This system aims to represent subnets efficiently within a larger network space. * The goal is to produce the shortest possible string representation for subnets, * which is particularly effective when dealing with hierarchical network designs. * * Key concept: * - We represent a subnet by its ordinal position within a larger network, * along with its mask size. * - This approach is most efficient when subnets are relatively close together * in the address space and of similar sizes. * * Benefits: * 1. Compact representation: Often results in very short strings (e.g., "7k"). * 2. Hierarchical: Naturally represents subnet hierarchy. * 3. Efficient for common cases: Works best for typical network designs where * subnets are grouped and of similar sizes. * * Trade-offs: * - Less efficient for representing widely dispersed or highly varied subnet sizes. * - Requires knowledge of the base network to interpret. * * Extreme Example... Representing the value 192.168.200.210/31 within the base * network of 192.168.200.192/27. These are arbitrary but long subnets to represent * as a string. * - Normal Way - '192.168.200.210/31' * - Nth Position Way - '9v' * - '9' represents the 9th /31 subnet within the /27 * - 'v' represents the /31 mask size converted to Base 36 (31 -> 'v') */ /** * Converts a specific subnet to its Nth position representation within a base network. * * @param {string} baseNetwork - The larger network containing the subnet (e.g., "10.0.0.0/16") * @param {string} specificSubnet - The subnet to be represented (e.g., "10.0.112.0/20") * @returns {string} A compact string representing the subnet's position and size (e.g., "7k") */ function getNthSubnet(baseNetwork, specificSubnet) { const [baseIp, baseMask] = baseNetwork.split('/'); const [specificIp, specificMask] = specificSubnet.split('/'); const baseInt = ip2int(baseIp); const specificInt = ip2int(specificIp); const baseSize = 32 - parseInt(baseMask, 10); const specificSize = 32 - parseInt(specificMask, 10); const offset = specificInt - baseInt; const nthSubnet = offset >>> specificSize; return `${nthSubnet}${toBase36(parseInt(specificMask, 10))}`; } /** * Reconstructs a subnet from its Nth position representation within a base network. * * @param {string} baseNetwork - The larger network containing the subnet (e.g., "10.0.0.0/16") * @param {string} nthString - The compact representation of the subnet (e.g., "7k") * @returns {string} The full subnet representation (e.g., "10.0.112.0/20") */ // Takes 10.0.0.0/16 and '7k' and returns 10.0.96.0/20 // '10.0.96.0/20' being the 7th /20 (base36 'k' is 20 int) within the /16. function getSubnetFromNth(baseNetwork, nthString) { const [baseIp, baseMask] = baseNetwork.split('/'); const baseInt = ip2int(baseIp); const size = fromBase36(nthString.slice(-1)); const nth = parseInt(nthString.slice(0, -1), 10); const innerSizeInt = 32 - size; const subnetInt = baseInt + (nth << innerSizeInt); return `${int2ip(subnetInt)}/${size}`; } function subnet_last_address(subnet, netSize) { return subnet + subnet_addresses(netSize) - 1; } function subnet_addresses(netSize) { return 2**(32-netSize); } function subnet_usable_first(network, netSize, operatingMode) { if (netSize < 31) { // https://docs.aws.amazon.com/vpc/latest/userguide/subnet-sizing.html // AWS reserves 3 additional IPs // https://learn.microsoft.com/en-us/azure/virtual-network/virtual-networks-faq#are-there-any-restrictions-on-using-ip-addresses-within-these-subnets // Azure reserves 3 additional IPs return network + (operatingMode == 'Standard' ? 1 : 4); } else { return network; } } function subnet_usable_last(network, netSize) { let last_address = subnet_last_address(network, netSize); if (netSize < 31) { return last_address - 1; } else { return last_address; } } function get_dict_max_depth(dict, curDepth) { let maxDepth = curDepth for (let mapKey in dict) { if (mapKey.startsWith('_')) { continue; } let newDepth = get_dict_max_depth(dict[mapKey], curDepth + 1) if (newDepth > maxDepth) { maxDepth = newDepth } } return maxDepth } function get_join_children(subnetTree, childCount) { for (let mapKey in subnetTree) { if (mapKey.startsWith('_')) { continue; } if (has_network_sub_keys(subnetTree[mapKey])) { childCount += get_join_children(subnetTree[mapKey]) } else { return childCount } } } function has_network_sub_keys(dict) { let allKeys = Object.keys(dict) // Maybe an efficient way to do this with a Lambda? for (let i in allKeys) { if (!allKeys[i].startsWith('_') && allKeys[i] !== 'n' && allKeys[i] !== 'c') { return true } } return false } function count_network_children(network, subnetTree, ancestryList) { // TODO: This might be able to be optimized. Ultimately it needs to count the number of keys underneath // the current key are unsplit networks (IE rows in the table, IE keys with a value of {}). let childCount = 0 for (let mapKey in subnetTree) { if (mapKey.startsWith('_')) { continue; } if (has_network_sub_keys(subnetTree[mapKey])) { childCount += count_network_children(network, subnetTree[mapKey], ancestryList.concat([mapKey])) } else { if (ancestryList.includes(network)) { childCount += 1 } } } return childCount } function get_network_children(network, subnetTree) { // TODO: This might be able to be optimized. Ultimately it needs to count the number of keys underneath // the current key are unsplit networks (IE rows in the table, IE keys with a value of {}). let subnetList = [] for (let mapKey in subnetTree) { if (mapKey.startsWith('_')) { continue; } if (has_network_sub_keys(subnetTree[mapKey])) { subnetList.push.apply(subnetList, get_network_children(network, subnetTree[mapKey])) } else { subnetList.push(mapKey) } } return subnetList } function get_matching_network_list(network, subnetTree) { let subnetList = [] for (let mapKey in subnetTree) { if (mapKey.startsWith('_')) { continue; } if (has_network_sub_keys(subnetTree[mapKey])) { subnetList.push.apply(subnetList, get_matching_network_list(network, subnetTree[mapKey])) } if (mapKey.split('/')[0] === network) { subnetList.push(mapKey) } } return subnetList } function get_consolidated_property(subnetTree, property) { let allValues = get_property_values(subnetTree, property) // https://stackoverflow.com/questions/14832603/check-if-all-values-of-array-are-equal let allValuesMatch = allValues.every( (val, i, arr) => val === arr[0] ) if (allValuesMatch) { return allValues[0] } else { return '' } } function get_property_values(subnetTree, property) { let propValues = [] for (let mapKey in subnetTree) { if (has_network_sub_keys(subnetTree[mapKey])) { propValues.push.apply(propValues, get_property_values(subnetTree[mapKey], property)) } else { // The "else" above is a bit different because it will start tracking values for subnets which are // in the hierarchy, but not displayed. Those are always blank so it messes up the value list propValues.push(subnetTree[mapKey][property] || '') } } return propValues } function get_network(networkInput, netSize) { let ipInt = ip2int(networkInput) netSize = parseInt(netSize) for (let i=31-netSize; i>=0; i--) { ipInt &= ~ 1<
More Information:
Amazon Virtual Private Cloud > User Guide > Subnet CIDR Blocks > Subnet Sizing for IPv4' break; case 'AZURE': var modal_error_message = 'The minimum IPv4 subnet size for Azure is /' + minSubnetSizes[operatingMode] + '.

More Information:
Azure Virtual Network FAQ > How small and how large can virtual networks and subnets be?' break; default: var modal_error_message = 'The minimum size for an IPv4 subnet is /' + minSubnetSizes[operatingMode] + '.

More Information:
Wikipedia - Classless Inter-Domain Routing' break; } show_warning_modal('
' + modal_error_message + '
') } } else if (verb === 'join') { // Options: // [ Selected ] Keep note if all the notes are the same, blank them out if they differ. Most intuitive // [ Possible ] Lose note data for all deleted subnets. // [ Possible ] Keep note from first subnet in the join scope. Reasonable but I think rarely will the note be kept by the user // [ Possible ] Concatenate all notes. Ugly and won't really be useful for more than two subnets being joined subnetTree[mapKey] = { '_note': get_consolidated_property(subnetTree[mapKey], '_note'), '_color': get_consolidated_property(subnetTree[mapKey], '_color') } } else if (verb === 'note') { subnetTree[mapKey]['_note'] = propValue } else if (verb === 'color') { subnetTree[mapKey]['_color'] = propValue } else { // How did you get here? } } } } function switchMode(operatingMode) { let isSwitched = true; if (subnetMap !== null) { if (validateSubnetSizes(subnetMap, minSubnetSizes[operatingMode])) { renderTable(operatingMode); set_usable_ips_title(operatingMode); $('#netsize').attr('pattern', netsizePatterns[operatingMode]); $('#input_form').removeClass('was-validated'); $('#input_form').rules('remove', 'netsize'); switch (operatingMode) { case 'AWS': var validate_error_message = 'AWS Mode - Smallest size is /' + minSubnetSizes[operatingMode] break; case 'AZURE': var validate_error_message = 'Azure Mode - Smallest size is /' + minSubnetSizes[operatingMode] break; default: var validate_error_message = 'Smallest size is /' + minSubnetSizes[operatingMode] break; } // Modify jquery validation rule $('#input_form #netsize').rules('add', { required: true, pattern: netsizePatterns[operatingMode], messages: { required: 'Please enter a network size', pattern: validate_error_message } }); // Remove active class from all buttons if needed $('#dropdown_standard, #dropdown_azure, #dropdown_aws').removeClass('active'); $('#dropdown_' + operatingMode.toLowerCase()).addClass('active'); isSwitched = true; } else { switch (operatingMode) { case 'AWS': var modal_error_message = 'One or more subnets are smaller than the minimum allowed for AWS.
The smallest size allowed is /' + minSubnetSizes[operatingMode] + '.
See: Amazon Virtual Private Cloud > User Guide > Subnet CIDR Blocks > Subnet Sizing for IPv4' break; case 'AZURE': var modal_error_message = 'One or more subnets are smaller than the minimum allowed for Azure.
The smallest size allowed is /' + minSubnetSizes[operatingMode] + '.
See: Azure Virtual Network FAQ > How small and how large can virtual networks and subnets be?' break; default: var validate_error_message = 'Unknown Error' break; } show_warning_modal('
' + modal_error_message + '
'); isSwitched = false; } } else { //unlikely to get here. reset(); } return isSwitched; } function validateSubnetSizes(subnetMap, minSubnetSize) { let isValid = true; const validate = (subnetTree) => { for (let key in subnetTree) { if (key.startsWith('_')) continue; // Skip special keys let [_, size] = key.split('/'); if (parseInt(size) > minSubnetSize) { isValid = false; return; // Early exit if any subnet is invalid } if (typeof subnetTree[key] === 'object') { validate(subnetTree[key]); // Recursively validate subnets } } }; validate(subnetMap); return isValid; } function set_usable_ips_title(operatingMode) { switch (operatingMode) { case 'AWS': $('#useableHeader').html('Usable IPs (AWS)') break; case 'AZURE': $('#useableHeader').html('Usable IPs (Azure)') break; default: $('#useableHeader').html('Usable IPs') break; } $('[data-bs-toggle="tooltip"]').tooltip() } function show_warning_modal(message) { var notifyModal = new bootstrap.Modal(document.getElementById('notifyModal'), {}); $('#notifyModal .modal-body').html(message) notifyModal.show() } $( document ).ready(function() { // Initialize the jQuery Validation on the form var validator = $('#input_form').validate({ onfocusout: function (element) { $(element).valid(); }, rules: { network: { required: true, pattern: '^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$' }, netsize: { required: true, pattern: '^([0-9]|[12][0-9]|3[0-2])$' } }, messages: { network: { required: 'Please enter a network', pattern: 'Must be a valid IPv4 Address' }, netsize: { required: 'Please enter a network size', pattern: 'Smallest size is /32' } }, errorPlacement: function(error, element) { //console.log(error); //console.log(element); if (error[0].innerHTML !== '') { //console.log('Error Placement - Text') if (!element.data('errorIsVisible')) { bootstrap.Tooltip.getInstance(element).setContent({'.tooltip-inner': error[0].innerHTML}) element.tooltip('show'); element.data('errorIsVisible', true) } } else { //console.log('Error Placement - Empty') //console.log(element); if (element.data('errorIsVisible')) { element.tooltip('hide'); element.data('errorIsVisible', false) } } //console.log(element); }, // This success function appears to be required as errorPlacement() does not fire without the success function // being defined. success: function(label, element) { }, // When the form is valid, add the 'was-validated' class submitHandler: function(form) { form.classList.add('was-validated'); form.submit(); // Submit the form } }); let autoConfigResult = processConfigUrl(); if (!autoConfigResult) { reset(); } }); function exportConfig(isMinified = true) { const baseNetwork = Object.keys(subnetMap)[0] let miniSubnetMap = {}; if (isMinified) { minifySubnetMap(miniSubnetMap, subnetMap, baseNetwork) } if (operatingMode !== 'Standard') { return { 'config_version': configVersion, 'operating_mode': operatingMode, 'base_network': baseNetwork, 'subnets': isMinified ? miniSubnetMap : subnetMap, } } else { return { 'config_version': configVersion, 'base_network': baseNetwork, 'subnets': isMinified ? miniSubnetMap : subnetMap, } } } function getConfigUrl() { // Deep Copy let defaultExport = JSON.parse(JSON.stringify(exportConfig(true))); renameKey(defaultExport, 'config_version', 'v') renameKey(defaultExport, 'base_network', 'b') if (defaultExport.hasOwnProperty('operating_mode')) { renameKey(defaultExport, 'operating_mode', 'm') } renameKey(defaultExport, 'subnets', 's') //console.log(JSON.stringify(defaultExport)) return '/index.html?c=' + urlVersion + LZString.compressToEncodedURIComponent(JSON.stringify(defaultExport)) } function processConfigUrl() { const params = new Proxy(new URLSearchParams(window.location.search), { get: (searchParams, prop) => searchParams.get(prop), }); if (params['c'] !== null) { // First character is the version of the URL string, in case the mechanism of encoding changes let urlVersion = params['c'].substring(0, 1) let urlData = params['c'].substring(1) let urlConfig = JSON.parse(LZString.decompressFromEncodedURIComponent(params['c'].substring(1))) renameKey(urlConfig, 'v', 'config_version') if (urlConfig.hasOwnProperty('m')) { renameKey(urlConfig, 'm', 'operating_mode') } renameKey(urlConfig, 's', 'subnets') if (urlConfig['config_version'] === '1') { // Version 1 Configs used full subnet strings as keys and just shortned the _note->_n and _color->_c keys expandKeys(urlConfig['subnets']) } else if (urlConfig['config_version'] === '2') { // Version 2 Configs uses the Nth Position representation for subnet keys and requires the base_network // option. It also uses n/c for note/color if (urlConfig.hasOwnProperty('b')) { renameKey(urlConfig, 'b', 'base_network') } let expandedSubnetMap = {}; expandSubnetMap(expandedSubnetMap, urlConfig['subnets'], urlConfig['base_network']) urlConfig['subnets'] = expandedSubnetMap } importConfig(urlConfig) return true } } function minifySubnetMap(minifiedMap, referenceMap, baseNetwork) { for (let subnet in referenceMap) { if (subnet.startsWith('_')) continue; const nthRepresentation = getNthSubnet(baseNetwork, subnet); minifiedMap[nthRepresentation] = {} if (referenceMap[subnet].hasOwnProperty('_note')) { minifiedMap[nthRepresentation]['n'] = referenceMap[subnet]['_note'] } if (referenceMap[subnet].hasOwnProperty('_color')) { minifiedMap[nthRepresentation]['c'] = referenceMap[subnet]['_color'] } if (Object.keys(referenceMap[subnet]).some(key => !key.startsWith('_'))) { minifySubnetMap(minifiedMap[nthRepresentation], referenceMap[subnet], baseNetwork); } } } function expandSubnetMap(expandedMap, miniMap, baseNetwork) { for (let mapKey in miniMap) { if (mapKey === 'n' || mapKey === 'c') { continue; } let subnetKey = getSubnetFromNth(baseNetwork, mapKey) expandedMap[subnetKey] = {} if (has_network_sub_keys(miniMap[mapKey])) { expandSubnetMap(expandedMap[subnetKey], miniMap[mapKey], baseNetwork) } else { if (miniMap[mapKey].hasOwnProperty('n')) { expandedMap[subnetKey]['_note'] = miniMap[mapKey]['n'] } if (miniMap[mapKey].hasOwnProperty('c')) { expandedMap[subnetKey]['_color'] = miniMap[mapKey]['c'] } } } } // For Config Version 1 Backwards Compatibility function expandKeys(subnetTree) { for (let mapKey in subnetTree) { if (mapKey.startsWith('_')) { continue; } if (has_network_sub_keys(subnetTree[mapKey])) { expandKeys(subnetTree[mapKey]) } else { if (subnetTree[mapKey].hasOwnProperty('_n')) { renameKey(subnetTree[mapKey], '_n', '_note') } if (subnetTree[mapKey].hasOwnProperty('_c')) { renameKey(subnetTree[mapKey], '_c', '_color') } } } } function renameKey(obj, oldKey, newKey) { if (oldKey !== newKey) { Object.defineProperty(obj, newKey, Object.getOwnPropertyDescriptor(obj, oldKey)); delete obj[oldKey]; } } function importConfig(text) { if (text['config_version'] === '1') { var [subnetNet, subnetSize] = Object.keys(text['subnets'])[0].split('/') } else if (text['config_version'] === '2') { var [subnetNet, subnetSize] = text['base_network'].split('/') } $('#network').val(subnetNet) $('#netsize').val(subnetSize) subnetMap = text['subnets']; operatingMode = text['operating_mode'] || 'Standard' switchMode(operatingMode); } const rgba2hex = (rgba) => `#${rgba.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+\.{0,1}\d*))?\)$/).slice(1).map((n, i) => (i === 3 ? Math.round(parseFloat(n) * 255) : parseFloat(n)).toString(16).padStart(2, '0').replace('NaN', '')).join('')}`