diff --git a/src/main/java/io/github/dsheirer/alias/AliasFactory.java b/src/main/java/io/github/dsheirer/alias/AliasFactory.java index 6911a759..f2b2dd0f 100644 --- a/src/main/java/io/github/dsheirer/alias/AliasFactory.java +++ b/src/main/java/io/github/dsheirer/alias/AliasFactory.java @@ -30,12 +30,16 @@ import io.github.dsheirer.alias.id.legacy.mobileID.Min; import io.github.dsheirer.alias.id.legacy.siteID.SiteID; import io.github.dsheirer.alias.id.lojack.LoJackFunctionAndID; import io.github.dsheirer.alias.id.priority.Priority; +import io.github.dsheirer.alias.id.radio.NXDNTypeDRadio; +import io.github.dsheirer.alias.id.radio.NXDNTypeDRadioRange; import io.github.dsheirer.alias.id.radio.P25FullyQualifiedRadio; import io.github.dsheirer.alias.id.radio.Radio; import io.github.dsheirer.alias.id.radio.RadioRange; import io.github.dsheirer.alias.id.record.Record; import io.github.dsheirer.alias.id.status.UnitStatusID; import io.github.dsheirer.alias.id.status.UserStatusID; +import io.github.dsheirer.alias.id.talkgroup.NXDNTypeDTalkgroup; +import io.github.dsheirer.alias.id.talkgroup.NXDNTypeDTalkgroupRange; import io.github.dsheirer.alias.id.talkgroup.P25FullyQualifiedTalkgroup; import io.github.dsheirer.alias.id.talkgroup.Talkgroup; import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange; @@ -79,6 +83,31 @@ public class AliasFactory Min copyMin = new Min(); copyMin.setMin(originalMin.getMin()); return copyMin; + case NXDN_TYPE_D_RADIO_ID: + NXDNTypeDRadio originalTypeDRadio = (NXDNTypeDRadio)id; + NXDNTypeDRadio copyTypeDRadio = new NXDNTypeDRadio(originalTypeDRadio.getHomeRepeater(), + originalTypeDRadio.getIdentifier()); + copyTypeDRadio.setOverlap(originalTypeDRadio.overlapProperty().get()); + return copyTypeDRadio; + case NXDN_TYPE_D_RADIO_ID_RANGE: + NXDNTypeDRadioRange originalTypeDRadioRange = (NXDNTypeDRadioRange)id; + NXDNTypeDRadioRange copyTypeDRadioRange = new NXDNTypeDRadioRange(originalTypeDRadioRange.getHomeRepeater(), + originalTypeDRadioRange.getMinIdentifier(), originalTypeDRadioRange.getMaxIdentifier()); + copyTypeDRadioRange.setOverlap(originalTypeDRadioRange.overlapProperty().get()); + return copyTypeDRadioRange; + case NXDN_TYPE_D_TALKGROUP: + NXDNTypeDTalkgroup originalTypeDTalkgroup = (NXDNTypeDTalkgroup)id; + NXDNTypeDTalkgroup copyTypeDTalkgroup = new NXDNTypeDTalkgroup(originalTypeDTalkgroup.getHomeRepeater(), + originalTypeDTalkgroup.getIdentifier()); + copyTypeDTalkgroup.setOverlap(originalTypeDTalkgroup.overlapProperty().get()); + return copyTypeDTalkgroup; + case NXDN_TYPE_D_TALKGROUP_RANGE: + NXDNTypeDTalkgroupRange originalTypeDTalkgroupRange = (NXDNTypeDTalkgroupRange)id; + NXDNTypeDTalkgroupRange copyTypeDTalkgroupRange = new NXDNTypeDTalkgroupRange( + originalTypeDTalkgroupRange.getHomeRepeater(), originalTypeDTalkgroupRange.getMinIdentifier(), + originalTypeDTalkgroupRange.getMaxIdentifier()); + copyTypeDTalkgroupRange.setOverlap(originalTypeDTalkgroupRange.overlapProperty().get()); + return copyTypeDTalkgroupRange; case P25_FULLY_QUALIFIED_RADIO_ID: P25FullyQualifiedRadio originalP25 = (P25FullyQualifiedRadio) id; P25FullyQualifiedRadio copyP25 = new P25FullyQualifiedRadio(originalP25.getWacn(), diff --git a/src/main/java/io/github/dsheirer/alias/AliasList.java b/src/main/java/io/github/dsheirer/alias/AliasList.java index a3c0ac72..24357f4e 100644 --- a/src/main/java/io/github/dsheirer/alias/AliasList.java +++ b/src/main/java/io/github/dsheirer/alias/AliasList.java @@ -134,6 +134,7 @@ public class AliasList switch(id.getType()) { case TALKGROUP: + case NXDN_TYPE_D_TALKGROUP: Talkgroup talkgroup = (Talkgroup)id; TalkgroupAliasList talkgroupAliasList = mTalkgroupProtocolMap.get(talkgroup.getProtocol()); @@ -147,6 +148,7 @@ public class AliasList talkgroupAliasList.add(talkgroup, alias); break; case TALKGROUP_RANGE: + case NXDN_TYPE_D_TALKGROUP_RANGE: TalkgroupRange talkgroupRange = (TalkgroupRange)id; TalkgroupAliasList talkgroupRangeAliasList = mTalkgroupProtocolMap.get(talkgroupRange.getProtocol()); @@ -186,6 +188,7 @@ public class AliasList p25TalkgroupAliasList.add(qualifiedTalkgroup, alias); break; case RADIO_ID: + case NXDN_TYPE_D_RADIO_ID: Radio radio = (Radio)id; RadioAliasList radioAliasList = mRadioProtocolMap.get(radio.getProtocol()); @@ -199,6 +202,7 @@ public class AliasList radioAliasList.add(radio, alias); break; case RADIO_ID_RANGE: + case NXDN_TYPE_D_RADIO_ID_RANGE: RadioRange radioRange = (RadioRange)id; RadioAliasList radioRangeAliasList = mRadioProtocolMap.get(radioRange.getProtocol()); diff --git a/src/main/java/io/github/dsheirer/alias/id/AliasID.java b/src/main/java/io/github/dsheirer/alias/id/AliasID.java index 79e79f06..fc4c9091 100644 --- a/src/main/java/io/github/dsheirer/alias/id/AliasID.java +++ b/src/main/java/io/github/dsheirer/alias/id/AliasID.java @@ -35,12 +35,16 @@ import io.github.dsheirer.alias.id.legacy.talkgroup.LegacyTalkgroupID; import io.github.dsheirer.alias.id.legacy.uniqueID.UniqueID; import io.github.dsheirer.alias.id.lojack.LoJackFunctionAndID; import io.github.dsheirer.alias.id.priority.Priority; +import io.github.dsheirer.alias.id.radio.NXDNTypeDRadio; +import io.github.dsheirer.alias.id.radio.NXDNTypeDRadioRange; import io.github.dsheirer.alias.id.radio.P25FullyQualifiedRadio; import io.github.dsheirer.alias.id.radio.Radio; import io.github.dsheirer.alias.id.radio.RadioRange; import io.github.dsheirer.alias.id.record.Record; import io.github.dsheirer.alias.id.status.UnitStatusID; import io.github.dsheirer.alias.id.status.UserStatusID; +import io.github.dsheirer.alias.id.talkgroup.NXDNTypeDTalkgroup; +import io.github.dsheirer.alias.id.talkgroup.NXDNTypeDTalkgroupRange; import io.github.dsheirer.alias.id.talkgroup.P25FullyQualifiedTalkgroup; import io.github.dsheirer.alias.id.talkgroup.StreamAsTalkgroup; import io.github.dsheirer.alias.id.talkgroup.Talkgroup; @@ -64,6 +68,10 @@ import javafx.util.Callback; @JsonSubTypes.Type(value = Min.class, name = "min"), @JsonSubTypes.Type(value = MPT1327ID.class, name = "mpt1327ID"), @JsonSubTypes.Type(value = NonRecordable.class, name = "nonRecordable"), + @JsonSubTypes.Type(value = NXDNTypeDRadio.class, name = "nxdnTypeDRadio"), + @JsonSubTypes.Type(value = NXDNTypeDRadioRange.class, name = "nxdnTypeDRadioRange"), + @JsonSubTypes.Type(value = NXDNTypeDTalkgroup.class, name = "nxdnTypeDTalkgroup"), + @JsonSubTypes.Type(value = NXDNTypeDTalkgroupRange.class, name = "nxdnTypeDTalkgroupRange"), @JsonSubTypes.Type(value = P25FullyQualifiedRadio.class, name = "p25FullyQualifiedRadio"), @JsonSubTypes.Type(value = P25FullyQualifiedTalkgroup.class, name = "p25FullyQualifiedTalkgroup"), @JsonSubTypes.Type(value = Priority.class, name = "priority"), diff --git a/src/main/java/io/github/dsheirer/alias/id/AliasIDType.java b/src/main/java/io/github/dsheirer/alias/id/AliasIDType.java index 292e7a8d..26115c59 100644 --- a/src/main/java/io/github/dsheirer/alias/id/AliasIDType.java +++ b/src/main/java/io/github/dsheirer/alias/id/AliasIDType.java @@ -29,6 +29,10 @@ public enum AliasIDType LOJACK("LoJack"), LTR_NET_UID("LTR-Net UID"), MIN("Passport MIN"), + NXDN_TYPE_D_RADIO_ID("NXDN Type-D Radio ID"), + NXDN_TYPE_D_RADIO_ID_RANGE("NXDN Type-D Radio ID Range"), + NXDN_TYPE_D_TALKGROUP("NXDN Type-D Talkgroup"), + NXDN_TYPE_D_TALKGROUP_RANGE("NXDN Type-D Talkgroup Range"), P25_FULLY_QUALIFIED_RADIO_ID("P25 Fully Qualified Radio ID"), P25_FULLY_QUALIFIED_TALKGROUP("P25 Fully Qualified Talkgroup"), PRIORITY("Audio Priority"), @@ -58,6 +62,7 @@ public enum AliasIDType //Values used by the View-By alias editor public static EnumSet VIEW_BY_VALUES = EnumSet.of(TALKGROUP, TALKGROUP_RANGE, RADIO_ID, RADIO_ID_RANGE, + NXDN_TYPE_D_RADIO_ID, NXDN_TYPE_D_RADIO_ID_RANGE, NXDN_TYPE_D_TALKGROUP, NXDN_TYPE_D_TALKGROUP_RANGE, P25_FULLY_QUALIFIED_RADIO_ID, P25_FULLY_QUALIFIED_TALKGROUP, UNIT_STATUS, STATUS, TONES); public String toString() diff --git a/src/main/java/io/github/dsheirer/alias/id/INXDNTypeD.java b/src/main/java/io/github/dsheirer/alias/id/INXDNTypeD.java new file mode 100644 index 00000000..4e2ee467 --- /dev/null +++ b/src/main/java/io/github/dsheirer/alias/id/INXDNTypeD.java @@ -0,0 +1,46 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2026 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** + */ + +package io.github.dsheirer.alias.id; + +/** + * NXDN Type-D alias identifier composed of a home repeater and a radio or talkgroup identifier value. + */ +public interface INXDNTypeD +{ + /** + * Home repeater for the identifier + */ + int getHomeRepeater(); + + /** + * Sets the home repeater for the identifier + */ + void setHomeRepeater(int homeRepeater); + + /** + * Radio or talkgroup value, without the home repeater. + */ + int getIdentifier(); + + /** + * Sets the radio or talkgroup value, without the home repeater. + */ + void setIdentifier(int identifier); +} diff --git a/src/main/java/io/github/dsheirer/alias/id/INXDNTypeDRange.java b/src/main/java/io/github/dsheirer/alias/id/INXDNTypeDRange.java new file mode 100644 index 00000000..bb6e15cc --- /dev/null +++ b/src/main/java/io/github/dsheirer/alias/id/INXDNTypeDRange.java @@ -0,0 +1,56 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2026 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** + */ + +package io.github.dsheirer.alias.id; + +/** + * NXDN Type-D alias identifier range of radio or talkgroup values that share a common home repeater. + */ +public interface INXDNTypeDRange +{ + /** + * Home repeater for the range + */ + int getHomeRepeater(); + + /** + * Sets the home repeater for the range + */ + void setHomeRepeater(int homeRepeater); + + /** + * Minimum radio or talkgroup value, without the home repeater. + */ + int getMinIdentifier(); + + /** + * Sets the minimum radio or talkgroup value, without the home repeater. + */ + void setMinIdentifier(int identifier); + + /** + * Maximum radio or talkgroup value, without the home repeater. + */ + int getMaxIdentifier(); + + /** + * Sets the maximum radio or talkgroup value, without the home repeater. + */ + void setMaxIdentifier(int identifier); +} diff --git a/src/main/java/io/github/dsheirer/alias/id/radio/NXDNTypeDRadio.java b/src/main/java/io/github/dsheirer/alias/id/radio/NXDNTypeDRadio.java new file mode 100644 index 00000000..db90fea0 --- /dev/null +++ b/src/main/java/io/github/dsheirer/alias/id/radio/NXDNTypeDRadio.java @@ -0,0 +1,96 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2026 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** + */ + +package io.github.dsheirer.alias.id.radio; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.github.dsheirer.alias.id.AliasIDType; +import io.github.dsheirer.alias.id.INXDNTypeD; +import io.github.dsheirer.preference.identifier.talkgroup.NXDNTalkgroupFormatter; +import io.github.dsheirer.protocol.Protocol; + +/** + * NXDN Type-D radio. The value is the over-the-air 16-bit identifier that combines the home repeater and radio so that + * it matches the value of an NXDN Type-D radio identifier. + */ +public class NXDNTypeDRadio extends Radio implements INXDNTypeD +{ + public NXDNTypeDRadio() + { + //No-arg JAXB constructor + setProtocol(Protocol.NXDN); + } + + /** + * Constructs an instance + * @param homeRepeater for the radio + * @param radio value, without the home repeater + */ + public NXDNTypeDRadio(int homeRepeater, int radio) + { + super(Protocol.NXDN, NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, radio)); + } + + @Override + public AliasIDType getType() + { + return AliasIDType.NXDN_TYPE_D_RADIO_ID; + } + + @JsonIgnore + @Override + public int getHomeRepeater() + { + return NXDNTalkgroupFormatter.getTypeDHomeRepeater(getValue()); + } + + @Override + public void setHomeRepeater(int homeRepeater) + { + setValue(NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, getIdentifier())); + } + + @JsonIgnore + @Override + public int getIdentifier() + { + return NXDNTalkgroupFormatter.getTypeDIdentifier(getValue()); + } + + @Override + public void setIdentifier(int identifier) + { + setValue(NXDNTalkgroupFormatter.toTypeDValue(getHomeRepeater(), identifier)); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("Type-D Radio ID:").append(NXDNTalkgroupFormatter.toTypeD(getHomeRepeater(), getIdentifier(), true)); + sb.append(" Protocol:").append(getProtocol()); + + if(!isValid()) + { + sb.append(" **NOT VALID**"); + } + + return sb.toString(); + } +} diff --git a/src/main/java/io/github/dsheirer/alias/id/radio/NXDNTypeDRadioRange.java b/src/main/java/io/github/dsheirer/alias/id/radio/NXDNTypeDRadioRange.java new file mode 100644 index 00000000..274aa32c --- /dev/null +++ b/src/main/java/io/github/dsheirer/alias/id/radio/NXDNTypeDRadioRange.java @@ -0,0 +1,124 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2026 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** + */ + +package io.github.dsheirer.alias.id.radio; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.github.dsheirer.alias.id.AliasIDType; +import io.github.dsheirer.alias.id.INXDNTypeDRange; +import io.github.dsheirer.preference.identifier.talkgroup.NXDNTalkgroupFormatter; +import io.github.dsheirer.protocol.Protocol; + +/** + * NXDN Type-D radio range for a single home repeater. The min and max values are the over-the-air 16-bit identifiers + * that combine the home repeater and radio. + */ +public class NXDNTypeDRadioRange extends RadioRange implements INXDNTypeDRange +{ + public NXDNTypeDRadioRange() + { + //No-arg JAXB constructor + setProtocol(Protocol.NXDN); + } + + /** + * Constructs an instance + * @param homeRepeater for the range + * @param minRadio value, without the home repeater + * @param maxRadio value, without the home repeater + */ + public NXDNTypeDRadioRange(int homeRepeater, int minRadio, int maxRadio) + { + super(Protocol.NXDN, NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, minRadio), + NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, maxRadio)); + } + + @Override + public AliasIDType getType() + { + return AliasIDType.NXDN_TYPE_D_RADIO_ID_RANGE; + } + + @JsonIgnore + @Override + public int getHomeRepeater() + { + return NXDNTalkgroupFormatter.getTypeDHomeRepeater(getMinRadio()); + } + + @Override + public void setHomeRepeater(int homeRepeater) + { + int min = getMinIdentifier(); + int max = getMaxIdentifier(); + setMinRadio(NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, min)); + setMaxRadio(NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, max)); + } + + @JsonIgnore + @Override + public int getMinIdentifier() + { + return NXDNTalkgroupFormatter.getTypeDIdentifier(getMinRadio()); + } + + @Override + public void setMinIdentifier(int identifier) + { + setMinRadio(NXDNTalkgroupFormatter.toTypeDValue(getHomeRepeater(), identifier)); + } + + @JsonIgnore + @Override + public int getMaxIdentifier() + { + return NXDNTalkgroupFormatter.getTypeDIdentifier(getMaxRadio()); + } + + @Override + public void setMaxIdentifier(int identifier) + { + setMaxRadio(NXDNTalkgroupFormatter.toTypeDValue(getHomeRepeater(), identifier)); + } + + /** + * Valid when the base range is valid and both ends of the range use the same home repeater. + */ + @Override + public boolean isValid() + { + return super.isValid() && getHomeRepeater() == NXDNTalkgroupFormatter.getTypeDHomeRepeater(getMaxRadio()); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("Type-D Radio ID Range:").append(NXDNTalkgroupFormatter.toTypeD(getHomeRepeater(), getMinIdentifier(), true)); + sb.append(" to ").append(NXDNTalkgroupFormatter.toTypeD(getHomeRepeater(), getMaxIdentifier(), true)); + sb.append(" Protocol:").append(getProtocol()); + + if(!isValid()) + { + sb.append(" **NOT VALID**"); + } + + return sb.toString(); + } +} diff --git a/src/main/java/io/github/dsheirer/alias/id/talkgroup/NXDNTypeDTalkgroup.java b/src/main/java/io/github/dsheirer/alias/id/talkgroup/NXDNTypeDTalkgroup.java new file mode 100644 index 00000000..bab64ca0 --- /dev/null +++ b/src/main/java/io/github/dsheirer/alias/id/talkgroup/NXDNTypeDTalkgroup.java @@ -0,0 +1,96 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2026 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** + */ + +package io.github.dsheirer.alias.id.talkgroup; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.github.dsheirer.alias.id.AliasIDType; +import io.github.dsheirer.alias.id.INXDNTypeD; +import io.github.dsheirer.preference.identifier.talkgroup.NXDNTalkgroupFormatter; +import io.github.dsheirer.protocol.Protocol; + +/** + * NXDN Type-D talkgroup. The value is the over-the-air 16-bit identifier that combines the home repeater and talkgroup + * so that it matches the value of an NXDN Type-D talkgroup identifier. + */ +public class NXDNTypeDTalkgroup extends Talkgroup implements INXDNTypeD +{ + public NXDNTypeDTalkgroup() + { + //No-arg JAXB constructor + setProtocol(Protocol.NXDN); + } + + /** + * Constructs an instance + * @param homeRepeater for the talkgroup + * @param talkgroup value, without the home repeater + */ + public NXDNTypeDTalkgroup(int homeRepeater, int talkgroup) + { + super(Protocol.NXDN, NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, talkgroup)); + } + + @Override + public AliasIDType getType() + { + return AliasIDType.NXDN_TYPE_D_TALKGROUP; + } + + @JsonIgnore + @Override + public int getHomeRepeater() + { + return NXDNTalkgroupFormatter.getTypeDHomeRepeater(getValue()); + } + + @Override + public void setHomeRepeater(int homeRepeater) + { + setValue(NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, getIdentifier())); + } + + @JsonIgnore + @Override + public int getIdentifier() + { + return NXDNTalkgroupFormatter.getTypeDIdentifier(getValue()); + } + + @Override + public void setIdentifier(int identifier) + { + setValue(NXDNTalkgroupFormatter.toTypeDValue(getHomeRepeater(), identifier)); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("Type-D Talkgroup:").append(NXDNTalkgroupFormatter.toTypeD(getHomeRepeater(), getIdentifier(), true)); + sb.append(" Protocol:").append(getProtocol()); + + if(!isValid()) + { + sb.append(" **NOT VALID**"); + } + + return sb.toString(); + } +} diff --git a/src/main/java/io/github/dsheirer/alias/id/talkgroup/NXDNTypeDTalkgroupRange.java b/src/main/java/io/github/dsheirer/alias/id/talkgroup/NXDNTypeDTalkgroupRange.java new file mode 100644 index 00000000..4e0435a4 --- /dev/null +++ b/src/main/java/io/github/dsheirer/alias/id/talkgroup/NXDNTypeDTalkgroupRange.java @@ -0,0 +1,125 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2026 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** + */ + +package io.github.dsheirer.alias.id.talkgroup; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.github.dsheirer.alias.id.AliasIDType; +import io.github.dsheirer.alias.id.INXDNTypeDRange; +import io.github.dsheirer.preference.identifier.talkgroup.NXDNTalkgroupFormatter; +import io.github.dsheirer.protocol.Protocol; + +/** + * NXDN Type-D talkgroup range for a single home repeater. The min and max values are the over-the-air 16-bit + * identifiers that combine the home repeater and talkgroup. + */ +public class NXDNTypeDTalkgroupRange extends TalkgroupRange implements INXDNTypeDRange +{ + public NXDNTypeDTalkgroupRange() + { + //No-arg JAXB constructor + setProtocol(Protocol.NXDN); + } + + /** + * Constructs an instance + * @param homeRepeater for the range + * @param minTalkgroup value, without the home repeater + * @param maxTalkgroup value, without the home repeater + */ + public NXDNTypeDTalkgroupRange(int homeRepeater, int minTalkgroup, int maxTalkgroup) + { + super(Protocol.NXDN, NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, minTalkgroup), + NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, maxTalkgroup)); + } + + @Override + public AliasIDType getType() + { + return AliasIDType.NXDN_TYPE_D_TALKGROUP_RANGE; + } + + @JsonIgnore + @Override + public int getHomeRepeater() + { + return NXDNTalkgroupFormatter.getTypeDHomeRepeater(getMinTalkgroup()); + } + + @Override + public void setHomeRepeater(int homeRepeater) + { + int min = getMinIdentifier(); + int max = getMaxIdentifier(); + setMinTalkgroup(NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, min)); + setMaxTalkgroup(NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, max)); + } + + @JsonIgnore + @Override + public int getMinIdentifier() + { + return NXDNTalkgroupFormatter.getTypeDIdentifier(getMinTalkgroup()); + } + + @Override + public void setMinIdentifier(int identifier) + { + setMinTalkgroup(NXDNTalkgroupFormatter.toTypeDValue(getHomeRepeater(), identifier)); + } + + @JsonIgnore + @Override + public int getMaxIdentifier() + { + return NXDNTalkgroupFormatter.getTypeDIdentifier(getMaxTalkgroup()); + } + + @Override + public void setMaxIdentifier(int identifier) + { + setMaxTalkgroup(NXDNTalkgroupFormatter.toTypeDValue(getHomeRepeater(), identifier)); + } + + /** + * Valid when the base range is valid and both ends of the range use the same home repeater. + */ + @Override + public boolean isValid() + { + return super.isValid() && + getHomeRepeater() == NXDNTalkgroupFormatter.getTypeDHomeRepeater(getMaxTalkgroup()); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("Type-D Talkgroup Range:").append(NXDNTalkgroupFormatter.toTypeD(getHomeRepeater(), getMinIdentifier(), true)); + sb.append(" to ").append(NXDNTalkgroupFormatter.toTypeD(getHomeRepeater(), getMaxIdentifier(), true)); + sb.append(" Protocol:").append(getProtocol()); + + if(!isValid()) + { + sb.append(" **NOT VALID**"); + } + + return sb.toString(); + } +} diff --git a/src/main/java/io/github/dsheirer/gui/playlist/alias/AliasItemEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/alias/AliasItemEditor.java index bc96a1a6..5cd2ceb1 100644 --- a/src/main/java/io/github/dsheirer/gui/playlist/alias/AliasItemEditor.java +++ b/src/main/java/io/github/dsheirer/gui/playlist/alias/AliasItemEditor.java @@ -33,16 +33,22 @@ import io.github.dsheirer.alias.action.clip.ClipAction; import io.github.dsheirer.alias.action.script.ScriptAction; import io.github.dsheirer.alias.id.AliasID; import io.github.dsheirer.alias.id.AliasIDType; +import io.github.dsheirer.alias.id.INXDNTypeD; +import io.github.dsheirer.alias.id.INXDNTypeDRange; import io.github.dsheirer.alias.id.broadcast.BroadcastChannel; import io.github.dsheirer.alias.id.dcs.Dcs; import io.github.dsheirer.alias.id.esn.Esn; import io.github.dsheirer.alias.id.lojack.LoJackFunctionAndID; +import io.github.dsheirer.alias.id.radio.NXDNTypeDRadio; +import io.github.dsheirer.alias.id.radio.NXDNTypeDRadioRange; import io.github.dsheirer.alias.id.radio.P25FullyQualifiedRadio; import io.github.dsheirer.alias.id.radio.Radio; import io.github.dsheirer.alias.id.radio.RadioFormatter; import io.github.dsheirer.alias.id.radio.RadioRange; import io.github.dsheirer.alias.id.status.UnitStatusID; import io.github.dsheirer.alias.id.status.UserStatusID; +import io.github.dsheirer.alias.id.talkgroup.NXDNTypeDTalkgroup; +import io.github.dsheirer.alias.id.talkgroup.NXDNTypeDTalkgroupRange; import io.github.dsheirer.alias.id.talkgroup.P25FullyQualifiedTalkgroup; import io.github.dsheirer.alias.id.talkgroup.StreamAsTalkgroup; import io.github.dsheirer.alias.id.talkgroup.Talkgroup; @@ -70,6 +76,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.Supplier; import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.beans.value.ChangeListener; @@ -770,6 +777,12 @@ public class AliasItemEditor extends Editor nxdnMenu.getItems().add(new AddTalkgroupRangeItem(Protocol.NXDN)); nxdnMenu.getItems().add(new AddRadioIdItem(Protocol.NXDN)); nxdnMenu.getItems().add(new AddRadioIdRangeItem(Protocol.NXDN)); + nxdnMenu.getItems().add(new SeparatorMenuItem()); + nxdnMenu.getItems().add(new AddNXDNTypeDItem("Type-D Talkgroup", NXDNTypeDTalkgroup::new)); + nxdnMenu.getItems().add(new AddNXDNTypeDItem("Type-D Talkgroup Range", NXDNTypeDTalkgroupRange::new)); + nxdnMenu.getItems().add(new AddNXDNTypeDItem("Type-D Radio ID", NXDNTypeDRadio::new)); + nxdnMenu.getItems().add(new AddNXDNTypeDItem("Type-D Radio ID Range", NXDNTypeDRadioRange::new)); + nxdnMenu.getItems().add(new SeparatorMenuItem()); nxdnMenu.getItems().add(new AddTonesItem("AMBE Audio Tones")); Menu passportMenu = new ProtocolMenu(Protocol.PASSPORT); @@ -1456,6 +1469,24 @@ public class AliasItemEditor extends Editor } } + /** + * Menu Item for adding a new NXDN Type-D alias identifier + */ + public class AddNXDNTypeDItem extends MenuItem + { + public AddNXDNTypeDItem(String label, Supplier supplier) + { + super(label); + setOnAction(event -> { + AliasID aliasID = supplier.get(); + getIdentifiersList().getItems().add(aliasID); + getIdentifiersList().getSelectionModel().select(aliasID); + getIdentifiersList().scrollTo(aliasID); + modifiedProperty().set(true); + }); + } + } + /** * Menu Item for adding a new protocol-specific Talkgroup alias identifier */ @@ -1617,7 +1648,11 @@ public class AliasItemEditor extends Editor if(item != null) { - if(item instanceof P25FullyQualifiedTalkgroup fqt) + if(item instanceof INXDNTypeD || item instanceof INXDNTypeDRange) + { + setText(item.overlapProperty().get() ? item + " - Error: Overlap" : item.toString()); + } + else if(item instanceof P25FullyQualifiedTalkgroup fqt) { StringBuilder sb = new StringBuilder(); sb.append("APCO-25 Fully Qualified Talkgroup:").append(fqt.getWacn()); diff --git a/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/IdentifierEditorFactory.java b/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/IdentifierEditorFactory.java index 354e0ee4..0f5183f0 100644 --- a/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/IdentifierEditorFactory.java +++ b/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/IdentifierEditorFactory.java @@ -20,6 +20,10 @@ package io.github.dsheirer.gui.playlist.alias.identifier; import io.github.dsheirer.alias.id.AliasIDType; +import io.github.dsheirer.alias.id.radio.NXDNTypeDRadio; +import io.github.dsheirer.alias.id.radio.NXDNTypeDRadioRange; +import io.github.dsheirer.alias.id.talkgroup.NXDNTypeDTalkgroup; +import io.github.dsheirer.alias.id.talkgroup.NXDNTypeDTalkgroupRange; import io.github.dsheirer.preference.UserPreferences; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,6 +45,14 @@ public class IdentifierEditorFactory return new EsnEditor(); case LOJACK: return new LojackEditor(); + case NXDN_TYPE_D_RADIO_ID: + return new NXDNTypeDEditor("Radio"); + case NXDN_TYPE_D_RADIO_ID_RANGE: + return new NXDNTypeDRangeEditor("Radio Range"); + case NXDN_TYPE_D_TALKGROUP: + return new NXDNTypeDEditor("Talkgroup"); + case NXDN_TYPE_D_TALKGROUP_RANGE: + return new NXDNTypeDRangeEditor("Talkgroup Range"); case P25_FULLY_QUALIFIED_RADIO_ID: return new P25FullyQualifiedRadioIdEditor(userPreferences); case P25_FULLY_QUALIFIED_TALKGROUP: diff --git a/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/NXDNTypeDEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/NXDNTypeDEditor.java new file mode 100644 index 00000000..fb5375af --- /dev/null +++ b/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/NXDNTypeDEditor.java @@ -0,0 +1,122 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2026 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** + */ + +package io.github.dsheirer.gui.playlist.alias.identifier; + +import io.github.dsheirer.alias.id.AliasID; +import io.github.dsheirer.alias.id.INXDNTypeD; +import io.github.dsheirer.gui.control.IntegerFormatter; +import io.github.dsheirer.preference.identifier.talkgroup.NXDNTalkgroupFormatter; +import javafx.beans.value.ChangeListener; +import javafx.geometry.HPos; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.control.TextFormatter; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.GridPane; + +/** + * Editor for NXDN Type-D radio and talkgroup alias identifiers + * @param alias identifier to edit + */ +public class NXDNTypeDEditor extends IdentifierEditor +{ + private final TextField mHomeRepeaterField = new TextField(); + private final TextField mIdentifierField = new TextField(); + private final TextFormatter mHomeRepeaterFormatter = + new IntegerFormatter(0, NXDNTalkgroupFormatter.TYPE_D_MAX_HOME_REPEATER); + private final TextFormatter mIdentifierFormatter = + new IntegerFormatter(0, NXDNTalkgroupFormatter.TYPE_D_MAX_IDENTIFIER); + private final ChangeListener mHomeRepeaterListener = (observable, oldValue, newValue) -> { + if(getItem() != null) + { + getItem().setHomeRepeater(newValue != null ? newValue : 0); + modifiedProperty().set(true); + } + }; + private final ChangeListener mIdentifierListener = (observable, oldValue, newValue) -> { + if(getItem() != null) + { + getItem().setIdentifier(newValue != null ? newValue : 0); + modifiedProperty().set(true); + } + }; + + /** + * Constructs an instance + * @param identifierLabel for the identifier field: Radio or Talkgroup + */ + public NXDNTypeDEditor(String identifierLabel) + { + mHomeRepeaterField.setTextFormatter(mHomeRepeaterFormatter); + mHomeRepeaterField.setTooltip(new Tooltip("Home repeater: 0 - " + NXDNTalkgroupFormatter.TYPE_D_MAX_HOME_REPEATER)); + mIdentifierField.setTextFormatter(mIdentifierFormatter); + mIdentifierField.setTooltip(new Tooltip(identifierLabel + ": 0 - " + NXDNTalkgroupFormatter.TYPE_D_MAX_IDENTIFIER)); + + GridPane gridPane = new GridPane(); + gridPane.setHgap(5); + gridPane.setVgap(3); + + gridPane.add(new Label("NXDN Type-D"), 0, 0); + + Label homeRepeaterLabel = new Label("Home Repeater"); + GridPane.setHalignment(homeRepeaterLabel, HPos.RIGHT); + gridPane.add(homeRepeaterLabel, 1, 0); + gridPane.add(mHomeRepeaterField, 2, 0); + + Label identifierFieldLabel = new Label(identifierLabel); + GridPane.setHalignment(identifierFieldLabel, HPos.RIGHT); + gridPane.add(identifierFieldLabel, 3, 0); + gridPane.add(mIdentifierField, 4, 0); + + getChildren().add(gridPane); + } + + @Override + public void setItem(T item) + { + //Clear the item and listeners so that loading the field values doesn't change the item + mHomeRepeaterFormatter.valueProperty().removeListener(mHomeRepeaterListener); + mIdentifierFormatter.valueProperty().removeListener(mIdentifierListener); + + super.setItem(item); + + mHomeRepeaterField.setDisable(item == null); + mIdentifierField.setDisable(item == null); + mHomeRepeaterFormatter.setValue(item != null ? item.getHomeRepeater() : null); + mIdentifierFormatter.setValue(item != null ? item.getIdentifier() : null); + + mHomeRepeaterFormatter.valueProperty().addListener(mHomeRepeaterListener); + mIdentifierFormatter.valueProperty().addListener(mIdentifierListener); + + modifiedProperty().set(false); + } + + @Override + public void save() + { + //no-op + } + + @Override + public void dispose() + { + //no-op + } +} diff --git a/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/NXDNTypeDRangeEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/NXDNTypeDRangeEditor.java new file mode 100644 index 00000000..f9a77256 --- /dev/null +++ b/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/NXDNTypeDRangeEditor.java @@ -0,0 +1,141 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2026 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** + */ + +package io.github.dsheirer.gui.playlist.alias.identifier; + +import io.github.dsheirer.alias.id.AliasID; +import io.github.dsheirer.alias.id.INXDNTypeDRange; +import io.github.dsheirer.gui.control.IntegerFormatter; +import io.github.dsheirer.preference.identifier.talkgroup.NXDNTalkgroupFormatter; +import javafx.beans.value.ChangeListener; +import javafx.geometry.HPos; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.control.TextFormatter; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.GridPane; + +/** + * Editor for NXDN Type-D radio and talkgroup range alias identifiers + * @param alias identifier to edit + */ +public class NXDNTypeDRangeEditor extends IdentifierEditor +{ + private final TextField mHomeRepeaterField = new TextField(); + private final TextField mMinField = new TextField(); + private final TextField mMaxField = new TextField(); + private final TextFormatter mHomeRepeaterFormatter = + new IntegerFormatter(0, NXDNTalkgroupFormatter.TYPE_D_MAX_HOME_REPEATER); + private final TextFormatter mMinFormatter = + new IntegerFormatter(0, NXDNTalkgroupFormatter.TYPE_D_MAX_IDENTIFIER); + private final TextFormatter mMaxFormatter = + new IntegerFormatter(0, NXDNTalkgroupFormatter.TYPE_D_MAX_IDENTIFIER); + private final ChangeListener mHomeRepeaterListener = (observable, oldValue, newValue) -> { + if(getItem() != null) + { + getItem().setHomeRepeater(newValue != null ? newValue : 0); + modifiedProperty().set(true); + } + }; + private final ChangeListener mMinListener = (observable, oldValue, newValue) -> { + if(getItem() != null) + { + getItem().setMinIdentifier(newValue != null ? newValue : 0); + modifiedProperty().set(true); + } + }; + private final ChangeListener mMaxListener = (observable, oldValue, newValue) -> { + if(getItem() != null) + { + getItem().setMaxIdentifier(newValue != null ? newValue : 0); + modifiedProperty().set(true); + } + }; + + /** + * Constructs an instance + * @param rangeLabel for the range fields: Radio Range or Talkgroup Range + */ + public NXDNTypeDRangeEditor(String rangeLabel) + { + String identifierTooltip = rangeLabel + ": 0 - " + NXDNTalkgroupFormatter.TYPE_D_MAX_IDENTIFIER; + mHomeRepeaterField.setTextFormatter(mHomeRepeaterFormatter); + mHomeRepeaterField.setTooltip(new Tooltip("Home repeater: 0 - " + NXDNTalkgroupFormatter.TYPE_D_MAX_HOME_REPEATER)); + mMinField.setTextFormatter(mMinFormatter); + mMinField.setTooltip(new Tooltip(identifierTooltip)); + mMaxField.setTextFormatter(mMaxFormatter); + mMaxField.setTooltip(new Tooltip(identifierTooltip)); + + GridPane gridPane = new GridPane(); + gridPane.setHgap(5); + gridPane.setVgap(3); + + gridPane.add(new Label("NXDN Type-D"), 0, 0); + + Label homeRepeaterLabel = new Label("Home Repeater"); + GridPane.setHalignment(homeRepeaterLabel, HPos.RIGHT); + gridPane.add(homeRepeaterLabel, 1, 0); + gridPane.add(mHomeRepeaterField, 2, 0); + + Label rangeFieldLabel = new Label(rangeLabel); + GridPane.setHalignment(rangeFieldLabel, HPos.RIGHT); + gridPane.add(rangeFieldLabel, 3, 0); + gridPane.add(mMinField, 4, 0); + gridPane.add(new Label("-"), 5, 0); + gridPane.add(mMaxField, 6, 0); + + getChildren().add(gridPane); + } + + @Override + public void setItem(T item) + { + //Clear the item and listeners so that loading the field values doesn't change the item + mHomeRepeaterFormatter.valueProperty().removeListener(mHomeRepeaterListener); + mMinFormatter.valueProperty().removeListener(mMinListener); + mMaxFormatter.valueProperty().removeListener(mMaxListener); + + super.setItem(item); + + mHomeRepeaterField.setDisable(item == null); + mMinField.setDisable(item == null); + mMaxField.setDisable(item == null); + mHomeRepeaterFormatter.setValue(item != null ? item.getHomeRepeater() : null); + mMinFormatter.setValue(item != null ? item.getMinIdentifier() : null); + mMaxFormatter.setValue(item != null ? item.getMaxIdentifier() : null); + + mHomeRepeaterFormatter.valueProperty().addListener(mHomeRepeaterListener); + mMinFormatter.valueProperty().addListener(mMinListener); + mMaxFormatter.valueProperty().addListener(mMaxListener); + + modifiedProperty().set(false); + } + + @Override + public void save() + { + //no-op + } + + @Override + public void dispose() + { + //no-op + } +} diff --git a/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/RadioIdRangeEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/RadioIdRangeEditor.java index 18224491..c8acf611 100644 --- a/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/RadioIdRangeEditor.java +++ b/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/RadioIdRangeEditor.java @@ -293,9 +293,9 @@ public class RadioIdRangeEditor extends IdentifierEditor details.add(new RadioIdDetail(Protocol.APCO25, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFFFF), new HexFormatter(0,0xFFFFFF), "Format: 0 - FFFFFF")); details.add(new RadioIdDetail(Protocol.NXDN, IntegerFormat.DECIMAL, new IntegerFormatter(0,0xFFFF), - new IntegerFormatter(0,0xFFFF),"Format: 0 - 65,535")); + new IntegerFormatter(0,0xFFFF),"Format: 1 - 65,535")); details.add(new RadioIdDetail(Protocol.NXDN, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFF), - new IntegerFormatter(0,0xFFFF),"Format: 0 - FFFF")); + new HexFormatter(0,0xFFFF),"Format: 1 - FFFF")); details.add(new RadioIdDetail(Protocol.PASSPORT, IntegerFormat.DECIMAL, new IntegerFormatter(0,0x7FFFFF), new IntegerFormatter(0,0x7FFFFF), "Format: 0 - 8388607")); details.add(new RadioIdDetail(Protocol.PASSPORT, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0x7FFFFF), diff --git a/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/TalkgroupRangeEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/TalkgroupRangeEditor.java index a9826866..91323004 100644 --- a/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/TalkgroupRangeEditor.java +++ b/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/TalkgroupRangeEditor.java @@ -292,8 +292,8 @@ public class TalkgroupRangeEditor extends IdentifierEditor List details = new ArrayList<>(); details.add(new TalkgroupDetail(Protocol.AM, IntegerFormat.DECIMAL, new IntegerFormatter(0,65535), new IntegerFormatter(0,65535), "Format 0 - 65535")); - details.add(new TalkgroupDetail(Protocol.AM, IntegerFormat.HEXADECIMAL, new IntegerFormatter(0,65535), - new IntegerFormatter(0,65535), "Format 0 - FFFF")); + details.add(new TalkgroupDetail(Protocol.AM, IntegerFormat.HEXADECIMAL, new HexFormatter(0,65535), + new HexFormatter(0,65535), "Format 0 - FFFF")); details.add(new TalkgroupDetail(Protocol.APCO25, IntegerFormat.DECIMAL, new IntegerFormatter(0,65535), new IntegerFormatter(0,65535), "Format: 0 - 65535")); details.add(new TalkgroupDetail(Protocol.APCO25, IntegerFormat.HEXADECIMAL, new HexFormatter(0,65535), @@ -316,12 +316,12 @@ public class TalkgroupRangeEditor extends IdentifierEditor "Format: PPP-IIII = Prefix (0-127), Ident (1-8191)")); details.add(new TalkgroupDetail(Protocol.NBFM, IntegerFormat.DECIMAL, new IntegerFormatter(0,65535), new IntegerFormatter(0,65535), "Format 0 - 65535")); - details.add(new TalkgroupDetail(Protocol.NBFM, IntegerFormat.HEXADECIMAL, new IntegerFormatter(0,65535), - new IntegerFormatter(0,65535), "Format 0 - FFFF")); + details.add(new TalkgroupDetail(Protocol.NBFM, IntegerFormat.HEXADECIMAL, new HexFormatter(0,65535), + new HexFormatter(0,65535), "Format 0 - FFFF")); details.add(new TalkgroupDetail(Protocol.NXDN, IntegerFormat.DECIMAL, new IntegerFormatter(1,0xFFFF), new IntegerFormatter(1,0xFFFF),"Format: 1 - 65,535")); details.add(new TalkgroupDetail(Protocol.NXDN, IntegerFormat.HEXADECIMAL, new HexFormatter(1,0xFFFF), - new IntegerFormatter(1,0xFFFF),"Format: 1 - FFFF")); + new HexFormatter(1,0xFFFF),"Format: 1 - FFFF")); details.add(new TalkgroupDetail(Protocol.PASSPORT, IntegerFormat.DECIMAL, new IntegerFormatter(0,0xFFFF), new IntegerFormatter(0,0xFFFF), "Format: 0 - 65535")); details.add(new TalkgroupDetail(Protocol.PASSPORT, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFF), diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/RemoteControl.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/RemoteControl.java index 94503edc..b3e1f612 100644 --- a/src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/RemoteControl.java +++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/call/RemoteControl.java @@ -91,9 +91,9 @@ public abstract class RemoteControl extends CallControl } else if(getCallControlOption().hasLocationId() && getLocationIDOption().isDestination()) { -// mDestinationIdentifier = NXDNFullyQualifiedRadioIdentifier.createFrom(getLocationID().getSystem().getValue(), -// getMessage().getInt(IDENTIFIER_OCTET_3)); - mDestinationIdentifier = NXDNRadioIdentifier.createFrom(getMessage().getInt(IDENTIFIER_OCTET_3)); +// mDestinationIdentifier = NXDNFullyQualifiedRadioIdentifier.createTo(getLocationID().getSystem().getValue(), +// getMessage().getInt(IDENTIFIER_OCTET_5)); + mDestinationIdentifier = NXDNRadioIdentifier.createTo(getMessage().getInt(IDENTIFIER_OCTET_5)); } } diff --git a/src/main/java/io/github/dsheirer/preference/identifier/talkgroup/NXDNTalkgroupFormatter.java b/src/main/java/io/github/dsheirer/preference/identifier/talkgroup/NXDNTalkgroupFormatter.java index 4adb2db7..1e792e00 100644 --- a/src/main/java/io/github/dsheirer/preference/identifier/talkgroup/NXDNTalkgroupFormatter.java +++ b/src/main/java/io/github/dsheirer/preference/identifier/talkgroup/NXDNTalkgroupFormatter.java @@ -33,6 +33,9 @@ public class NXDNTalkgroupFormatter extends AbstractIntegerFormatter public static final int TYPE_D_REPEATER_WIDTH = 2; public static final int TYPE_D_VALUE_WIDTH = 4; public static final String TYPE_D_SEPARATOR = "-"; + public static final int TYPE_D_IDENTIFIER_BIT_WIDTH = 11; + public static final int TYPE_D_MAX_HOME_REPEATER = 0x1F; + public static final int TYPE_D_MAX_IDENTIFIER = 0x7FF; public static final int GROUP_DECIMAL_WIDTH = 5; public static final int UNIT_DECIMAL_WIDTH = 5; public static final int GROUP_HEXADECIMAL_WIDTH = 4; @@ -129,6 +132,37 @@ public class NXDNTalkgroupFormatter extends AbstractIntegerFormatter } } + /** + * Combines the Type-D home repeater and radio or talkgroup into the over-the-air 16-bit identifier value. + * @param homeRepeater for the ID + * @param identifier radio or talkgroup value + * @return combined value + */ + public static int toTypeDValue(int homeRepeater, int identifier) + { + return (homeRepeater & TYPE_D_MAX_HOME_REPEATER) << TYPE_D_IDENTIFIER_BIT_WIDTH | (identifier & TYPE_D_MAX_IDENTIFIER); + } + + /** + * Home repeater from a combined Type-D identifier value + * @param value that combines the home repeater and the radio or talkgroup + * @return home repeater + */ + public static int getTypeDHomeRepeater(int value) + { + return (value >> TYPE_D_IDENTIFIER_BIT_WIDTH) & TYPE_D_MAX_HOME_REPEATER; + } + + /** + * Radio or talkgroup from a combined Type-D identifier value + * @param value that combines the home repeater and the radio or talkgroup + * @return radio or talkgroup, without the home repeater + */ + public static int getTypeDIdentifier(int value) + { + return value & TYPE_D_MAX_IDENTIFIER; + } + /** * Formats a Type-D identifier * @param homeRepeater for the ID diff --git a/src/test/java/io/github/dsheirer/alias/NXDNAliasTest.java b/src/test/java/io/github/dsheirer/alias/NXDNAliasTest.java new file mode 100644 index 00000000..49625ef5 --- /dev/null +++ b/src/test/java/io/github/dsheirer/alias/NXDNAliasTest.java @@ -0,0 +1,203 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2026 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** + */ + +package io.github.dsheirer.alias; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import io.github.dsheirer.alias.id.AliasID; +import io.github.dsheirer.alias.id.radio.NXDNTypeDRadio; +import io.github.dsheirer.alias.id.radio.NXDNTypeDRadioRange; +import io.github.dsheirer.alias.id.radio.Radio; +import io.github.dsheirer.alias.id.talkgroup.NXDNTypeDTalkgroup; +import io.github.dsheirer.alias.id.talkgroup.NXDNTypeDTalkgroupRange; +import io.github.dsheirer.alias.id.talkgroup.Talkgroup; +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.identifier.Role; +import io.github.dsheirer.module.decode.nxdn.identifier.NXDNRadioIdentifier; +import io.github.dsheirer.module.decode.nxdn.identifier.NXDNTalkgroupIdentifier; +import io.github.dsheirer.module.decode.nxdn.layer3.NXDNMessageType; +import io.github.dsheirer.module.decode.nxdn.layer3.call.RemoteControlRequest; +import io.github.dsheirer.protocol.Protocol; +import java.util.List; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class NXDNAliasTest +{ + private static final int HOME_REPEATER = 3; + private static final int RAW_TALKGROUP_03_0123 = 6267; + private static final int RAW_RADIO_03_0456 = 6600; + + private static Alias alias(AliasID id, AliasList list) + { + Alias alias = new Alias(); + alias.setName(id.toString()); + alias.addAliasID(id); + list.addAlias(alias); + return alias; + } + + private static void assertAlias(Alias expected, AliasList list, Identifier identifier) + { + assertEquals(List.of(expected), list.getAliases(identifier), "Alias for " + identifier); + } + + @Test + void standardIdentifiersStillMatch() + { + AliasList list = new AliasList("test"); + Alias talkgroup = alias(new Talkgroup(Protocol.NXDN, 123), list); + Alias radio = alias(new Radio(Protocol.NXDN, 456), list); + assertAlias(talkgroup, list, NXDNTalkgroupIdentifier.createTo(123)); + assertAlias(radio, list, NXDNRadioIdentifier.createFrom(456)); + } + + @Test + void typeDTalkgroupMatchesDisplayedRepeaterAndTalkgroup() + { + NXDNTypeDTalkgroup id = new NXDNTypeDTalkgroup(HOME_REPEATER, 123); + assertEquals(RAW_TALKGROUP_03_0123, id.getValue()); + assertEquals(HOME_REPEATER, id.getHomeRepeater()); + assertEquals(123, id.getIdentifier()); + assertTrue(id.isValid()); + + AliasList list = new AliasList("test"); + Alias alias = alias(id, list); + NXDNTalkgroupIdentifier identifier = NXDNTalkgroupIdentifier.createTypeDTo(RAW_TALKGROUP_03_0123); + assertEquals("03-0123", identifier.toString()); + assertAlias(alias, list, identifier); + assertTrue(list.getAliases(NXDNTalkgroupIdentifier.createTypeDTo(RAW_TALKGROUP_03_0123 + 2048)).isEmpty(), + "Same talkgroup on another home repeater must not match"); + } + + @Test + void typeDRadioMatchesDisplayedRepeaterAndRadio() + { + AliasList list = new AliasList("test"); + Alias alias = alias(new NXDNTypeDRadio(HOME_REPEATER, 456), list); + assertAlias(alias, list, NXDNRadioIdentifier.createTypeDFrom(RAW_RADIO_03_0456)); + assertAlias(alias, list, NXDNRadioIdentifier.createTypeDTo(RAW_RADIO_03_0456)); + } + + @Test + void typeDRangesMatchOnlyTheirHomeRepeater() + { + AliasList list = new AliasList("test"); + Alias talkgroups = alias(new NXDNTypeDTalkgroupRange(HOME_REPEATER, 100, 200), list); + Alias radios = alias(new NXDNTypeDRadioRange(HOME_REPEATER, 400, 500), list); + + assertAlias(talkgroups, list, NXDNTalkgroupIdentifier.createTypeDTo(RAW_TALKGROUP_03_0123)); + assertAlias(radios, list, NXDNRadioIdentifier.createTypeDFrom(RAW_RADIO_03_0456)); + assertTrue(list.getAliases(NXDNTalkgroupIdentifier.createTypeDTo((4 << 11) | 123)).isEmpty()); + assertTrue(list.getAliases(NXDNRadioIdentifier.createTypeDFrom((4 << 11) | 456)).isEmpty()); + } + + @Test + void typeDFieldsCanBeEditedIndependently() + { + NXDNTypeDTalkgroup talkgroup = new NXDNTypeDTalkgroup(); + assertEquals(Protocol.NXDN, talkgroup.getProtocol()); + talkgroup.setIdentifier(123); + talkgroup.setHomeRepeater(HOME_REPEATER); + assertEquals(RAW_TALKGROUP_03_0123, talkgroup.getValue()); + + NXDNTypeDRadioRange range = new NXDNTypeDRadioRange(); + range.setMinIdentifier(400); + range.setMaxIdentifier(500); + range.setHomeRepeater(HOME_REPEATER); + assertEquals(HOME_REPEATER, range.getHomeRepeater()); + assertEquals(400, range.getMinIdentifier()); + assertEquals(500, range.getMaxIdentifier()); + assertTrue(range.isValid()); + } + + @Test + void typeDRangeAcrossHomeRepeatersIsNotValid() + { + NXDNTypeDTalkgroupRange range = new NXDNTypeDTalkgroupRange(); + range.setMinTalkgroup((3 << 11) | 100); + range.setMaxTalkgroup((4 << 11) | 100); + assertFalse(range.isValid()); + } + + @Test + void copyRetainsTypeDIdentifierType() + { + for(AliasID id : List.of(new NXDNTypeDTalkgroup(HOME_REPEATER, 123), new NXDNTypeDRadio(HOME_REPEATER, 456), + new NXDNTypeDTalkgroupRange(HOME_REPEATER, 100, 200), new NXDNTypeDRadioRange(HOME_REPEATER, 400, 500))) + { + Alias original = new Alias(); + original.setName("original"); + original.addAliasID(id); + AliasID copy = AliasFactory.copyOf(original).getAliasIdentifiers().getFirst(); + assertNotSame(id, copy); + assertEquals(id.getClass(), copy.getClass()); + assertEquals(id.toString(), copy.toString()); + } + } + + @Test + void typeDIdentifiersRoundTripThroughPlaylistXml() throws Exception + { + XmlMapper mapper = new XmlMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + for(AliasID id : List.of(new NXDNTypeDTalkgroup(HOME_REPEATER, 123), new NXDNTypeDRadio(HOME_REPEATER, 456), + new NXDNTypeDTalkgroupRange(HOME_REPEATER, 100, 200), new NXDNTypeDRadioRange(HOME_REPEATER, 400, 500))) + { + String xml = mapper.writeValueAsString(id); + assertFalse(xml.contains("homeRepeater"), xml); + AliasID restored = mapper.readValue(xml, AliasID.class); + assertEquals(id.getClass(), restored.getClass(), xml); + assertEquals(id.toString(), restored.toString(), xml); + } + } + + @Test + void remoteControlDestinationWithLocationIdIsTheDestinationRadio() + { + int source = 456; + int destination = 200; + + //Location option values: 0=COMMON, 1=DESTINATION, 2=SOURCE + for(int locationOption = 0; locationOption < 3; locationOption++) + { + CorrectedBinaryMessage message = new CorrectedBinaryMessage(200); + message.load(8, 8, 0x40); //Call control option: has location ID + message.load(16, 3, 4); //Destination is a radio + message.load(24, 16, source); + message.load(40, 16, destination); + message.load(72, 2, locationOption); + + RemoteControlRequest request = new RemoteControlRequest(message, 1, NXDNMessageType.CONTROL_OUT_52_CC_REMOTE_CONTROL_REQUEST, 1, null); + + NXDNRadioIdentifier radio = assertInstanceOf(NXDNRadioIdentifier.class, request.getDestination()); + assertEquals(destination, radio.getValue(), "Location option " + locationOption); + assertEquals(Role.TO, radio.getRole(), "Location option " + locationOption); + assertEquals(source, request.getSource().getValue(), "Location option " + locationOption); + } + } +}