specpress
Version:
Export PDF and/or DOCX files from a subset of Markdown, ASN.1 and JSON files
1,095 lines • 58 kB
JavaScript
// BandCombinations.ts — Band combination data model & list
import { BaseClass, BaseList, KEYS, IsInt, UnsupportedKeyException, MissingParentObjectException } from "./Utils.js";
import { logger } from "./Logger.js";
import { BandNumber } from "./BandNumber.js";
import { BC_ID } from "./BC_ID.js";
import { BWC_ID, bwcValuesFR1, bwcValuesFR2 } from "./BWC_ID.js";
import { MAX_CARRIER_BW_MHZ, MissingBandNumberException } from "./ChannelBandwidthPerBand.js";
import { LoadJsonFileToDict } from "./JsonTools.js";
import { HtmlTable } from "./HtmlTable.js";
import * as path from "node:path";
//////////////////////////////
// CONSTANTS
/** Maximum valid BCS-ID value (inclusive). */
const MAX_BCS_ID = 31;
//////////////////////////////
// Exceptions
/** Thrown when a BCS-ID value is out of range or not an integer. */
export class InvalidBcsIdException extends RangeError {
}
/** Thrown when a carrier bandwidth value is invalid. */
export class InvalidCarrierBandwidthException extends RangeError {
}
/** Thrown when a band number is not defined in the channel bandwidth table. */
export class UndefinedBandNumberException extends RangeError {
}
/** Thrown when a band number does not match the BC-ID. */
export class UnexpectedBandNumberException extends RangeError {
}
/** Thrown when non-contiguous variants are used incorrectly. */
export class NonContVariantException extends RangeError {
}
/** Thrown when contiguous and non-contiguous carriers are mixed. */
export class ContNonContMixException extends RangeError {
}
/** Thrown when more non-contiguous carriers are specified than the BC-ID allows. */
export class TooManyNonContCarrierException extends RangeError {
}
/** Thrown when contiguous carrier variant counts are inconsistent. */
export class ContCarrierVariantMismatchException extends RangeError {
}
/** Thrown when fewer than two contiguous carriers are specified. */
export class LessThanTwoContigiousCarriersException extends RangeError {
}
/** Thrown when more contiguous carriers are specified than the BC-ID allows. */
export class TooManyContCarrierException extends RangeError {
}
/** Thrown when expected carrier components are not defined. */
export class UndefinedCarriersException extends RangeError {
}
/** Thrown when maxAggBwPerBand is not a valid positive integer. */
export class InvalidMaxBandwidthException extends RangeError {
}
/** Thrown when a referenced BC-ID is not found in the BC table. */
export class UndefinedBandCombinationException extends RangeError {
}
/** Thrown when a referenced channel bandwidth is not found. */
export class UndefinedChannelBandwidthException extends RangeError {
}
/** Thrown when a band number in a RefComponent doesn't match its parent BandEntry. */
export class BandNumberMismatchException extends RangeError {
}
/** Thrown when a BCS contains duplicate band entries for the same band. */
export class DuplicateBandEntryException extends RangeError {
}
/** Thrown when a BCS is missing its bcsId. */
export class MissingBandwidthCombinationSetIdException extends RangeError {
}
/** Thrown when a BCS has no band entries. */
export class MissingBandListException extends RangeError {
}
/** Thrown when an UL configuration references an undefined band or BC. */
export class UndefinedUplinkConfigException extends RangeError {
}
/** Thrown when a BC is missing its bcId. */
export class MissingBcIdException extends RangeError {
}
/** Thrown when a RefComponent references a multi-band BC-ID. */
export class InvalidReferenceBcId extends RangeError {
}
/** Thrown when a RefComponent's BWC doesn't match the BC-ID. */
export class UnexpectedBwcException extends RangeError {
}
/** Thrown when a BC contains duplicate BCS entries. */
export class DuplicateBcsException extends RangeError {
}
/** Thrown when a BC has no BCS entries. */
export class MissingBcsListException extends RangeError {
}
/** Thrown when a referenced BCS-ID is not found in a BC. */
export class UndefinedBcsException extends RangeError {
}
/** Thrown when a BCS contains duplicate UL configurations. */
export class DuplicateUplinkConfigException extends RangeError {
}
/** Thrown when the BandCombinationList is empty during validation. */
export class NoBandCombinationsException extends RangeError {
}
/** Thrown when the specification field does not match the expected value. */
export class InvalidSpecificationException extends Error {
}
////////////////////////////////////////////////////////////////
// BCS_ID //
////////////////////////////////////////////////////////////////
/**
* A Bandwidth Combination Set identifier (0–31).
*
* Wraps an integer BCS-ID with validation. Accepts numeric values,
* string integers, or strings prefixed with `"BCS"`.
*/
export class BCS_ID {
value;
/**
* @param aValue — a number, string (e.g. `"0"`, `"BCS3"`), or existing BCS_ID to copy.
* @throws InvalidBcsIdException if the value is out of range [0..31].
*/
constructor(aValue) {
if (aValue instanceof BCS_ID) {
this.value = aValue.value;
return;
}
if (aValue === null || aValue === undefined) {
throw new InvalidBcsIdException("The BCS_ID value shall not be None");
}
let v = String(aValue);
if (v.startsWith("BCS")) {
v = v.substring(3);
}
if (!IsInt(v)) {
throw new InvalidBcsIdException(`The given BCS_ID value '${v}' is not an integer`);
}
const bcs = parseInt(v);
if (bcs < 0 || bcs > MAX_BCS_ID) {
throw new InvalidBcsIdException(`The BCS_ID must be in the range [0..31] but was ${bcs}`);
}
this.value = bcs;
}
/** Returns the numeric BCS-ID value. */
valueOf() { return this.value; }
/** Returns the BCS-ID as a string. */
toString() { return String(this.value); }
/** Returns true if this BCS-ID equals another. */
equals(aOther) { return this.value === aOther.value; }
}
////////////////////////////////////////////////////////////////
// RefComponent //
////////////////////////////////////////////////////////////////
/**
* A reference to another band combination or a single band number.
*
* Used within a BandEntry to indicate that the carrier configuration for
* this band is defined by referencing another BC (with a specific BCS-ID)
* or simply by the band's channel bandwidth table.
*/
export class RefComponent extends BaseClass {
/** The referenced BC-ID, or null if referencing by band number. */
bcId = null;
/** The BCS-ID within the referenced BC, or -1 if not applicable. */
bcsId = -1;
/** The referenced band number, or null if referencing by BC-ID. */
bandNumber = null;
/**
* @param aValue — a dict from JSON or an existing RefComponent to copy.
* @param aParent — the parent BandEntry.
*/
constructor(aValue, aParent = null) {
super(aParent);
if (aValue instanceof RefComponent) {
this.bcId = aValue.bcId;
this.bcsId = aValue.bcsId;
this.bandNumber = aValue.bandNumber;
}
else {
for (const oneKey of Object.keys(aValue)) {
if (oneKey === KEYS.bcId) {
this.bcId = new BC_ID(aValue[KEYS.bcId]);
}
else if (oneKey === KEYS.bcsId) {
this.bcsId = new BCS_ID(aValue[KEYS.bcsId]);
}
else if (oneKey === KEYS.bandNumber) {
this.bandNumber = new BandNumber(aValue[KEYS.bandNumber]);
}
else {
throw new UnsupportedKeyException(`${this.getDescriptor()}: Does not support the given key '${oneKey}' as element`);
}
}
}
}
/**
* Validates the RefComponent against the channel bandwidth and BC tables.
* Checks that exactly one of bcId/bandNumber is set, band numbers match,
* and referenced BCs/BCSs exist.
*/
validate(aChBwList, aBcList) {
if (this.bcId === null && this.bandNumber === null) {
throw new Error(`${this.getDescriptor()}: Contains neither a BC-ID nor a BandNumber.`);
}
if (this.bcId !== null && this.bandNumber !== null) {
throw new Error(`${this.getDescriptor()}: Contains both a BC-ID (${this.bcId}) and a BandNumber (${this.bandNumber}).`);
}
if (this.bandNumber !== null && +this.bcsId >= 0) {
throw new Error(`${this.getDescriptor()}: Contains both a BandNumber (${this.bandNumber}) and a BCS-ID (${this.bcsId}).`);
}
const ownBandNumber = this.getParent(BandEntry).bandNumber;
if (this.bcId !== null) {
const refBandNumber = this.bcId.getBandNumbers()[0];
if (ownBandNumber.asInt() !== refBandNumber.asInt()) {
throw new BandNumberMismatchException(`${this.getDescriptor()}: The RefComponent is in a BandEntry for band ${ownBandNumber} but refers to band ${refBandNumber}.`);
}
if (this.bcId.isSingleCarrier()) {
throw new Error(`${this.getDescriptor()}: A single carrier configuration shall reference to the BandNumber (${ownBandNumber}) but referred to a single carrier BC-ID (${this.bcId})`);
}
else {
if (+this.bcsId < 0) {
throw new MissingBandwidthCombinationSetIdException(`${this.getDescriptor()}: Does not contain a BCS-ID (bcsId).`);
}
if (this.bcId.getBandNumbers().length > 1) {
throw new InvalidReferenceBcId(`${this.getDescriptor()}: The reference BC-ID comprises of several bands ${this.bcId.getBandNumbers()}. This is not allowed.`);
}
if (aBcList === undefined) {
throw new Error(`${this.getDescriptor()}: Could not access BC table and hence not validate.`);
}
if (!aBcList.hasBC(this.bcId)) {
throw new UndefinedBandCombinationException(`${this.getDescriptor()}: Referenced BC-ID ${this.bcId} is not defined in BC-table.`);
}
const bc = aBcList.get(this.bcId.toString());
if (!bc.hasBCS(this.bcsId)) {
throw new UndefinedBcsException(`${this.getDescriptor()}: Referenced BC-ID ${this.bcId} does not have a BCS-ID ${this.bcsId}.`);
}
}
}
else {
if (!aChBwList.hasBand(this.bandNumber)) {
throw new UndefinedChannelBandwidthException(`${this.getDescriptor()}: channelBandwidthTable does not have an entry for bandNumber ${this.bandNumber}.`);
}
}
}
toString() {
if (this.bandNumber !== null)
return this.bandNumber.toString();
if (+this.bcsId >= 0)
return `${this.bcId}_BCS${this.bcsId}`;
return String(this.bcId);
}
toHTMLLink() {
const text = this.toString();
if (this.bandNumber !== null) {
// Link to band file: n3.json
return `<a href="#" class="bc-ref-link" data-ref="${this.bandNumber.toString()}">${text}</a>`;
}
if (this.bcId !== null) {
// Link to BC file: CA_n3B.json (with BCS info in data attribute)
const bcsInfo = +this.bcsId >= 0 ? `_BCS${this.bcsId}` : '';
return `<a href="#" class="bc-ref-link" data-ref="${this.bcId.getPrefix()}${this.bcId.valueOf()}" data-bcs="${+this.bcsId}">${text}</a>`;
}
return text;
}
getTag() { return this.toString(); }
toJSON(anEncoder, aLevel) {
anEncoder.write("{\n", aLevel);
if (this.bandNumber !== null) {
anEncoder.writeKeyAndValue(KEYS.bandNumber, this.bandNumber.toString(), aLevel + 1);
}
else {
anEncoder.writeKeyAndValue(KEYS.bcId, this.bcId.getPrefix() + this.bcId.valueOf(), aLevel + 1);
if (!this.bcId.isSingleCarrier()) {
anEncoder.writeKeyAndValue(KEYS.bcsId, +this.bcsId, aLevel + 1, ",\n");
}
}
anEncoder.write("\n}", aLevel);
}
/** Returns the effective BWC-ID of this reference after validation. */
getBWC_ID(aChBwList, aBcList) {
this.validate(aChBwList, aBcList);
if (this.bandNumber !== null)
return new BWC_ID("A");
return [...new BC_ID(this.bcId.valueOf()).getBwcIDsPerBand().values()][0];
}
}
////////////////////////////////////////////////////////////////
// Carrier //
////////////////////////////////////////////////////////////////
/**
* A single carrier component within a band entry.
*
* Contains a list of supported bandwidth values (integers in MHz) or a
* single BandNumber reference indicating that the carrier uses the band's
* full channel bandwidth table.
*/
export class Carrier extends BaseClass {
/** Bandwidth values (integers) or a single BandNumber reference. */
bwList = [];
/**
* @param aValue — an array of bandwidth values or an existing Carrier to copy.
* @param aParent — the parent BandEntry.
*/
constructor(aValue, aParent = null) {
super(aParent);
let aBandwidthList;
if (aValue instanceof Carrier) {
aBandwidthList = aValue.bwList;
}
else if (Array.isArray(aValue)) {
aBandwidthList = aValue;
}
else {
throw new Error(`${this.getDescriptor()}: Expecting a Carrier object or a list of integers but got ${aValue} (${typeof aValue}).`);
}
try {
const first = String(aBandwidthList[0]);
if (!first.startsWith("n"))
throw new MissingBandNumberException("Not a band number");
const refBandNumber = new BandNumber(aBandwidthList[0]);
this.bwList = [refBandNumber];
}
catch {
const bwAsIntList = [];
for (const oneBW of aBandwidthList) {
if (!IsInt(oneBW)) {
throw new InvalidCarrierBandwidthException(`${this.getDescriptor()}: Carrier bandwidths must be integer values but got: ${oneBW}`);
}
const oneBwAsInt = Number(oneBW);
if (oneBwAsInt < 0 || oneBwAsInt > MAX_CARRIER_BW_MHZ) {
throw new InvalidCarrierBandwidthException(`${this.getDescriptor()}: Carrier bandwidths must be in the range (0..${MAX_CARRIER_BW_MHZ}] but got ${oneBwAsInt}.`);
}
bwAsIntList.push(oneBwAsInt);
}
this.bwList = bwAsIntList;
}
}
validate(aChBwList, aBcList) {
if (this.bwList.length === 1 && this.bwList[0] instanceof BandNumber) {
const refBandNumber = this.bwList[0];
const ownBandNumber = new BandNumber(this.getParent(BandEntry).bandNumber);
if (ownBandNumber.asInt() !== refBandNumber.asInt()) {
throw new BandNumberMismatchException(`${this.getDescriptor()}: Belongs to a BandEntry for band ${ownBandNumber} but the referenced BC is for band ${refBandNumber}.`);
}
if (!aChBwList.hasBand(ownBandNumber)) {
throw new UndefinedChannelBandwidthException(`${this.getDescriptor()}: channelBandwidthTable does not have an entry for bandNumber ${ownBandNumber}.`);
}
}
}
toString() {
if (this.bwList.length === 1 && this.bwList[0] instanceof BandNumber) {
return JSON.stringify([this.bwList[0].toString()]);
}
return JSON.stringify(this.bwList);
}
getTag() { return ""; }
toJSON(anEncoder, aLevel) {
const serializable = this.bwList.map(v => v instanceof BandNumber ? v.toString() : v);
anEncoder.writeValue(serializable, aLevel);
}
}
////////////////////////////////////////////////////////////////
// CCGV //
////////////////////////////////////////////////////////////////
/**
* A Contiguous Carrier Group Variant — one possible arrangement of
* contiguous carriers within a band.
*
* Contains a list of Carrier objects representing the individual
* component carriers in this contiguous group.
*/
export class CCGV extends BaseClass {
/** The component carriers in this contiguous group. */
ccList = [];
/**
* @param aValue — an array of carrier arrays or an existing CCGV to copy.
* @param aParent — the parent BandEntry or ContCarrierGroup.
*/
constructor(aValue, aParent = null) {
super(aParent);
const aCCList = aValue instanceof CCGV ? aValue.ccList : aValue;
for (const oneCC of aCCList) {
this.ccList.push(new Carrier(oneCC, this));
}
}
toString() { return String(this.ccList); }
getTag() { return ""; }
validate(aChBwList, aBcList) {
for (const oneCarrier of this.ccList) {
oneCarrier.validate(aChBwList, aBcList);
}
}
toJSON(anEncoder, aLevel) {
anEncoder.writeValue(this.ccList, aLevel);
}
/** Returns the number of component carriers in this group. */
getNrofCarriers() { return this.ccList.length; }
}
////////////////////////////////////////////////////////////////
// ContCarrierGroup //
////////////////////////////////////////////////////////////////
/**
* A Contiguous Carrier Group — a set of CCGV variants that all describe
* the same number of contiguous carriers with different bandwidth options.
*/
export class ContCarrierGroup extends BaseClass {
/** The list of CCGV variants. */
ccgvList = [];
/**
* @param aCCGVList — array of CCGV arrays from JSON.
* @param aParent — the parent BandEntry.
*/
constructor(aCCGVList, aParent = null) {
super(aParent);
for (const oneCCGV of aCCGVList) {
const ccgv = new CCGV(oneCCGV, this);
if (aCCGVList.length > 1 && ccgv.getNrofCarriers() === 1) {
throw new NonContVariantException(`${this.getDescriptor()}: Multiple variants are not allowed for single-carrier configurations: ${aCCGVList}`);
}
this.ccgvList.push(ccgv);
}
}
validate(aChBwList, aBcList) {
let n = -1;
for (const oneCCGVariant of this.ccgvList) {
oneCCGVariant.validate(aChBwList, aBcList);
if (n === -1) {
n = oneCCGVariant.getNrofCarriers();
}
else if (oneCCGVariant.getNrofCarriers() !== n) {
throw new ContCarrierVariantMismatchException(`${this.getDescriptor()}: All CCGVariants must have the same number of carriers.`);
}
}
}
toString() { return String(this.ccgvList); }
getTag() { return ""; }
toJSON(anEncoder, aLevel) {
anEncoder.writeValue(this.ccgvList, aLevel);
}
/** Returns the number of contiguous carriers (from the first variant). */
getNrofContCarriers() {
if (this.ccgvList.length === 0)
return 0;
return this.ccgvList[0].getNrofCarriers();
}
/** Returns the number of CCGV variants. */
getNrofVariants() { return this.ccgvList.length; }
}
////////////////////////////////////////////////////////////////
// BandEntry //
////////////////////////////////////////////////////////////////
/**
* A band entry within a Bandwidth Combination Set (BCS).
*
* Describes the carrier configuration for one band within a band combination:
* non-contiguous carriers, contiguous carrier groups, referenced components,
* and the maximum aggregated bandwidth.
*/
export class BandEntry extends BaseClass {
/** The band number for this entry. */
bandNumber = null;
/** Non-contiguous (individual) carriers. */
nonContiguousCarriers = [];
/** Contiguous carrier group variants. */
contiguousCarriers = [];
/** References to other BCs or band numbers. */
referencedComponents = [];
/** Maximum aggregated bandwidth per band in MHz (0 if not specified). */
maxAggBwPerBand = 0;
/**
* @param aValue — a dict from JSON or an existing BandEntry to copy.
* @param aParent — the parent BCS.
*/
constructor(aValue, aParent = null) {
super(aParent);
if (aValue instanceof BandEntry) {
this.bandNumber = aValue.bandNumber;
for (const oneElement of aValue.nonContiguousCarriers) {
this.nonContiguousCarriers.push(new Carrier(oneElement, this));
}
for (const oneElement of aValue.contiguousCarriers) {
this.contiguousCarriers.push(new CCGV(oneElement, this));
}
for (const oneElement of aValue.referencedComponents) {
this.referencedComponents.push(new RefComponent(oneElement, this));
}
this.maxAggBwPerBand = aValue.maxAggBwPerBand;
}
else {
for (const oneKey of Object.keys(aValue)) {
if (oneKey === KEYS.bandNumber) {
this.bandNumber = new BandNumber(aValue[KEYS.bandNumber]);
}
else if (oneKey === KEYS.contCarrierGroups) {
const ccgs = aValue[KEYS.contCarrierGroups];
if (ccgs.length > 1) {
for (const oneCCG of ccgs) {
const ccg = new ContCarrierGroup(oneCCG, this);
if (ccg.getNrofVariants() > 1) {
throw new NonContVariantException(`${this.getDescriptor()}: For non-contiguous CA, multiple variants are not allowed in contCarrierGroups: '${ccg}'.`);
}
const oneCCGV = new CCGV(ccg.ccgvList[0], this);
if (oneCCGV.getNrofCarriers() > 1) {
throw new ContNonContMixException(`${this.getDescriptor()}: List of contCarrierGroups '${ccgs}' defines both contiguous and non-contiguous CA, which is not allowed.`);
}
this.nonContiguousCarriers.push(new Carrier(oneCCGV.ccList[0], this));
}
}
else {
const oneCCG = new ContCarrierGroup(ccgs[0], this);
if (oneCCG.getNrofContCarriers() === 1) {
this.nonContiguousCarriers.push(oneCCG.ccgvList[0].ccList[0]);
}
else {
for (const oneCCGV of oneCCG.ccgvList) {
this.contiguousCarriers.push(oneCCGV);
}
}
}
}
else if (oneKey === KEYS.nonContiguousCarriers) {
for (const oneElement of aValue[KEYS.nonContiguousCarriers]) {
this.nonContiguousCarriers.push(new Carrier(oneElement, this));
}
}
else if (oneKey === KEYS.contiguousCarriers) {
for (const oneElement of aValue[KEYS.contiguousCarriers]) {
this.contiguousCarriers.push(new CCGV(oneElement, this));
}
}
else if (oneKey === KEYS.referencedComponents) {
for (const oneElement of aValue[KEYS.referencedComponents]) {
this.referencedComponents.push(new RefComponent(oneElement, this));
}
}
else if (oneKey === KEYS.maxAggBwPerBand) {
const maxBW = aValue[KEYS.maxAggBwPerBand];
if (!IsInt(maxBW)) {
throw new InvalidMaxBandwidthException(`${this.getDescriptor()}: maxAggBwPerBand shall be a positive integer value but was '${maxBW}'.`);
}
this.maxAggBwPerBand = Number(maxBW);
}
else {
throw new UnsupportedKeyException(`${this.getDescriptor()}: Does not support the given key '${oneKey}' as element`);
}
}
}
}
/**
* Validates this band entry against the channel bandwidth and BC tables.
* Checks band number existence, BWC-ID consistency, carrier counts, and
* referenced component validity.
*/
validate(aChBwList, aBcList) {
if (this.bandNumber === null) {
throw new MissingBandNumberException(`${this.getDescriptor()}: BandEntry must contain a 'bandNumber'.`);
}
if (!aChBwList.hasBand(this.bandNumber)) {
throw new UndefinedBandNumberException(`${this.getDescriptor()}: The bandNumber '${this.bandNumber}' is not defined in the channel-bandwidth list.`);
}
const bc = this.getParent(BC);
const bwcMap = bc.bcId.getBwcIDsPerBand();
const bandKey = this.bandNumber.toString();
if (!bwcMap.has(bandKey)) {
throw new UnexpectedBandNumberException(`${this.getDescriptor()}: The bandNumber '${this.bandNumber}' of the band entry does not occur in the BC's bcId '${bc.bcId}'.`);
}
const bwcForBand = bwcMap.get(bandKey);
const expectedBwcIds = [...(bwcForBand instanceof BWC_ID ? bwcForBand.getContGroups() : [bwcForBand])];
if (this.referencedComponents.length > 0) {
for (const oneRefComponent of this.referencedComponents) {
oneRefComponent.validate(aChBwList, aBcList);
for (const oneContGroupId of oneRefComponent.getBWC_ID(aChBwList, aBcList).getContGroups()) {
const idx = expectedBwcIds.indexOf(oneContGroupId);
if (idx < 0) {
throw new UnexpectedBwcException(`${this.getDescriptor()}: RefComponent '${oneRefComponent}' for band ${this.bandNumber} has a BWC value '${oneContGroupId}' which is not present according in the BandCombination's BC-ID '${bc.getTag()}'.`);
}
expectedBwcIds.splice(idx, 1);
}
}
}
if (this.nonContiguousCarriers.length > 0 && this.contiguousCarriers.length > 0) {
throw new ContNonContMixException(`${this.getDescriptor()}: Contains both nonContiguousCarriers and contiguousCarriers which is not allowed.`);
}
if (this.nonContiguousCarriers.length > 0) {
for (const oneCarrier of this.nonContiguousCarriers) {
oneCarrier.validate(aChBwList, aBcList);
const idx = expectedBwcIds.indexOf("A");
if (idx >= 0) {
expectedBwcIds.splice(idx, 1);
}
else {
throw new TooManyNonContCarrierException(`${this.getDescriptor()}: nonContiguousCarriers '${this.nonContiguousCarriers}' specifies more carriers for band ${this.bandNumber} than the BandCombination's BC-ID '${bc.getTag()}' suggests.`);
}
}
}
if (this.contiguousCarriers.length > 0) {
let nrofContCarriers = -1;
for (const oneCCGVariant of this.contiguousCarriers) {
oneCCGVariant.validate(aChBwList, aBcList);
if (nrofContCarriers === -1) {
nrofContCarriers = oneCCGVariant.getNrofCarriers();
}
else if (oneCCGVariant.getNrofCarriers() !== nrofContCarriers) {
throw new ContCarrierVariantMismatchException(`${this.getDescriptor()}: All CCGVariants must have the same number of carriers.`);
}
}
if (nrofContCarriers <= 1) {
throw new LessThanTwoContigiousCarriersException(`${this.getDescriptor()}: There should be at least two contiguousCarriers but found only ${nrofContCarriers}: ${this.contiguousCarriers}`);
}
const bwcTable = this.getBwcTable();
for (const oneExpectedBwcId of expectedBwcIds) {
const expectedNrofCarriers = bwcTable[oneExpectedBwcId].nrofCarriers;
if (nrofContCarriers === expectedNrofCarriers) {
expectedBwcIds.splice(expectedBwcIds.indexOf(oneExpectedBwcId), 1);
nrofContCarriers = 0;
}
break;
}
if (nrofContCarriers > 0) {
throw new TooManyContCarrierException(`${this.getDescriptor()}: contiguousCarriers '${this.contiguousCarriers}' for band ${this.bandNumber} supports ${nrofContCarriers} (more) contiguous carriers. But this does not match the BandCombination's BC-ID '${bc.getTag()}'.`);
}
}
if (this.nonContiguousCarriers.length > 0 || this.contiguousCarriers.length > 0 || this.referencedComponents.length > 0) {
if (expectedBwcIds.length > 0) {
throw new UndefinedCarriersException(`${this.getDescriptor()}: BandEntry shall specify the components ${expectedBwcIds} expected according to the BC's bcId '${bc.getTag()}'.`);
}
}
else {
if (aBcList === undefined) {
throw new MissingParentObjectException(`${this.getDescriptor()}: Could not access BC table and hence not validate.`);
}
const refBWCs = String(bwcMap.get(bandKey)).replace(/\(/g, "").replace(/\)/g, "").split("-");
for (const oneRefBWC of refBWCs) {
let refBCID;
try {
if (oneRefBWC[0] >= "0" && oneRefBWC[0] <= "9") {
refBCID = new BC_ID(`${this.bandNumber}(${oneRefBWC})`);
}
else {
refBCID = new BC_ID(`${this.bandNumber}${oneRefBWC}`);
}
}
catch (e) {
throw new Error(`${this.getDescriptor()}: ${e.message}`);
}
if (!refBCID.isSingleCarrier()) {
if (!aBcList.hasBC(refBCID)) {
throw new UndefinedBandCombinationException(`${this.getDescriptor()}: Referenced BC-ID ${refBCID} is not defined in BC-table.`);
}
}
}
}
}
toString() {
if (this.bandNumber !== null)
return this.bandNumber.toString();
return super.toString();
}
getTag() { return this.toString(); }
/** Returns the BWC value table (FR1 or FR2) based on this band's frequency range. */
getBwcTable() {
if (this.bandNumber.isFr1())
return bwcValuesFR1;
return bwcValuesFR2;
}
toJSON(anEncoder, aLevel) {
anEncoder.write("{\n", aLevel);
anEncoder.writeKeyAndValue(KEYS.bandNumber, this.bandNumber.toString(), aLevel + 1);
if (this.nonContiguousCarriers.length > 0) {
anEncoder.writeKeyAndValue(KEYS.nonContiguousCarriers, this.nonContiguousCarriers, aLevel + 1, ",\n");
}
if (this.contiguousCarriers.length > 0) {
anEncoder.writeKeyAndValue(KEYS.contiguousCarriers, this.contiguousCarriers, aLevel + 1, ",\n");
}
if (this.referencedComponents.length > 0) {
anEncoder.writeKeyAndValue(KEYS.referencedComponents, this.referencedComponents, aLevel + 1, ",\n");
}
if (this.maxAggBwPerBand > 0) {
anEncoder.writeKeyAndValue(KEYS.maxAggBwPerBand, this.maxAggBwPerBand, aLevel + 1, ",\n");
}
anEncoder.write("\n}", aLevel);
}
toHTML(aHtmlTable, aRow = 0, aColumn = 0) {
let rowIndexToUse = aRow;
aHtmlTable.setValue(rowIndexToUse, aColumn, this.bandNumber.toString());
if (this.maxAggBwPerBand > 0) {
aHtmlTable.setValue(rowIndexToUse, aColumn + 2, this.maxAggBwPerBand);
}
else {
aHtmlTable.setValue(rowIndexToUse, aColumn + 2, " ");
}
let noDefinitionYet = true;
if (this.contiguousCarriers.length > 0) {
noDefinitionYet = false;
let s = "";
for (const oneVariant of this.contiguousCarriers) {
if (s !== "")
s += "<br>";
let cc = "";
for (const oneCC of oneVariant.ccList) {
if (cc !== "")
cc += " + ";
cc += String(oneCC);
}
s += "[" + cc + "]";
}
aHtmlTable.setValue(rowIndexToUse, aColumn + 1, s);
rowIndexToUse = aHtmlTable.getNrofRows();
}
if (this.nonContiguousCarriers.length > 0) {
noDefinitionYet = false;
let s = "";
for (const oneCarrier of this.nonContiguousCarriers) {
if (s !== "")
s += " + ";
s += String(oneCarrier);
}
aHtmlTable.setValue(rowIndexToUse, aColumn + 1, s);
rowIndexToUse = aHtmlTable.getNrofRows();
}
if (this.referencedComponents.length > 0) {
noDefinitionYet = false;
let s = "";
for (const oneRef of this.referencedComponents) {
if (s !== "")
s += " + ";
s += oneRef.toHTMLLink();
}
aHtmlTable.setValue(rowIndexToUse, aColumn + 1, s);
}
if (noDefinitionYet) {
aHtmlTable.setValue(rowIndexToUse, aColumn + 1, " ");
}
}
}
////////////////////////////////////////////////////////////////
// UlConfig //
////////////////////////////////////////////////////////////////
/**
* An uplink configuration within a CA band combination.
*
* Identifies one UL component by either a BC-ID (for multi-carrier UL) or
* a band number (for single-carrier UL). If the bcId is a single-carrier
* configuration, it is automatically converted to a bandNumber.
*/
export class UlConfig extends BaseClass {
/** The BC-ID for multi-carrier UL, or null. */
bcId = null;
/** The band number for single-carrier UL, or null. */
bandNumber = null;
/** Optional specification notes. */
notes = {};
/**
* @param aValue — a dict from JSON or an existing UlConfig to copy.
* @param aParent — the parent BCS.
*/
constructor(aValue, aParent = null) {
super(aParent);
if (aValue instanceof UlConfig) {
this.bcId = aValue.bcId;
this.bandNumber = aValue.bandNumber;
this.notes = { ...aValue.notes };
}
else {
for (const oneKey of Object.keys(aValue)) {
if (oneKey === KEYS.bcId) {
const newBcId = new BC_ID(aValue[KEYS.bcId]);
if (newBcId.isSingleCarrier()) {
this.bandNumber = new BandNumber(newBcId.getBandNumbers()[0]);
}
else {
this.bcId = newBcId;
}
}
else if (oneKey === KEYS.bandNumber) {
this.bandNumber = new BandNumber(aValue[KEYS.bandNumber]);
}
else if (oneKey === KEYS.notes) {
this.notes = { ...aValue[KEYS.notes] };
}
else {
throw new UnsupportedKeyException(`${this.getDescriptor()}: Does not support the given key '${oneKey}' as element`);
}
}
}
}
validate(aChBwList, aBcList) {
if (this.bcId === null && this.bandNumber === null) {
throw new Error(`${this.getDescriptor()}: Contains neither a valid bcId nor a valid bandNumber`);
}
if (this.bcId !== null && this.bandNumber !== null) {
throw new Error(`${this.getDescriptor()}: Contains both a bcId (${this.bcId}) and a bandNumber (${this.bandNumber}) which is not allowed.`);
}
if (this.bandNumber !== null) {
if (!aChBwList.hasBand(this.bandNumber)) {
throw new UndefinedUplinkConfigException(`${this.getDescriptor()}: UL-Config ${this.bandNumber} is not defined in ChBw-table.`);
}
}
else {
if (!aBcList.hasBC(this.bcId)) {
throw new UndefinedUplinkConfigException(`${this.getDescriptor()}: UL-Config ${this.bcId} is not defined in BC-table.`);
}
}
}
toJSON(anEncoder, aLevel) {
anEncoder.write("{\n", aLevel);
if (this.bcId !== null) {
anEncoder.writeKeyAndValue(KEYS.bcId, this.bcId.getPrefix() + this.bcId.valueOf(), aLevel + 1);
}
else {
anEncoder.writeKeyAndValue(KEYS.bandNumber, this.bandNumber.toString(), aLevel + 1);
}
if (Object.keys(this.notes).length > 0) {
anEncoder.writeKeyAndValue(KEYS.notes, this.notes, aLevel + 1, ",\n");
}
anEncoder.write("\n}", aLevel);
}
toString() {
if (this.bcId !== null)
return String(this.bcId);
if (this.bandNumber !== null)
return String(this.bandNumber);
return super.toString();
}
toStringWithNotes(ulNoteDescriptions) {
let base = this.toString();
if (Object.keys(this.notes).length === 0)
return base;
const noteKeys = Object.keys(this.notes).filter(k => this.notes[k] === true).sort();
if (noteKeys.length === 0)
return base;
const notesHtml = noteKeys.map(k => {
const desc = ulNoteDescriptions && ulNoteDescriptions[k] ? ulNoteDescriptions[k] : k;
return `<sup title="${desc}">${k}</sup>`;
}).join(", ");
return base + notesHtml;
}
getTag() { return this.toString(); }
}
////////////////////////////////////////////////////////////////
// BCS //
////////////////////////////////////////////////////////////////
/**
* A Bandwidth Combination Set within a band combination.
*
* Groups a BCS-ID with its list of band entries (one per band) and
* optional UL configurations. Each BCS represents one set of allowed
* bandwidth combinations for the parent BC.
*/
export class BCS extends BaseClass {
/** The BCS-ID (0–31). */
bcsId = -1;
/** UL configurations for this BCS. */
ulConfigList = [];
/** Band entries (one per band in the combination). */
bandList = [];
/**
* @param aValue — a dict from JSON or an existing BCS to copy.
* @param aParent — the parent BC.
*/
constructor(aValue, aParent = null) {
super(aParent);
if (aValue instanceof BCS) {
this.bcsId = aValue.bcsId;
this.ulConfigList = aValue.ulConfigList.slice();
for (const oneElement of aValue.bandList) {
if (this.hasBandEntry(oneElement.bandNumber)) {
throw new DuplicateBandEntryException(`${this.getDescriptor()}: Contains already an entry for band '${oneElement.bandNumber}'`);
}
this.bandList.push(new BandEntry(oneElement, this));
}
}
else {
for (const oneKey of Object.keys(aValue)) {
if (oneKey === KEYS.bcsId) {
this.bcsId = new BCS_ID(aValue[KEYS.bcsId]);
}
else if (oneKey === KEYS.bandList) {
for (const oneElement of aValue[KEYS.bandList]) {
const newBandEntry = new BandEntry(oneElement, this);
if (this.hasBandEntry(newBandEntry.bandNumber)) {
throw new DuplicateBandEntryException(`${this.getDescriptor()}: Contains already an entry for band '${newBandEntry.bandNumber}'`);
}
this.bandList.push(newBandEntry);
}
}
else if (oneKey === KEYS.ulConfigList) {
for (const oneUlConfig of aValue[KEYS.ulConfigList]) {
const newUlConfig = new UlConfig(oneUlConfig, this);
if (this.hasUlConfig(newUlConfig)) {
throw new DuplicateUplinkConfigException(`${this.getDescriptor()}: Contains already an UL Config '${newUlConfig}'.`);
}
this.ulConfigList.push(newUlConfig);
}
}
else {
throw new UnsupportedKeyException(`${this.getDescriptor()}: Does not support the given key '${oneKey}' as element`);
}
}
}
}
validate(aChBwList, aBcList) {
if (+this.bcsId < 0) {
throw new MissingBandwidthCombinationSetIdException(`${this.getDescriptor()}: A band combination shall contain a valid 'bcsId'`);
}
if (this.bandList.length === 0) {
throw new MissingBandListException(`${this.getDescriptor()}: A band combination shall contain a 'bandList' with at least one entry.`);
}
for (const oneElement of this.bandList) {
oneElement.validate(aChBwList, aBcList);
}
for (const oneUlConfig of this.ulConfigList) {
oneUlConfig.validate(aChBwList, aBcList);
}
}
/** Returns true if this BCS already contains a band entry for the given band. */
hasBandEntry(aBandNumber) {
return this.bandList.some(one => one.bandNumber !== null && one.bandNumber.asInt() === aBandNumber.asInt());
}
/** Returns true if this BCS already contains the given UL configuration. */
hasUlConfig(anUlConfig) {
for (const oneUlConfig of this.ulConfigList) {
if (anUlConfig.bcId !== null && oneUlConfig.bcId !== null && anUlConfig.bcId.equals(oneUlConfig.bcId))
return true;
if (anUlConfig.bandNumber !== null && oneUlConfig.bandNumber !== null && anUlConfig.bandNumber.asInt() === oneUlConfig.bandNumber.asInt())
return true;
}
return false;
}
toString() {
if (+this.bcsId >= 0)
return String(this.bcsId);
return super.toString();
}
getTag() { return this.toString(); }
toJSON(anEncoder, aLevel) {
anEncoder.write("{\n", aLevel);
anEncoder.writeKeyAndValue(KEYS.bcsId, +this.bcsId, aLevel + 1);
if (this.ulConfigList.length > 0) {
const sorted = this.ulConfigList.slice().sort(compareUlConfigs);
anEncoder.writeKeyAndValue(KEYS.ulConfigList, sorted, aLevel + 1, ",\n");
}
anEncoder.writeKeyAndValue(KEYS.bandList, this.bandList, aLevel + 1, ",\n");
anEncoder.write("\n}", aLevel);
}
toHTML(aHtmlTable, aRow = 0, aColumn = 0, ulNoteDescriptions) {
let rowIndexToUse = aRow;
aHtmlTable.setValue(rowIndexToUse, aColumn, +this.bcsId);
if (this.ulConfigList.length > 0) {
let s = "";
for (const oneUlConfig of this.ulConfigList) {
if (s !== "")
s += ", ";
s += oneUlConfig.toStringWithNotes(ulNoteDescriptions);
}
aHtmlTable.setValue(rowIndexToUse, aColumn + 1, s);
}
else {
aHtmlTable.setValue(rowIndexToUse, aColumn + 1, " ");
}
for (const oneBandEntry of this.bandList) {
oneBandEntry.toHTML(aHtmlTable, rowIndexToUse, aColumn + 2);
rowIndexToUse = aHtmlTable.getNrofRows();
}
}
}
////////////////////////////////////////////////////////////////
// BC //
////////////////////////////////////////////////////////////////
/**
* A band combination (BC) — the top-level CA configuration.
*
* Contains a BC-ID, one or more Bandwidth Combination Sets (BCS), and
* optional notes. Corresponds to one `CA_*.json` file.
*/
export class BC extends BaseClass {
/** The BC-ID identifying this band combination. */
bcId = null;
/** The list of Bandwidth Combination Sets. */
bcsList = [];
/** Optional specification notes. */
notes = {};
/** Specification number (e.g. "38.101-1"). */
specification = null;
/** Schema version */
schemaVersion = null;
/**
* @param aValue — a dict from JSON or an existing BC to copy.
* @param aParent — the parent BandCombinationList.
*/
constructor(aValue, aParent = null) {
super(aParent);
if (aValue instanceof BC) {
this.bcId = aValue.bcId;
for (const oneElement of aValue.bcsList) {
const newBCS = new BCS(oneElement, this);
if (this.hasBCS(newBCS.bcsId)) {
throw new DuplicateBcsException(`${this.getDescriptor()}: BC contains already a BCS with bcsId '${newBCS.bcsId}' as element`);
}
this.bcsList.push(newBCS);
}
this.notes = { ...aValue.notes };
this.specification = aValue.specification;
this.schemaVersion = aValue.schemaVersion;
}
else {
for (const oneKey of Object.keys(aValue)) {
if (oneKey === KEYS.bcId) {
try {
this.bcId = new BC_ID(aValue[KEYS.bcId]);
}
catch (e) {
throw new Error(`BC('${aValue[KEYS.bcId]}'): ${e.message}`);
}
}
else if (oneKey === KEYS.bcsList) {
for (const oneElement of aValue[KEYS.bcsList]) {
const newBCS = new BCS(oneElement, this);
if (this.hasBCS(newBCS.bcsId)) {
throw new DuplicateBcsException(`${this.getDescriptor()}: BC contains already a BCS with bcsId '${newBCS.bcsId}' as element`);
}
this.bcsList.push(newBCS);
}
}
else if (oneKey === KEYS.notes) {
this.notes = { ...aValue[KEYS.notes] };
}
else if (oneKey === KEYS.specification) {
this.specification = aValue[KEYS.specification];
}
else if (oneKey === KEYS.schemaVersion) {
this.schemaVersion = aValue[KEYS.schemaVersion];
}
else {
throw new UnsupportedKeyException(`${this.getDescriptor()}: Does not support the given key '${oneKey}' as element`);
}
}
}
}
validate(aChBwList, aBcList) {
if (this.bcId === null) {
throw new MissingBcIdException(`${this.getDescriptor()}: A band combination shall contain a 'bcId' key`);
}
const expectedSpec = this.bcId.getSpecification();
if (this.specification === null) {
this.specification = expectedSpec;
}
else if (this.specification !== expectedSpec) {
throw new InvalidSpecificationException(`${this.getDescriptor()}: specification is '${this.specification}' but expected '${expectedSpec}' based on BC-ID.`);
}
if (this.bcsList.length === 0) {
throw new MissingBcsListException(`${this.getDescriptor()}: A band combination shall contain at least one BCS`);
}
for (const oneBCS of this.bcsList) {
oneBCS.validate(aChBwList, aBcList);
}
}
/** Returns true if this BC contains a BCS with the given BCS-ID. */
hasBCS(aBcsId) {
return this.bcsList.some(one => +one.bcsId === +aBcsId);
}
toString() {
if (this.bcId !== null)
return this.bcId.toString();
return super.toString();
}
getTag() {
if (this.bcId !== null)
return this.bcId.toString();
return super.toString();
}
toJSON(anEncoder, aLevel) {
anEncoder.write("{\n", aLevel);
if (this.specification !== null) {
anEncoder.writeKeyAndValue(KEYS.specification, this.specification, aLevel + 1, "", ",\n");
}
if (this.schemaVersion !== null) {
anEncoder.writeKeyAndValue(KEYS.schemaVersion, this.schemaVersion, aLevel + 1, "", ",\n");
}
anEncoder.writeKeyAndValue(KEYS.bcId, this.bcId.getPrefix() + this.bcId.valueOf(), aLevel + 1, "", ",\n");
anEncoder.writeKeyAndValue(KEYS.bcsList, this.bcsList, aLevel + 1);
if (Object.keys(this.notes).length > 0) {
anEncoder.writeKeyAndValue(KEYS.notes, this.notes, aLevel + 1, ",\n");
}
anEncoder.write("\n}", aLevel);
}
toHTML(aHtmlTable, aRow = 0, aColumn = 0, ulNoteDescriptions, dlNoteDescriptions) {
let rowIndexToUse = aHtmlTable.getNrofRows();
let bcIdStr = this.bcId.valueOf();
// Add BC-level notes as superscript footnotes
if (Object.keys(this.notes).length > 0) {
const noteKeys = Object.keys(this.notes).filter(k => this.notes[k] === true).sort();
if (noteKeys.length > 0) {
const notesHtml = noteKeys.map(k => {
const desc = dlNoteDescriptions && dlNoteDescriptions[k] ? dlNoteDescriptions[k] : k;
return `<sup title="${desc}">${k}</sup>`;
}).join(", ");
bcIdStr += notesHtml;
}
}
aHtmlTable.setValue(rowIndexToUse, aColumn, bcIdStr);
for (const oneBCS of