init
|
@ -0,0 +1,5 @@
|
|||
# yetanotheraprsc
|
||||
|
||||
_A git mirror of the YAAC SVN repository_
|
||||
|
||||
From https://sourceforge.net/projects/yetanotheraprsc/
|
|
@ -0,0 +1,11 @@
|
|||
#!/bin/bash
|
||||
svn checkout https://svn.code.sf.net/p/yetanotheraprsc/code/trunk yetanotheraprsc-code
|
||||
|
||||
wkhtmltopdf https://sourceforge.net/p/yetanotheraprsc/wiki/Building%20and%20Modifying%20YAAC/ "Wiki/Building and Modifying YAAC.pdf"
|
||||
wkhtmltopdf https://sourceforge.net/p/yetanotheraprsc/wiki/Downloading%20YAAC/ "Wiki/Downloading YAAC.pdf"
|
||||
wkhtmltopdf https://sourceforge.net/p/yetanotheraprsc/wiki/Home/ "Wiki/Home.pdf"
|
||||
wkhtmltopdf https://sourceforge.net/p/yetanotheraprsc/wiki/Running%20YAAC/ "Wiki/Running YAAC.pdf"
|
||||
wkhtmltopdf https://sourceforge.net/p/yetanotheraprsc/wiki/download%20maps/ "Wiki/Downloading OpenStreetMap Data for YAAC.pdf"
|
||||
wkhtmltopdf https://sourceforge.net/p/yetanotheraprsc/wiki/supported%20computer%20platforms%20for%20YAAC/ "Wiki/Supported Computer Platforms for YAAC.pdf"
|
||||
wkhtmltopdf https://sourceforge.net/p/yetanotheraprsc/wiki/supported%20hardware/ "Wiki/Supported Hardware for YAAC.pdf"
|
||||
wkhtmltopdf https://www.ka2ddo.org/ka2ddo/YAAC.html "Wiki/Welcome to YAAC.pdf"
|
|
@ -0,0 +1 @@
|
|||
12
|
|
@ -0,0 +1 @@
|
|||
12
|
|
@ -0,0 +1,38 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE index
|
||||
PUBLIC "-//Sun Microsystems Inc.//DTD JavaHelp Index Version 2.0//EN"
|
||||
"http://java.sun.com/products/javahelp/index_2_0.dtd">
|
||||
<!--
|
||||
Copyright (C) 2011-2020 Andrew Pavlin, KA2DDO
|
||||
This file is part of YAAC (Yet Another APRS Client).
|
||||
|
||||
YAAC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
YAAC 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
|
||||
and GNU Lesser General Public License along with YAAC. If not,
|
||||
see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<index version="2.0">
|
||||
<indexitem text="configuring YAAC">
|
||||
<indexitem text="small screen" target="configure.smallscreen"/>
|
||||
</indexitem>
|
||||
<indexitem text="first window">
|
||||
<indexitem text="small screen" target="menu.View.SmallScreen.first"/>
|
||||
</indexitem>
|
||||
<indexitem text="Mic-E">
|
||||
<indexitem text="changing Mic-E status" target="menu.View.SmallScreen.Status"/>
|
||||
</indexitem>
|
||||
<indexitem text="small screen" target="menu.View.SmallScreen.view">
|
||||
<indexitem text="configuring" target="configure.smallscreen"/>
|
||||
<indexitem text="viewing" target="menu.View.SmallScreen"/>
|
||||
</indexitem>
|
||||
</index>
|
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 700 B |
|
@ -0,0 +1,29 @@
|
|||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<title>Locate Landmark</title>
|
||||
</head>
|
||||
<body lang="en-US" dir="LTR">
|
||||
<h1>Locate Landmark</h1>
|
||||
<p>The Locate Landmarks menu choice allows you to find a named
|
||||
feature in the OpenStreetMap data on the map. A popup window is
|
||||
opened, displaying a text field for typing in a partial name string
|
||||
match (made case-insensitive); a Java compatible regular expression may
|
||||
also be used here.</p>
|
||||
<img src="./locatelandmark.png" alt="locate landmark dialog"/>
|
||||
<p>A second name string may be used to find intersections of two streets;
|
||||
you may also search for occurrences of bridges or tunnels.</p>
|
||||
<p>When the Enter key is pressed on the keyboard, all map features
|
||||
within the specified distance of the current center of the map will
|
||||
be checked to see if their name matches the specified match string.
|
||||
All matches will be displayed by name, distance from the map center,
|
||||
and type in a table in the dialog. Clicking on any row in the table
|
||||
will pan and zoom the map to display the matching feature in the
|
||||
center of the map.</p>
|
||||
<p>Changing the string in the text field and pressing Enter again
|
||||
will start another search. The search dialog can be closed with the
|
||||
operating system standard window closing mark on the dialog's title
|
||||
bar.</p>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,76 @@
|
|||
package org.ka2ddo.yaac.gui.io;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2011-2019 Andrew Pavlin, KA2DDO
|
||||
* This file is part of YAAC (Yet Another APRS Client).
|
||||
*
|
||||
* YAAC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* YAAC 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
|
||||
* and GNU Lesser General Public License along with YAAC. If not,
|
||||
* see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import org.ka2ddo.yaac.io.PortConfig;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.ItemEvent;
|
||||
import java.awt.event.ItemListener;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
/**
|
||||
* This class implements the code needed to create with port configuration widgets to select
|
||||
* AX.25 protocols should be permitted on a port.
|
||||
* @author Andrew Pavlin, KA2DDO
|
||||
*/
|
||||
public class ProtocolSelectionControls implements ItemListener {
|
||||
/**
|
||||
* Create a generic set of controls for enabling or disabling the protocols to be supported
|
||||
* over the asscoiated AX.25 port interface.
|
||||
* @param msgBundle ResourceBundle for obtaining localized label translations
|
||||
* @param cfg PortConfig.Cfg object to store protocol selection bits
|
||||
* @return JPanel loaded with controls
|
||||
*/
|
||||
public static JPanel createWidgets(ResourceBundle msgBundle, PortConfig.Cfg cfg) {
|
||||
JPanel p = new JPanel(new FlowLayout(FlowLayout.LEADING));
|
||||
p.add(new JLabel(msgBundle.getString("configure.tab.Ports.Protocols")));
|
||||
for (int bit = 0; bit <= PortConfig.MAX_PROTOCOL_BIT; bit++) {
|
||||
JCheckBox cb;
|
||||
int bitNum = 1 << bit;
|
||||
if (msgBundle.containsKey("configure.tab.Ports.Protocols." + bit)) {
|
||||
p.add(cb = new JCheckBox(msgBundle.getString("configure.tab.Ports.Protocols." + bit), (cfg.acceptableProtocolsMask & bitNum) != 0));
|
||||
cb.addItemListener(new ProtocolSelectionControls(cfg, bitNum));
|
||||
}
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
private final PortConfig.Cfg cfg;
|
||||
private final int bitMask;
|
||||
|
||||
private ProtocolSelectionControls(PortConfig.Cfg cfg, int bitMask) {
|
||||
this.cfg = cfg;
|
||||
this.bitMask = bitMask;
|
||||
}
|
||||
|
||||
/**
|
||||
* DO NOT CALL. Handle state changes of an associated JCheckBox for the protocol selection controls.
|
||||
* @param e ItemEvent identifying the checkbox
|
||||
*/
|
||||
public void itemStateChanged(ItemEvent e) {
|
||||
if (e.getStateChange() == ItemEvent.SELECTED) {
|
||||
cfg.acceptableProtocolsMask |= bitMask;
|
||||
} else {
|
||||
cfg.acceptableProtocolsMask &= ~bitMask;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,191 @@
|
|||
package org.ka2ddo.aprs;
|
||||
/*
|
||||
* Copyright (C) 2011-2019 Andrew Pavlin, KA2DDO
|
||||
* This file is part of YAAC (Yet Another APRS Client).
|
||||
*
|
||||
* YAAC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* YAAC 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
|
||||
* and GNU Lesser General Public License along with YAAC. If not,
|
||||
* see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import org.ka2ddo.util.DistanceUnit;
|
||||
import org.ka2ddo.util.EnumWithImageName;
|
||||
import org.ka2ddo.util.EnumWithUnits;
|
||||
import org.ka2ddo.util.PressureUnit;
|
||||
|
||||
/**
|
||||
* Symbolic codes for different types of hurricane/tropical storm information. The letter codes correspond to the
|
||||
* hurricane comments in APRS position reports, as defined in the APRS protocol specification, chapter 12.
|
||||
* @see PositionReport
|
||||
* @author Andrew Pavlin, KA2DDO
|
||||
*/
|
||||
public enum HurricaneEnum implements EnumWithImageName, EnumWithUnits {
|
||||
/**
|
||||
* Type of severe storm.
|
||||
* @see StormType
|
||||
*/
|
||||
STORM_TYPE(2, true, '\u007F', "images/stormtype.png", StormType.HC, 0.0F),
|
||||
/**
|
||||
* Sustained windspeed of storm in knots (nm/hr).
|
||||
* @see DistanceUnit#NM
|
||||
*/
|
||||
SUSTAINED_WIND(3, true, '\0', "images/windspeed.png", DistanceUnit.NM, true, 1.0F),
|
||||
/**
|
||||
* Peak gust speed of wind in storm in knots (nm/hr).
|
||||
* @see DistanceUnit#NM
|
||||
*/
|
||||
GUST(3, true, '^', "images/gust.png", DistanceUnit.NM, true, 1.0F),
|
||||
/**
|
||||
* Barometric pressure at center of storm in hectoPascals (millibars).
|
||||
* @see PressureUnit#HPa
|
||||
*/
|
||||
PRESSURE(4, true, '/', "images/pressure.png", PressureUnit.HPa, 0.1F),
|
||||
/**
|
||||
* Radius of hurricane winds in nautical miles.
|
||||
* @see DistanceUnit#NM
|
||||
*/
|
||||
RADIUS_HURRICANE(3, false, '>', "images/radius_hc.png", DistanceUnit.NM, 1.0F),
|
||||
/**
|
||||
* Radius of tropical-storm-level winds in nautical miles.
|
||||
* @see DistanceUnit#NM
|
||||
*/
|
||||
RADIUS_TROP_STORM(3, false, '&', "images/radius_ts.png", DistanceUnit.NM, 1.0F),
|
||||
/**
|
||||
* Radius of gale-force winds in nautical miles.
|
||||
* @see DistanceUnit#NM
|
||||
*/
|
||||
RADIUS_GALE(3, false, '%', "images/radius_gale.png", DistanceUnit.NM, 1.0F)
|
||||
;
|
||||
|
||||
/**
|
||||
* Number of characters for parameter identified by this enum.
|
||||
*/
|
||||
public final int paramLen;
|
||||
final boolean required;
|
||||
final char letter;
|
||||
final String imgName;
|
||||
final Enum<?> unit;
|
||||
final boolean isUnitARate;
|
||||
final float unitScaling;
|
||||
|
||||
/**
|
||||
* Values allowed for type of storm in a hurricane position report.
|
||||
*/
|
||||
public enum StormType {
|
||||
/**
|
||||
* StormType of Hurricane.
|
||||
*/
|
||||
HC,
|
||||
/**
|
||||
* StormType of Tropical Storm.
|
||||
*/
|
||||
TS,
|
||||
/**
|
||||
* StormType of Tropical Depression.
|
||||
*/
|
||||
TD,
|
||||
/**
|
||||
* StormType of Typhoon.
|
||||
*/
|
||||
TY,
|
||||
/**
|
||||
* StormType of Tropical Cyclone
|
||||
*/
|
||||
TC,
|
||||
/**
|
||||
* StormType of Tropical Low.
|
||||
*/
|
||||
TL
|
||||
}
|
||||
|
||||
private static final HurricaneEnum[] valuesArray = values();
|
||||
|
||||
private HurricaneEnum(int paramLen, boolean required, char letter, String imageFileName, Enum<?> unit, float unitScaling) {
|
||||
this.paramLen = paramLen;
|
||||
this.required = required;
|
||||
this.letter = letter;
|
||||
this.imgName = imageFileName;
|
||||
this.unit = unit;
|
||||
this.isUnitARate = false;
|
||||
this.unitScaling = unitScaling;
|
||||
}
|
||||
|
||||
private HurricaneEnum(int paramLen, boolean required, char letter, String imageFileName, Enum<?> unit, boolean isUnitARate, float unitScaling) {
|
||||
this.paramLen = paramLen;
|
||||
this.required = required;
|
||||
this.letter = letter;
|
||||
this.imgName = imageFileName;
|
||||
this.unit = unit;
|
||||
this.isUnitARate = isUnitARate;
|
||||
this.unitScaling = unitScaling;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the relative path name of the image file for this enum value.
|
||||
*
|
||||
* @return path name String, or a two-character APRS symbol table ID and code
|
||||
*/
|
||||
public String getImagePath() {
|
||||
return imgName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the enumeration associated with the hurricane message letter code.
|
||||
* @param ch letter to match against the known enums
|
||||
* @return HurricaneEnum corresponding to the letter, or null if not matching any known hurricane parameter
|
||||
*/
|
||||
public static HurricaneEnum getEnumForLetter(char ch) {
|
||||
for (HurricaneEnum e : valuesArray) {
|
||||
if (ch == e.letter) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the unit (if any) for values for this WeatherEnum,
|
||||
* @return unit Enum, or null if no unit known
|
||||
*/
|
||||
public Enum<?> getUnit() {
|
||||
return unit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate whether values and units should be interpreted as a rate over an appropriate time unit
|
||||
* rather than a fixed quantity.
|
||||
*
|
||||
* @return boolean true if unit is a rate rather than a scalar
|
||||
*/
|
||||
public boolean isUnitARate() {
|
||||
return isUnitARate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the scaling factor to apply to the unit for values for this WeatherEnum.
|
||||
* @return float scale factor to apply to unit for displaying value, or Float.NaN if no unit
|
||||
*/
|
||||
public float getUnitScaling() {
|
||||
return unitScaling;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a single copy of the list of WeatherEnums. Callers should ensure they do not
|
||||
* modify the returned array, as this could have unpredictable consequences to other users
|
||||
* of the array.
|
||||
* @return array of defined WeatherEnum values in enumerated order
|
||||
*/
|
||||
public static HurricaneEnum[] nonClonedValues() {
|
||||
return valuesArray;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
package org.ka2ddo.yaac.osm;
|
||||
/*
|
||||
* Copyright (C) 2011-2020 Andrew Pavlin, KA2DDO
|
||||
* This file is part of YAAC (Yet Another APRS Client).
|
||||
*
|
||||
* YAAC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* YAAC 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
|
||||
* and GNU Lesser General Public License along with YAAC. If not,
|
||||
* see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* Supported types of the OSM waterway="*" attribute.
|
||||
* See the <a href="http://wiki.openstreetmap.org/wiki/Key:waterway" target="OSM">definition of waterway</a>
|
||||
* on the OpenStreetMap wiki.
|
||||
*/
|
||||
public enum Waterway implements AmenityOrWay, EnumMayHaveFlags {
|
||||
user_defined(WayType.WWY_UNKNOWN),
|
||||
stream(WayType.WWY_STREAM),
|
||||
river(WayType.WWY_RIVER),
|
||||
riverbank(WayType.WWY_RIVERBANK),
|
||||
canal(WayType.WWY_CANAL),
|
||||
ditch(WayType.WWY_DITCH),
|
||||
drain(WayType.WWY_DITCH),
|
||||
rapids(WayType.WWY_RAPIDS),
|
||||
waterfall(WayType.WWY_WATERFALL),
|
||||
dam(AmenityType.dam),
|
||||
weir(AmenityType.dam);
|
||||
|
||||
private final WayType wayType;
|
||||
private final AmenityType amenityType;
|
||||
private final short flagMask;
|
||||
|
||||
private Waterway(WayType wayType) {
|
||||
this.wayType = wayType;
|
||||
this.amenityType = null;
|
||||
this.flagMask = GenericTaggedNode.IS_WATER;
|
||||
}
|
||||
|
||||
private Waterway(WayType wayType, short extraFlags) {
|
||||
this.wayType = wayType;
|
||||
this.amenityType = null;
|
||||
this.flagMask = (short)(extraFlags | GenericTaggedNode.IS_WATER);
|
||||
}
|
||||
|
||||
private Waterway(AmenityType amenityType) {
|
||||
this.wayType = null;
|
||||
this.amenityType = amenityType;
|
||||
this.flagMask = GenericTaggedNode.IS_WATER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Report the WayType for rendering this type of Way.
|
||||
* @return WayType
|
||||
*/
|
||||
public WayType wayType() {
|
||||
return wayType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get amenity type associated with this enumeration; only for types that are amenities.
|
||||
*
|
||||
* @return AmenityType for this type, or null if not an amenity
|
||||
*/
|
||||
public AmenityType getAmenityType() {
|
||||
return amenityType;
|
||||
}
|
||||
|
||||
private static HashMap<String,Waterway> enumStringToEnumMap = null;
|
||||
|
||||
/**
|
||||
* Get the enum value associated with the string name specified.
|
||||
* @param name String to convert into an Enum
|
||||
* @return Waterway for the string name, or null if no match
|
||||
*/
|
||||
public static Waterway value(String name) {
|
||||
HashMap<String,Waterway> map;
|
||||
if ((map = enumStringToEnumMap) == null) {
|
||||
// first call, so initialize map
|
||||
map = enumStringToEnumMap = new HashMap<String,Waterway>();
|
||||
for (Waterway e : values()) {
|
||||
map.put(e.name(), e);
|
||||
}
|
||||
}
|
||||
return map.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates any flag bits implied by this enum type.
|
||||
*
|
||||
* @return bitmask of flags if this enum implies flags
|
||||
*/
|
||||
@Override
|
||||
public short getFlagMask() {
|
||||
return flagMask;
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 497 B |
After Width: | Height: | Size: 428 B |
|
@ -0,0 +1,51 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE helpset
|
||||
PUBLIC "-//Sun Microsystems Inc.//DTD JavaHelp HelpSet Version 2.0//EN"
|
||||
"http://java.sun.com/products/javahelp/helpset_2_0.dtd">
|
||||
|
||||
<helpset version="2.0">
|
||||
|
||||
<!-- title -->
|
||||
<title>YAAC Legacy RXTX Library Support</title>
|
||||
|
||||
<!-- maps -->
|
||||
<maps>
|
||||
<homeID>legacyrxtx</homeID>
|
||||
<mapref location="LegacyRxtxHelpMap.jhm"/>
|
||||
</maps>
|
||||
|
||||
<!-- views -->
|
||||
<view mergetype="javax.help.UniteAppendMerge">
|
||||
<name>TOC</name>
|
||||
<label>Table Of Contents</label>
|
||||
<type>javax.help.TOCView</type>
|
||||
<data>LegacyRxtxHelpTOC.xml</data>
|
||||
</view>
|
||||
|
||||
<view mergetype="javax.help.SortMerge">
|
||||
<name>Index</name>
|
||||
<label>Index</label>
|
||||
<type>javax.help.IndexView</type>
|
||||
<data>LegacyRxtxHelpIndex.xml</data>
|
||||
</view>
|
||||
|
||||
<presentation xml:lang="en" default="true" displayviewimages="false">
|
||||
<name>main window</name>
|
||||
<size width="700" height="400" />
|
||||
<location x="200" y="200" />
|
||||
<title>YAAC - Online Help</title>
|
||||
<image>icon</image>
|
||||
<toolbar>
|
||||
<helpaction>javax.help.BackAction</helpaction>
|
||||
<helpaction>javax.help.ForwardAction</helpaction>
|
||||
<helpaction>javax.help.SeparatorAction</helpaction>
|
||||
<helpaction>javax.help.HomeAction</helpaction>
|
||||
<helpaction>javax.help.ReloadAction</helpaction>
|
||||
<helpaction>javax.help.SeparatorAction</helpaction>
|
||||
<helpaction>javax.help.PrintAction</helpaction>
|
||||
<helpaction>javax.help.PrintSetupAction</helpaction>
|
||||
<helpaction>javax.help.SeparatorAction</helpaction>
|
||||
<helpaction>javax.help.FavoritesAction</helpaction>
|
||||
</toolbar>
|
||||
</presentation>
|
||||
</helpset>
|
|
@ -0,0 +1,146 @@
|
|||
package org.ka2ddo.yaac.smallscreen.gui;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2011-2020 Andrew Pavlin, KA2DDO
|
||||
* This file is part of YAAC.
|
||||
*
|
||||
* YAAC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* YAAC 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
|
||||
* and GNU Lesser General Public License along with YAAC. If not,
|
||||
* see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import org.ka2ddo.ax25.Scope;
|
||||
import org.ka2ddo.yaac.YAAC;
|
||||
import org.ka2ddo.yaac.ax25.Age;
|
||||
import org.ka2ddo.yaac.ax25.StationState;
|
||||
import org.ka2ddo.yaac.ax25.StationTracker;
|
||||
import org.ka2ddo.yaac.gui.MainGui;
|
||||
import org.ka2ddo.yaac.gui.NullableBooleanRenderer;
|
||||
import org.ka2ddo.yaac.gui.SelectableSymbol;
|
||||
import org.ka2ddo.yaac.gui.StationListTableModel;
|
||||
import org.ka2ddo.yaac.gui.TablePersister;
|
||||
import org.ka2ddo.yaac.gui.help.HelpAdapter;
|
||||
import org.ka2ddo.yaac.gui.table.AgeRenderer;
|
||||
import org.ka2ddo.yaac.gui.table.EnumCellRenderer;
|
||||
import org.ka2ddo.yaac.gui.table.FastTableRowSorter;
|
||||
import org.ka2ddo.yaac.gui.table.PrintableTable;
|
||||
import org.ka2ddo.yaac.gui.table.SelectableSymbolEditor;
|
||||
import org.ka2ddo.yaac.gui.table.SelectableSymbolRenderer;
|
||||
import org.ka2ddo.yaac.gui.table.StringCellRenderer;
|
||||
import org.ka2ddo.yaac.pluginapi.GuiContentType;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.ListSelectionEvent;
|
||||
import javax.swing.event.ListSelectionListener;
|
||||
import javax.swing.table.TableColumnModel;
|
||||
import javax.swing.table.TableModel;
|
||||
import java.awt.*;
|
||||
import java.awt.event.InputEvent;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import static org.ka2ddo.yaac.gui.StationListViewer.DEFAULT_SORT_KEYS;
|
||||
|
||||
/**
|
||||
* This class provides a compact version of the Station/Object list.
|
||||
*/
|
||||
public class StationList extends JScrollPane implements HasMenuToggleButton {
|
||||
int selectedObjectRow = -1;
|
||||
private JToggleButton tbShowMenu;
|
||||
|
||||
/**
|
||||
* Create a panel for displaying the currently known stations on a small screen.
|
||||
* @param smallScreen SmallScreen that wll contain this panel
|
||||
*/
|
||||
public StationList(SmallScreen smallScreen) {
|
||||
setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
|
||||
final StationListTableModel tm = new StationListTableModel(YAAC.getMsgBundle(), false);
|
||||
final PrintableTable table = new PrintableTable(tm, YAAC.getMsg("StationList.printTitle"));
|
||||
FastTableRowSorter<TableModel> sorter = new FastTableRowSorter<TableModel>(tm);
|
||||
sorter.setSortKeys(DEFAULT_SORT_KEYS);
|
||||
table.setRowSorter(sorter);
|
||||
table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
|
||||
table.setPreferredScrollableViewportSize(new Dimension(500, 250));
|
||||
table.setDefaultRenderer(Age.class, new AgeRenderer());
|
||||
table.setDefaultRenderer(Boolean.class, new NullableBooleanRenderer());
|
||||
table.setDefaultRenderer(String.class, new StringCellRenderer(null));
|
||||
table.setDefaultRenderer(Scope.class, new EnumCellRenderer());
|
||||
table.setDefaultRenderer(SelectableSymbol.class, new SelectableSymbolRenderer());
|
||||
table.setDefaultEditor(SelectableSymbol.class, SelectableSymbolEditor.createSelectableSymbolEditor());
|
||||
table.setUpdateSelectionOnSort(true);
|
||||
TableColumnModel tcm = table.getColumnModel();
|
||||
for (int col = 0; col < tm.getColumnCount(); col++) {
|
||||
int preferredWidth = tm.getColumnWidth(col);
|
||||
if (preferredWidth > -1) {
|
||||
tcm.getColumn(col).setPreferredWidth(preferredWidth);
|
||||
}
|
||||
}
|
||||
table.setColumnSelectionAllowed(true);
|
||||
table.setRowSelectionAllowed(true);
|
||||
table.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
|
||||
public void valueChanged(ListSelectionEvent e) {
|
||||
selectedObjectRow = table.getSelectedRow();
|
||||
}
|
||||
});
|
||||
table.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
if (e.getClickCount() > 1) {
|
||||
String objName = (String) tm.getValueAt(table.convertRowIndexToModel(selectedObjectRow), 0);
|
||||
StationState ss = StationTracker.getInstance().getTrackedObject(objName);
|
||||
if (ss != null) {
|
||||
// just center and zoom to the target
|
||||
YAAC.getGui().displayAttentionAlert(ss);
|
||||
}
|
||||
} else if (e.getButton() == MouseEvent.BUTTON3 ||
|
||||
(e.getButton() == MouseEvent.BUTTON1 && (e.getModifiersEx() & InputEvent.ALT_DOWN_MASK) != 0)) {
|
||||
Point p = e.getPoint();
|
||||
int row = table.rowAtPoint(p);
|
||||
int col = table.columnAtPoint(p);
|
||||
if (row >= 0 && col >= 0 && !tm.isCellEditable(row, col)) {
|
||||
row = table.convertRowIndexToModel(row);
|
||||
StationState ss = tm.getSortedRow(row);
|
||||
ArrayList<StationState> ssList = new ArrayList<StationState>(1);
|
||||
ssList.add(ss);
|
||||
JPopupMenu popup = MainGui.buildPopupMenu(p.x, p.y, null, ssList, GuiContentType.STATIONS);
|
||||
// display the popup menu
|
||||
if (popup != null) {
|
||||
popup.show((Component) e.getSource(), e.getX(), e.getY());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
table.addPropertyChangeListener("tableCellEditor", tm); // use this to stop async model updates when cell is being edited
|
||||
TablePersister.getInstance().register(table, "StationList");
|
||||
setViewportView(table);
|
||||
setCorner(JScrollPane.UPPER_TRAILING_CORNER, tbShowMenu = smallScreen.createMenuDisplayToggleButton());
|
||||
HelpAdapter.setHelpTag(this, "menu.View.SmallScreen.Map");
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the state of this panel's menu toggle button to the specified state. If the
|
||||
* state of the panel's toggle button already matches the specified state, do nothing
|
||||
* (to avoid spurious event triggering).
|
||||
*
|
||||
* @param isShowing boolean true if button should be selected because menu is
|
||||
* being displayed, false if not
|
||||
*/
|
||||
public void setMenuToggleButton(boolean isShowing) {
|
||||
if (tbShowMenu.isSelected() != isShowing) {
|
||||
tbShowMenu.setSelected(isShowing);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,204 @@
|
|||
package org.ka2ddo.yaac.io;
|
||||
/*
|
||||
* Copyright (C) 2011-2018 Andrew Pavlin, KA2DDO
|
||||
* This file is part of YAAC (Yet Another APRS Client).
|
||||
*
|
||||
* YAAC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* YAAC 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
|
||||
* and GNU Lesser General Public License along with YAAC. If not,
|
||||
* see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import java.io.DataInput;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* This class provides an adapter to convert an arbitrary DataInput implementer into a
|
||||
* sequential input stream.
|
||||
* @author Andrew Pavlin, KA2DDO
|
||||
*/
|
||||
public class ArbitraryDataInputStream extends InputStream {
|
||||
private DataInput dataInput;
|
||||
|
||||
/**
|
||||
* Create an new ArbitraryDataInputStream wrapper around the specified DataInput.
|
||||
* Note that one could do this to a DataInputStream, but it would be redundant.
|
||||
* @param dataInput dataInput implementor to wrap
|
||||
*/
|
||||
public ArbitraryDataInputStream(DataInput dataInput) {
|
||||
this.dataInput = dataInput;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the next byte of data from the input stream. The value byte is
|
||||
* returned as an <code>int</code> in the range <code>0</code> to
|
||||
* <code>255</code>. If no byte is available because the end of the stream
|
||||
* has been reached, the value <code>-1</code> is returned. This method
|
||||
* blocks until input data is available, the end of the stream is detected,
|
||||
* or an exception is thrown.
|
||||
* <p> A subclass must provide an implementation of this method.
|
||||
*
|
||||
* @return the next byte of data, or <code>-1</code> if the end of the
|
||||
* stream is reached.
|
||||
* @throws java.io.IOException if an I/O error occurs.
|
||||
*/
|
||||
public int read() throws IOException {
|
||||
return dataInput.readUnsignedByte();
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the current position in this input stream. A subsequent call to
|
||||
* the <code>reset</code> method repositions this stream at the last marked
|
||||
* position so that subsequent reads re-read the same bytes.
|
||||
* <p> The <code>readlimit</code> arguments tells this input stream to
|
||||
* allow that many bytes to be read before the mark position gets
|
||||
* invalidated.
|
||||
* </p>
|
||||
* <p> The general contract of <code>mark</code> is that, if the method
|
||||
* <code>markSupported</code> returns <code>true</code>, the stream somehow
|
||||
* remembers all the bytes read after the call to <code>mark</code> and
|
||||
* stands ready to supply those same bytes again if and whenever the method
|
||||
* <code>reset</code> is called. However, the stream is not required to
|
||||
* remember any data at all if more than <code>readlimit</code> bytes are
|
||||
* read from the stream before <code>reset</code> is called.
|
||||
* </p>
|
||||
* <p> Marking a closed stream should not have any effect on the stream.
|
||||
* </p>
|
||||
* <p> The <code>mark</code> method of <code>InputStream</code> does
|
||||
* nothing.
|
||||
*
|
||||
* @param readlimit the maximum limit of bytes that can be read before
|
||||
* the mark position becomes invalid.
|
||||
* @see java.io.InputStream#reset()
|
||||
*/
|
||||
@Override
|
||||
public synchronized void mark(int readlimit) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if this input stream supports the <code>mark</code> and
|
||||
* <code>reset</code> methods. Whether or not <code>mark</code> and
|
||||
* <code>reset</code> are supported is an invariant property of a
|
||||
* particular input stream instance. The <code>markSupported</code> method
|
||||
* of <code>InputStream</code> returns <code>false</code>.
|
||||
*
|
||||
* @return <code>true</code> if this stream instance supports the mark
|
||||
* and reset methods; <code>false</code> otherwise.
|
||||
* @see java.io.InputStream#mark(int)
|
||||
* @see java.io.InputStream#reset()
|
||||
*/
|
||||
@Override
|
||||
public boolean markSupported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads some number of bytes from the input stream and stores them into
|
||||
* the buffer array <code>b</code>. The number of bytes actually read is
|
||||
* returned as an integer. This method blocks until input data is
|
||||
* available, end of file is detected, or an exception is thrown.
|
||||
* <p> If the length of <code>b</code> is zero, then no bytes are read and
|
||||
* <code>0</code> is returned; otherwise, there is an attempt to read at
|
||||
* least one byte. If no byte is available because the stream is at the
|
||||
* end of the file, the value <code>-1</code> is returned; otherwise, at
|
||||
* least one byte is read and stored into <code>b</code>.
|
||||
* </p>
|
||||
* <p> The first byte read is stored into element <code>b[0]</code>, the
|
||||
* next one into <code>b[1]</code>, and so on. The number of bytes read is,
|
||||
* at most, equal to the length of <code>b</code>. Let <i>k</i> be the
|
||||
* number of bytes actually read; these bytes will be stored in elements
|
||||
* <code>b[0]</code> through <code>b[</code><i>k</i><code>-1]</code>,
|
||||
* leaving elements <code>b[</code><i>k</i><code>]</code> through
|
||||
* <code>b[b.length-1]</code> unaffected.
|
||||
* </p>
|
||||
* <p> The <code>read(b)</code> method for class <code>InputStream</code>
|
||||
* has the same effect as: <pre><code> read(b, 0, b.length) </code></pre>
|
||||
*
|
||||
* @param b the buffer into which the data is read.
|
||||
* @return the total number of bytes read into the buffer, or
|
||||
* <code>-1</code> is there is no more data because the end of
|
||||
* the stream has been reached.
|
||||
* @throws java.io.IOException If the first byte cannot be read for any reason
|
||||
* other than the end of the file, if the input stream has been closed, or
|
||||
* if some other I/O error occurs.
|
||||
* @throws NullPointerException if <code>b</code> is <code>null</code>.
|
||||
* @see java.io.InputStream#read(byte[], int, int)
|
||||
*/
|
||||
@Override
|
||||
public int read(byte[] b) throws IOException {
|
||||
dataInput.readFully(b);
|
||||
return b.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads up to <code>len</code> bytes of data from the input stream into
|
||||
* an array of bytes. An attempt is made to read as many as
|
||||
* <code>len</code> bytes, but a smaller number may be read.
|
||||
* The number of bytes actually read is returned as an integer.
|
||||
* <p> This method blocks until input data is available, end of file is
|
||||
* detected, or an exception is thrown.
|
||||
* </p>
|
||||
* <p> If <code>len</code> is zero, then no bytes are read and
|
||||
* <code>0</code> is returned; otherwise, there is an attempt to read at
|
||||
* least one byte. If no byte is available because the stream is at end of
|
||||
* file, the value <code>-1</code> is returned; otherwise, at least one
|
||||
* byte is read and stored into <code>b</code>.
|
||||
* </p>
|
||||
* <p> The first byte read is stored into element <code>b[off]</code>, the
|
||||
* next one into <code>b[off+1]</code>, and so on. The number of bytes read
|
||||
* is, at most, equal to <code>len</code>. Let <i>k</i> be the number of
|
||||
* bytes actually read; these bytes will be stored in elements
|
||||
* <code>b[off]</code> through <code>b[off+</code><i>k</i><code>-1]</code>,
|
||||
* leaving elements <code>b[off+</code><i>k</i><code>]</code> through
|
||||
* <code>b[off+len-1]</code> unaffected.
|
||||
* </p>
|
||||
* <p> In every case, elements <code>b[0]</code> through
|
||||
* <code>b[off]</code> and elements <code>b[off+len]</code> through
|
||||
* <code>b[b.length-1]</code> are unaffected.
|
||||
* </p>
|
||||
* <p> The <code>read(b,</code> <code>off,</code> <code>len)</code> method
|
||||
* for class <code>InputStream</code> simply calls the method
|
||||
* <code>read()</code> repeatedly. If the first such call results in an
|
||||
* <code>IOException</code>, that exception is returned from the call to
|
||||
* the <code>read(b,</code> <code>off,</code> <code>len)</code> method. If
|
||||
* any subsequent call to <code>read()</code> results in a
|
||||
* <code>IOException</code>, the exception is caught and treated as if it
|
||||
* were end of file; the bytes read up to that point are stored into
|
||||
* <code>b</code> and the number of bytes read before the exception
|
||||
* occurred is returned. The default implementation of this method blocks
|
||||
* until the requested amount of input data <code>len</code> has been read,
|
||||
* end of file is detected, or an exception is thrown. Subclasses are encouraged
|
||||
* to provide a more efficient implementation of this method.
|
||||
*
|
||||
* @param b the buffer into which the data is read.
|
||||
* @param off the start offset in array <code>b</code>
|
||||
* at which the data is written.
|
||||
* @param len the maximum number of bytes to read.
|
||||
* @return the total number of bytes read into the buffer, or
|
||||
* <code>-1</code> if there is no more data because the end of
|
||||
* the stream has been reached.
|
||||
* @throws java.io.IOException If the first byte cannot be read for any reason
|
||||
* other than end of file, or if the input stream has been closed, or if
|
||||
* some other I/O error occurs.
|
||||
* @throws NullPointerException If <code>b</code> is <code>null</code>.
|
||||
* @throws IndexOutOfBoundsException If <code>off</code> is negative,
|
||||
* <code>len</code> is negative, or <code>len</code> is greater than
|
||||
* <code>b.length - off</code>
|
||||
* @see java.io.InputStream#read()
|
||||
*/
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
dataInput.readFully(b, off, len);
|
||||
return len;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,168 @@
|
|||
package org.ka2ddo.yaac.repeaterfinder.gui;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2011-2021 Andrew Pavlin, KA2DDO
|
||||
* This file is part of YAAC.
|
||||
*
|
||||
* YAAC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* YAAC 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
|
||||
* and GNU Lesser General Public License along with YAAC. If not,
|
||||
* see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import org.ka2ddo.yaac.YAAC;
|
||||
import org.ka2ddo.yaac.gps.GPSDistributor;
|
||||
import org.ka2ddo.yaac.gui.FirstWindowInitIfc;
|
||||
import org.ka2ddo.yaac.gui.FontCache;
|
||||
import org.ka2ddo.yaac.gui.FontChangingMenuBar;
|
||||
import org.ka2ddo.yaac.gui.MainGui;
|
||||
import org.ka2ddo.yaac.gui.WindowPersister;
|
||||
import org.ka2ddo.yaac.repeaterfinder.RepeaterFinderProvider;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.awt.event.WindowListener;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
/**
|
||||
* This frame wraps the {@link RepeaterView} pane, so that the pane can also be used in
|
||||
* the SmallScreen plugin.
|
||||
* @author Andrew Pavlin, KA2DDO
|
||||
*/
|
||||
public class RepeaterFrame extends JFrame implements WindowListener, FirstWindowInitIfc {
|
||||
private final RepeaterView repeaterView;
|
||||
/**
|
||||
* Create a RepeaterView based on the current GUI core.
|
||||
*/
|
||||
public RepeaterFrame() {
|
||||
this((MainGui) YAAC.getGui());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a RepeaterView based on the specified GUI core.
|
||||
*
|
||||
* @param gui MainGui to use for service functions
|
||||
*/
|
||||
public RepeaterFrame(MainGui gui) {
|
||||
final ResourceBundle msgBundle = YAAC.getMsgBundle(RepeaterView.BUNDLE_NAME);
|
||||
String title = msgBundle.getString("title");
|
||||
setTitle(title);
|
||||
setIconImage(gui.getIconImage());
|
||||
setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
|
||||
setJMenuBar(new FontChangingMenuBar());
|
||||
FontCache.setupJFrameForMenuFonts(this);
|
||||
setContentPane(repeaterView = new RepeaterView());
|
||||
|
||||
addWindowListener(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 2 of initializing the GUI (stuff that needs the GUI property of the main YAAC
|
||||
* object to be already initialized).
|
||||
*/
|
||||
public void initMenuBar() {
|
||||
MainGui.buildMenus(YAAC.getMenuBarActionList(), getJMenuBar(), this);
|
||||
pack();
|
||||
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
|
||||
Dimension frameSize = getSize();
|
||||
if (screenSize.getWidth() < frameSize.getWidth() ||
|
||||
screenSize.getHeight() < frameSize.getHeight()) {
|
||||
System.out.println("squeezed window size from " + frameSize + " to " + screenSize);
|
||||
setUndecorated(true);
|
||||
getGraphicsConfiguration().getDevice().setFullScreenWindow(this);
|
||||
getRootPane().revalidate();
|
||||
}
|
||||
repeaterView.initMenuBar();
|
||||
WindowPersister.getInstance().register(this, getTitle());
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked the first time a window is made visible.
|
||||
*/
|
||||
public void windowOpened(WindowEvent e) {
|
||||
repeaterView.windowOpened(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when the user attempts to close the window
|
||||
* from the window's system menu.
|
||||
*/
|
||||
public void windowClosing(WindowEvent e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a window has been closed as the result
|
||||
* of calling dispose on the window.
|
||||
*/
|
||||
public void windowClosed(WindowEvent e) {
|
||||
repeaterView.windowClosed(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a window is changed from a normal to a
|
||||
* minimized state. For many platforms, a minimized window
|
||||
* is displayed as the icon specified in the window's
|
||||
* iconImage property.
|
||||
*
|
||||
* @see java.awt.Frame#setIconImage
|
||||
*/
|
||||
public void windowIconified(WindowEvent e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a window is changed from a minimized
|
||||
* to a normal state.
|
||||
*/
|
||||
public void windowDeiconified(WindowEvent e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when the Window is set to be the active Window. Only a Frame or
|
||||
* a Dialog can be the active Window. The native windowing system may
|
||||
* denote the active Window or its children with special decorations, such
|
||||
* as a highlighted title bar. The active Window is always either the
|
||||
* focused Window, or the first Frame or Dialog that is an owner of the
|
||||
* focused Window.
|
||||
*/
|
||||
public void windowActivated(WindowEvent e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a Window is no longer the active Window. Only a Frame or a
|
||||
* Dialog can be the active Window. The native windowing system may denote
|
||||
* the active Window or its children with special decorations, such as a
|
||||
* highlighted title bar. The active Window is always either the focused
|
||||
* Window, or the first Frame or Dialog that is an owner of the focused
|
||||
* Window.
|
||||
*/
|
||||
public void windowDeactivated(WindowEvent e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases all of the native screen resources used by this
|
||||
* <code>Window</code>, its subcomponents, and all of its owned
|
||||
* children. That is, the resources for these <code>Component</code>s
|
||||
* will be destroyed, any memory they consume will be returned to the
|
||||
* OS, and they will be marked as undisplayable.
|
||||
*/
|
||||
@Override
|
||||
public void dispose() {
|
||||
repeaterView.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package org.ka2ddo.yaac.core;
|
||||
/*
|
||||
* Copyright (C) 2011-2012 Andrew Pavlin, KA2DDO
|
||||
* This file is part of YAAC (Yet Another APRS Client).
|
||||
*
|
||||
* YAAC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* YAAC 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
|
||||
* and GNU Lesser General Public License along with YAAC. If not,
|
||||
* see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This exception is thrown to indicate that a ProgressDialog's cancel button has been pressed.
|
||||
* @see org.ka2ddo.yaac.gui.ProgressDialog
|
||||
* @see org.ka2ddo.yaac.core.StatusListener
|
||||
*/
|
||||
public class UserAbort extends RuntimeException {
|
||||
/**
|
||||
* Constructs a new runtime exception with <code>null</code> as its
|
||||
* detail message. The cause is not initialized, and may subsequently be
|
||||
* initialized by a call to {@link #initCause}.
|
||||
*/
|
||||
public UserAbort() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new runtime exception with the specified detail message.
|
||||
* The cause is not initialized, and may subsequently be initialized by a
|
||||
* call to {@link #initCause}.
|
||||
*
|
||||
* @param message the detail message. The detail message is saved for
|
||||
* later retrieval by the {@link #getMessage()} method.
|
||||
*/
|
||||
public UserAbort(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
package org.ka2ddo.yaac.io;
|
||||
/*
|
||||
* Copyright (C) 2011-2020 Andrew Pavlin, KA2DDO
|
||||
* This file is part of YAAC (Yet Another APRS Client).
|
||||
*
|
||||
* YAAC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* YAAC 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
|
||||
* and GNU Lesser General Public License along with YAAC. If not,
|
||||
* see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* InputStream to count the number of bytes read from the input, so as to drive
|
||||
* a progress bar, but it avoids using the read() method of the passed-in
|
||||
* InputStream, as SocketInputStream malloc's a 1-byte byte array for each such read,
|
||||
* then throws the array away.
|
||||
* @author Andrew Pavlin, KA2DDO
|
||||
*/
|
||||
final public class CountingInputStreamForSockets extends java.io.FilterInputStream {
|
||||
/**
|
||||
* The byte count.
|
||||
*/
|
||||
private long byteCount = 0L;
|
||||
/**
|
||||
* Temporary buffer array.
|
||||
*/
|
||||
private byte[] buf = new byte[1];
|
||||
|
||||
/**
|
||||
* Create a CountingInputStream wrapped around the specified InputStream.
|
||||
*
|
||||
* @param in the InputStream to count.
|
||||
*/
|
||||
public CountingInputStreamForSockets(java.io.InputStream in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the markSupported operation.
|
||||
*/
|
||||
public boolean markSupported() {
|
||||
// Keep from marking so the byte count isn't corrupted.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override mark to do nothing.
|
||||
*/
|
||||
public void mark(int readlimit) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Override reset to reset the byte count.
|
||||
*/
|
||||
public void reset() {
|
||||
byteCount = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a byte.
|
||||
*/
|
||||
public final int read() throws java.io.IOException {
|
||||
int answer;
|
||||
if (in.read(buf, 0, 1) > 0) {
|
||||
byteCount++;
|
||||
return buf[0] & 0xFF;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read an array of bytes.
|
||||
*/
|
||||
public final int read(byte[] b) throws java.io.IOException {
|
||||
int answer;
|
||||
if ((answer = in.read(b, 0, b.length)) > 0) {
|
||||
byteCount += answer;
|
||||
}
|
||||
return answer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read bytes into an offset array.
|
||||
*/
|
||||
public final int read(byte[] b, int off, int len) throws java.io.IOException {
|
||||
int answer;
|
||||
if ((answer = in.read(b, off, len)) > 0) {
|
||||
byteCount += answer;
|
||||
}
|
||||
return answer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip some bytes.
|
||||
*/
|
||||
public final long skip(long n) throws java.io.IOException {
|
||||
long answer;
|
||||
if ((answer = in.skip(n)) > 0) {
|
||||
byteCount += answer;
|
||||
}
|
||||
return answer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of bytes passed through this stream (so far).
|
||||
* @return the number of read bytes so far.
|
||||
*/
|
||||
public final long getByteCount() {
|
||||
return byteCount;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,276 @@
|
|||
package org.ka2ddo.yaac.io;
|
||||
/*
|
||||
* Copyright (C) 2011-2021 Andrew Pavlin, KA2DDO
|
||||
* This file is part of YAAC (Yet Another APRS Client).
|
||||
*
|
||||
* YAAC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* YAAC 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
|
||||
* and GNU Lesser General Public License along with YAAC. If not,
|
||||
* see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import org.ka2ddo.ax25.Connector;
|
||||
import org.ka2ddo.util.DebugCtl;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.UnknownServiceException;
|
||||
import java.util.*;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* This class defines the generic API for an object that connects a bidirectional
|
||||
* real-time data stream to this application.
|
||||
*
|
||||
* <p>To support the dynamically plugin-built support for multiple port types, each
|
||||
* non-abstract PortConnector subclass must define the following public static final
|
||||
* fields:
|
||||
* <ul>
|
||||
* <li>TYPE_NAME - a String specifying the internal type name of the class (stored in the
|
||||
* {@link PortConfig#portType} field of PortConfig records)
|
||||
* <li>CONFIG_GUI - a wildcarded fully-qualified String name of a Class which implements an
|
||||
* editor for the configuration parameters for the PortConnector
|
||||
* subclass
|
||||
* </ul>
|
||||
* <p>To support configuration transfer, the subclass must also define the following
|
||||
* public static final fields:</p>
|
||||
* <ul>
|
||||
* <li>BLANK_FIELDS - a Set of the {@link PortConfig.Fields} enums for the fields that should not be copied for this port driver</li>
|
||||
* <li>REQ_FIELDS - a Map of the {@link PortConfig.Fields} enums for the fields that must be defined for this port driver,
|
||||
* mapping to {@link PortConfig.RequireHints} structures containing the ResourceBundle keys for their UI prompt strings
|
||||
* if they are undefined</li>
|
||||
* </ul>
|
||||
* <p>If a field is both required and blanked, the configuration import code will prompt for the missing field.</p>
|
||||
*
|
||||
* <p>To support the -createport command-line option, a port type whose port editor class changes the
|
||||
* defaults in the PortConfig.Cfg object for a new port should have those changes performed by a
|
||||
* public static method named fillinConfigDefaults(PortConfig.Cfg) in the PortConnector subclass,
|
||||
* so that the command-line port creation logic can fill in those defaults to deal with new port
|
||||
* specifications that are not complete.</p>
|
||||
*
|
||||
* @author Andrew Pavlin, KA2DDO
|
||||
* @see org.ka2ddo.yaac.io.PortConfig.RequireHints
|
||||
*/
|
||||
abstract public class PortConnector extends Connector {
|
||||
/**
|
||||
* Timer for use by PortConnectors to handle inactivity tests.
|
||||
*/
|
||||
private static Timer portConnectorInactivityTimer = null;
|
||||
/**
|
||||
* The port configuration associated with this PortConnector.
|
||||
*/
|
||||
public PortConfig portConfig;
|
||||
/**
|
||||
* The port type-specific configuration associated with this PortConnector.
|
||||
*/
|
||||
public PortConfig.Cfg currentCfg;
|
||||
private final ArrayList<PortEventListener> eventListeners = new ArrayList<PortEventListener>();
|
||||
private static final HashMap<String, Class<? extends PortConnector>> PORT_CONNECTOR_MAP =
|
||||
new LinkedHashMap<String, Class<? extends PortConnector>>();
|
||||
|
||||
/**
|
||||
* Get the Timer for use by PortConnectors to handle inactivity tests.
|
||||
* @return Timer dedicated to PortConnector timeout checks
|
||||
*/
|
||||
protected static Timer getPortConnectorInactivityTimer() {
|
||||
if (portConnectorInactivityTimer == null) {
|
||||
if (DebugCtl.isDebug("thread")) {
|
||||
new Throwable("creating PortConnectorInactivityTimer...").printStackTrace(System.out);
|
||||
}
|
||||
portConnectorInactivityTimer = new Timer("PortConnector Inactivity Timer", true);
|
||||
}
|
||||
return portConnectorInactivityTimer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new subclass of PortConnector to the supported map of PortConnector types.
|
||||
* Note that putting a new Class object with the same typeName will replace any
|
||||
* existing driver for that type name.
|
||||
* @param typeName String name of PortConnector subclass
|
||||
* @param pcClazz the Class object for instantiating objects of this type
|
||||
*/
|
||||
public static void registerPortConnectorType(String typeName, Class<? extends PortConnector> pcClazz) {
|
||||
PORT_CONNECTOR_MAP.put(typeName, pcClazz);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the registered PortConnector names that should be displayed in the port configuration
|
||||
* dialog as a port type.
|
||||
* @return Set of String names to be displayed as port driver types.
|
||||
*/
|
||||
public static Set<String> portTypeNames() {
|
||||
return PORT_CONNECTOR_MAP.keySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the concrete subclasses of PortConnector currently registered in YAAC for
|
||||
* which instances can be created and configured.
|
||||
* @return Collection of subclasses of PortConnector
|
||||
*/
|
||||
public static Collection<Class<? extends PortConnector>> portConnectorClasses() {
|
||||
return PORT_CONNECTOR_MAP.values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the class corresponding to a particular port type name.
|
||||
* @param portTypeName String name of port connector type
|
||||
* @return Class providing that port connector type, or null if no match
|
||||
*/
|
||||
public static Class<? extends PortConnector> getPortConnectorClass(String portTypeName) {
|
||||
return PORT_CONNECTOR_MAP.get(portTypeName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a listener for port transmission and/or reception events.
|
||||
* @param l PortEventListener to register
|
||||
*/
|
||||
public void addPortEventListener(PortEventListener l) {
|
||||
synchronized (eventListeners) {
|
||||
for (int i = 0; i < eventListeners.size(); i++) {
|
||||
if (eventListeners.get(i) == l) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
eventListeners.add(l);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a listener so it no longer is informed of transmission and
|
||||
* reception events.
|
||||
* @param l PortEventListener to unregister
|
||||
*/
|
||||
public void removePortEventListener(PortEventListener l) {
|
||||
synchronized (eventListeners) {
|
||||
for (int i = 0; i < eventListeners.size(); i++) {
|
||||
if (eventListeners.get(i) == l) {
|
||||
eventListeners.remove(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Report to interested listeners that the port has started or stopped transmitting.
|
||||
* @param isTransmitting boolean true if transmission has started, false if stopped
|
||||
*/
|
||||
protected void fireTransmitting(boolean isTransmitting) {
|
||||
ArrayList<PortEventListener> eventListeners = this.eventListeners;
|
||||
for (int i = 0; i < eventListeners.size(); i++) {
|
||||
eventListeners.get(i).portTransmitting(this, isTransmitting);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Report to interested listeners that the port has started or stopped receiving.
|
||||
* @param isReceiving boolean true if reception has started, false if stopped
|
||||
*/
|
||||
public void fireReceiving(boolean isReceiving) {
|
||||
ArrayList<PortEventListener> eventListeners = this.eventListeners;
|
||||
for (int i = 0; i < eventListeners.size(); i++) {
|
||||
eventListeners.get(i).portReceiving(this, isReceiving);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Report to interested listeners that the port has failed for some reason.
|
||||
*/
|
||||
public void fireFailed() {
|
||||
System.out.println(new Date().toString() + ": port " + toString() + " failed");
|
||||
ArrayList<PortEventListener> eventListeners = this.eventListeners;
|
||||
for (int i = 0; i < eventListeners.size(); i++) {
|
||||
eventListeners.get(i).portFailed(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill in the port-type-specific defaults for a port configuration.
|
||||
* @param cfg PortConfig whose default values are to be adjusted
|
||||
* @throws UnknownServiceException if the port type is unknown
|
||||
* @throws IOException if some other problem occurs trying to fill in the defaults
|
||||
*/
|
||||
public static void fillinConfigDefaultsForType(PortConfig cfg) throws UnknownServiceException, IOException {
|
||||
Class<? extends PortConnector> connectorClass = PORT_CONNECTOR_MAP.get(cfg.portType);
|
||||
if (connectorClass == null) {
|
||||
throw new UnknownServiceException("no driver for port type " + cfg.portType);
|
||||
}
|
||||
try {
|
||||
Method mInitCfg = connectorClass.getDeclaredMethod("fillinConfigDefaults", PortConfig.Cfg.class);
|
||||
mInitCfg.invoke(null, cfg.current());
|
||||
} catch (NoSuchMethodException e) {
|
||||
// ignore, this port driver doesn't need anything different than the PortConfig.Cfg's constructor
|
||||
} catch (Exception e) {
|
||||
throw new IOException("unable to instantiate PortConnector for " + cfg, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a PortConnector object of the type specified in the configuration object.
|
||||
* @param cfg PortConfig object identifying what type of port to create
|
||||
* @return instantiated but not configured PortConnector subclass object
|
||||
* @throws IOException if port could not be instantiated
|
||||
* @throws UnknownServiceException if port type not recognized
|
||||
*/
|
||||
public static PortConnector createPort(PortConfig cfg) throws UnknownServiceException, IOException {
|
||||
Class<? extends PortConnector> connectorClass = PORT_CONNECTOR_MAP.get(cfg.portType);
|
||||
if (connectorClass == null) {
|
||||
throw new UnknownServiceException("no driver for port type " + cfg.portType);
|
||||
}
|
||||
try {
|
||||
return connectorClass.newInstance();
|
||||
} catch (Exception e) {
|
||||
throw new IOException("unable to instantiate PortConnector for " + cfg, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the configuration of the connector to match the updated
|
||||
* setup.
|
||||
* @param cfg PortConfig defining new port settings
|
||||
* @throws java.io.IOException if interface changes could not be applied
|
||||
* @throws IllegalArgumentException if type information is invalid for
|
||||
* changing the settings of this PortConnector
|
||||
*/
|
||||
abstract public void configure(PortConfig cfg) throws IOException, IllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Get the current configuration object for this PortConnector. Note that the caller should not modify
|
||||
* this data structure; if the port parameters are to be changed, the PortConnector should be closed
|
||||
* and then configured with an updated PortConfig object.
|
||||
* @return PortConfig for this PortConnector (may return null for a non-configured port)
|
||||
* @see #configure(PortConfig)
|
||||
* @see #close()
|
||||
*/
|
||||
public PortConfig getPortConfig() {
|
||||
return portConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the configuration associated with this PortConnector. The PortConfig is assumed to
|
||||
* be of the correct type here, such that the current() method will return the appropriate
|
||||
* type-specific configuration data.
|
||||
* @param portConfig PortConfig matching the PortConnector subclass's type
|
||||
*/
|
||||
public void setPortConfig(PortConfig portConfig) {
|
||||
this.portConfig = portConfig;
|
||||
this.currentCfg = portConfig.current();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bit transmission rate on this port. This abstract superclass does not know what the
|
||||
* data rate is, and expects subclasses to override this method.
|
||||
* @return bit rate in bits per second (baud)
|
||||
*/
|
||||
public int getBitRate() {
|
||||
return 0;
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 738 B |
After Width: | Height: | Size: 373 B |
|
@ -0,0 +1,383 @@
|
|||
###############################################################################
|
||||
# Copyright (C) 2011-2021 Andrew Pavlin, KA2DDO
|
||||
# This file is part of YAAC (Yet Another APRS Client).
|
||||
#
|
||||
# YAAC is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# YAAC 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
|
||||
# and GNU Lesser General Public License along with YAAC. If not,
|
||||
# see <http://www.gnu.org/licenses/>.
|
||||
###############################################################################
|
||||
|
||||
#
|
||||
# this file contains the hexadecimal encoding of variable-length OpenTRAC symbol IDs, translating to
|
||||
# a text string description followed by a comma and either:
|
||||
# a, a 2-character APRS symbol code (for an APRS icon equivalent to the OpenTrac icon)
|
||||
# (backslash has to be escaped per programming standards),
|
||||
# b. a relative path name within the YAAC distro for an image file for the icon (not matching
|
||||
# any existing APRS symbol code),
|
||||
# c. nothing (no comma), for a symbol code without a corresponding icon.
|
||||
#
|
||||
|
||||
10=Space,
|
||||
11=Manned Space,
|
||||
1110=Shuttle,/S
|
||||
1120=Soyuz,\\O
|
||||
1130=Station,\\S
|
||||
12=Unmanned Space
|
||||
1210=Satellite,\\S
|
||||
1220=Cargo Ship,\\O
|
||||
1230=Debris,\\x
|
||||
20=Air
|
||||
21=Lighter than Air
|
||||
2110=Manned Lighter than Air,/O
|
||||
2111=Hot Air Balloon,/O
|
||||
2112=Helium,/O
|
||||
2120=Unmanned Lighter than Air,/O
|
||||
2121=Radiosonde,/O
|
||||
2122=Experimental Balloon,/O
|
||||
22=Fixed Wing Air
|
||||
2210=Private Fixed Wing Air
|
||||
2211=Experimental,X^
|
||||
2212=Sailplane,/g
|
||||
2213=Piston Airplane,/'
|
||||
221310=Piston Airplane (1-engine),/'
|
||||
221320=Piston Airplane (multi-engine),/^
|
||||
2214=Turboprop Airplane,P^
|
||||
221410=Turboprop Airplane (1-engine),P^
|
||||
221420=Turboprop Airplane (multi-engine),P^
|
||||
2215=Jet Airplane
|
||||
221510=Jet Airplane (1-engine),/'
|
||||
221520=Jet Airplane (multi-engine),J^
|
||||
2220=Commercial Airplane,/^
|
||||
2221=Cargo Airplane,/^
|
||||
2222=Medevac Airplane,/'
|
||||
2223=Photo Airplane,/'
|
||||
2224=Utility Airplane,/'
|
||||
2225=SAR Airplane,\\^
|
||||
2226=UAV/Drone/RC Airplane,D^
|
||||
2230=Military,/^
|
||||
23=Rotary Wing Aircraft,/X
|
||||
2310=Private Rotary Wing,/X
|
||||
2311=Experimental Private Rotary Wing,/X
|
||||
2320=Commercial Rotary Wing,/X
|
||||
2321=Cargo Helicopter,/X
|
||||
2322=Medevac Helicopter,/X
|
||||
2323=Photo Helicopter,/X
|
||||
2324=Utility Helicopter,/X
|
||||
2325=UAV/Drone/RC Helicopter,D^
|
||||
24=Ultralight
|
||||
2410=Powered Ultralight,/'
|
||||
2420=Unpowered Ultralight,/'
|
||||
2421=Paraglider,/g
|
||||
2422=Hang Glider,/g
|
||||
30=Ground
|
||||
31=Ground Facilities
|
||||
3120=Government Facility,Gz
|
||||
3130=Medical Facility,/h
|
||||
3131=Hospital,/h
|
||||
3132=Clinic,Cz
|
||||
3133=Triage Facility,Tz
|
||||
3134=First Aid Station,/A
|
||||
3135=Pharmacy,\\X
|
||||
3136=Morgue,Mz
|
||||
3140=Emergency Services,\\c
|
||||
3141=Fire Station,/d
|
||||
3142=Decontamination,Dc
|
||||
3143=Shelter,\\z
|
||||
3144=EOC,/o
|
||||
3145=Incident Command Post,/c
|
||||
3146=Red Cross,/+
|
||||
3147=RACES/Civil Defense,\\c
|
||||
3150=Utilities
|
||||
3151=Power,\\%
|
||||
3152=Water,/w
|
||||
3160=Ham Radio
|
||||
3161=Repeater Site,/r
|
||||
3162=Ham Store,\\h
|
||||
3163=Club Station,C-
|
||||
3170=Residential
|
||||
3171=House,/-
|
||||
3172=Apartment
|
||||
3173=Condo
|
||||
3180=Church,\\+
|
||||
3190=Services
|
||||
3191=Hotel,/H
|
||||
3192=Gas,\\9
|
||||
3193=Restaurant,\\R
|
||||
3194=Information,\\?
|
||||
3195=Parking,\\P
|
||||
3196=Truck Stop,/t
|
||||
3197=Restrooms,\\r
|
||||
3198=Bank/ATM,\\$
|
||||
31A0=School,/K
|
||||
31A1=Elementary School,/K
|
||||
31A2=Middle School,/K
|
||||
31A3=Secondary School,/K
|
||||
31A4=College,/K
|
||||
31B0=Law Enforcement,/!
|
||||
31B1=Police Station,/!
|
||||
31B2=Sheriff Station,/!
|
||||
31B3=Ranger Station,/!
|
||||
31C0=Community Org
|
||||
31C1=Boy Scouts,/,
|
||||
31C2=Girl Scouts,\\,
|
||||
31D0=Recreation
|
||||
31D1=Park,\\;
|
||||
31D2=Picnic Area,\\;
|
||||
31D3=Campground,/;
|
||||
31E0=Transportation
|
||||
31E1=Air Transport
|
||||
31E110=Airport,AD
|
||||
31E120=Ultralight Flight Area
|
||||
31E130=VORTAC Nav Aid,\\V
|
||||
31E140=Helipad,HD
|
||||
31E2=Ground Transport
|
||||
31E210=Train Station,RD
|
||||
31E220=Bus Station,BD
|
||||
31E230=Light Rail,LD
|
||||
31E3=Sea Transport,SD
|
||||
31E310=Port,SD
|
||||
31E320=Ferry Landing,FD
|
||||
31E330=Lighthouse,\\L
|
||||
31F0=Communications
|
||||
31F1=Telephone,/$
|
||||
31F110=Switching Center,/$
|
||||
31F120=Cell Site,/r
|
||||
31F2=Satellite Ground Station,/`
|
||||
32=Equipment
|
||||
3210=Vehicle,\\>
|
||||
3211=Passenger Vehicle,/>
|
||||
321110=Car,/>
|
||||
321120=Van,/v
|
||||
321130=Truck,\\u
|
||||
321140=SUV,\\k
|
||||
321150=RV,/R
|
||||
321151=Class A RV,/R
|
||||
321152=Class B RV,/R
|
||||
321153=Class C RV,/R
|
||||
321160=Motorcycle,/<
|
||||
321170=Bicycle,/b
|
||||
3212=Commercial Vehicle,/u
|
||||
321210=Tractor/Trailer,/u
|
||||
321220=Bus,/U
|
||||
3213=Rail Vehicle,/=
|
||||
321310=Locomotive,/=
|
||||
3214=Snow Vehicle
|
||||
321410=Snowmobile,/*
|
||||
321420=Snow Plow,Pu
|
||||
3215=Construction Vehicle
|
||||
321510=Bulldozer,Bu
|
||||
321520=Backhoe,Bu
|
||||
321530=Loader
|
||||
321540=Crane,\\j
|
||||
3216=Public Safety Vehicle,/P
|
||||
321610=Law Enforcement,/P
|
||||
321620=Ambulance,/a
|
||||
321630=Fire Truck,/f
|
||||
321631=Brush Fire Truck,/f
|
||||
321632=Aerial Ladder Truck,/f
|
||||
3220=Emergency Equipment
|
||||
3221=Generator,\\%
|
||||
322110=Portable Generator,P%
|
||||
322120=Towed Generator,P%
|
||||
322130=Fixed Generator,E%
|
||||
33=Communications Equipment
|
||||
3310=Mobile Satellite Station,/(
|
||||
3430=People,/[
|
||||
40=Sea
|
||||
41=Sea Vessel,\\s
|
||||
4110=Naval Vessel,Ms
|
||||
4120=Merchant Vessel,\\s
|
||||
4121=Bulk Carrier Vessel,Cs
|
||||
4122=Container Vessel,Cs
|
||||
4123=Roll-on/off Vessel,Cs
|
||||
4124=Tanker Vessel,Cs
|
||||
412410=Coastal Tanker,Cs
|
||||
412420=LCC Tanker,Cs
|
||||
412430=VLCC Tanker,Cs
|
||||
412440=ULCC Tanker,Cs
|
||||
412450=Barge Tanker,Cs
|
||||
4130=Support Vessel
|
||||
4131=Tugboat,Ts
|
||||
413110=Harbor Tug,Ts
|
||||
413120=Ocean Tug,Ts
|
||||
4132=Pilot Boat,Ps
|
||||
4133=Dredge,\\s
|
||||
4134=Barge,\\s
|
||||
4135=Ferry,Xs
|
||||
413510=Passenger Ferry,Xs
|
||||
413520=Vehicle/Train Ferry,Xs
|
||||
413530=Hovercraft,Ws
|
||||
413540=Hydrofoil,Ws
|
||||
4137=Towing Vessel,\\s
|
||||
4140=Fishing Vessel,Fs
|
||||
4141=Long Liner,Fs
|
||||
4142=Seiner,Fs
|
||||
4143=Drifter,Fs
|
||||
4144=Dredge Fisher,Fs
|
||||
4145=Trawler,Fs
|
||||
4150=Leisure Craft,Bs
|
||||
4151=Canoe,/C
|
||||
4152=Kayak,/C
|
||||
4153=Rowboat,/s
|
||||
4154=Yacht,/Y
|
||||
415410=Sail Yacht,/Y
|
||||
415420=Power Yacht,/Y
|
||||
415430=SportFishing Yacht,/Y
|
||||
4155=PWC,Js
|
||||
4156=Skiff,/s
|
||||
4157=Launch,/s
|
||||
4158=DaySail,/Y
|
||||
4159=Cruise Ship,\\s
|
||||
415A=Tall Ship,Ys
|
||||
4160=Law Enforcement Vessel,Ls
|
||||
4161=Harbor Patrol,Ls
|
||||
4162=Coast Guard,\\C
|
||||
42=Sea Structure
|
||||
4210=Floating Structure
|
||||
4211=Oil Rig,Os
|
||||
4212=Dry Dock
|
||||
4213=Bridge
|
||||
4220=Fixed Structure
|
||||
4221=Oil Rig,Os
|
||||
4222=Dry Dock
|
||||
4223=Anchor Area
|
||||
4224=Mooring
|
||||
4225=Mooring Area
|
||||
4226=Bridge
|
||||
422610=Fixed Bridge
|
||||
422620=Lift/Swing Bridge
|
||||
43=Buoy,\\N
|
||||
4310=Navigation Buoy,\\N
|
||||
4320=Weather Buoy,\\N
|
||||
50=Subsurface
|
||||
51=Subsurface Equipment
|
||||
5110=Subsurface Vehicle,Us
|
||||
5111=ROV,Us
|
||||
5112=Submersible,Us
|
||||
52=features
|
||||
5210=Dive Site
|
||||
5220=Channel
|
||||
60=Activities
|
||||
61=Ham Radio
|
||||
6110=Ham Radio Contest
|
||||
6111=Field Day,F;
|
||||
6120=Swap Meet,/E
|
||||
6130=Convention,/E
|
||||
6140=DX Cluster,/%
|
||||
62=Sports
|
||||
6210=Geocache
|
||||
6220=Bike Race,/b
|
||||
6230=Foot Race,R[
|
||||
63=Public Events
|
||||
6310=Parade,/[
|
||||
6311=Parade Staging Area,S0
|
||||
6312=Parade Review Stand,/E
|
||||
6420=Fire,/:
|
||||
6421=Wildland Fire,/:
|
||||
642110=Small Wildfire,/:
|
||||
622120=Medium Wildfire,/:
|
||||
622130=Large Wildfire,/:
|
||||
6422=Structure Fire,/:
|
||||
65=SAR
|
||||
6510=SAR Victim
|
||||
6520=SAR Tracks
|
||||
6530=SAR Sign
|
||||
6540=SAR Evidence
|
||||
6550=SAR Crash Site,\\'
|
||||
6560=Shipwreck,6s
|
||||
6570=SAR Man Overboard,\\C
|
||||
6580=ELT/EPIRB Hit,E!
|
||||
66=Hazard
|
||||
6610=Earthquake,\\Q
|
||||
6620=HAZMAT,WH
|
||||
6630=Flooding,\\w
|
||||
6640=Landslide/Mudslide,Mw
|
||||
6650=Avalanche,Aw
|
||||
6660=Volcanic Eruption,V!
|
||||
70=Weather
|
||||
71=Atmospheric Weather
|
||||
7110=Weather Pressure Systems
|
||||
7111=Low Pressure Center
|
||||
7112=High Pressure Center
|
||||
7113=Frontal Systems
|
||||
711310=Cold Front
|
||||
711311=Upper Cold Front
|
||||
711320=Warm Front
|
||||
711321=Upper Warm Front
|
||||
711330=Occluded Front
|
||||
711340=Stationary Front
|
||||
7114=Weather Lines
|
||||
711410=Trough Line
|
||||
711420=Ridge Line
|
||||
711430=Squall Line
|
||||
7120=Turbulence
|
||||
7121=Light Turbulence
|
||||
7122=Medium Turbulence
|
||||
7123=Severe Turbulence
|
||||
7124=Extreme Turbulence
|
||||
7130=Icing
|
||||
7131=Clear Icing
|
||||
713110=Light Clear Icing
|
||||
713120=Moderate Clear Icing
|
||||
713130=Severe Clear Icing
|
||||
7132=Rime Icing
|
||||
713210=Light Rime Icing
|
||||
713220=Moderate Rime Icing
|
||||
713230=Severe Rime Icing
|
||||
7133=Mixed Icing
|
||||
713310=Light Mixed Icing
|
||||
713320=Moderate Mixed Icing
|
||||
713330=Severe Mixed Icing
|
||||
7140=Wind Barb
|
||||
7141=Jet Stream
|
||||
7150=Flight Rules
|
||||
7151=Instrument Ceiling
|
||||
7152=Visual Ceiling
|
||||
7160=Coverage Symbols
|
||||
7161=Clear Sky,\\U
|
||||
7162=Scattered,\\p
|
||||
7163=Broken Clouds,\\p
|
||||
7164=Overcast with Breaks,\\(
|
||||
7165=Overcast,\\(
|
||||
7166=Sky Obscured,\\(
|
||||
7170=Precipitation
|
||||
7171=Rain,\\`
|
||||
717110=Rain Shower,\\I
|
||||
717120=Freezing Rain,\\F
|
||||
717130=Drizzle,\\D
|
||||
717131=Freezing Drizzle,\\F
|
||||
7172=Snow,\\*
|
||||
717210=Snow Showers,\\G
|
||||
717220=Snow Grains,\\*
|
||||
7173=Hail,\\:
|
||||
7174=Ice Pellets
|
||||
7175=Ice Crystals
|
||||
7180=Storms
|
||||
7181=Thunderstorm,\\T
|
||||
718110=Thunderstorm with Rain,\\T
|
||||
718120=Funnel Cloud,\\f
|
||||
718130=Lightning,\\J
|
||||
7182=Storm Systems
|
||||
718210=Tropical Storm,\\@
|
||||
718220=Hurricane,\\@
|
||||
7190=Visibility Obstructions
|
||||
7191=Blowing Snow,\\B
|
||||
7192=Fog,\\{
|
||||
719210=Freezing Fog,\\{
|
||||
7193=Dust/Sand Storm,DE
|
||||
7194=Dust Devil
|
||||
7195=Smoke,\\E
|
||||
7196=Haze,\\H
|
||||
7197=Blowing Sand/Dust,DE
|
||||
72=Oceanic
|
||||
73=Space
|
|
@ -0,0 +1,124 @@
|
|||
package org.ka2ddo.yaac.core.queries;
|
||||
/*
|
||||
* Copyright (C) 2011-2021 Andrew Pavlin, KA2DDO
|
||||
* This file is part of YAAC (Yet Another APRS Client).
|
||||
*
|
||||
* YAAC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* YAAC 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
|
||||
* and GNU Lesser General Public License along with YAAC. If not,
|
||||
* see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import org.ka2ddo.aprs.Message;
|
||||
import org.ka2ddo.aprs.MessageMessage;
|
||||
import org.ka2ddo.aprs.NWSMultiLine;
|
||||
import org.ka2ddo.aprs.ObjectReport;
|
||||
import org.ka2ddo.ax25.AX25Callsign;
|
||||
import org.ka2ddo.ax25.AX25Frame;
|
||||
import org.ka2ddo.ax25.Connector;
|
||||
import org.ka2ddo.util.DistanceUnit;
|
||||
import org.ka2ddo.yaac.YAAC;
|
||||
import org.ka2ddo.ax25.AX25Stack;
|
||||
import org.ka2ddo.yaac.core.AlohaRanger;
|
||||
import org.ka2ddo.yaac.core.AlohaTracker;
|
||||
import org.ka2ddo.yaac.core.YAACPreferences;
|
||||
import org.ka2ddo.yaac.io.BeaconData;
|
||||
import org.ka2ddo.yaac.io.PortConnector;
|
||||
import org.ka2ddo.yaac.io.Transmitter;
|
||||
import org.ka2ddo.yaac.pluginapi.AbstractQueryHandler;
|
||||
|
||||
/**
|
||||
* This class handles queries for the local station's RF Aloha circle radius and position.
|
||||
* @author Andrew Pavlin, KA2DDO
|
||||
*/
|
||||
public class AlohaQueryHandler extends AbstractQueryHandler {
|
||||
/**
|
||||
* Handle the passed in message and generate whatever appropriate response should be made.
|
||||
*
|
||||
* @param mm MessageMessage addressed explicitly to local station whose content begins with
|
||||
* one of the prefixes specified by the subclass.
|
||||
*/
|
||||
public void handleQuery(Message mm) {
|
||||
// send description of YAAC's local aloha circle on the port receiving this message
|
||||
AX25Frame f = mm.getAx25Frame();
|
||||
if (f == null) {
|
||||
return; // didn't come from a real interface
|
||||
}
|
||||
AlohaRanger[] alohaRangers = AlohaTracker.getInstance().getAlohaRanges();
|
||||
if (alohaRangers.length > 0) {
|
||||
// we'll try to find the one corresponding to the interface the request came in on
|
||||
AlohaRanger target = null;
|
||||
for (AlohaRanger ar : alohaRangers) {
|
||||
if (ar.port == f.sourcePort) {
|
||||
target = ar;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (target == null) {
|
||||
target = alohaRangers[0];
|
||||
}
|
||||
double centerLat = AlohaTracker.getInstance().getLastLatitude();
|
||||
double centerLon = AlohaTracker.getInstance().getLastLongitude();
|
||||
|
||||
// draw a 16-sided polygon as an approximation of a circle
|
||||
double[] lats = new double[16];
|
||||
double[] lons = new double[16];
|
||||
double lonScale = 1.0 / Math.cos(Math.toRadians(centerLat));
|
||||
double rangeInDeg = (double) DistanceUnit.CONV_METERS_TO_DEGLAT * target.getRangeM();
|
||||
int index = 0;
|
||||
for (double angle = 0.0; angle < 2.0 * Math.PI; angle += 0.125 * Math.PI, index++) {
|
||||
lats[index] = centerLat + Math.sin(angle) * rangeInDeg;
|
||||
lons[index] = centerLon + Math.cos(angle) * rangeInDeg * lonScale;
|
||||
}
|
||||
NWSMultiLine nws = new NWSMultiLine(lats, lons, true);
|
||||
nws.dataType = (target.getNumMsgs() < AlohaTracker.MAX_ALOHA_1200_CAPACITY) ? 'n' : 'm';
|
||||
|
||||
// get the appropriate BeaconData for the receiving port
|
||||
BeaconData beaconData = null;
|
||||
if (mm.ax25Frame != null) {
|
||||
Connector connector = mm.ax25Frame.sourcePort;
|
||||
if (connector != null && connector.hasCapability(Connector.CAP_XMT_PACKET_DATA)) {
|
||||
PortConnector portConnector = (PortConnector)connector;
|
||||
if (portConnector.currentCfg.transmitAllowed && portConnector.currentCfg.beaconNames.length > 0) {
|
||||
for (String beaconName : portConnector.currentCfg.beaconNames) {
|
||||
beaconData = YAAC.getBeaconData(beaconName);
|
||||
if (beaconData != null) {
|
||||
if (beaconData.enabled) {
|
||||
break;
|
||||
}
|
||||
beaconData = null; // try another one that might be enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (beaconData == null) {
|
||||
beaconData = YAAC.getBeaconData(); // get the default
|
||||
}
|
||||
|
||||
// transmit the resulting message as an Item-in-Msg
|
||||
DistanceUnit prefDistanceUnit = YAAC.getDistanceUnit();
|
||||
ObjectReport or = nws.generateMessage(target.port.getCallsign(), beaconData.symTableId, beaconData.symbolCode,
|
||||
" Aloha" + (target.isMaxedOut() ? "" : "?") + " " + Math.round(target.getRangeM() * prefDistanceUnit.from(DistanceUnit.METER)) +
|
||||
prefDistanceUnit.name() + " to " + target.getMostDistantStation().getIdentifier());
|
||||
MessageMessage replyMM = new MessageMessage(mm.originatingCallsign, or.getStringAPRSBody(false));
|
||||
AX25Frame replyFrame = new AX25Frame();
|
||||
replyFrame.dest = new AX25Callsign(YAAC.getPreferences().get(YAACPreferences.PREF_BEACON_DESTINATION, YAACPreferences.DEFAULT_PREF_BEACON_DESTINATION));
|
||||
replyFrame.sender = new AX25Callsign(f.sourcePort.getCallsign());
|
||||
replyFrame.digipeaters = AX25Stack.reverseDigipeaters(f.digipeaters);
|
||||
replyFrame.ctl = (AX25Frame.FRAMETYPE_U | AX25Frame.UTYPE_UI);
|
||||
replyFrame.pid = AX25Frame.PID_NOLVL3;
|
||||
replyFrame.body = replyMM.getBody(false, AX25Frame.PID_NOLVL3, replyFrame);
|
||||
Transmitter.getInstance().queue(replyFrame);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package org.ka2ddo.yaac.aisdecoder.messages;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2011-2020 Andrew Pavlin, KA2DDO
|
||||
* This file is part of YAAC.
|
||||
*
|
||||
* YAAC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* YAAC 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
|
||||
* and GNU Lesser General Public License along with YAAC. If not,
|
||||
* see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This enum defines the values of the NavigationStatus field of AIS Position Reports.
|
||||
* @author Andrew Pavlin, KA2DDO
|
||||
*/
|
||||
public enum NavigationStatus {
|
||||
UNDER_WAY_ENGINE,
|
||||
AT_ANCHOR,
|
||||
NOT_UNDER_COMMAND,
|
||||
RESTRICTED_MANOEUVERABILIY,
|
||||
DRAUGHT_CONSTRAINED,
|
||||
MOORED,
|
||||
AGROUND,
|
||||
FISHING,
|
||||
UNDER_WAY_SAIL,
|
||||
reserved_HSC,
|
||||
reserved_WIG,
|
||||
reserved_11,
|
||||
reserved_12,
|
||||
reserved_13,
|
||||
AIS_SART,
|
||||
UNDEFINED
|
||||
}
|
After Width: | Height: | Size: 920 B |
After Width: | Height: | Size: 25 KiB |
|
@ -0,0 +1,29 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE map
|
||||
PUBLIC "-//Sun Microsystems Inc.//DTD JavaHelp Map Version 2.0//EN"
|
||||
"http://java.sun.com/products/javahelp/map_2_0.dtd">
|
||||
<!--
|
||||
Copyright (C) 2011-2020 Andrew Pavlin, KA2DDO
|
||||
This file is part of YAAC (Yet Another APRS Client).
|
||||
|
||||
YAAC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
YAAC 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
|
||||
and GNU Lesser General Public License along with YAAC. If not,
|
||||
see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<map version="2.0">
|
||||
<mapID target="configure" url="docs/configure.html"/>
|
||||
<mapID target="configure.ports" url="docs/config_ports.html"/>
|
||||
<mapID target="configure.AdsB" url="docs/config_adsb.html"/>
|
||||
<mapID target="menu.View.Aircraft" url="docs/viewaircraft.html"/>
|
||||
</map>
|
|
@ -0,0 +1,38 @@
|
|||
package org.ka2ddo.yaac.core;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2011-2018 Andrew Pavlin, KA2DDO
|
||||
* This file is part of YAAC (Yet Another APRS Client).
|
||||
*
|
||||
* YAAC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* YAAC 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
|
||||
* and GNU Lesser General Public License along with YAAC. If not,
|
||||
* see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This interface provides a callback so something can be asynchronously notified that
|
||||
* the user has requested cancellation of a long-running background job. Otherwise,
|
||||
* the long-running job is only notified of the cancellation the next time updates
|
||||
* are sent to a StatusListener.
|
||||
* @see StatusListener
|
||||
* @see UserAbort
|
||||
* @author Andrew Pavlin, KA2DDO
|
||||
*/
|
||||
public interface AbortListener {
|
||||
/**
|
||||
* Called from the user interface when the user requests a cancel. Implementations of this
|
||||
* method should not block, but should quickly inform the associated long-running background
|
||||
* thread to abort its operation.
|
||||
*/
|
||||
public void cancelRequested();
|
||||
}
|
After Width: | Height: | Size: 54 KiB |
|
@ -0,0 +1 @@
|
|||
YAAC-Provider: org.ka2ddo.yaac.sounds.SoundsProvider
|
|
@ -0,0 +1,278 @@
|
|||
package org.ka2ddo.yaac.gui;
|
||||
/*
|
||||
* Copyright (C) 2011-2021 Andrew Pavlin, KA2DDO
|
||||
* This file is part of YAAC (Yet Another APRS Client).
|
||||
*
|
||||
* YAAC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* YAAC 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
|
||||
* and GNU Lesser General Public License along with YAAC. If not,
|
||||
* see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import org.ka2ddo.aprs.Message;
|
||||
import org.ka2ddo.aprs.Symbols;
|
||||
import org.ka2ddo.yaac.YAAC;
|
||||
import org.ka2ddo.yaac.io.NonshareableBufferedInputStream;
|
||||
import org.ka2ddo.yaac.util.Json;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
* This class collects the localized mappings of to-call patterns to sender descriptions.
|
||||
*/
|
||||
public class TocallToDescriptionMap {
|
||||
private static final TocallToDescriptionMap instance = new TocallToDescriptionMap();
|
||||
private static final String FMT_GPS = YAAC.getMsg("tocallFmt.GPS");
|
||||
private static final String FMT_SPC = YAAC.getMsg("tocallFmt.SPC");
|
||||
private final HashMap<String, TocallDescription> tocallToDescriptionMap = new HashMap<String, TocallDescription>();
|
||||
|
||||
/**
|
||||
* Get the singleton object for translating tocalls into descriptions.
|
||||
* @return TocallToDescriptionMap singleton object
|
||||
*/
|
||||
public static TocallToDescriptionMap getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data structure containing the description and capabilities of an APRS station with a specific tocall.
|
||||
*/
|
||||
public static class TocallDescription {
|
||||
private static final String fmtVendorModel = YAAC.getMsg("tocallFmt.VendorModel");
|
||||
private static final String fmtVendorModelOs = YAAC.getMsg("tocallFmt.VendorModelOs");
|
||||
private static final String fmtVendorOs = YAAC.getMsg("tocallFmt.VendorOs");
|
||||
private static final String fmtModelOs = YAAC.getMsg("tocallFmt.ModelOs");
|
||||
private static final String fmtUnknown = YAAC.getMsg("tocallFmt.Unknown");
|
||||
final String tocall;
|
||||
String model;
|
||||
String vendor;
|
||||
String clazz;
|
||||
String os;
|
||||
HashMap<String,String> features;
|
||||
|
||||
TocallDescription(String tocall) {
|
||||
this.tocall = tocall;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the class of station for this tocall (not associated with symbol categories).
|
||||
* @return station class String, or null if class is not known
|
||||
*/
|
||||
public String getClazz() {
|
||||
return clazz;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the collection of feature strings and their associated values for this tocall.
|
||||
* @return the feature Map
|
||||
*/
|
||||
public Map<String, String> getFeatures() {
|
||||
if (null != features) {
|
||||
return Collections.unmodifiableMap(features);
|
||||
} else {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the model name of the software or hardware associated with this tocall.
|
||||
* @return model name
|
||||
*/
|
||||
public String getModel() {
|
||||
return model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the computer operating system this software is restricted to.
|
||||
* @return operating system name String, or null if not known or not applicable
|
||||
*/
|
||||
public String getOs() {
|
||||
return os;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tocall described by this structure.
|
||||
* @return tocall String
|
||||
*/
|
||||
public String getTocall() {
|
||||
return tocall;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the vendor who manufactured this APRS hardware or software.
|
||||
* @return vendor name, or null if not known
|
||||
*/
|
||||
public String getVendor() {
|
||||
return vendor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the object. The representation is the vendor's name (in the
|
||||
* possessive syntax) of the model string for the specified operating system (if relevant).
|
||||
*
|
||||
* @return a string representation of the object.
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
if (model != null) {
|
||||
if (vendor != null && !vendor.equals("unknown")) {
|
||||
if (os != null) {
|
||||
return MessageFormat.format(fmtVendorModelOs, vendor, model, os);
|
||||
} else {
|
||||
return MessageFormat.format(fmtVendorModel, vendor, model);
|
||||
}
|
||||
} else {
|
||||
if (os != null) {
|
||||
return MessageFormat.format(fmtModelOs, model, os);
|
||||
} else {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (vendor != null) {
|
||||
if (os != null) {
|
||||
return MessageFormat.format(fmtVendorOs, vendor, os);
|
||||
} else {
|
||||
return vendor;
|
||||
}
|
||||
} else {
|
||||
return fmtUnknown;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private TocallToDescriptionMap() {
|
||||
// read the JSON map
|
||||
Reader rdr = null;
|
||||
try {
|
||||
rdr = new InputStreamReader(new NonshareableBufferedInputStream(new FileInputStream(new File(YAAC.getBaseDir(), "images/tocalls.dense.json")), 4096), StandardCharsets.UTF_8);
|
||||
Map<String,Object> tocallJsonMap = Json.decodeJSON(rdr, null);
|
||||
rdr.close();
|
||||
rdr = null;
|
||||
Map<String,Object> tocallSubMap = (Map<String,Object>)tocallJsonMap.get("tocalls");
|
||||
for (Map.Entry<String,Object> entry : tocallSubMap.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
while (key.length() > 0 && key.endsWith("?")) {
|
||||
key = key.substring(0, key.length() - 1);
|
||||
}
|
||||
if (key.length() > 0) {
|
||||
TocallDescription descr = new TocallDescription(entry.getKey());
|
||||
Map<String,Object> infoMap = (Map<String,Object>)entry.getValue();
|
||||
descr.model = (String)infoMap.get("model");
|
||||
descr.vendor = (String)infoMap.get("vendor");
|
||||
if ("open source".equalsIgnoreCase(descr.vendor)) {
|
||||
descr.vendor = null;
|
||||
}
|
||||
if ("unknown".equalsIgnoreCase(descr.model) && null != descr.vendor) {
|
||||
descr.model = null;
|
||||
}
|
||||
descr.clazz = (String)infoMap.get("class");
|
||||
descr.os = (String)infoMap.get("os");
|
||||
Object features = infoMap.get("features");
|
||||
if (features instanceof Object[]) {
|
||||
descr.features = new LinkedHashMap<String, String>();
|
||||
for (Object o : (Object[])features) {
|
||||
descr.features.put((String)o, "");
|
||||
}
|
||||
} else if (features instanceof Map) {
|
||||
descr.features = new LinkedHashMap<String, String>((Map)features);
|
||||
}
|
||||
int midrangeWildcardPos;
|
||||
if ((midrangeWildcardPos = key.indexOf('?')) == -1) {
|
||||
tocallToDescriptionMap.put(key, descr);
|
||||
} else {
|
||||
//TODO: how to handle matching when version not at end
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace(System.out);
|
||||
} finally {
|
||||
if (null != rdr) {
|
||||
try {
|
||||
rdr.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace(System.out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// now read in the resource bundle's definitions to fill in any holes
|
||||
ResourceBundle msgBundle = YAAC.getMsgBundle();
|
||||
TreeSet<String> redundantTocalls = new TreeSet<String>();
|
||||
TreeSet<String> missingTocalls = new TreeSet<String>();
|
||||
for (String key : msgBundle.keySet()) {
|
||||
if (key.startsWith("tocall.")) {
|
||||
String tocall = key.substring(7);
|
||||
if (!tocallToDescriptionMap.containsKey(tocall)) {
|
||||
String model = msgBundle.getString(key);
|
||||
TocallDescription descr = new TocallDescription(tocall);
|
||||
descr.model = model;
|
||||
tocallToDescriptionMap.put(tocall, descr);
|
||||
missingTocalls.add(tocall);
|
||||
} else {
|
||||
redundantTocalls.add(tocall);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (redundantTocalls.size() > 0) {
|
||||
System.out.println("redundant tocalls from ResourceBundle: " + redundantTocalls);
|
||||
}
|
||||
if (missingTocalls.size() > 0) {
|
||||
System.out.println("missing tocalls in JSON file, using fallback: " + missingTocalls);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the description of a tocall string.
|
||||
* @param tocall String tocall to decode
|
||||
* @return TocallDescription data structure, or null if tocall could not be decoded
|
||||
*/
|
||||
public static TocallDescription get(String tocall) {
|
||||
TocallDescription descr = null;
|
||||
if (tocall.startsWith("AP")) { // general prefix for standard APRS stations?
|
||||
//TODO: replace this with JSON tree lookup
|
||||
for (int len = tocall.length(); len > 2; len--) {
|
||||
String trimmedTocall = tocall.substring(0, len);
|
||||
if ((descr = instance.tocallToDescriptionMap.get(trimmedTocall)) != null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (tocall.startsWith("GPS")) {
|
||||
String symbol;
|
||||
if ((symbol = Message.extractSymbol(tocall)) != null) {
|
||||
descr = new TocallDescription(tocall);
|
||||
descr.model = MessageFormat.format(FMT_GPS, Symbols.getTypeName(symbol.charAt(0), symbol.charAt(1)));
|
||||
}
|
||||
} else if (tocall.startsWith("SPC")) {
|
||||
String symbol;
|
||||
if ((symbol = Message.extractSymbol(tocall)) != null) {
|
||||
descr = new TocallDescription(tocall);
|
||||
descr.model = MessageFormat.format(FMT_SPC, Symbols.getTypeName(symbol.charAt(0), symbol.charAt(1)));
|
||||
}
|
||||
}
|
||||
return descr;
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 962 B |
|
@ -0,0 +1,57 @@
|
|||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<title>Configuring YAAC</title>
|
||||
</head>
|
||||
<body lang="en-US" dir="LTR">
|
||||
<h1>Configuring YAAC</h1>
|
||||
<p>Once YAAC is started, its graphical user interface (GUI) should
|
||||
appear on your desktop. If you have not previously configured YAAC,
|
||||
you will need to invoke the configuration option to set up YAAC for
|
||||
your environment. YAAC may detect the lack of configuration and automatically
|
||||
ask if you would like to use the <a href="./quickstart.html">configuration wizard</a>. Otherwise, click on the
|
||||
File menu (or press Alt-F on the keyboard, then
|
||||
select the Configure option on the menu (or press C). At this point, you have
|
||||
four options:</p>
|
||||
<ul>
|
||||
<li>invoking the <a href="./quickstart.html">Configuration Wizard</a>, which will walk you through setting
|
||||
up a basic and standard YAAC setup, or</li>
|
||||
<li>directly accessing the configuration parameters, which gives you detailed
|
||||
control over all aspects of YAAC, in a manner organized to keep related
|
||||
data together.</li>
|
||||
<li>exporting the current YAAC configuration into a XML file, suitable for sending to the
|
||||
YAAC author to support troubleshooting a problem.</li>
|
||||
<li>importing a YAAC configuration exported from another system with appropriate adjustments for this station.
|
||||
Note this option is experimental and may not give the desired results, as it attempts to prevent creating
|
||||
identical duplicate stations that would interfere with each other on the APRS networks.</li>
|
||||
</ul>
|
||||
|
||||
<p>The Expert Mode choice will open
|
||||
a dialog with several tabbed panes of configuration options.</p>
|
||||
<ul>
|
||||
<li><a href="./config_general.html">General options</a>, which control the basic operation of YAAC.
|
||||
<li><a href="./config_transmit.html">transmit options</a>, which control how YAAC transmits
|
||||
messages to other APRS stations.
|
||||
<li><a href="./config_digipeat.html">digipeat options</a>, which control how YAAC
|
||||
digipeats messages to other APRS stations.
|
||||
<li><a href="./config_ports.html">Ports</a>, which configure the interfaces between YAAC and the
|
||||
outside world. This includes radio/TNC ports, GPS receivers, weather stations, and
|
||||
Internet connections to APRS-IS servers.
|
||||
<li><a href="./config_beacon.html">Beacon data</a>, which specifies what information your computer
|
||||
will announce back to the APRS network.
|
||||
<li><a href="./config_prefs.html">Display preferences</a>, such as the font for monospaced message displays,
|
||||
the units for distances and temperatures, etc.
|
||||
<li><a href="./config_behavior.html">Behavioral preferences</a>, such as whether YAAC should beep if
|
||||
a message addressed to this station arrives,
|
||||
whether or not to do dead reckoning of moving stations, and whether or not to do
|
||||
<a href="./config_behavior.html#vicinity">vicinity plotting</a> for stations not providing position information.
|
||||
</ul>
|
||||
<p>Additional tabs may appear if you have installed YAAC <a href="./help.html#plugin">plugins</a> that are configurable.</p>
|
||||
|
||||
<p>If you need to use the same copy of YAAC in several different environments with different
|
||||
configurations, use YAAC's <a href="./profiles.html">configuration profile feature</a> to set up multiple configuration profiles and select the
|
||||
appropriate one at startup time. Note that the configuration dialog will announce the name of the profile being
|
||||
used (if any).</p>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,2 @@
|
|||
RXTX.cantFindLibrary=can't find RXTX Java or native library
|
||||
About.2=uses customized variant of RXTX library version 2.2pre2
|
|
@ -0,0 +1,107 @@
|
|||
package org.ka2ddo.util;
|
||||
/*
|
||||
* Copyright (C) 2011-2016 Andrew Pavlin, KA2DDO
|
||||
* This file is part of YAAC (Yet Another APRS Client).
|
||||
*
|
||||
* YAAC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* YAAC 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
|
||||
* and GNU Lesser General Public License along with YAAC. If not,
|
||||
* see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Enumeration of supported units of atmospheric pressure. All scaling factors are in units
|
||||
* relative to the Peet Bros weather station standard resolution of 0.1 millibars.
|
||||
*/
|
||||
public enum PressureUnit implements EnumWithImageName, UnitEnum<PressureUnit> {
|
||||
/**
|
||||
* Pressure in pascals.
|
||||
*/
|
||||
Pa(0.1F, 0, "Pa", "images/pressure_Pa.png"),
|
||||
/**
|
||||
* Pressure in hectopascals (also known as millibars).
|
||||
*/
|
||||
HPa(10.0F, 1, "HPa", "images/pressure_hPa.png"), // or millibars
|
||||
/**
|
||||
* Pressure in kilopascals (also known as 10's of millibars).
|
||||
*/
|
||||
KPa(100.0F, 2, "KPa", "images/pressure_kPa.png"),
|
||||
/**
|
||||
* Pressure in millimeters of mercury.
|
||||
*/
|
||||
mmHg(13.33224F, 2, "mmHg", "images/pressure_mmHg.png"),
|
||||
/**
|
||||
* Pressure in pounds per square inch.
|
||||
*/
|
||||
PSI(689.48F, 2, "psi", "images/pressure_psi.png"),
|
||||
/**
|
||||
* Pressure in inches of mercury.
|
||||
*/
|
||||
inHg(338.639F, 2, "inHg", "images/pressure_inHg.png");
|
||||
|
||||
private final float in0_1mbar;
|
||||
private final int digitsAfterDecimalPoint;
|
||||
private final String displayName;
|
||||
private final String imgName;
|
||||
|
||||
private PressureUnit(float scaleFactor, int digitsAfterDecimalPoint, String displayName, String imageFileName) {
|
||||
this.in0_1mbar = scaleFactor;
|
||||
this.digitsAfterDecimalPoint = digitsAfterDecimalPoint;
|
||||
this.displayName = displayName;
|
||||
this.imgName = imageFileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the multiplicative conversion factor to convert a pressure from the specified
|
||||
* units to this unit.
|
||||
* @param other another PressureUnit representing the existing units of a pressure value
|
||||
* @return the conversion factor to change a pressure value into this unit
|
||||
*/
|
||||
public final float from(PressureUnit other) {
|
||||
return other.in0_1mbar / this.in0_1mbar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the additive offset to convert a value from the specified unit to this unit.
|
||||
*
|
||||
* @param other another PressureUnit representing the existing units of a value
|
||||
* @return the offset (after multiplying by the from() factor) to add to get the value in this unit
|
||||
*/
|
||||
public float getOffset(PressureUnit other) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of digits after the decimal point for displaying in these units.
|
||||
* @return digits to display after decimal point
|
||||
*/
|
||||
public final int getDigitsAfterDecimalPoint() {
|
||||
return digitsAfterDecimalPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the string to use when displaying values of this unit.
|
||||
* @return suffix display String
|
||||
*/
|
||||
public final String unitString() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the relative path name of the image file for this enum value.
|
||||
*
|
||||
* @return path name String
|
||||
*/
|
||||
public String getImagePath() {
|
||||
return imgName;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
package org.ka2ddo.yaac.gui.filter;
|
||||
/*
|
||||
* Copyright (C) 2011-2019 Andrew Pavlin, KA2DDO
|
||||
* This file is part of YAAC (Yet Another APRS Client).
|
||||
*
|
||||
* YAAC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* YAAC 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
|
||||
* and GNU Lesser General Public License along with YAAC. If not,
|
||||
* see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import org.ka2ddo.yaac.YAAC;
|
||||
import org.ka2ddo.yaac.filter.Filter;
|
||||
import org.ka2ddo.yaac.filter.FilterChangeListener;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.util.ResourceBundle;
|
||||
import java.awt.event.ItemListener;
|
||||
import java.awt.event.ItemEvent;
|
||||
|
||||
/**
|
||||
* This filter optionally eliminates all routine precedence messages.
|
||||
* @author Andrew Pavlin, KA2DDO
|
||||
*/
|
||||
public class PriorityFilter extends JPanel implements Closeable, FilterChangeListener {
|
||||
final transient org.ka2ddo.yaac.filter.PriorityFilter filter;
|
||||
private final JCheckBox cbPriorityOnly;
|
||||
|
||||
/**
|
||||
* Create a UI for controlling a PriorityFilter instance.
|
||||
* @param myFilter the PriorityFilter instance to control
|
||||
*/
|
||||
public PriorityFilter(org.ka2ddo.yaac.filter.PriorityFilter myFilter) {
|
||||
this.filter = myFilter;
|
||||
ResourceBundle msgBundle = YAAC.getMsgBundle();
|
||||
cbPriorityOnly = new JCheckBox(msgBundle.getString("filter.PriorityFilter.priorityOnly"), filter.isPriorityOnly());
|
||||
add(cbPriorityOnly);
|
||||
cbPriorityOnly.addItemListener(new ItemListener() {
|
||||
public void itemStateChanged(ItemEvent e) {
|
||||
if (filter.isPriorityOnly() != cbPriorityOnly.isSelected()) {
|
||||
filter.setPriorityOnly(cbPriorityOnly.isSelected());
|
||||
}
|
||||
}
|
||||
});
|
||||
filter.addFilterChangeListener(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes this stream and releases any system resources associated
|
||||
* with it. If the stream is already closed then invoking this
|
||||
* method has no effect.
|
||||
*
|
||||
* @throws java.io.IOException if an I/O error occurs
|
||||
*/
|
||||
public void close() throws IOException {
|
||||
filter.removeFilterChangeListener(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the specified Filter's matching criteria have been changed.
|
||||
*
|
||||
* @param changedFilter Filter that has changed
|
||||
* @param changedByUser boolean true if change was manually made by user, false if
|
||||
* change was made automatically by dynamic filter logic
|
||||
*/
|
||||
public void filterSettingsChanged(Filter changedFilter, boolean changedByUser) {
|
||||
boolean newState = filter.isPriorityOnly();
|
||||
if (newState != cbPriorityOnly.isSelected()) {
|
||||
cbPriorityOnly.setSelected(newState);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package org.ka2ddo.yaac.pluginapi;
|
||||
/*
|
||||
* Copyright (C) 2011-2020 Andrew Pavlin, KA2DDO
|
||||
* This file is part of YAAC (Yet Another APRS Client).
|
||||
*
|
||||
* YAAC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* YAAC 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
|
||||
* and GNU Lesser General Public License along with YAAC. If not,
|
||||
* see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This enum defines content types for UI panels that can launch popup menus,
|
||||
* to help popup menu actions decide whether they can be used in the launching
|
||||
* context.
|
||||
* @author Andrew Pavlin, KA2DDO
|
||||
*/
|
||||
public enum GuiContentType {
|
||||
/**
|
||||
* GuiContentType of a UI displaying only APRS text messages (code ':').
|
||||
*/
|
||||
TEXT_MESSAGE,
|
||||
/**
|
||||
* GuiContentType of a UI displaying only APRS text messages that are broadcast bulletins.
|
||||
*/
|
||||
BULLETIN,
|
||||
/**
|
||||
* GuiContentType of a tabular UI displaying the current status of stations and objects.
|
||||
*/
|
||||
STATIONS,
|
||||
/**
|
||||
* GuiContentType of a tabular UI displaying current severe weather zones.
|
||||
*/
|
||||
WEATHER_ZONES,
|
||||
/**
|
||||
* GuiContentType of a tabular UI displaying raw frames, with the sending station's callsign in model column 1.
|
||||
*/
|
||||
RAW_PACKETS,
|
||||
/**
|
||||
* GuiContentType of a geographical map display with stations and objects on it.
|
||||
*/
|
||||
MAP
|
||||
|
||||
// Any new content types must be added here for backwards compatibility.
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package org.ka2ddo.yaac.gui.config;
|
||||
/*
|
||||
* Copyright (C) 2011-2012 Andrew Pavlin, KA2DDO
|
||||
* This file is part of YAAC (Yet Another APRS Client).
|
||||
*
|
||||
* YAAC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* YAAC 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
|
||||
* and GNU Lesser General Public License along with YAAC. If not,
|
||||
* see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* This class renders a list of family-related Font object in a JList.
|
||||
*/
|
||||
class FontFamilyRenderer extends JLabel implements ListCellRenderer {
|
||||
/**
|
||||
* Return a component that has been configured to display the specified
|
||||
* value. That component's <code>paint</code> method is then called to
|
||||
* "render" the cell. If it is necessary to compute the dimensions
|
||||
* of a list because the list cells do not have a fixed size, this method
|
||||
* is called to generate a component on which <code>getPreferredSize</code>
|
||||
* can be invoked.
|
||||
*
|
||||
* @param list The JList we're painting.
|
||||
* @param value The value returned by list.getModel().getElementAt(index).
|
||||
* @param index The cells index.
|
||||
* @param isSelected True if the specified cell was selected.
|
||||
* @param cellHasFocus True if the specified cell has the focus.
|
||||
* @return A component whose paint() method will render the specified value.
|
||||
* @see javax.swing.JList
|
||||
* @see javax.swing.ListSelectionModel
|
||||
* @see javax.swing.ListModel
|
||||
*/
|
||||
public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
|
||||
ArrayList<Font> fList = (ArrayList<Font>)value;
|
||||
if (null != value) {
|
||||
setText(fList.get(0).getFamily());
|
||||
setFont(new Font(fList.get(0).getFamily(), Font.PLAIN, 12));
|
||||
} else {
|
||||
setText("");
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<title>Configure Callsign Database</title>
|
||||
</head>
|
||||
<body lang="en-US" dir="LTR">
|
||||
<h1>Configure Callsign Database</h1>
|
||||
<p>The Callsign DB tab in the expert-mode configuration dialog allows you to save your authentication
|
||||
information for <a href="https://www.qrz.com/" target="QRZ">QRZ.COM</a>'s international callsign database (so YAAC can automatically log into QRZ.COM
|
||||
if needed to complete a lookup), and test that the callsign lookup mechanism is working.</p>
|
||||
<p>Note that access to QRZ.COM's backend database this way requires purchasing from them
|
||||
(at minimum) an "XML Logbook Data Subscription" to supplement your account's capabilities.</p>
|
||||
<p><a name="offline">Note that if you are going to use YAAC offline (without Internet connectivity), you should clear
|
||||
your QRZ username. This will prevent lookup attempts from hanging trying to access QRZ.COM until
|
||||
the requests time out. Alternatively, you can check the "Do not use Internet databases for
|
||||
dynamic lookups" option to temporarily block attempts to reach the Internet services.</a></p>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,8 @@
|
|||
/**
|
||||
* This package supports the automatic upgrader, that handles installing a freshly-downloaded
|
||||
* new build of YAAC over a previous build, Note that it runs out of separate JAR files not used by
|
||||
* YAAC, such that all YAAC files can be overwritten. If this was not the case, operating systems
|
||||
* such as Microsoft Windows (that prohibit deleting and overwriting files that are open for
|
||||
* read/execution) would not be able to be upgraded automatically.
|
||||
*/
|
||||
package org.ka2ddo.yaac.upgrade;
|
|
@ -0,0 +1,480 @@
|
|||
/*-------------------------------------------------------------------------
|
||||
| RXTX License v 2.1 - LGPL v 2.1 + Linking Over Controlled Interface.
|
||||
| RXTX is a native interface to serial ports in java.
|
||||
| Copyright 1997-2007 by Trent Jarvi tjarvi@qbang.org and others who
|
||||
| actually wrote it. See individual source files for more information.
|
||||
|
|
||||
| A copy of the LGPL v 2.1 may be found at
|
||||
| http://www.gnu.org/licenses/lgpl.txt on March 4th 2007. A copy is
|
||||
| here for your convenience.
|
||||
|
|
||||
| This library is free software; you can redistribute it and/or
|
||||
| modify it under the terms of the GNU Lesser General Public
|
||||
| License as published by the Free Software Foundation; either
|
||||
| version 2.1 of the License, or (at your option) any later version.
|
||||
|
|
||||
| This library 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
|
||||
| Lesser General Public License for more details.
|
||||
|
|
||||
| An executable that contains no derivative of any portion of RXTX, but
|
||||
| is designed to work with RXTX by being dynamically linked with it,
|
||||
| is considered a "work that uses the Library" subject to the terms and
|
||||
| conditions of the GNU Lesser General Public License.
|
||||
|
|
||||
| The following has been added to the RXTX License to remove
|
||||
| any confusion about linking to RXTX. We want to allow in part what
|
||||
| section 5, paragraph 2 of the LGPL does not permit in the special
|
||||
| case of linking over a controlled interface. The intent is to add a
|
||||
| Java Specification Request or standards body defined interface in the
|
||||
| future as another exception but one is not currently available.
|
||||
|
|
||||
| http://www.fsf.org/licenses/gpl-faq.html#LinkingOverControlledInterface
|
||||
|
|
||||
| As a special exception, the copyright holders of RXTX give you
|
||||
| permission to link RXTX with independent modules that communicate with
|
||||
| RXTX solely through the Sun Microsytems CommAPI interface version 2,
|
||||
| regardless of the license terms of these independent modules, and to copy
|
||||
| and distribute the resulting combined work under terms of your choice,
|
||||
| provided that every copy of the combined work is accompanied by a complete
|
||||
| copy of the source code of RXTX (the version of RXTX used to produce the
|
||||
| combined work), being distributed under the terms of the GNU Lesser General
|
||||
| Public License plus this exception. An independent module is a
|
||||
| module which is not derived from or based on RXTX.
|
||||
|
|
||||
| Note that people who make modified versions of RXTX are not obligated
|
||||
| to grant this special exception for their modified versions; it is
|
||||
| their choice whether to do so. The GNU Lesser General Public License
|
||||
| gives permission to release a modified version without this exception; this
|
||||
| exception also makes it possible to release a modified version which
|
||||
| carries forward this exception.
|
||||
|
|
||||
| You should have received a copy of the GNU Lesser General Public
|
||||
| License along with this library; if not, write to the Free
|
||||
| Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
| All trademarks belong to their respective owners.
|
||||
--------------------------------------------------------------------------*/
|
||||
package gnu.io;
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.lang.Math;
|
||||
|
||||
|
||||
/**
|
||||
* @author Trent Jarvi
|
||||
* @version $Id: Raw.java,v 1.1.2.17 2008-09-14 22:29:30 jarvi Exp $
|
||||
* @since JDK1.0
|
||||
*/
|
||||
|
||||
final class Raw extends RawPort {
|
||||
|
||||
static
|
||||
{
|
||||
SystemAPI.loadLibrary( "rxtxRaw" );
|
||||
Initialize();
|
||||
}
|
||||
|
||||
|
||||
/** Initialize the native library */
|
||||
private native static void Initialize();
|
||||
|
||||
|
||||
/** Actual RawPort wrapper class */
|
||||
|
||||
|
||||
/** Open the named port */
|
||||
public Raw( String name ) throws PortInUseException {
|
||||
ciAddress=Integer.parseInt(name);
|
||||
open( ciAddress );
|
||||
}
|
||||
private native int open( int ciAddress ) throws PortInUseException;
|
||||
|
||||
|
||||
/** File descriptor */
|
||||
private int ciAddress;
|
||||
|
||||
/** DSR flag **/
|
||||
static boolean dsrFlag = false;
|
||||
|
||||
/** Output stream */
|
||||
private final RawOutputStream out = new RawOutputStream();
|
||||
public OutputStream getOutputStream() { return out; }
|
||||
|
||||
|
||||
/** Input stream */
|
||||
private final RawInputStream in = new RawInputStream();
|
||||
public InputStream getInputStream() { return in; }
|
||||
|
||||
|
||||
|
||||
|
||||
/** Set the RawPort parameters */
|
||||
public void setRawPortParams( int b, int d, int s, int p )
|
||||
throws UnsupportedCommOperationException
|
||||
{
|
||||
nativeSetRawPortParams( b, d, s, p );
|
||||
speed = b;
|
||||
dataBits = d;
|
||||
stopBits = s;
|
||||
parity = p;
|
||||
}
|
||||
|
||||
/** Set the native Raw port parameters */
|
||||
private native void nativeSetRawPortParams( int speed, int dataBits,
|
||||
int stopBits, int parity ) throws UnsupportedCommOperationException;
|
||||
|
||||
/** Line speed in bits-per-second */
|
||||
private int speed=9600;
|
||||
public int getBaudRate() { return speed; }
|
||||
|
||||
/** Data bits port parameter */
|
||||
private int dataBits=DATABITS_8;
|
||||
public int getDataBits() { return dataBits; }
|
||||
|
||||
/** Stop bits port parameter */
|
||||
private int stopBits=RawPort.STOPBITS_1;
|
||||
public int getStopBits() { return stopBits; }
|
||||
|
||||
/** Parity port parameter */
|
||||
private int parity= RawPort.PARITY_NONE;
|
||||
public int getParity() { return parity; }
|
||||
|
||||
|
||||
/** Flow control */
|
||||
private int flowmode = RawPort.FLOWCONTROL_NONE;
|
||||
public void setFlowControlMode( int flowcontrol ) {
|
||||
try { setflowcontrol( flowcontrol ); }
|
||||
catch( IOException e ) {
|
||||
e.printStackTrace();
|
||||
return;
|
||||
}
|
||||
flowmode=flowcontrol;
|
||||
}
|
||||
public int getFlowControlMode() { return flowmode; }
|
||||
native void setflowcontrol( int flowcontrol ) throws IOException;
|
||||
|
||||
|
||||
/*
|
||||
linux/drivers/char/n_hdlc.c? FIXME
|
||||
taj@www.linux.org.uk
|
||||
*/
|
||||
/** Receive framing control
|
||||
*/
|
||||
public void enableReceiveFraming( int f )
|
||||
throws UnsupportedCommOperationException
|
||||
{
|
||||
throw new UnsupportedCommOperationException( "Not supported" );
|
||||
}
|
||||
public void disableReceiveFraming() {}
|
||||
public boolean isReceiveFramingEnabled() { return false; }
|
||||
public int getReceiveFramingByte() { return 0; }
|
||||
|
||||
|
||||
/** Receive timeout control */
|
||||
private int timeout = 0;
|
||||
|
||||
public native int NativegetReceiveTimeout();
|
||||
public native boolean NativeisReceiveTimeoutEnabled();
|
||||
public native void NativeEnableReceiveTimeoutThreshold(int time, int threshold,int InputBuffer);
|
||||
public void disableReceiveTimeout(){
|
||||
enableReceiveTimeout(0);
|
||||
}
|
||||
public void enableReceiveTimeout( int time ){
|
||||
if( time >= 0 ) {
|
||||
timeout = time;
|
||||
NativeEnableReceiveTimeoutThreshold( time , threshold, InputBuffer );
|
||||
}
|
||||
else {
|
||||
System.out.println("Invalid timeout");
|
||||
}
|
||||
}
|
||||
public boolean isReceiveTimeoutEnabled(){
|
||||
return(NativeisReceiveTimeoutEnabled());
|
||||
}
|
||||
public int getReceiveTimeout(){
|
||||
return(NativegetReceiveTimeout( ));
|
||||
}
|
||||
|
||||
/** Receive threshold control */
|
||||
|
||||
private int threshold = 0;
|
||||
|
||||
public void enableReceiveThreshold( int thresh ){
|
||||
if(thresh >=0)
|
||||
{
|
||||
threshold=thresh;
|
||||
NativeEnableReceiveTimeoutThreshold(timeout, threshold, InputBuffer);
|
||||
}
|
||||
else /* invalid thresh */
|
||||
{
|
||||
System.out.println("Invalid Threshold");
|
||||
}
|
||||
}
|
||||
public void disableReceiveThreshold() {
|
||||
enableReceiveThreshold(0);
|
||||
}
|
||||
public int getReceiveThreshold(){
|
||||
return threshold;
|
||||
}
|
||||
public boolean isReceiveThresholdEnabled(){
|
||||
return(threshold>0);
|
||||
}
|
||||
|
||||
/** Input/output buffers */
|
||||
/** FIXME I think this refers to
|
||||
FOPEN(3)/SETBUF(3)/FREAD(3)/FCLOSE(3)
|
||||
taj@www.linux.org.uk
|
||||
|
||||
These are native stubs...
|
||||
*/
|
||||
private int InputBuffer=0;
|
||||
private int OutputBuffer=0;
|
||||
public void setInputBufferSize( int size )
|
||||
{
|
||||
InputBuffer=size;
|
||||
}
|
||||
public int getInputBufferSize()
|
||||
{
|
||||
return(InputBuffer);
|
||||
}
|
||||
public void setOutputBufferSize( int size )
|
||||
{
|
||||
OutputBuffer=size;
|
||||
}
|
||||
public int getOutputBufferSize()
|
||||
{
|
||||
return(OutputBuffer);
|
||||
}
|
||||
|
||||
/** Line status methods */
|
||||
public native boolean isDTR();
|
||||
public native void setDTR( boolean state );
|
||||
public native void setRTS( boolean state );
|
||||
private native void setDSR( boolean state );
|
||||
public native boolean isCTS();
|
||||
public native boolean isDSR();
|
||||
public native boolean isCD();
|
||||
public native boolean isRI();
|
||||
public native boolean isRTS();
|
||||
|
||||
|
||||
/** Write to the port */
|
||||
public native void sendBreak( int duration );
|
||||
private native void writeByte( int b ) throws IOException;
|
||||
private native void writeArray( byte b[], int off, int len )
|
||||
throws IOException;
|
||||
private native void drain() throws IOException;
|
||||
|
||||
|
||||
/** Raw read methods */
|
||||
private native int nativeavailable() throws IOException;
|
||||
private native int readByte() throws IOException;
|
||||
private native int readArray( byte b[], int off, int len )
|
||||
throws IOException;
|
||||
|
||||
|
||||
/** Raw Port Event listener */
|
||||
private RawPortEventListener SPEventListener;
|
||||
|
||||
/** Thread to monitor data */
|
||||
private MonitorThread monThread;
|
||||
|
||||
/** Process RawPortEvents */
|
||||
native void eventLoop();
|
||||
private int dataAvailable=0;
|
||||
public void sendEvent( int event, boolean state ) {
|
||||
switch( event ) {
|
||||
case RawPortEvent.DATA_AVAILABLE:
|
||||
dataAvailable=1;
|
||||
if( monThread.Data ) break;
|
||||
return;
|
||||
case RawPortEvent.OUTPUT_BUFFER_EMPTY:
|
||||
if( monThread.Output ) break;
|
||||
return;
|
||||
/*
|
||||
if( monThread.DSR ) break;
|
||||
return;
|
||||
if (isDSR())
|
||||
{
|
||||
if (!dsrFlag)
|
||||
{
|
||||
dsrFlag = true;
|
||||
RawPortEvent e = new RawPortEvent(this, RawPortEvent.DSR, !dsrFlag, dsrFlag );
|
||||
}
|
||||
}
|
||||
else if (dsrFlag)
|
||||
{
|
||||
dsrFlag = false;
|
||||
RawPortEvent e = new RawPortEvent(this, RawPortEvent.DSR, !dsrFlag, dsrFlag );
|
||||
}
|
||||
*/
|
||||
case RawPortEvent.CTS:
|
||||
if( monThread.CTS ) break;
|
||||
return;
|
||||
case RawPortEvent.DSR:
|
||||
if( monThread.DSR ) break;
|
||||
return;
|
||||
case RawPortEvent.RI:
|
||||
if( monThread.RI ) break;
|
||||
return;
|
||||
case RawPortEvent.CD:
|
||||
if( monThread.CD ) break;
|
||||
return;
|
||||
case RawPortEvent.OE:
|
||||
if( monThread.OE ) break;
|
||||
return;
|
||||
case RawPortEvent.PE:
|
||||
if( monThread.PE ) break;
|
||||
return;
|
||||
case RawPortEvent.FE:
|
||||
if( monThread.FE ) break;
|
||||
return;
|
||||
case RawPortEvent.BI:
|
||||
if( monThread.BI ) break;
|
||||
return;
|
||||
default:
|
||||
System.err.println("unknown event:"+event);
|
||||
return;
|
||||
}
|
||||
RawPortEvent e = new RawPortEvent(this, event, !state, state );
|
||||
if( SPEventListener != null ) SPEventListener.RawEvent( e );
|
||||
}
|
||||
|
||||
/** Add an event listener */
|
||||
public void addEventListener( RawPortEventListener lsnr )
|
||||
throws TooManyListenersException
|
||||
{
|
||||
if( SPEventListener != null ) throw new TooManyListenersException();
|
||||
SPEventListener = lsnr;
|
||||
monThread = new MonitorThread();
|
||||
monThread.start();
|
||||
}
|
||||
/** Remove the Raw port event listener */
|
||||
public void removeEventListener() {
|
||||
SPEventListener = null;
|
||||
if( monThread != null ) {
|
||||
monThread.interrupt();
|
||||
monThread = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void notifyOnDataAvailable( boolean enable ) { monThread.Data = enable; }
|
||||
|
||||
public void notifyOnOutputEmpty( boolean enable ) { monThread.Output = enable; }
|
||||
|
||||
public void notifyOnCTS( boolean enable ) { monThread.CTS = enable; }
|
||||
public void notifyOnDSR( boolean enable ) { monThread.DSR = enable; }
|
||||
public void notifyOnRingIndicator( boolean enable ) { monThread.RI = enable; }
|
||||
public void notifyOnCarrierDetect( boolean enable ) { monThread.CD = enable; }
|
||||
public void notifyOnOverrunError( boolean enable ) { monThread.OE = enable; }
|
||||
public void notifyOnParityError( boolean enable ) { monThread.PE = enable; }
|
||||
public void notifyOnFramingError( boolean enable ) { monThread.FE = enable; }
|
||||
public void notifyOnBreakInterrupt( boolean enable ) { monThread.BI = enable; }
|
||||
|
||||
|
||||
/** Close the port */
|
||||
private native int nativeClose();
|
||||
public void close() {
|
||||
setDTR(false);
|
||||
setDSR(false);
|
||||
nativeClose();
|
||||
super.close();
|
||||
ciAddress = 0;
|
||||
}
|
||||
|
||||
|
||||
/** Finalize the port */
|
||||
protected void finalize() {
|
||||
close();
|
||||
}
|
||||
|
||||
|
||||
/** Inner class for RawOutputStream */
|
||||
class RawOutputStream extends OutputStream {
|
||||
public void write( int b ) throws IOException {
|
||||
writeByte( b );
|
||||
}
|
||||
public void write( byte b[] ) throws IOException {
|
||||
writeArray( b, 0, b.length );
|
||||
}
|
||||
public void write( byte b[], int off, int len ) throws IOException {
|
||||
writeArray( b, off, len );
|
||||
}
|
||||
public void flush() throws IOException {
|
||||
drain();
|
||||
}
|
||||
}
|
||||
|
||||
/** Inner class for RawInputStream */
|
||||
class RawInputStream extends InputStream {
|
||||
public int read() throws IOException {
|
||||
dataAvailable=0;
|
||||
return readByte();
|
||||
}
|
||||
public int read( byte b[] ) throws IOException
|
||||
{
|
||||
return read ( b, 0, b.length);
|
||||
}
|
||||
public int read( byte b[], int off, int len ) throws IOException
|
||||
{
|
||||
dataAvailable=0;
|
||||
int i=0, Minimum=0;
|
||||
int intArray[] =
|
||||
{
|
||||
b.length,
|
||||
InputBuffer,
|
||||
len
|
||||
};
|
||||
/*
|
||||
find the lowest nonzero value
|
||||
timeout and threshold are handled on the native side
|
||||
see NativeEnableReceiveTimeoutThreshold in
|
||||
RawImp.c
|
||||
*/
|
||||
while(intArray[i]==0 && i < intArray.length) i++;
|
||||
Minimum=intArray[i];
|
||||
while( i < intArray.length )
|
||||
{
|
||||
if(intArray[i] > 0 )
|
||||
{
|
||||
Minimum=Math.min(Minimum,intArray[i]);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
Minimum=Math.min(Minimum,threshold);
|
||||
if(Minimum == 0) Minimum=1;
|
||||
int Available=available();
|
||||
int Ret = readArray( b, off, Minimum);
|
||||
return Ret;
|
||||
}
|
||||
public int available() throws IOException {
|
||||
return nativeavailable();
|
||||
}
|
||||
}
|
||||
class MonitorThread extends Thread {
|
||||
/** Note: these have to be separate boolean flags because the
|
||||
RawPortEvent constants are NOT bit-flags, they are just
|
||||
defined as integers from 1 to 10 -DPL */
|
||||
private boolean CTS=false;
|
||||
private boolean DSR=false;
|
||||
private boolean RI=false;
|
||||
private boolean CD=false;
|
||||
private boolean OE=false;
|
||||
private boolean PE=false;
|
||||
private boolean FE=false;
|
||||
private boolean BI=false;
|
||||
private boolean Data=false;
|
||||
private boolean Output=false;
|
||||
MonitorThread() { }
|
||||
public void run() {
|
||||
eventLoop();
|
||||
}
|
||||
}
|
||||
public String getVersion()
|
||||
{
|
||||
String Version="$Id: Raw.java,v 1.1.2.17 2008-09-14 22:29:30 jarvi Exp $";
|
||||
return(Version);
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 448 B |
After Width: | Height: | Size: 21 KiB |
|
@ -0,0 +1,55 @@
|
|||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<title>Download Topographic Tiles</title>
|
||||
</head>
|
||||
<body lang="en-US" dir="LTR">
|
||||
<h1>Download Topographic Tiles</h1>
|
||||
<p>YAAC supports topographic data rendered from the NASA Digital Elevation Model.
|
||||
The <a name="NASA">NASA Digital Elevation Model data is retrieved from the online Data Pool,
|
||||
courtesy of the NASA EOSDIS Land Processes Distributed Active Archive Center
|
||||
(LP DAAC),</a> USGS Earth Resources Observation and Science (EROS) Center, Sioux
|
||||
Falls, South Dakota,
|
||||
<a href="https://lpdaac.usgs.gov/tools/data-pool/" target="NASA">https://lpdaac.usgs.gov/tools/data-pool/</a>.
|
||||
A downloader is built into YAAC for your convenience.
|
||||
All you have to specify what area of the planet you want to download,
|
||||
and YAAC will do it for you. Note you must have connectivity to the global Internet for
|
||||
this to work, and an account on the NASA Earthdata data pool web service.
|
||||
If you do not already have such an account, go to
|
||||
<a href="https://urs.earthdata.nasa.gov/" target="NASA">the
|
||||
User Registration Site</a> for Earthlink to create your free account, and then request
|
||||
access to the LP DAAC Data Pool application.</p>
|
||||
<p>Upon selecting the
|
||||
File->Topographic->Download Tiles option, a
|
||||
dialog box will be displayed showing what map data (if any) you have.</p>
|
||||
<img src="./downloadsrtm1.png" alt="dialog for selecting tiles to download"/>
|
||||
<p>The dialog shows yellow dots
|
||||
where you have data downloaded, and dark gray dots where you do not.
|
||||
The red box identifies the area that will be downloaded should you
|
||||
choose to proceed. You can move and resize the red box by changing
|
||||
the center latitude and longitude parameters, and the radius
|
||||
parameter.</p>
|
||||
<p>You can also choose to download higher resolution (1-arc-second) topographic
|
||||
data. Currently, this data is only available for the United States
|
||||
and its territories. Note the higher-resolution data will take up approximately 9 times
|
||||
as much disk space as the default 3-arc-second data that is available
|
||||
world-wide, and will also take longer to render topographic information
|
||||
on the map. Choose this only if you have a need for such precision data
|
||||
(such as for Search and Rescue operations).</p>
|
||||
<p>If you click “OK”, YAAC will bring
|
||||
up a progress dialog reporting how far through the download process
|
||||
you are and which file it is downloading at any given point in time, then prompt
|
||||
you for your Earthdata login credentials.
|
||||
The progress dialog will automatically go away when the download is complete.
|
||||
Note that, if you stop YAAC in the middle of the download, you will
|
||||
only have what was successfully downloaded up to that point. You can
|
||||
also prematurely cancel the download by clicking the “Cancel”
|
||||
button on the progress dialog. Note the cancel will wait until the
|
||||
current tile file is complete before actually stopping.</p>
|
||||
<img src="./downloadsrtm2.png" alt="download progress dialog"/>
|
||||
<p>Once the topographic elevation data is downloaded, you can choose how to display
|
||||
it from the <a href="./viewmaplayers.html#terrain">View->Layers...->Show Topography menu choice</a>,
|
||||
or use the elevation data in generating line-of-sight plots.</p>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE index
|
||||
PUBLIC "-//Sun Microsystems Inc.//DTD JavaHelp Index Version 2.0//EN"
|
||||
"http://java.sun.com/products/javahelp/index_2_0.dtd">
|
||||
<!--
|
||||
Copyright (C) 2011-2020 Andrew Pavlin, KA2DDO
|
||||
This file is part of YAAC (Yet Another APRS Client).
|
||||
|
||||
YAAC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
YAAC 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
|
||||
and GNU Lesser General Public License along with YAAC. If not,
|
||||
see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<index version="2.0">
|
||||
<indexitem text="configuring YAAC">
|
||||
<indexitem text="weather alerts" target="configure.WeatherAlert"/>
|
||||
</indexitem>
|
||||
<indexitem text="email">
|
||||
<indexitem text="weather alerts by" target="configure.WeatherAlert.email"/>
|
||||
</indexitem>
|
||||
<indexitem text="MADIS quality checks">
|
||||
<indexitem text="reporting quality failures" target="configure.WeatherAlert.MADIS"/>
|
||||
</indexitem>
|
||||
<indexitem text="speech">
|
||||
<indexitem text="weather alerts by" target="configure.WeatherAlert.speech"/>
|
||||
</indexitem>
|
||||
<indexitem text="weather">
|
||||
<indexitem text="alerts" target="configure.WeatherAlert"/>
|
||||
</indexitem>
|
||||
</index>
|
|
@ -0,0 +1,421 @@
|
|||
package org.ka2ddo.yaac.gui.filter;
|
||||
/*
|
||||
* Copyright (C) 2011-2020 Andrew Pavlin, KA2DDO
|
||||
* This file is part of YAAC (Yet Another APRS Client).
|
||||
*
|
||||
* YAAC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* YAAC 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
|
||||
* and GNU Lesser General Public License along with YAAC. If not,
|
||||
* see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import org.ka2ddo.yaac.ax25.*;
|
||||
import org.ka2ddo.yaac.aprs.*;
|
||||
import org.ka2ddo.yaac.YAAC;
|
||||
import org.ka2ddo.yaac.core.PacketExportMode;
|
||||
import org.ka2ddo.yaac.filter.Filter;
|
||||
import org.ka2ddo.yaac.filter.FilterChangeListener;
|
||||
import org.ka2ddo.yaac.gui.SaveableFilter;
|
||||
import org.ka2ddo.yaac.gui.table.FastComparableTableModel;
|
||||
import org.ka2ddo.yaac.gui.table.FastTableRowSorter;
|
||||
|
||||
import javax.swing.event.TableModelEvent;
|
||||
import javax.swing.event.TableModelListener;
|
||||
import javax.swing.filechooser.FileNameExtensionFilter;
|
||||
import javax.swing.*;
|
||||
import javax.swing.table.TableModel;
|
||||
import java.awt.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintStream;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* This filter accepts or rejects messages based on the last station to transmit
|
||||
* the message.
|
||||
* @author Andrew Pavlin, KA2DDO
|
||||
*/
|
||||
public class LastDigipeatFilterUI extends JPanel implements TableModel, DigipeatListener, SaveableFilter, Closeable, FilterChangeListener, FastComparableTableModel {
|
||||
private static final Class[] COLUMN_CLASSES = { String.class, Boolean.class, Integer.class };
|
||||
private static final String[] COLUMN_NAMES = new String[COLUMN_CLASSES.length];
|
||||
private final transient org.ka2ddo.yaac.filter.LastDigipeatFilter filter;
|
||||
final transient StationTracker TRACKER = StationTracker.getInstance();
|
||||
private final ArrayList<StationTracker.DigipeatStatistics> digiListCopy = new ArrayList<>();
|
||||
private final transient ArrayList<TableModelListener> listeners = new ArrayList<TableModelListener>();
|
||||
private static final java.util.List<RowSorter.SortKey> INITIAL_SORT = new ArrayList<RowSorter.SortKey>(1);
|
||||
private final MessageFormat mfNumDigis;
|
||||
private final JLabel lNumDigis;
|
||||
|
||||
static {
|
||||
INITIAL_SORT.add(new RowSorter.SortKey(0, SortOrder.ASCENDING));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a UI for controlling a LastDigipeatFilter
|
||||
* @param myFilter the LastDigipeatFilter instance to control
|
||||
* @see org.ka2ddo.yaac.filter.LastDigipeatFilter
|
||||
*/
|
||||
public LastDigipeatFilterUI(org.ka2ddo.yaac.filter.LastDigipeatFilter myFilter) {
|
||||
super(new BorderLayout());
|
||||
this.filter = myFilter;
|
||||
ResourceBundle msgBundle = YAAC.getMsgBundle();
|
||||
JPanel header = new JPanel(new FlowLayout(FlowLayout.CENTER));
|
||||
JButton bSetAll = new JButton(msgBundle.getString("SymbolFilter.setAll"));
|
||||
bSetAll.addActionListener(new ActionListener(){
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
boolean changed = false;
|
||||
int row = 0;
|
||||
for (StationTracker.DigipeatStatistics ds : filter.getDigipeatCache()) {
|
||||
if (!ds.enabled) {
|
||||
ds.enabled = true;
|
||||
changed = true;
|
||||
TableModelEvent tme = new TableModelEvent(LastDigipeatFilterUI.this, row, row, 1, TableModelEvent.UPDATE);
|
||||
for (TableModelListener l : listeners) {
|
||||
l.tableChanged(tme);
|
||||
}
|
||||
}
|
||||
row++;
|
||||
}
|
||||
if (changed) {
|
||||
filter.fireFilterChange(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
header.add(bSetAll);
|
||||
JButton bClearAll = new JButton(msgBundle.getString("SymbolFilter.clearAll"));
|
||||
bClearAll.addActionListener(new ActionListener() {
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
boolean changed = false;
|
||||
int row = 0;
|
||||
for (StationTracker.DigipeatStatistics ds : filter.getDigipeatCache()) {
|
||||
if (ds.enabled) {
|
||||
ds.enabled = false;
|
||||
changed = true;
|
||||
TableModelEvent tme = new TableModelEvent(LastDigipeatFilterUI.this, row, row, 1, TableModelEvent.UPDATE);
|
||||
for (TableModelListener l : listeners) {
|
||||
l.tableChanged(tme);
|
||||
}
|
||||
}
|
||||
row++;
|
||||
}
|
||||
if (changed) {
|
||||
filter.fireFilterChange(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
header.add(bClearAll);
|
||||
header.add(lNumDigis = new JLabel(" "));
|
||||
mfNumDigis = new MessageFormat(msgBundle.getString("filter.Digipeat.numDigis"));
|
||||
add(BorderLayout.NORTH, header);
|
||||
COLUMN_NAMES[0] = msgBundle.getString("filter.Digipeat.Station");
|
||||
COLUMN_NAMES[1] = msgBundle.getString("filter.Digipeat.Enabled");
|
||||
COLUMN_NAMES[2] = msgBundle.getString("filter.Digipeat.Usage");
|
||||
fillInDigiTotals();
|
||||
TRACKER.addRelayListener(this);
|
||||
digiListCopy.addAll(filter.getDigipeatCache());
|
||||
JTable table;
|
||||
add(BorderLayout.CENTER, new JScrollPane(table = new JTable(this)));
|
||||
RowSorter<LastDigipeatFilterUI> sorter;
|
||||
table.setRowSorter(sorter = new FastTableRowSorter<LastDigipeatFilterUI>(this));
|
||||
sorter.setSortKeys(INITIAL_SORT);
|
||||
filter.addFilterChangeListener(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of columns in the model.
|
||||
*
|
||||
* @return the number of columns in the model
|
||||
* @see #getRowCount
|
||||
*/
|
||||
public int getColumnCount() {
|
||||
return COLUMN_CLASSES.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of rows in the model.
|
||||
*
|
||||
* @return the number of rows in the model
|
||||
* @see #getColumnCount
|
||||
*/
|
||||
public int getRowCount() {
|
||||
return digiListCopy.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value for the cell at <code>columnIndex</code> and
|
||||
* <code>rowIndex</code>.
|
||||
*
|
||||
* @param rowIndex the row whose value is to be queried
|
||||
* @param columnIndex the column whose value is to be queried
|
||||
* @return the value Object at the specified cell
|
||||
*/
|
||||
public Object getValueAt(int rowIndex, int columnIndex) {
|
||||
StationTracker.DigipeatStatistics entry = filter.getDigipeatCache().get(rowIndex);
|
||||
switch (columnIndex) {
|
||||
case 0:
|
||||
return entry.getKey();
|
||||
case 1:
|
||||
return entry.getValue().enabled;
|
||||
case 2:
|
||||
return entry.getValue().numUsed;
|
||||
default:
|
||||
throw new IndexOutOfBoundsException("" + columnIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare the selected column of the two rows.
|
||||
*
|
||||
* @param rowIndex1 zero-based model row index of first row to compare
|
||||
* @param rowIndex2 zero-based model row index of second row to compare
|
||||
* @param columnIndex zero-based model column index of column to compare
|
||||
* @return +1 if the 1st row's column is after the 2nd row's column, -1 if before, or 0 if equal precedence
|
||||
*/
|
||||
public int compareRows(int rowIndex1, int rowIndex2, int columnIndex) {
|
||||
ArrayList<StationTracker.DigipeatStatistics> digipeatCache = filter.getDigipeatCache();
|
||||
StationTracker.DigipeatStatistics entry1 = digipeatCache.get(rowIndex1);
|
||||
StationTracker.DigipeatStatistics entry2 = digipeatCache.get(rowIndex2);
|
||||
switch (columnIndex) {
|
||||
case 0:
|
||||
return entry1.getKey().compareTo(entry2.getKey());
|
||||
case 1:
|
||||
return entry1.getValue().enabled ? (entry2.getValue().enabled ? 0 : -1) : (entry2.getValue().enabled ? +1 : 0);
|
||||
case 2:
|
||||
return entry1.getValue().numUsed - entry2.getValue().numUsed;
|
||||
default:
|
||||
throw new IndexOutOfBoundsException("" + columnIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data Class for the specified column.
|
||||
*
|
||||
* @param columnIndex the column being queried
|
||||
* @return the Class object for the column's data
|
||||
*/
|
||||
@Override
|
||||
public Class<?> getColumnClass(int columnIndex) {
|
||||
return COLUMN_CLASSES[columnIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the localized name for the column.
|
||||
*
|
||||
* @param column the column being queried
|
||||
* @return a string containing the name of <code>column</code>
|
||||
*/
|
||||
@Override
|
||||
public String getColumnName(int column) {
|
||||
return COLUMN_NAMES[column];
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCellEditable(int rowIndex, int columnIndex) {
|
||||
return 1 == columnIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
|
||||
StationTracker.DigipeatStatistics entry = filter.getDigipeatCache().get(rowIndex);
|
||||
switch (columnIndex) {
|
||||
case 1:
|
||||
entry.getValue().enabled = (Boolean)aValue;
|
||||
fireTableModelEvent(new TableModelEvent(LastDigipeatFilterUI.this, rowIndex, rowIndex,
|
||||
columnIndex, TableModelEvent.UPDATE));
|
||||
filter.fireFilterChange(true);
|
||||
fillInDigiTotals();
|
||||
break;
|
||||
case 0:
|
||||
case 2:
|
||||
default:
|
||||
throw new IndexOutOfBoundsException("" + columnIndex);
|
||||
}
|
||||
}
|
||||
|
||||
void fillInDigiTotals() {
|
||||
ArrayList<StationTracker.DigipeatStatistics> digiList = filter.getDigipeatCache();
|
||||
int size = digiList.size();
|
||||
int numEnabled = 0;
|
||||
for (int i = size - 1; i >= 0; i--) {
|
||||
if (digiList.get(i).enabled) {
|
||||
numEnabled++;
|
||||
}
|
||||
}
|
||||
lNumDigis.setText(mfNumDigis.format(new Object[]{numEnabled, size}));
|
||||
}
|
||||
|
||||
public void digipeaterAdded(final String digipeat) {
|
||||
SwingUtilities.invokeLater(new Runnable() {
|
||||
public void run() {
|
||||
final StationTracker.DigipeatStatistics rs;
|
||||
if ((rs = TRACKER.getDigipeaterStatisticsFor(digipeat)) != null) {
|
||||
int index;
|
||||
if ((index = digiListCopy.indexOf(rs)) < 0) {
|
||||
index = digiListCopy.size();
|
||||
digiListCopy.add(rs);
|
||||
}
|
||||
fireTableModelEvent(new TableModelEvent(LastDigipeatFilterUI.this, index, index,
|
||||
TableModelEvent.ALL_COLUMNS, TableModelEvent.INSERT));
|
||||
fillInDigiTotals();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void digipeaterUsedAgain(final String digipeat) {
|
||||
SwingUtilities.invokeLater(new Runnable() {
|
||||
public void run() {
|
||||
final StationTracker.DigipeatStatistics rs;
|
||||
int index;
|
||||
if ((rs = TRACKER.getDigipeaterStatisticsFor(digipeat)) != null) {
|
||||
if ((index = digiListCopy.indexOf(rs)) >= 0) {
|
||||
fireTableModelEvent(new TableModelEvent(LastDigipeatFilterUI.this, index, index,
|
||||
2, TableModelEvent.UPDATE));
|
||||
} else {
|
||||
index = digiListCopy.size();
|
||||
digiListCopy.add(rs);
|
||||
fireTableModelEvent(new TableModelEvent(LastDigipeatFilterUI.this, index, index,
|
||||
TableModelEvent.ALL_COLUMNS, TableModelEvent.INSERT));
|
||||
fillInDigiTotals();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate if this Filter is saveable. Meant for use by combining filters whose
|
||||
* sub-Filters may not all be savable.
|
||||
*
|
||||
* @return boolean true if the current Filter can be saved
|
||||
*/
|
||||
public boolean isSaveable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the preferred filetype for files saving this Filter's data set.
|
||||
*
|
||||
* @return FileNameExtensionFilter that will be used in the saving JFileChooser
|
||||
*/
|
||||
public FileNameExtensionFilter getPreferredFileType() {
|
||||
return new FileNameExtensionFilter(PacketExportMode.CSV.toString(), PacketExportMode.CSV.getFileType());
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the contents of the Filter to the specified DataOutput object.
|
||||
*
|
||||
* @param out DataOutput implementing object for writing the file contents in its preferred format
|
||||
* @throws java.io.IOException if the write failed for some reason
|
||||
*/
|
||||
public void saveFilterToFile(BufferedOutputStream out) throws IOException {
|
||||
JTable table = null;
|
||||
for (TableModelListener l : listeners) {
|
||||
if (l instanceof JTable) {
|
||||
table = (JTable)l;
|
||||
break;
|
||||
}
|
||||
}
|
||||
PrintStream ps = new PrintStream(out, false, "US-ASCII");
|
||||
StringBuilder b = new StringBuilder();
|
||||
for (int row = 0; row < getRowCount(); row++) {
|
||||
int sortedRow = table.convertRowIndexToModel(row);
|
||||
for (int col = 0; col < getColumnCount(); col++) {
|
||||
if (col != 0) {
|
||||
ps.print(",");
|
||||
}
|
||||
Object valueAt = getValueAt(sortedRow, col);
|
||||
if (null != valueAt) {
|
||||
if (valueAt instanceof String) {
|
||||
String v = (String)valueAt;
|
||||
if (v.indexOf('"') >= 0 || v.indexOf(',') >= 0 || v.indexOf('\'') >= 0 ||
|
||||
v.indexOf('-') >= 0 || v.indexOf('/') >= 0 || v.indexOf('*') >= 0 ||
|
||||
v.indexOf('+') >= 0) {
|
||||
// string needs to be quoted
|
||||
b.append('"');
|
||||
for (int i = 0; i < v.length(); i++) {
|
||||
char ch = v.charAt(i);
|
||||
if ('"' == ch) {
|
||||
b.append("\"\"");
|
||||
} else {
|
||||
b.append(ch);
|
||||
}
|
||||
}
|
||||
valueAt = b.append('"').toString();
|
||||
b.setLength(0);
|
||||
}
|
||||
}
|
||||
ps.print(valueAt);
|
||||
}
|
||||
}
|
||||
ps.println();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes this stream and releases any system resources associated
|
||||
* with it. If the stream is already closed then invoking this
|
||||
* method has no effect.
|
||||
*
|
||||
* @throws java.io.IOException if an I/O error occurs
|
||||
*/
|
||||
public void close() throws IOException {
|
||||
TRACKER.removeRelayListener(this);
|
||||
filter.removeFilterChangeListener(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the specified Filter's matching criteria have been changed.
|
||||
*
|
||||
* @param changedFilter Filter that has changed
|
||||
* @param changedByUser boolean true if change was manually made by user, false if
|
||||
* change was made automatically by dynamic filter logic
|
||||
*/
|
||||
public void filterSettingsChanged(Filter changedFilter, boolean changedByUser) {
|
||||
if (changedFilter == filter && changedByUser) {
|
||||
fireTableModelEvent(new TableModelEvent(this));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener to the list that is notified each time a change
|
||||
* to the data model occurs.
|
||||
*
|
||||
* @param l the TableModelListener
|
||||
*/
|
||||
public void addTableModelListener(TableModelListener l) {
|
||||
if (!listeners.contains(l)) {
|
||||
listeners.add(l);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a listener from the list that is notified each time a
|
||||
* change to the data model occurs.
|
||||
*
|
||||
* @param l the TableModelListener
|
||||
*/
|
||||
public void removeTableModelListener(TableModelListener l) {
|
||||
listeners.remove(l);
|
||||
}
|
||||
|
||||
void fireTableModelEvent(TableModelEvent event) {
|
||||
ArrayList<TableModelListener> listeners = this.listeners;
|
||||
for (int i = listeners.size() - 1; i >= 0; i--) {
|
||||
listeners.get(i).tableChanged(event);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
<?xml version="1.0" encoding="ISO-8859-1" ?>
|
||||
<!DOCTYPE helpset
|
||||
PUBLIC "-//Sun Microsystems Inc.//DTD JavaHelp HelpSet Version 2.0//EN"
|
||||
"http://java.sun.com/products/javahelp/helpset_2_0.dtd">
|
||||
|
||||
<helpset version="2.0">
|
||||
|
||||
<!-- title -->
|
||||
<title>ADS-B Port Driver</title>
|
||||
|
||||
<!-- maps -->
|
||||
<maps>
|
||||
<homeID>ADS-B</homeID>
|
||||
<mapref location="AdsBHelpMap.jhm"/>
|
||||
</maps>
|
||||
|
||||
<!-- views -->
|
||||
<view mergetype="javax.help.UniteAppendMerge">
|
||||
<name>TOC</name>
|
||||
<label>Table Of Contents</label>
|
||||
<type>javax.help.TOCView</type>
|
||||
<data>AdsBHelpTOC.xml</data>
|
||||
</view>
|
||||
|
||||
<view mergetype="javax.help.SortMerge">
|
||||
<name>Index</name>
|
||||
<label>Index</label>
|
||||
<type>javax.help.IndexView</type>
|
||||
<data>AdsBHelpIndex.xml</data>
|
||||
</view>
|
||||
|
||||
<presentation xml:lang="en" default="true" displayviewimages="false">
|
||||
<name>main window</name>
|
||||
<size width="700" height="400" />
|
||||
<location x="200" y="200" />
|
||||
<title>YAAC - Online Help</title>
|
||||
<image>icon</image>
|
||||
<toolbar>
|
||||
<helpaction>javax.help.BackAction</helpaction>
|
||||
<helpaction>javax.help.ForwardAction</helpaction>
|
||||
<helpaction>javax.help.SeparatorAction</helpaction>
|
||||
<helpaction>javax.help.HomeAction</helpaction>
|
||||
<helpaction>javax.help.ReloadAction</helpaction>
|
||||
<helpaction>javax.help.SeparatorAction</helpaction>
|
||||
<helpaction>javax.help.PrintAction</helpaction>
|
||||
<helpaction>javax.help.PrintSetupAction</helpaction>
|
||||
<helpaction>javax.help.SeparatorAction</helpaction>
|
||||
<helpaction>javax.help.FavoritesAction</helpaction>
|
||||
</toolbar>
|
||||
</presentation>
|
||||
</helpset>
|
After Width: | Height: | Size: 945 B |
After Width: | Height: | Size: 27 KiB |
|
@ -0,0 +1,31 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE toc
|
||||
PUBLIC "-//Sun Microsystems Inc.//DTD JavaHelp TOC Version 2.0//EN"
|
||||
"http://java.sun.com/products/javahelp/toc_2_0.dtd">
|
||||
<!--
|
||||
Copyright (C) 2011-2020 Andrew Pavlin, KA2DDO
|
||||
This file is part of YAAC (Yet Another APRS Client).
|
||||
|
||||
YAAC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
YAAC 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
|
||||
and GNU Lesser General Public License along with YAAC. If not,
|
||||
see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<toc version="2.0">
|
||||
<tocitem text="YAAC Legacy RXTX Port Drivers" image="toplevelfolder" target="legacyRXTX">
|
||||
<tocitem text="Serial TNC Port" target="configure.ports.SerialTNC"/>
|
||||
<tocitem text="Serial GPS Port" target="configure.ports.SerialGPS"/>
|
||||
<tocitem text="Kenwood radio with built-in TNC" target="configure.ports.Kenwood"/>
|
||||
<tocitem text="Yaesu radio with built-in TNC" target="configure.ports.Yaesu"/>
|
||||
<tocitem text="Peet Bros Weather Station" target="configure.ports.Weather"/>
|
||||
</tocitem>
|
||||
</toc>
|
|
@ -0,0 +1,58 @@
|
|||
<project name="BikeStats" basedir="." default="BIKESTATS_JAR">
|
||||
|
||||
<property name="YAAC.root" location=".."/>
|
||||
<property name="plugin.root" location="."/>
|
||||
<property name="YAAC.classes" location="${YAAC.root}/YAAC/target/classes"/>
|
||||
<property name="target" location="${plugin.root}/target"/>
|
||||
<property name="classes" location="${target}/classes"/>
|
||||
<property name="src" location="${plugin.root}/src/main/java"/>
|
||||
<property name="resrc" location="${plugin.root}/src/main/resources"/>
|
||||
|
||||
<property name="compiler" value="modern"/>
|
||||
<property name="source" value="1.6"/>
|
||||
<property name="lib.dir" value="${YAAC.root}/lib"/>
|
||||
<property name="client.jar" value="${YAAC.root}/YAAC/target/YAAC.jar"/>
|
||||
<property name="bikestats.jar" value="${plugin.root}/target/bikestats.jar"/>
|
||||
<property name="javadoc.dir" value="${plugin.root}/site/apidocs"/>
|
||||
<property environment="env"/>
|
||||
|
||||
<path id="YAAC.classpath">
|
||||
<pathelement path="${classes}"/>
|
||||
<pathelement path="${YAAC.classes}"/>
|
||||
<fileset dir="${lib.dir}" includes="*.jar"/>
|
||||
</path>
|
||||
|
||||
<target name="CLEAN">
|
||||
<delete dir="${classes}"/>
|
||||
<delete file="${bikestats.jar}"/>
|
||||
</target>
|
||||
|
||||
<target name="BIKESTATS_JAVAC">
|
||||
<mkdir dir="${classes}"/>
|
||||
<depend cache="depend_cache" srcdir="${src}" destdir="${classes}"/>
|
||||
<javac encoding="utf8" compiler="${compiler}" source="${source}" target="${source}" classpathref="YAAC.classpath" srcdir="${src}" debug="On" debuglevel="lines,vars,source" destdir="${classes}">
|
||||
<!--compilerarg value="-Xlint:unchecked"/-->
|
||||
</javac>
|
||||
</target>
|
||||
|
||||
<target name="BIKESTATS_COPY">
|
||||
<copy todir="${classes}/org/ka2ddo/yaac/bikestats">
|
||||
<fileset dir="${resrc}/org/ka2ddo/yaac/bikestats">
|
||||
</fileset>
|
||||
</copy>
|
||||
</target>
|
||||
|
||||
<target name="BIKESTATS_JAVADOC">
|
||||
<javadoc encoding="utf8" sourcepath="${src}"
|
||||
protected="yes" package="no" private="no"
|
||||
destdir="${javadoc.dir}" classpathref="YAAC.classpath">
|
||||
</javadoc>
|
||||
</target>
|
||||
|
||||
<target name="BIKESTATS_JAR" depends="BIKESTATS_JAVAC,BIKESTATS_COPY">
|
||||
<jar destfile="${bikestats.jar}" basedir="${classes}" update="No" manifest="${src}/../Manifest.txt">
|
||||
<include name="org/ka2ddo/yaac/**"/>
|
||||
</jar>
|
||||
</target>
|
||||
|
||||
</project>
|
After Width: | Height: | Size: 1.9 KiB |
|
@ -0,0 +1,518 @@
|
|||
package org.ka2ddo.yaac.gui.rastermap;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2011-2021 Andrew Pavlin, KA2DDO
|
||||
* This file is part of YAAC (Yet Another APRS Client).
|
||||
*
|
||||
* YAAC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* YAAC 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
|
||||
* and GNU Lesser General Public License along with YAAC. If not,
|
||||
* see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import org.apache.commons.imaging.FormatCompliance;
|
||||
import org.apache.commons.imaging.ImagingException;
|
||||
import org.apache.commons.imaging.common.bytesource.ByteSourceInputStream;
|
||||
import org.apache.commons.imaging.formats.tiff.TiffContents;
|
||||
import org.apache.commons.imaging.formats.tiff.TiffDirectory;
|
||||
import org.apache.commons.imaging.formats.tiff.TiffReader;
|
||||
import org.apache.commons.imaging.formats.tiff.constants.GeoTiffTagConstants;
|
||||
import org.ka2ddo.util.DebugCtl;
|
||||
import org.ka2ddo.util.ReschedulableTimer;
|
||||
import org.ka2ddo.util.ReschedulableTimerTask;
|
||||
import org.ka2ddo.yaac.YAAC;
|
||||
import org.ka2ddo.yaac.core.ErrorLogger;
|
||||
import org.ka2ddo.yaac.core.provider.CoreProvider;
|
||||
import org.ka2ddo.yaac.gui.GeographicalMap;
|
||||
import org.ka2ddo.util.NonshareableBufferedDataInputStream;
|
||||
import org.ka2ddo.yaac.io.NonshareableDataInputStream;
|
||||
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.table.AbstractTableModel;
|
||||
import java.awt.Color;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.prefs.Preferences;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
|
||||
/**
|
||||
* This class maintains a catalog of registered raster map images that can be superimposed
|
||||
* over the MapBean. Each image has its RasterMapEntry describing the image.
|
||||
* @see RasterMapEntry
|
||||
* @author Andrew Pavlin, KA2DDO
|
||||
*/
|
||||
public final class RasterMapCatalog extends AbstractTableModel {
|
||||
private static final Class[] COLUMN_CLASSES = { String.class, Boolean.class, Integer.class, Boolean.class };
|
||||
private static final String[] COLUMN_NAMES = new String[COLUMN_CLASSES.length];
|
||||
/**
|
||||
* Prefix of URLs used to download radar mosaics from US National Weather Service.
|
||||
*/
|
||||
public static final String HTTPS_RADAR_WEATHER_PREFIX = "https://mrms.ncep.noaa.gov/data/RIDGEII/L2/";
|
||||
/**
|
||||
* Name of Java Preferences node containing RasterMapEntry persistence information.
|
||||
*/
|
||||
public static final String RASTER_MAP_PREFS_NODE = "RasterMaps";
|
||||
static final int MIN_DELAY_BEFORE_REFRESH_MSEC = 300000;
|
||||
/**
|
||||
* Collection of raster images to overlay in the containing RasterMapOverlay.
|
||||
*/
|
||||
public ArrayList<RasterMapEntry> imageList = new ArrayList<RasterMapEntry>();
|
||||
final RasterMapOverlay rasterMapOverlay;
|
||||
final GeographicalMap geoMap;
|
||||
private static final ReschedulableTimer reloadTimer = new ReschedulableTimer("Raster Image reload timer");
|
||||
static class MapBounds {
|
||||
final double minLat, maxLat, minLon, maxLon;
|
||||
final String dirName;
|
||||
|
||||
MapBounds(double minLat, double maxLat, double minLon, double maxLon, String dirName) {
|
||||
this.minLat = minLat;
|
||||
this.maxLat = maxLat;
|
||||
this.minLon = minLon;
|
||||
this.maxLon = maxLon;
|
||||
this.dirName = dirName;
|
||||
}
|
||||
|
||||
boolean intersects(double minLat, double maxLat, double minLon, double maxLon) {
|
||||
return (maxLon > this.minLon) &&
|
||||
(maxLat > this.minLat) &&
|
||||
(this.maxLon > minLon) &&
|
||||
(this.maxLat > minLat);
|
||||
}
|
||||
}
|
||||
static final MapBounds[] MAP_REGIONS = {
|
||||
new MapBounds(24, 50, -128, -64, "CONUS"),
|
||||
new MapBounds(50, 72, -176, -126, "ALASKA"),
|
||||
new MapBounds(13, 15, 144, 146, "GUAM"),
|
||||
new MapBounds(15, 32, -180, -154, "HAWAII"),
|
||||
new MapBounds(10, 25, -90, -60, "CARIB")
|
||||
};
|
||||
|
||||
/**
|
||||
* TimerTask for reloading updated NWS weather raster.
|
||||
*/
|
||||
public transient ReschedulableTimerTask WXRASTER_TIMERTASK = new ReschedulableTimerTask() {
|
||||
public void run() {
|
||||
RasterMapEntry rme = null;
|
||||
try {
|
||||
if (CoreProvider.getInstance().isNonencryptedHTTPForced()) {
|
||||
throw new IOException("unable to access secure website, YAAC is configured to prohibit encryption");
|
||||
}
|
||||
// search for any pre-existing copy of the image
|
||||
long now = System.currentTimeMillis();
|
||||
Date expireTime = new Date(now);
|
||||
for (int idx = 0; idx < imageList.size(); idx++) {
|
||||
RasterMapEntry testRme = imageList.get(idx);
|
||||
if (testRme.enabled && testRme.pathname.startsWith(HTTPS_RADAR_WEATHER_PREFIX)) {
|
||||
rme = testRme;
|
||||
rme.pinpointList.clear();
|
||||
if (rme.imgCreationDate != null && expireTime.before(rme.imgCreationDate)) {
|
||||
expireTime.setTime(rme.imgCreationDate.getTime());
|
||||
} else {
|
||||
Date tmpExpireTime = loadReloadableWebMap(rme, now); // note completion of this enables the raster
|
||||
if (expireTime.before(tmpExpireTime)) {
|
||||
expireTime.setTime(tmpExpireTime.getTime());
|
||||
}
|
||||
}
|
||||
// have to keep looping as there might be more than one of them
|
||||
}
|
||||
}
|
||||
if (rme == null) {
|
||||
// see which ones we need based on the bounding box of our map
|
||||
final int oldNumImgs = imageList.size();
|
||||
double minLat = geoMap.getBottomLatitude();
|
||||
double maxLat = geoMap.getTopLatitude();
|
||||
double minLon = geoMap.getLeftLongitude();
|
||||
double maxLon = geoMap.getRightLongitude();
|
||||
for (MapBounds mb : MAP_REGIONS) {
|
||||
if (mb.intersects(minLat, maxLat, minLon, maxLon)) {
|
||||
rme = new RasterMapEntry();
|
||||
rme.pathname = HTTPS_RADAR_WEATHER_PREFIX + mb.dirName + "/CREF_QCD/";
|
||||
rme.isLinear = true;
|
||||
rme.keepImg = true;
|
||||
rme.enabled = true;
|
||||
rme.persist = true;
|
||||
imageList.add(rme);
|
||||
rme.writeToPreferences(YAAC.getPreferences().node(RASTER_MAP_PREFS_NODE));
|
||||
Date tmpExpireTime = loadReloadableWebMap(rme, now); // note completion of this enables the raster
|
||||
if (expireTime.before(tmpExpireTime)) {
|
||||
expireTime.setTime(tmpExpireTime.getTime());
|
||||
}
|
||||
}
|
||||
}
|
||||
final int newNumImgs = imageList.size();
|
||||
if (newNumImgs > oldNumImgs) {
|
||||
SwingUtilities.invokeLater(new Runnable() {
|
||||
public void run() {
|
||||
fireTableRowsInserted(oldNumImgs, newNumImgs-1);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// in case we need to cancel it
|
||||
if (rme != null) {
|
||||
rme.reloadTimerTask = this;
|
||||
}
|
||||
|
||||
// add the image to the raster list
|
||||
rasterMapOverlay.setShowRasterMaps(true);
|
||||
rasterMapOverlay.startRegenerate();
|
||||
geoMap.refresh();
|
||||
|
||||
// schedule fetching an update of the map
|
||||
if (expireTime.getTime() > now) {
|
||||
int delay = Math.max((int) (expireTime.getTime() - now + 5000 + Math.random() * 10000), MIN_DELAY_BEFORE_REFRESH_MSEC); // wait at least 3 minutes before trying
|
||||
if (DebugCtl.isDebug("raster")) System.out.println("scheduling NWS radar overlay reload of " + rme.pathname + " in " + delay + " millisec");
|
||||
resched(reloadTimer, delay);
|
||||
} else if (rme != null) {
|
||||
System.out.println(new Date().toString() + ": somehow reloadable maps are expired before download (" +
|
||||
expireTime.toString() + ')');
|
||||
}
|
||||
} catch (Exception e1) {
|
||||
if (rme != null) {
|
||||
ErrorLogger.reportError(geoMap, e1, MessageFormat.format(YAAC.getMsg("RasterMap.UnableToLoadImage"), rme.pathname));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a RasterMapCatalog instance for a given GeographicalMap window's RasterMapOverlay.
|
||||
* @param geoMap GeographicalMap using this instance of RasterMapCatalog
|
||||
* @param rasterMapOverlay RasterMapOverlay using this instance of RasterMapCatalog
|
||||
*/
|
||||
public RasterMapCatalog(GeographicalMap geoMap, final RasterMapOverlay rasterMapOverlay) {
|
||||
this.geoMap = geoMap;
|
||||
this.rasterMapOverlay = rasterMapOverlay;
|
||||
rasterMapOverlay.rasterMapCatalog = this;
|
||||
boolean hasEnabledRasters = false;
|
||||
Preferences rootNode = YAAC.getPreferences();
|
||||
try {
|
||||
if (rootNode.nodeExists(RASTER_MAP_PREFS_NODE)) {
|
||||
Preferences rasterNode = rootNode.node(RASTER_MAP_PREFS_NODE);
|
||||
for (String key : rasterNode.keys()) {
|
||||
byte[] data = rasterNode.getByteArray(key, null);
|
||||
if (data != null && data.length > 0) {
|
||||
final RasterMapEntry rme = new RasterMapEntry();
|
||||
rme.pathname = key;
|
||||
NonshareableDataInputStream dis = new NonshareableDataInputStream(new ByteArrayInputStream(data));
|
||||
rme.read(dis);
|
||||
dis.close();
|
||||
if (rme.pathname.startsWith("https:") ||
|
||||
rme.pathname.startsWith("http:")) {
|
||||
imageList.add(rme);
|
||||
if (rme.enabled) {
|
||||
hasEnabledRasters = true;
|
||||
|
||||
// run this in the background in case remote webserver is slow when YAAC is starting up
|
||||
if (!WXRASTER_TIMERTASK.isActive()) {
|
||||
WXRASTER_TIMERTASK.resched(reloadTimer, 5000); // wait 5 seconds to avoid collisions
|
||||
}
|
||||
}
|
||||
} else if (new File(rme.pathname).exists()) {
|
||||
imageList.add(rme);
|
||||
hasEnabledRasters |= rme.enabled;
|
||||
} else {
|
||||
System.out.println("saved RasterMapEntry cannot be restored because file is missing: " + key);
|
||||
rasterNode.remove(key);
|
||||
continue;
|
||||
}
|
||||
System.out.println(new Date().toString() + ": RasterMapCatalog: reloading persisted raster image " + rme);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace(System.out);
|
||||
}
|
||||
if (hasEnabledRasters) {
|
||||
rasterMapOverlay.setShowRasterMaps(true);
|
||||
rasterMapOverlay.startRegenerate();
|
||||
geoMap.refresh();
|
||||
}
|
||||
COLUMN_NAMES[0] = YAAC.getMsg("RasterMap.table.ImageName");
|
||||
COLUMN_NAMES[1] = YAAC.getMsg("RasterMap.table.Enable");
|
||||
COLUMN_NAMES[2] = YAAC.getMsg("RasterMap.table.Transparency");
|
||||
COLUMN_NAMES[3] = YAAC.getMsg("RasterMap.table.Persist");
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a fresh gzipped raster GeoTIFF image from a web source.
|
||||
* @param rme RasterMapEntry for the web-sourced overlay layer
|
||||
* @param now current time in milliseconds since Jan 1970 UTC
|
||||
* @return the Date when the loaded raster image will expire and should be loaded again
|
||||
* @throws IOException if image file cannot be downloaded
|
||||
*/
|
||||
public static Date loadReloadableWebMap(RasterMapEntry rme, long now) throws IOException {
|
||||
// temporarily show this as disabled to prevent render attempts before everything is loaded
|
||||
rme.enabled = false; // prevent loading race condition when map is re-rendered while still reading the world file
|
||||
Date expireTime;
|
||||
HttpURLConnection urlConnection = null;
|
||||
try {
|
||||
// load the directory listing to find the most recent image file
|
||||
long startTime = System.currentTimeMillis();
|
||||
URL dirUrl = new URL(rme.pathname);
|
||||
urlConnection = (HttpURLConnection) dirUrl.openConnection();
|
||||
urlConnection.setRequestProperty("User-Agent", CoreProvider.getInstance().getUserAgent());
|
||||
NonshareableBufferedDataInputStream dis = null;
|
||||
String preferredFile = null;
|
||||
byte[] buf = new byte[65536];
|
||||
try {
|
||||
if (urlConnection.getResponseCode() != HttpURLConnection.HTTP_OK) {
|
||||
throw new IOException("unable to read image directory");
|
||||
}
|
||||
if (!urlConnection.getContentType().startsWith("text/html")) {
|
||||
throw new IOException("unable to get directory listing of images");
|
||||
}
|
||||
dis = new NonshareableBufferedDataInputStream(urlConnection.getInputStream(), buf);
|
||||
String line;
|
||||
while ((line = dis.readLine()) != null) {
|
||||
int hrefPos;
|
||||
if ((hrefPos = line.indexOf("href=\"")) > 0) {
|
||||
int closeQuotePos = line.indexOf("\"", hrefPos + 8);
|
||||
preferredFile = line.substring(hrefPos+6, closeQuotePos);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (dis != null) {
|
||||
dis.close();
|
||||
}
|
||||
urlConnection.disconnect();
|
||||
urlConnection = null;
|
||||
}
|
||||
|
||||
// load the image into memory
|
||||
long afterDir = System.currentTimeMillis();
|
||||
URL url = new URL(rme.pathname + preferredFile);
|
||||
int gzPos = preferredFile.lastIndexOf(".gz");
|
||||
urlConnection = (HttpURLConnection) url.openConnection();
|
||||
urlConnection.setRequestProperty("User-Agent", CoreProvider.getInstance().getUserAgent());
|
||||
GZIPInputStream gzis = null;
|
||||
try {
|
||||
gzis = new GZIPInputStream(new NonshareableBufferedDataInputStream(urlConnection.getInputStream(), buf), 32768);
|
||||
ByteSourceInputStream bsis = new ByteSourceInputStream(gzis, preferredFile.substring(0, gzPos));
|
||||
TiffReader tiffReader = new TiffReader(false);
|
||||
HashMap<String, Object> imgParams = new HashMap<>();
|
||||
FormatCompliance formatCompliance = new FormatCompliance("reading NWS GeoTIFF");
|
||||
TiffContents tiffContents = tiffReader.readContents(bsis, imgParams, formatCompliance);
|
||||
long afterRead = System.currentTimeMillis();
|
||||
TiffDirectory tiffDirectory = tiffContents.directories.get(0);
|
||||
if (tiffDirectory.hasTiffImageData()) {
|
||||
rme.img = tiffDirectory.getTiffImage();
|
||||
rme.transparentColor = Color.BLACK;
|
||||
rme.keepImg = true;
|
||||
rme.pinpointList.clear();
|
||||
int width = rme.img.getWidth();
|
||||
int height = rme.img.getHeight();
|
||||
// get the GeoTIFF parameters for where this image is displayed on the world
|
||||
// tiepoints, [3]= UL X coordinate (deg), [4] = UL Y coordinate (deg)
|
||||
double[] tiepoints = tiffDirectory.getFieldValue(GeoTiffTagConstants.EXIF_TAG_MODEL_TIEPOINT_TAG, true);
|
||||
// degrees/pixel, [0] for X coord, [1] for Y coord
|
||||
double[] pixelScaleXYZ = tiffDirectory.getFieldValue(GeoTiffTagConstants.EXIF_TAG_MODEL_PIXEL_SCALE_TAG, true);
|
||||
double sLat = tiepoints[4] - height * pixelScaleXYZ[1]; // note we need to negate yDim
|
||||
double eLon = tiepoints[3] + width * pixelScaleXYZ[0];
|
||||
rme.pinpointList.add(new RasterMapEntry.PinPoint(0, 0, tiepoints[4], tiepoints[3]));
|
||||
rme.pinpointList.add(new RasterMapEntry.PinPoint(width, height, sLat, eLon));
|
||||
rme.isLinear = true;
|
||||
rme.enabled = true;
|
||||
long done = System.currentTimeMillis();
|
||||
System.out.println(new Date(done).toString() + ": downloading " + preferredFile + " took " + (afterDir-startTime) + "d+" + (afterRead-afterDir)
|
||||
+ "r+" + (done-afterRead) + "e msec");
|
||||
} else {
|
||||
throw new IOException("TIFF file doesn't contain any images");
|
||||
}
|
||||
|
||||
// headers of interest: Last-Modified (when image was created), Expires (when next image will be created)
|
||||
rme.imgCreationDate = new Date(urlConnection.getHeaderFieldDate("Last-Modified", now));
|
||||
expireTime = new Date(urlConnection.getHeaderFieldDate("Expires", now + 30000));
|
||||
System.out.println("at " + new Date().toString() + ", update time of NWS radar image is " + rme.imgCreationDate.toString() + ", expire=" + expireTime.toString());
|
||||
} catch (ImagingException ie) {
|
||||
throw new IOException("unable to read GeoTIFF image", ie);
|
||||
} finally {
|
||||
if (gzis != null) {
|
||||
gzis.close();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (urlConnection != null) {
|
||||
urlConnection.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
return expireTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the specified RasterMapEntry to the persisted list of raster map overlays.
|
||||
* @param rme RasterMapEntry to add
|
||||
* @return int index of newly added entry
|
||||
*/
|
||||
public int add(RasterMapEntry rme) {
|
||||
imageList.add(rme);
|
||||
if (rme.persist) {
|
||||
rme.writeToPreferences(YAAC.getPreferences().node(RASTER_MAP_PREFS_NODE));
|
||||
}
|
||||
return imageList.size() - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the indexed record from the persisted RasterMapCatalog.
|
||||
* @param index zero-based int index of the record to remove
|
||||
*/
|
||||
public void remove(int index) {
|
||||
RasterMapEntry rme = imageList.remove(index);
|
||||
if (rme.reloadTimerTask != null) {
|
||||
rme.reloadTimerTask.cancel();
|
||||
rme.reloadTimerTask = null;
|
||||
}
|
||||
YAAC.getPreferences().node(RASTER_MAP_PREFS_NODE).remove(rme.pathname);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data Class for the specified column.
|
||||
*
|
||||
* @param columnIndex the column being queried
|
||||
* @return the Class object for the column's data
|
||||
*/
|
||||
@Override
|
||||
public Class<?> getColumnClass(int columnIndex) {
|
||||
return COLUMN_CLASSES[columnIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the localized name for the column.
|
||||
*
|
||||
* @param column the column being queried
|
||||
* @return a string containing the name of <code>column</code>
|
||||
*/
|
||||
@Override
|
||||
public String getColumnName(int column) {
|
||||
return COLUMN_NAMES[column];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate which columns can be edited.
|
||||
*
|
||||
* @param rowIndex the row being queried
|
||||
* @param columnIndex the column being queried
|
||||
* @return false
|
||||
*/
|
||||
@Override
|
||||
public boolean isCellEditable(int rowIndex, int columnIndex) {
|
||||
return 1 <= columnIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a new value for an editable table cell.
|
||||
*
|
||||
* @param aValue value to assign to cell
|
||||
* @param rowIndex row of cell
|
||||
* @param columnIndex column of cell
|
||||
*/
|
||||
@Override
|
||||
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
|
||||
RasterMapEntry rme = imageList.get(rowIndex);
|
||||
switch (columnIndex) {
|
||||
case 1:
|
||||
Boolean aValue1 = (Boolean) aValue;
|
||||
if (aValue1.booleanValue() != rme.enabled) {
|
||||
rme.enabled = aValue1;
|
||||
if (rme.persist) {
|
||||
rme.writeToPreferences(YAAC.getPreferences().node(RASTER_MAP_PREFS_NODE));
|
||||
}
|
||||
fireTableCellUpdated(rowIndex, columnIndex);
|
||||
rasterMapOverlay.repaint();
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
rme.transparency = ((Number)aValue).intValue();
|
||||
if (rme.persist) {
|
||||
rme.writeToPreferences(YAAC.getPreferences().node(RASTER_MAP_PREFS_NODE));
|
||||
}
|
||||
fireTableCellUpdated(rowIndex, columnIndex);
|
||||
rasterMapOverlay.repaint();
|
||||
break;
|
||||
case 3:
|
||||
rme.persist = ((Boolean)aValue).booleanValue();
|
||||
if (rme.persist) {
|
||||
rme.writeToPreferences(YAAC.getPreferences().node(RASTER_MAP_PREFS_NODE));
|
||||
} else {
|
||||
rme.removePersistence(YAAC.getPreferences().node(RASTER_MAP_PREFS_NODE));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new InternalError("attempt to set value on readonly column#" + columnIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of columns in the model.
|
||||
*
|
||||
* @return the number of columns in the model
|
||||
* @see #getRowCount
|
||||
*/
|
||||
public int getColumnCount() {
|
||||
return COLUMN_CLASSES.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of rows in the model. A
|
||||
* <code>JTable</code> uses this method to determine how many rows it
|
||||
* should display. This method should be quick, as it
|
||||
* is called frequently during rendering.
|
||||
*
|
||||
* @return the number of rows in the model
|
||||
* @see #getColumnCount
|
||||
*/
|
||||
public int getRowCount() {
|
||||
return imageList.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value for the cell at <code>columnIndex</code> and
|
||||
* <code>rowIndex</code>.
|
||||
*
|
||||
* @param rowIndex the row whose value is to be queried
|
||||
* @param columnIndex the column whose value is to be queried
|
||||
* @return the value Object at the specified cell
|
||||
*/
|
||||
public Object getValueAt(int rowIndex, int columnIndex) {
|
||||
RasterMapEntry rme = imageList.get(rowIndex);
|
||||
switch (columnIndex) {
|
||||
case 0:
|
||||
return rme.pathname;
|
||||
case 1:
|
||||
return rme.enabled;
|
||||
case 2:
|
||||
return rme.transparency;
|
||||
case 3:
|
||||
return rme.persist;
|
||||
default:
|
||||
throw new IllegalArgumentException("columnIndex=" + columnIndex + " not between 0 and " + (getColumnCount()-1));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an entire RasterMapEntry from the catalog.
|
||||
* @param rowIndex zero-based row index into the catalog table model
|
||||
*/
|
||||
public void deleteRow(int rowIndex) {
|
||||
remove(rowIndex);
|
||||
fireTableRowsDeleted(rowIndex, rowIndex);
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 25 KiB |
|
@ -0,0 +1,739 @@
|
|||
package org.ka2ddo.yaac.srtm;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2011-2021 Andrew Pavlin, KA2DDO
|
||||
* This file is part of YAAC (Yet Another APRS Client).
|
||||
*
|
||||
* YAAC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* YAAC 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
|
||||
* and GNU Lesser General Public License along with YAAC. If not,
|
||||
* see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import org.ka2ddo.util.Base64;
|
||||
import org.ka2ddo.yaac.YAAC;
|
||||
import org.ka2ddo.yaac.core.ErrorLogger;
|
||||
import org.ka2ddo.yaac.core.FullGeoMapIfc;
|
||||
import org.ka2ddo.yaac.core.GuiIfc;
|
||||
import org.ka2ddo.yaac.core.StatusListener;
|
||||
import org.ka2ddo.yaac.core.UserAbort;
|
||||
import org.ka2ddo.yaac.core.provider.CoreProvider;
|
||||
import org.ka2ddo.yaac.io.NonshareableBufferedDataOutputStream;
|
||||
import org.ka2ddo.yaac.io.NonshareableCountingBufferedDataInputStream;
|
||||
import org.ka2ddo.util.DistanceUnit;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.text.MessageFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.StringTokenizer;
|
||||
|
||||
/**
|
||||
* This class manages an in-memory cache of SRTMTile objects, as well as
|
||||
* providing I/O code to fetch new tiles from the USGS website.
|
||||
* @author Andrew Pavlin, KA2DDO
|
||||
*/
|
||||
public class SRTMTileManager {
|
||||
// singleton, so don't allow anyone else to create it
|
||||
private SRTMTileManager() {}
|
||||
private static SRTMTileManager instance = new SRTMTileManager();
|
||||
|
||||
/**
|
||||
* Get the reference to the singleton SRTMTileManager object.
|
||||
* @return SRTMTileManager instance
|
||||
*/
|
||||
public static SRTMTileManager getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum size of tile cache in tiles (to keep memory usage under control).
|
||||
*/
|
||||
private static final int MAX_TILES = 64;
|
||||
|
||||
private final LinkedHashMap<SRTMTileId, SRTMTile> cache = new LinkedHashMap<SRTMTileId, SRTMTile>(MAX_TILES, 1.0F, true);
|
||||
private final File[] latitudeDirectoryCache = new File[180];
|
||||
private final File[][] longitudeFileCache = new File[180][];
|
||||
private File tileRootDirectory;
|
||||
|
||||
/**
|
||||
* Get a SRTMTile object for the 1-degree by 1-degree region of the planet requested.
|
||||
* @param latitude int latitude in millionths of degrees North
|
||||
* @param longitude int longitude in millionths of degrees East
|
||||
* @return SRTMTile object containing elevation data for the specified latitude/longitude
|
||||
*/
|
||||
public SRTMTile getTile(int latitude, int longitude) {
|
||||
return getTile(latitude * 0.000001, longitude * 0.000001);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a SRTMTile object for the 1-degree by 1-degree region of the planet requested.
|
||||
* @param latitude double latitude in fractional degrees North
|
||||
* @param longitude double longitude in fractional degrees East
|
||||
* @return SRTMTile object containing elevation data for the specified latitude/longitude
|
||||
*/
|
||||
public SRTMTile getTile(double latitude, double longitude) {
|
||||
int iLat = (int) Math.floor(latitude);
|
||||
int iLon = (int) Math.floor(longitude);
|
||||
SRTMTileId id = new SRTMTileId(iLat, iLon);
|
||||
SRTMTile tile;
|
||||
if ((tile = cache.get(id)) == null) {
|
||||
if (cache.size() > MAX_TILES || getEstimatedCacheSize() > Runtime.getRuntime().maxMemory() / 10) {
|
||||
Iterator<SRTMTile> it = cache.values().iterator();
|
||||
it.next();
|
||||
it.remove();
|
||||
}
|
||||
// need to load tile from file
|
||||
if (tileRootDirectory == null) {
|
||||
tileRootDirectory = new File(YAAC.getProps().getProperty(YAAC.OPT_OSM_TILE_DIR));
|
||||
}
|
||||
File zTileFile = getLongitudeFile(iLat, iLon, ".zip");
|
||||
if (zTileFile.exists()) {
|
||||
tile = new SRTMTile();
|
||||
try {
|
||||
tile.loadZip(zTileFile, id.latitude, id.longitude);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace(System.out);
|
||||
}
|
||||
cache.put(id, tile);
|
||||
} else {
|
||||
zTileFile = getLongitudeFile(iLat, iLon, ".gz");
|
||||
if (zTileFile.exists()) {
|
||||
tile = new SRTMTile();
|
||||
try {
|
||||
tile.loadGzip(zTileFile, id.latitude, id.longitude);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace(System.out);
|
||||
}
|
||||
cache.put(id, tile);
|
||||
} else {
|
||||
// remember an empty tile
|
||||
cache.put(id, tile = new SRTMTile());
|
||||
tile.setCoord(id);
|
||||
}
|
||||
}
|
||||
if (cache.size() > MAX_TILES || getEstimatedCacheSize() > Runtime.getRuntime().maxMemory() / 10) {
|
||||
Iterator<SRTMTile> it = cache.values().iterator();
|
||||
it.next();
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
return tile;
|
||||
}
|
||||
|
||||
private File getLatitudeDirectory(int latitude) {
|
||||
int dirIdx = Math.min(latitude + 90, latitudeDirectoryCache.length - 1);
|
||||
if (latitudeDirectoryCache[dirIdx] == null) {
|
||||
if (latitude < 0) {
|
||||
latitudeDirectoryCache[dirIdx] = new File(tileRootDirectory, "S" + (-latitude));
|
||||
} else {
|
||||
latitudeDirectoryCache[dirIdx] = new File(tileRootDirectory, "N" + latitude);
|
||||
}
|
||||
}
|
||||
return latitudeDirectoryCache[dirIdx];
|
||||
}
|
||||
|
||||
File getLongitudeFile(int latitude, int longitude, String suffix) {
|
||||
int latIdx = Math.min(latitude + 90, 180);
|
||||
int lonIdx = Math.min(longitude + 180, 359);
|
||||
File[][] longitudeFileCache1 = longitudeFileCache; // avoid getfield opcode
|
||||
if (longitudeFileCache1[latIdx] == null) {
|
||||
longitudeFileCache1[latIdx] = new File[360];
|
||||
}
|
||||
if (longitudeFileCache1[latIdx][lonIdx] == null ||
|
||||
!longitudeFileCache1[latIdx][lonIdx].getPath().endsWith(suffix)) {
|
||||
if (longitude < 0) {
|
||||
longitudeFileCache1[latIdx][lonIdx] = new File(getLatitudeDirectory(latitude), "W" + (-longitude) + ".hgt" + suffix);
|
||||
} else {
|
||||
longitudeFileCache1[latIdx][lonIdx] = new File(getLatitudeDirectory(latitude), "E" + longitude + ".hgt" + suffix);
|
||||
}
|
||||
}
|
||||
return longitudeFileCache1[latIdx][lonIdx];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SRTM elevation at the specified lat/lon coordinates.
|
||||
* @param latitude double latitude in fractional degrees North
|
||||
* @param longitude double longitude in fractional degrees East
|
||||
* @return elevation at the tile pixel for the specified coordinates,
|
||||
* or NO_CELL if there is no tile for the specified coordinates
|
||||
*/
|
||||
public short getElevation(double latitude, double longitude) {
|
||||
SRTMTile tile = getTile(latitude, longitude);
|
||||
if (tile.zCells == null) {
|
||||
return SRTMTile.NO_CELL;
|
||||
}
|
||||
int ix = (int)((longitude - tile.minLon) * (tile.numCellsPerAxis - 1) + 0.5);
|
||||
int iy = (int)((latitude - tile.minLat) * (tile.numCellsPerAxis - 1) + 0.5);
|
||||
return tile.zCells[iy][ix];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cached pointers to tile files. This is called when the configuration dialog
|
||||
* changes the tile root directory path.
|
||||
*/
|
||||
public void flushFileCache() {
|
||||
tileRootDirectory = null;
|
||||
for (int i = 0; i < latitudeDirectoryCache.length; i++) {
|
||||
latitudeDirectoryCache[i] = null;
|
||||
File[] lonFile = longitudeFileCache[i];
|
||||
if (lonFile != null) {
|
||||
for (int j = 0; j < lonFile.length; j++) {
|
||||
lonFile[j] = null;
|
||||
}
|
||||
longitudeFileCache[i] = null;
|
||||
}
|
||||
}
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an estimate of the amount of memory used by the SRTM tile cache.
|
||||
* @return estimated byte count used by tiles in memory
|
||||
*/
|
||||
public int getEstimatedCacheSize() {
|
||||
/* note this only counts up the size of the tiles, because they are by far the largest
|
||||
components of this cache. However, the latitudeDirectoryCache and longitudeDirectoryCache
|
||||
can also consume a visible portion of memory.
|
||||
*/
|
||||
int totalSize = 0;
|
||||
for (SRTMTile tile : cache.values()) {
|
||||
totalSize += tile.getSize() + 32; // 28 is estimated overhead for File object in longitudeFileCache, 4 is estimate for fraction of higher arrays' overhead
|
||||
}
|
||||
return totalSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download NASADEM tiles from the US Geological Survey's LP DAAC website to cover a specified region
|
||||
* of the planet. Note these are exactly the same format as SRTM1 tiles from the former unsecured USGS website.
|
||||
* @param radius radius to cover in kilometers
|
||||
* @param lat center latitude of region to download in fractional degrees North
|
||||
* @param lon center longitude of region to download in fractional degrees East
|
||||
* @param hiRes boolean true if 1-arc-second tiles should be downloaded where available (currently ignored)
|
||||
*/
|
||||
public static void loadPrecompiledTiles(double radius, double lat, double lon, final boolean hiRes) {
|
||||
final StatusListener pd = YAAC.getGui().createProgressReporter(YAAC.getMsg("menu.File.Topo.DownloadTiles.downloading"), false);
|
||||
double deltaLat = DistanceUnit.DEG_LAT.from(DistanceUnit.KM) * radius;
|
||||
final int minLat = (int)Math.max(Math.floor(lat - deltaLat), -90.0);
|
||||
final int maxLat = (int)Math.min(Math.ceil(lat + deltaLat), 90.0);
|
||||
final int minLon = (int)Math.max(Math.floor(lon - deltaLat), -180.0);
|
||||
final int maxLon = (int)Math.min(Math.ceil(lon + deltaLat), 180.0);
|
||||
Thread t = new Thread(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
String rootTilePath = YAAC.getProps().getProperty(YAAC.OPT_OSM_TILE_DIR);
|
||||
File tileDir = new File(rootTilePath);
|
||||
double tileStep = 100.0 / ((maxLat - minLat) * (maxLon - minLon));
|
||||
double progress = 0.0;
|
||||
long[] numBytesTransferred = new long[1];
|
||||
long[] totalTransferTime = new long[1];
|
||||
totalTransferTime[0] = 1; // to avoid divide-by-zero errors
|
||||
String[] authentication = new String[1];
|
||||
HashMap<String, Cookie> cookieMap = new HashMap<>();
|
||||
for (int bLat = minLat; bLat < maxLat; bLat++) {
|
||||
for (int bLon = minLon; bLon < maxLon; bLon++) {
|
||||
try {
|
||||
// try to download height file
|
||||
if (!downloadTileFile("e4ftl01.cr.usgs.gov", tileDir, bLat, bLon, hiRes, pd, numBytesTransferred, totalTransferTime, authentication, cookieMap)) {
|
||||
//TODO: what's the backup now?
|
||||
}
|
||||
} catch (IOException e1) {
|
||||
e1.printStackTrace(System.out);
|
||||
}
|
||||
|
||||
pd.updateProgress(progress);
|
||||
progress += tileStep;
|
||||
}
|
||||
}
|
||||
} catch (UserAbort userAbort) {
|
||||
ErrorLogger.reportError(pd, YAAC.getMsg("menu.File.Topo.DownloadTiles.abort"));
|
||||
} catch (Throwable e) {
|
||||
ErrorLogger.reportError(pd, e, YAAC.getMsg("menu.File.Topo.DownloadTiles.downloadException"));
|
||||
}
|
||||
instance.flushFileCache();
|
||||
YAAC.getGui().iterateOverAllMaps(new GuiIfc.FullMapFunctor() {
|
||||
public void applyToMap(FullGeoMapIfc map) {
|
||||
map.regenerateTopo();
|
||||
}
|
||||
});
|
||||
pd.setOperationComplete();
|
||||
}
|
||||
}, "SRTM Tile Copier");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
}
|
||||
|
||||
private static class Cookie {
|
||||
final String cookieName;
|
||||
final String cookieValue;
|
||||
String domain;
|
||||
boolean supportSubDomains;
|
||||
String path;
|
||||
long expires;
|
||||
boolean secure;
|
||||
|
||||
Cookie(String name, String value, String host, String path) {
|
||||
this.cookieName = name;
|
||||
this.cookieValue = value;
|
||||
this.domain = host.toLowerCase(Locale.ENGLISH);
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
boolean isURLCompatible(URL url, long now) {
|
||||
return (now < expires || expires == 0L) &&
|
||||
(!secure || url.getProtocol().equalsIgnoreCase("https")) &&
|
||||
(supportSubDomains ? url.getHost().toLowerCase(Locale.ENGLISH).endsWith(domain) : url.getHost().equalsIgnoreCase(domain)) &&
|
||||
url.getPath().startsWith(path);
|
||||
}
|
||||
|
||||
boolean isExpired(long now) {
|
||||
return !(now < expires || expires == 0L);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "[" + cookieName + '=' + cookieValue + "; Domain=" + domain + "; Path=" +
|
||||
path + (expires > 0L ? "; Expires=" + new Date(expires) : "") +
|
||||
(secure ? "; Secure" : "") + ']';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download one tile file (ways or nodes) from a specific tile directory.
|
||||
* @param hostIpAddress String of numeric IP address of webserver (to avoid repeated lookups)
|
||||
* @param tileDir root directory of the tile hierarchy
|
||||
* @param bLat latitude of tile (degrees only)
|
||||
* @param bLon longitude of tile (degrees only)
|
||||
* @param hiRes if boolean true, try to get 1-arc-second tiles when available
|
||||
* @param pd StatusListener to receive progress reports
|
||||
* @param numBytesTransferred OUT parameter to accumulate the number of data bytes transferred
|
||||
* @param totalTransferTime OUT parameter to accumulate the time spent transferring data (in milliseconds) @throws IOException if data cannot be transferred for some reason
|
||||
* @param authentication 1-element String array caching the HTTP authentication
|
||||
* information (so the user doesn't have to be prompted more
|
||||
* than once for their authentication information); can be null
|
||||
* if there is no context to query a human user for authentication info
|
||||
* @param cookieMap HashMap of cookie names to values for any cookies needed by
|
||||
* the USGS website's authentication mechanism
|
||||
*/
|
||||
static boolean downloadTileFile(String hostIpAddress,
|
||||
File tileDir,
|
||||
int bLat,
|
||||
int bLon,
|
||||
boolean hiRes,
|
||||
StatusListener pd,
|
||||
long[] numBytesTransferred,
|
||||
long[] totalTransferTime,
|
||||
String[] authentication,
|
||||
HashMap<String, Cookie> cookieMap) throws IOException {
|
||||
if (CoreProvider.getInstance().isNonencryptedHTTPForced()) {
|
||||
throw new IOException(YAAC.getMsg("HttpEncryptionProhibited"));
|
||||
}
|
||||
String inputFileName = String.format("%s%02d%s%03d", (bLat < 0 ? "s" : "n"), Math.abs(bLat), (bLon < 0 ? "w" :"e"), Math.abs(bLon)) + ".zip";
|
||||
pd.updateMessage(inputFileName + " (" + ((float)numBytesTransferred[0] / totalTransferTime[0]) + "Kb/sec)");
|
||||
int numOverloadTimes = 0;
|
||||
boolean notFound = true;
|
||||
File latDir = new File(tileDir, bLat < 0 ? "S" + (-bLat) : "N" + bLat);
|
||||
File lonFile = new File(latDir, (bLon < 0 ? "W" + (-bLon) : "E" + bLon) + ".hgt.zip");
|
||||
if (hiRes || !lonFile.exists()) {
|
||||
File oldLonFile = new File(latDir, (bLon < 0 ? "W" + (-bLon) : "E" + bLon) + ".hgt.zip");
|
||||
if (oldLonFile.exists()) {
|
||||
oldLonFile.delete();
|
||||
}
|
||||
URL serverURL = new URL("https", hostIpAddress, "/MEASURES/NASADEM_HGT.001/2000.02.11/NASADEM_HGT_" + inputFileName);
|
||||
do {
|
||||
long startTime = System.currentTimeMillis();
|
||||
long endTime;
|
||||
HttpURLConnection conn = (HttpURLConnection) serverURL.openConnection();
|
||||
conn.setInstanceFollowRedirects(false);
|
||||
try {
|
||||
conn.setRequestProperty("User-Agent", CoreProvider.getInstance().getUserAgent());
|
||||
if (authentication != null) {
|
||||
if (authentication[0] != null) {
|
||||
conn.setRequestProperty("Authorization", authentication[0]);
|
||||
}
|
||||
}
|
||||
if (cookieMap.size() > 0) {
|
||||
// build up a string of cookies to send, based on current context
|
||||
StringBuilder b = new StringBuilder();
|
||||
long now = System.currentTimeMillis();
|
||||
for (Iterator<Cookie> itCookie = cookieMap.values().iterator(); itCookie.hasNext(); ) {
|
||||
Cookie cookie = itCookie.next();
|
||||
if (cookie.isURLCompatible(serverURL, now)) {
|
||||
if (b.length() > 0) {
|
||||
b.append("; ");
|
||||
}
|
||||
b.append(cookie.cookieName).append('=').append(cookie.cookieValue);
|
||||
} else if (cookie.isExpired(now)) {
|
||||
itCookie.remove();
|
||||
}
|
||||
}
|
||||
if (b.length() > 0) { // found some usable cookies?
|
||||
conn.setRequestProperty("Cookie", b.toString());
|
||||
}
|
||||
}
|
||||
int responseCode = conn.getResponseCode();
|
||||
String responseMsg;
|
||||
if ((responseMsg = conn.getResponseMessage()) == null) {
|
||||
responseMsg = "";
|
||||
}
|
||||
{
|
||||
Map<String, List<String>> headerFields = conn.getHeaderFields();
|
||||
List<String> cookies = headerFields.get("Set-Cookie");
|
||||
if (cookies != null && cookies.size() > 0) {
|
||||
SimpleDateFormat sdfExpires = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z");
|
||||
String host = serverURL.getHost().toLowerCase(Locale.ENGLISH);
|
||||
String path = serverURL.getPath();
|
||||
onecookieloop:
|
||||
for (String cookieStr : cookies) {
|
||||
StringTokenizer tk = new StringTokenizer(cookieStr, ";");
|
||||
String cookieKV = tk.nextToken().trim();
|
||||
int delim = cookieKV.indexOf('=');
|
||||
Cookie cookie = new Cookie(cookieKV.substring(0, delim), cookieKV.substring(delim+1), host, path);
|
||||
while (tk.hasMoreTokens()) {
|
||||
String param = tk.nextToken().trim();
|
||||
delim = param.indexOf('=');
|
||||
String paramKey;
|
||||
String paramVal;
|
||||
if (delim > 0) {
|
||||
paramKey = param.substring(0, delim);
|
||||
paramVal = param.substring(delim+1);
|
||||
} else {
|
||||
paramKey = param;
|
||||
paramVal = null;
|
||||
}
|
||||
if (paramKey.equalsIgnoreCase("Expires")) {
|
||||
try {
|
||||
cookie.expires = sdfExpires.parse(paramVal).getTime();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace(System.out);
|
||||
continue onecookieloop; // this cookie is bad, so discard it
|
||||
}
|
||||
} else if (paramKey.equalsIgnoreCase("Max-Age")) {
|
||||
try {
|
||||
cookie.expires = System.currentTimeMillis() + Long.parseLong(paramVal) * 1000L;
|
||||
} catch (NumberFormatException e) {
|
||||
e.printStackTrace(System.out);
|
||||
continue onecookieloop; // this cookie is bad, so discard it
|
||||
}
|
||||
} else if (paramKey.equalsIgnoreCase("Domain")) {
|
||||
String domain = paramVal.toLowerCase(Locale.ENGLISH);
|
||||
if (domain.contains(".")) { // must be at least second-level
|
||||
if (!domain.startsWith(".")) {
|
||||
if (host.equals(domain) || host.endsWith("." + domain)) {
|
||||
cookie.domain = domain;
|
||||
cookie.supportSubDomains = true;
|
||||
} else {
|
||||
// bad cookie domain, so throw away the whole cookie
|
||||
System.out.println("bad domain \"" + domain + "\" for " + cookie);
|
||||
continue onecookieloop;
|
||||
}
|
||||
} else {
|
||||
if (host.endsWith(domain)) {
|
||||
cookie.domain = domain;
|
||||
cookie.supportSubDomains = true;
|
||||
} else {
|
||||
// bad cookie domain, so throw away the whole cookie
|
||||
System.out.println("bad domain \"" + domain + "\" for " + cookie);
|
||||
continue onecookieloop;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// bad cookie domain, so throw away the whole cookie
|
||||
System.out.println("bad domain \"" + domain + "\" for " + cookie);
|
||||
continue onecookieloop;
|
||||
}
|
||||
} else if (paramKey.equalsIgnoreCase("Path")) {
|
||||
cookie.path = paramVal;
|
||||
} else if (paramKey.equalsIgnoreCase("Secure")) {
|
||||
cookie.secure = true;
|
||||
// don't need to check for HttpOnly since this code doesn't execute webpage scripts
|
||||
}
|
||||
}
|
||||
cookieMap.put(cookie.cookieName, cookie);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||
if (!latDir.exists()) {
|
||||
if (!latDir.mkdirs()) {
|
||||
throw new IOException("unable to create tile latitude subdirectory " + latDir.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
NonshareableCountingBufferedDataInputStream cin = null;
|
||||
NonshareableBufferedDataOutputStream bos = null;
|
||||
long expectedSize = -1L;
|
||||
String sizeStr = conn.getRequestProperty("Content-Length");
|
||||
if (sizeStr != null && sizeStr.trim().length() > 0) {
|
||||
expectedSize = Long.parseLong(sizeStr.trim());
|
||||
}
|
||||
try {
|
||||
try {
|
||||
cin = new NonshareableCountingBufferedDataInputStream(conn.getInputStream(), 16384);
|
||||
bos = new NonshareableBufferedDataOutputStream(new FileOutputStream(lonFile, false), 16384);
|
||||
int b;
|
||||
while ((b = cin.read()) > -1) {
|
||||
bos.write(b);
|
||||
}
|
||||
} catch (EOFException e2) {
|
||||
// do nothing
|
||||
} finally {
|
||||
endTime = System.currentTimeMillis();
|
||||
if (cin != null) {
|
||||
cin.close();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (bos != null) {
|
||||
bos.close();
|
||||
}
|
||||
}
|
||||
if (cin != null) {
|
||||
long actualSize = cin.getByteCount();
|
||||
// check if we got everything
|
||||
if (expectedSize > 0 && actualSize != expectedSize) {
|
||||
System.out.println("error loading SRTMTile " + inputFileName + ", expected " + expectedSize + ", got " + actualSize);
|
||||
}
|
||||
numBytesTransferred[0] += actualSize;
|
||||
totalTransferTime[0] += endTime - startTime;
|
||||
try {
|
||||
// stall to keep from overloading USGS webserver
|
||||
Thread.sleep(1500L);
|
||||
} catch (InterruptedException e) {
|
||||
// ignore premature wakeup
|
||||
}
|
||||
notFound = false;
|
||||
}
|
||||
break;
|
||||
} else if (HttpURLConnection.HTTP_UNAUTHORIZED == responseCode) {
|
||||
// the new NASA/USGS server requires authentication
|
||||
if (authentication != null) {
|
||||
//TODO: do we really need to look for multiple headers of this type to find one we understand?
|
||||
String authReq = conn.getHeaderField("WWW-Authenticate");
|
||||
if (authReq != null && authReq.length() > 0) {
|
||||
String scheme = null;
|
||||
HashMap<String, String> paramMap = new HashMap<>();
|
||||
int pos1 = 0;
|
||||
int pos2sp, pos2c, pos2q, pos2eq;
|
||||
authReq = authReq.trim();
|
||||
while (pos1 < authReq.length()) {
|
||||
pos2sp = authReq.indexOf(' ', pos1);
|
||||
pos2c = authReq.indexOf(',', pos1);
|
||||
pos2eq = authReq.indexOf('=', pos1);
|
||||
if (pos2sp > 0) {
|
||||
if (pos2eq < pos2sp) {
|
||||
// parameter of some sort, may be quoted
|
||||
String paramKey = authReq.substring(pos1, pos2eq);
|
||||
pos1 = pos2eq + 1; // past the equals sign
|
||||
String paramVal;
|
||||
if (authReq.charAt(pos1) == '"') { // quoted?
|
||||
// scan until close quote
|
||||
pos1++;
|
||||
pos2q = pos1;
|
||||
while ((pos2q = authReq.indexOf('"', pos2q + 1)) > 0) {
|
||||
if (authReq.charAt(pos2q - 1) != '\\') {
|
||||
break; // wasn't an escaped quote
|
||||
}
|
||||
}
|
||||
paramVal = authReq.substring(pos1, pos2q).replace("\\", "");
|
||||
pos1 = pos2q + 1;
|
||||
if (pos1 < authReq.length() && authReq.charAt(pos1) == ',') {
|
||||
pos1++;
|
||||
}
|
||||
} else {
|
||||
if (pos2c > pos1) {
|
||||
paramVal = authReq.substring(pos1, pos2c);
|
||||
pos1 = pos2c + 1;
|
||||
} else {
|
||||
paramVal = authReq.substring(pos1);
|
||||
pos1 = authReq.length(); // end of line
|
||||
}
|
||||
}
|
||||
// skip any spaces before the next token
|
||||
while (pos1 < authReq.length() && authReq.charAt(pos1) == ' ') {
|
||||
pos1++;
|
||||
}
|
||||
paramMap.put(paramKey.toLowerCase(Locale.ENGLISH), paramVal);
|
||||
} else if (pos2c > 0 && pos2c < pos2sp) {
|
||||
// parameter without value
|
||||
paramMap.put(authReq.substring(pos1, pos2c).toLowerCase(Locale.ENGLISH), "");
|
||||
pos1 = pos2c + 1;
|
||||
// skip any spaces before the next token
|
||||
while (pos1 < authReq.length() && authReq.charAt(pos1) == ' ') {
|
||||
pos1++;
|
||||
}
|
||||
} else {
|
||||
// did we have a previous scheme?
|
||||
if (scheme != null) {
|
||||
//TODO: test if we support it
|
||||
if (buildAuthenticationToken(scheme, paramMap, authentication)) {
|
||||
scheme = null; // mark so we don't do it again
|
||||
break; // this challenge is good enough, use it
|
||||
}
|
||||
}
|
||||
// must be a scheme name
|
||||
scheme = authReq.substring(pos1, pos2sp);
|
||||
if (scheme.startsWith("\"")) {
|
||||
if (scheme.endsWith("\"")) {
|
||||
scheme = scheme.substring(1, scheme.length() - 1);
|
||||
} else {
|
||||
//TODO: we need to extract more text until we end the quoted and space delimited scheme name
|
||||
throw new UnsupportedOperationException("don't support authentication schemes with spaces in scheme name");
|
||||
}
|
||||
}
|
||||
paramMap.clear();
|
||||
pos1 = pos2sp + 1;
|
||||
// skip any spaces before the next token
|
||||
while (pos1 < authReq.length() && authReq.charAt(pos1) == ' ') {
|
||||
pos1++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (scheme != null) {
|
||||
// last scheme, so let's try it
|
||||
if (!buildAuthenticationToken(scheme, paramMap, authentication)) {
|
||||
ErrorLogger.reportError(pd, MessageFormat.format(YAAC.getMsg("menu.File.Topo.DownloadTiles.downloadError"),
|
||||
responseCode, inputFileName, responseMsg));
|
||||
break; // we don't know how to authenticate for this website
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// no source to ask user for authentication credentials, so just quit
|
||||
throw new IOException("unable to ask user to authenticate access to website");
|
||||
}
|
||||
} else if (HttpURLConnection.HTTP_MOVED_PERM == responseCode ||
|
||||
HttpURLConnection.HTTP_MOVED_TEMP == responseCode) {
|
||||
String newUrl = conn.getHeaderField("Location");
|
||||
System.out.println(inputFileName + " moved to " + newUrl);
|
||||
int startOfHost = newUrl.indexOf("://") + 3;
|
||||
if (startOfHost < 2) {
|
||||
// no scheme or host prefix, what to do?
|
||||
throw new IOException("redirect with incomplete URL: " + newUrl);
|
||||
}
|
||||
int endOfHost = newUrl.indexOf('/', startOfHost + 1);
|
||||
int altEndOfHost = newUrl.indexOf(':', startOfHost + 1);
|
||||
if (altEndOfHost > startOfHost && altEndOfHost < endOfHost) {
|
||||
endOfHost = altEndOfHost;
|
||||
}
|
||||
boolean looksLikeHostName = false;
|
||||
for (int i = startOfHost; i < endOfHost; i++) {
|
||||
if (Character.isLetter(newUrl.charAt(i))) {
|
||||
looksLikeHostName = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!looksLikeHostName) {
|
||||
// try swapping in the original hostname
|
||||
newUrl = newUrl.substring(0, startOfHost) + "dds.cr.usgs.gov" + newUrl.substring(endOfHost);
|
||||
System.out.println(" changed URL to " + newUrl);
|
||||
}
|
||||
serverURL = new URL(newUrl);
|
||||
continue;
|
||||
} else if (HttpURLConnection.HTTP_NOT_FOUND == responseCode) {
|
||||
try {
|
||||
// stall to keep from overloading USGS webserver
|
||||
Thread.sleep(1500L);
|
||||
} catch (InterruptedException e) {
|
||||
// ignore premature wakeup
|
||||
}
|
||||
} else if (HttpURLConnection.HTTP_UNAVAILABLE == responseCode) {
|
||||
try {
|
||||
// stall to keep from overloading USGS webserver (they must be too busy)
|
||||
Thread.sleep(1500L * (1 + numOverloadTimes));
|
||||
} catch (InterruptedException e) {
|
||||
// ignore premature wakeup
|
||||
}
|
||||
if (numOverloadTimes > 0) {
|
||||
ErrorLogger.reportError(pd, MessageFormat.format(YAAC.getMsg("menu.File.Topo.DownloadTiles.downloadError"),
|
||||
responseCode, inputFileName, responseMsg));
|
||||
}
|
||||
numOverloadTimes++;
|
||||
notFound = false;
|
||||
} else {
|
||||
ErrorLogger.reportError(pd, MessageFormat.format(YAAC.getMsg("menu.File.Topo.DownloadTiles.downloadError"),
|
||||
responseCode, inputFileName, responseMsg));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace(System.out);
|
||||
ErrorLogger.reportError(YAAC.getGui(), e, YAAC.getMsg("menu.File.Topo.DownloadTiles.downloadException"));
|
||||
break;
|
||||
} finally {
|
||||
if (conn != null) {
|
||||
conn.disconnect();
|
||||
}
|
||||
}
|
||||
break;
|
||||
} while (true);
|
||||
}
|
||||
return !notFound;
|
||||
}
|
||||
|
||||
private static boolean buildAuthenticationToken(String scheme, HashMap<String,String> paramMap, String[] authentication) {
|
||||
if (scheme.equalsIgnoreCase("Basic")) {
|
||||
GuiIfc gui;
|
||||
if ((gui = YAAC.getGui()) != null) {
|
||||
// get parameters from challenge
|
||||
String charset = paramMap.get("charset");
|
||||
if (charset == null) {
|
||||
charset = "UTF-8";
|
||||
}
|
||||
String realm = paramMap.get("realm");
|
||||
if (realm == null) {
|
||||
realm = "";
|
||||
}
|
||||
String title = paramMap.get("title");
|
||||
if (title == null) {
|
||||
title = realm;
|
||||
}
|
||||
|
||||
// ask the user for the username
|
||||
String username = gui.showGenericInputDialog("AuthenticationDialog.defaultTitle",
|
||||
MessageFormat.format(YAAC.getMsg("AuthenticationDialog.enterUsername"),
|
||||
title, realm));
|
||||
if (username == null) {
|
||||
return false; // aborted
|
||||
}
|
||||
|
||||
// ask the user for the password
|
||||
String password = gui.showGenericPasswordDialog("AuthenticationDialog.defaultTitle",
|
||||
MessageFormat.format(YAAC.getMsg("AuthenticationDialog.enterPassword"),
|
||||
title));
|
||||
if (password == null) {
|
||||
return false; // aborted
|
||||
}
|
||||
|
||||
// build the response
|
||||
try {
|
||||
authentication[0] = "Basic " + Base64.byteArrayToBase64((username + ':' + password).getBytes(charset));
|
||||
return true;
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
e.printStackTrace(System.out);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,200 @@
|
|||
package org.ka2ddo.yaac.legacyrxtx.io;
|
||||
/*
|
||||
* Copyright (C) 2011-2019 Andrew Pavlin, KA2DDO
|
||||
* This file is part of YAAC (Yet Another APRS Client).
|
||||
*
|
||||
* YAAC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* YAAC 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
|
||||
* and GNU Lesser General Public License along with YAAC. If not,
|
||||
* see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import gnu.io.RXTXPort;
|
||||
import gnu.io.SerialPort;
|
||||
import gnu.io.SerialPortEvent;
|
||||
import gnu.io.SerialPortEventListener;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.TooManyListenersException;
|
||||
|
||||
/**
|
||||
* This class is a wrapper around a RXTX SerialPort's SerialInputStream,
|
||||
* so that the SerialPort can be closed while a blocking read is outstanding
|
||||
* and properly abort the read.
|
||||
*/
|
||||
public class CloseableWhileReadingSerialInputStream extends InputStream implements SerialPortEventListener {
|
||||
private transient InputStream serialInputStream;
|
||||
private static final int BUFSIZE = 512;
|
||||
private final byte[] buf = new byte[BUFSIZE];
|
||||
/**
|
||||
* The last position in buf written by received byte.
|
||||
*/
|
||||
private transient int inPos = -1;
|
||||
/**
|
||||
* The last position to be passed on to the reader using this stream.
|
||||
*/
|
||||
private transient int outPos = -1;
|
||||
|
||||
/**
|
||||
* Create an InputStream that can be closed even if there are outstanding reads on a flow-control-blocked
|
||||
* or disconnected USB serial device.
|
||||
* @param serialPort SerialPort object identifying the port
|
||||
* @throws IOException if the stream from the port could not be opened
|
||||
* @throws TooManyListenersException if SerialPort is already in use by some other code
|
||||
*/
|
||||
public CloseableWhileReadingSerialInputStream(SerialPort serialPort) throws IOException, TooManyListenersException {
|
||||
serialInputStream = serialPort.getInputStream();
|
||||
serialPort.addEventListener(this);
|
||||
serialPort.notifyOnDataAvailable(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the next byte of data from the input stream. The value byte is
|
||||
* returned as an <code>int</code> in the range <code>0</code> to
|
||||
* <code>255</code>. If no byte is available because the end of the stream
|
||||
* has been reached, the value <code>-1</code> is returned. This method
|
||||
* blocks until input data is available, the end of the stream is detected,
|
||||
* or an exception is thrown.
|
||||
*
|
||||
* @return the next byte of data, or <code>-1</code> if the end of the
|
||||
* stream is reached.
|
||||
* @throws java.io.IOException if an I/O error occurs.
|
||||
*/
|
||||
public int read() throws IOException {
|
||||
while (true) {
|
||||
synchronized (this) {
|
||||
if (inPos >= 0 && inPos != outPos) {
|
||||
++outPos;
|
||||
if (outPos >= BUFSIZE) {
|
||||
outPos = 0;
|
||||
}
|
||||
int b = buf[outPos] & 0xFF;
|
||||
// in case this thread let a backlog build up
|
||||
notifyAll();
|
||||
return b;
|
||||
}
|
||||
}
|
||||
if (serialInputStream == null) {
|
||||
return -1; // file closed
|
||||
}
|
||||
synchronized (this) {
|
||||
try {
|
||||
wait(500L);
|
||||
} catch (InterruptedException e) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes this input stream and releases any system resources associated
|
||||
* with the stream.
|
||||
*
|
||||
* @throws java.io.IOException if an I/O error occurs.
|
||||
*/
|
||||
@Override
|
||||
public synchronized void close() throws IOException {
|
||||
synchronized (RXTXPort.class) {
|
||||
if (serialInputStream != null) {
|
||||
serialInputStream.close();
|
||||
serialInputStream = null;
|
||||
}
|
||||
}
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an estimate of the number of bytes that can be read (or
|
||||
* skipped over) from this input stream without blocking by the next
|
||||
* invocation of a method for this input stream. The next invocation
|
||||
* might be the same thread or another thread. A single read or skip of this
|
||||
* many bytes will not block, but may read or skip fewer bytes.
|
||||
*
|
||||
* @return an estimate of the number of bytes that can be read (or skipped
|
||||
* over) from this input stream without blocking or {@code 0} when
|
||||
* it reaches the end of the input stream.
|
||||
* @throws java.io.IOException if an I/O error occurs.
|
||||
*/
|
||||
@Override
|
||||
public int available() throws IOException {
|
||||
if (serialInputStream == null) {
|
||||
throw new IOException("stream closed");
|
||||
}
|
||||
int avail = inPos - outPos;
|
||||
if (avail < 0) {
|
||||
avail = BUFSIZE + avail;
|
||||
}
|
||||
return avail;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if this input stream supports the <code>mark</code> and
|
||||
* <code>reset</code> methods. Whether or not <code>mark</code> and
|
||||
* <code>reset</code> are supported is an invariant property of a
|
||||
* particular input stream instance. The <code>markSupported</code> method
|
||||
* returns <code>false</code>.
|
||||
*
|
||||
* @return <code>true</code> if this stream instance supports the mark
|
||||
* and reset methods; <code>false</code> otherwise.
|
||||
*/
|
||||
@Override
|
||||
public boolean markSupported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* DO NOT CALL. Callback listener method for asynchronous notification of input characters
|
||||
* being available.
|
||||
* @param ev SerialPortEvent describing the asynchronous event
|
||||
*/
|
||||
public void serialEvent(SerialPortEvent ev) {
|
||||
switch (ev.getEventType()) {
|
||||
case SerialPortEvent.DATA_AVAILABLE:
|
||||
while (!(-1 == inPos || (inPos + 1) % BUFSIZE != outPos)) {
|
||||
synchronized (this) {
|
||||
try {
|
||||
wait(500L);
|
||||
} catch (InterruptedException e) {
|
||||
// ignore this
|
||||
}
|
||||
}
|
||||
}
|
||||
if (serialInputStream != null) {
|
||||
try {
|
||||
int read = serialInputStream.read();
|
||||
synchronized (this) {
|
||||
if (-1 == read) {
|
||||
inPos = -1;
|
||||
outPos = -1;
|
||||
serialInputStream.close();
|
||||
serialInputStream = null;
|
||||
} else {
|
||||
++inPos;
|
||||
if (inPos >= BUFSIZE) {
|
||||
inPos = 0;
|
||||
}
|
||||
buf[inPos] = (byte) read;
|
||||
}
|
||||
notifyAll();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
break;
|
||||
default: // we don't care about other event types
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,420 @@
|
|||
package org.ka2ddo.yaac.io;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2011-2021 Andrew Pavlin, KA2DDO
|
||||
* This file is part of YAAC (Yet Another APRS Client).
|
||||
*
|
||||
* YAAC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* YAAC 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
|
||||
* and GNU Lesser General Public License along with YAAC. If not,
|
||||
* see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import jssc.SerialPort;
|
||||
import jssc.SerialPortEvent;
|
||||
import jssc.SerialPortEventListener;
|
||||
import org.ka2ddo.ax25.AX25Callsign;
|
||||
import org.ka2ddo.ax25.AX25Frame;
|
||||
import org.ka2ddo.util.ReschedulableTimer;
|
||||
import org.ka2ddo.util.ReschedulableTimerTask;
|
||||
import org.ka2ddo.yaac.core.HelpTaggedIOException;
|
||||
import org.ka2ddo.yaac.core.StringLogger;
|
||||
import org.ka2ddo.yaac.util.Localizer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.MessageFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.EnumSet;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* This class implements a receive-only port for listening to the non-standard command-mode
|
||||
* style APRS packet output of the Yaesu FTM-400DR and related radios, and to Kantronics
|
||||
* TNCs in command mode.
|
||||
* <p>The expected packet format is in two lines. Line 1 contains the AX.25 frame header
|
||||
* information in the format:</p>
|
||||
* <code><i>src</i>><i>dest</i>,<i>digi</i> [<i>MM</i>/<i>dd</i>/<i>yy</i> <i>HH</i>:<i>mm</i>:<i>ss</i>] <UI>:</code>
|
||||
* <p>where the ",<i>digi</i>" part is optional (if the packet wasn't specified for digipeating). The
|
||||
* Kantronics variation is the format:</p>
|
||||
* <code><i>src</i>><i>dest</i>,<i>digi</i>: <UI>:</code>
|
||||
* <p>where no date is provided, and an extra colon character is immediately
|
||||
* after the last digipeater (much like the APRS-IS/TNC2 format, <b>if</b> there
|
||||
* wasn't an encoding of the frame type and a line break stuck in between the header and
|
||||
* the body).</p>
|
||||
* <p>The second line is the body of the packet in plain text (should be ASCII, but this class
|
||||
* supports UTF-8 just in case). For APRS packets, this should just be a printable packet.</p>
|
||||
* @author Andrew Pavlin, KA2DDO
|
||||
*/
|
||||
public class YaesuConnector extends TNCConnector implements SerialPortEventListener {
|
||||
/**
|
||||
* The PortConnector type name for YaesuConnector.
|
||||
*/
|
||||
public static final String TYPE_NAME = "Yaesu";
|
||||
/**
|
||||
* The fully-qualified platform-wildcarded class name for the configuration editor for YaesuConnector.
|
||||
*/
|
||||
public static final String CONFIG_GUI = "org.ka2ddo.yaac.*.io.YaesuPortEditor";
|
||||
|
||||
private static final EnumSet<PortConfig.Fields> x_BLANK_FIELDS = EnumSet.noneOf(PortConfig.Fields.class);
|
||||
/**
|
||||
* Set of PortConfig field identifiers for fields that should not be copied from another system and should therefore be
|
||||
* blanked out when copying configuration files.
|
||||
*/
|
||||
public static final Set<PortConfig.Fields> BLANK_FIELDS = Collections.unmodifiableSet(x_BLANK_FIELDS);
|
||||
private static final IdentityHashMap<PortConfig.Fields, PortConfig.RequireHints> x_REQ_FIELDS = new IdentityHashMap<PortConfig.Fields, PortConfig.RequireHints>();
|
||||
/**
|
||||
* List of PortConfig field identifiers that are mandated for this port type to work, and the ResourceBundle key to
|
||||
* prompt for a value if missing.
|
||||
*/
|
||||
public static final Map<PortConfig.Fields, PortConfig.RequireHints> REQ_FIELDS = Collections.unmodifiableMap(x_REQ_FIELDS);
|
||||
static {
|
||||
x_BLANK_FIELDS.add(PortConfig.Fields.deviceName);
|
||||
x_REQ_FIELDS.put(PortConfig.Fields.deviceName, new PortConfig.RequireHints("ConfigImporter.serialPort", PortConfig.HintType.SERIAL_PORTS));
|
||||
x_BLANK_FIELDS.add(PortConfig.Fields.callsign);
|
||||
x_REQ_FIELDS.put(PortConfig.Fields.callsign, new PortConfig.RequireHints("ConfigImporter.callsign", PortConfig.HintType.CALLSIGN_SSID));
|
||||
}
|
||||
|
||||
private SerialPort serialPort;
|
||||
private transient String cachedToString = null;
|
||||
private byte[] rcvBuf = new byte[1024];
|
||||
private int wPtr = 0;
|
||||
transient AX25Frame rcvdFrame = null;
|
||||
private SimpleDateFormat sdfTimestamp;
|
||||
private transient StringLogger logOut = null;
|
||||
private static ReschedulableTimer yaesuTimer = new ReschedulableTimer("Yaesu packet flush timer");
|
||||
private ReschedulableTimerTask yaesuTimerTask = new ReschedulableTimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (rcvdFrame != null) {
|
||||
// pass built frame to AX25Stack for further analysis
|
||||
flushFrame();
|
||||
fireReceiving(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the correct defaults for a port in process of being created.
|
||||
* @param cfg PortConfig.Cfg whose defaults should be adjusted
|
||||
*/
|
||||
public static void fillinConfigDefaults(PortConfig.Cfg cfg) {
|
||||
if (cfg.baudRate == 0) {
|
||||
cfg.baudRate = 9600;
|
||||
cfg.passcode = "yy/dd/MM"; // presumably, the Yaesu factory default
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the configuration of the connector to match the updated
|
||||
* setup.
|
||||
*
|
||||
* @param portConfig PortConfig defining new port settings
|
||||
* @throws java.io.IOException if interface changes could not be applied
|
||||
* @throws IllegalArgumentException if type information is invalid for
|
||||
* changing the settings of this PortConnector
|
||||
*/
|
||||
@Override
|
||||
public void configure(PortConfig portConfig) throws IOException, IllegalArgumentException {
|
||||
if (!TYPE_NAME.equals(portConfig.portType)) {
|
||||
throw new IllegalArgumentException("invalid portType=" + portConfig.portType);
|
||||
}
|
||||
if (serialPort != null) {
|
||||
this.close();
|
||||
}
|
||||
setPortConfig(portConfig);
|
||||
try {
|
||||
cachedToString = null;
|
||||
if (portConfig.enabled) {
|
||||
if (serialPort == null) {
|
||||
serialPort = new SerialPort(currentCfg.deviceName);
|
||||
serialPort.openPort();
|
||||
serialPort.addEventListener(this, SerialPort.MASK_RXCHAR | SerialPort.MASK_ERR);
|
||||
}
|
||||
serialPort.setParams(currentCfg.baudRate,
|
||||
SerialPort.DATABITS_8,
|
||||
SerialPort.STOPBITS_1,
|
||||
SerialPort.PARITY_NONE);
|
||||
serialPort.setDTR(true);
|
||||
serialPort.setRTS(true);
|
||||
|
||||
if (currentCfg.passcode == null || currentCfg.passcode.length() == 0) {
|
||||
currentCfg.passcode = "yy/dd/MM"; // presumed factory default
|
||||
}
|
||||
sdfTimestamp = new SimpleDateFormat(currentCfg.passcode);
|
||||
|
||||
if (currentCfg.callsign.length() > 0) {
|
||||
setPortAddress(new AX25Callsign(currentCfg.callsign));
|
||||
}
|
||||
if (currentCfg.flowControlled) {
|
||||
logOut = new StringLogger("Yaesu-", "yyyyMMMdd-HH", ".txt");
|
||||
logOut.setFlushInterval(300);
|
||||
}
|
||||
PortManager.firePortOpened(this);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new HelpTaggedIOException(MessageFormat.format(Localizer.getMsg("dialog.IOError.unableToOpen"), TYPE_NAME, currentCfg.deviceName), e, "FAQ.oldTNC");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports whether this PortConnector has an open connection to its port.
|
||||
*
|
||||
* @return boolean true if PortConnector is open
|
||||
*/
|
||||
public boolean isOpen() {
|
||||
return serialPort != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shut down this port connection. Expected to be overridden by sub-classes.
|
||||
*/
|
||||
@Override
|
||||
public void close() {
|
||||
if (serialPort != null) {
|
||||
try {
|
||||
serialPort.closePort();
|
||||
serialPort = null;
|
||||
} catch (Exception e) {
|
||||
System.out.print("Error closing Yaesu input stream: ");
|
||||
e.printStackTrace(System.out);
|
||||
}
|
||||
}
|
||||
cachedToString = null;
|
||||
PortManager.firePortClosed(this);
|
||||
fireReceiving(false);
|
||||
if (logOut != null) {
|
||||
logOut.shutdown();
|
||||
logOut = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify what capabilities a port of this type has.
|
||||
*
|
||||
* @return bitmask of capability flags
|
||||
* @see #CAP_RCV_PACKET_DATA
|
||||
* @see #CAP_XMT_PACKET_DATA
|
||||
* @see #CAP_FULL_DUPLEX
|
||||
* @see #CAP_GPS_DATA
|
||||
* @see #CAP_HF
|
||||
* @see #CAP_IGATE
|
||||
* @see #CAP_OPENTRAC
|
||||
* @see #CAP_RF
|
||||
* @see #CAP_WAYPOINT_SENDER
|
||||
* @see #CAP_WEATHER
|
||||
*/
|
||||
@Override
|
||||
public int getCapabilities() {
|
||||
return CAP_RCV_PACKET_DATA | CAP_RF;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process incoming serial port event.
|
||||
* @param event SerialPortEvent describing the handling needed
|
||||
*/
|
||||
public void serialEvent(SerialPortEvent event) {
|
||||
if (event.getEventType() == SerialPortEvent.RXCHAR) {
|
||||
int newData;
|
||||
try {
|
||||
while (serialPort != null && serialPort.getInputBufferBytesCount() > 0) {
|
||||
newData = serialPort.readBytes(1)[0] & 0xFF;
|
||||
switch (newData) {
|
||||
case '\n':
|
||||
if (wPtr == 0) {
|
||||
break;
|
||||
}
|
||||
// must be a radio that does Unix EOLs instead of Microsoft
|
||||
case '\r':
|
||||
String line = new String(rcvBuf, 0, wPtr, StandardCharsets.UTF_8);
|
||||
if (logOut != null) {
|
||||
logOut.log(line);
|
||||
}
|
||||
// is this the first line or second+ line of the Yaesu report?
|
||||
if (rcvdFrame == null || looksLikeFirstLine(line)) {
|
||||
try {
|
||||
if (rcvdFrame != null) {
|
||||
flushFrame();
|
||||
}
|
||||
// line format expected to be src>dest,digi,... [MM/dd/yy HH:mm:ss] <pkttype>:
|
||||
// reject line if format not correct
|
||||
int dstPos = line.indexOf('>');
|
||||
int kamEndOfDigiPos = line.indexOf(':', dstPos + 3);
|
||||
int datePos = line.indexOf('[', dstPos + 3);
|
||||
int endOfDigiPos = datePos > 0 ? datePos - 1 : kamEndOfDigiPos;
|
||||
int pidPos = line.indexOf('<', datePos + 17);
|
||||
// we think we can decode this
|
||||
AX25Frame f = new AX25Frame();
|
||||
f.sender = new AX25Callsign(line.substring(0, dstPos));
|
||||
int digiPos = line.indexOf(',', dstPos + 3);
|
||||
if (digiPos > dstPos && digiPos < dstPos + 10 && digiPos < endOfDigiPos) {
|
||||
f.dest = new AX25Callsign(line.substring(dstPos + 1, digiPos));
|
||||
int numDigis = 1;
|
||||
int nextDigiPos = digiPos + 1;
|
||||
while ((nextDigiPos = line.indexOf(',', nextDigiPos + 1)) > 0 && nextDigiPos < endOfDigiPos) {
|
||||
numDigis++;
|
||||
}
|
||||
f.digipeaters = new AX25Callsign[numDigis];
|
||||
for (int i = 0; i < numDigis; i++) {
|
||||
nextDigiPos = line.indexOf(',', digiPos + 1);
|
||||
String digi;
|
||||
if (nextDigiPos > digiPos) {
|
||||
digi = line.substring(digiPos + 1, nextDigiPos);
|
||||
digiPos = nextDigiPos; // move forward to next digi
|
||||
} else {
|
||||
digi = line.substring(digiPos + 1, endOfDigiPos).trim();
|
||||
}
|
||||
boolean hasBeenRepeated = false;
|
||||
if (digi.endsWith("*")) {
|
||||
hasBeenRepeated = true;
|
||||
digi = digi.substring(0, digi.length() - 1);
|
||||
}
|
||||
f.digipeaters[i] = new AX25Callsign(digi);
|
||||
if (hasBeenRepeated) {
|
||||
for (int j = 0; j <= i; j++) {
|
||||
f.digipeaters[j].h_c = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
f.dest = new AX25Callsign(line.substring(dstPos + 1, endOfDigiPos).trim());
|
||||
}
|
||||
if (datePos > 0) { // we have a Yaesu
|
||||
int endDatePos = line.indexOf(']', datePos + 1);
|
||||
Date rcvTime = sdfTimestamp.parse(line.substring(datePos + 1, endDatePos));
|
||||
f.rcptTime = rcvTime.getTime();
|
||||
} else { // we probably have a Kantronics TNC in command mode
|
||||
f.rcptTime = System.currentTimeMillis();
|
||||
}
|
||||
int endPidPod = line.indexOf('>', pidPos + 1);
|
||||
int midPidPos = line.indexOf(' ', pidPos + 1);
|
||||
String pidStr = line.substring(pidPos + 1, midPidPos > pidPos && midPidPos < endPidPod ? midPidPos : endPidPod);
|
||||
int uType;
|
||||
if ((uType = AX25Frame.findUTypeByName(pidStr)) >= 0) {
|
||||
f.ctl = (byte) ((uType << AX25Frame.SHIFT_UTYPE) | AX25Frame.FRAMETYPE_U);
|
||||
f.pid = AX25Frame.PID_NOLVL3;
|
||||
} else if (pidStr.equals("I")) {
|
||||
f.ctl = AX25Frame.FRAMETYPE_I;
|
||||
f.pid = AX25Frame.PID_NOLVL3;
|
||||
} else {
|
||||
uType = AX25Frame.findSTypeByName(pidStr);
|
||||
if (uType >= 0) {
|
||||
f.ctl = (byte)(AX25Frame.FRAMETYPE_S | ((uType << AX25Frame.SHIFT_STYPE)));
|
||||
} else {
|
||||
f.ctl = AX25Frame.FRAMETYPE_S; // this is weird that it didn't have a STYPE
|
||||
}
|
||||
}
|
||||
if (midPidPos > 0 && midPidPos < endPidPod - 1 && "R".equals(line.substring(midPidPos + 1, endPidPod).trim())) {
|
||||
f.dest.h_c = true;
|
||||
f.sender.h_c = false;
|
||||
} else {
|
||||
f.dest.h_c = false;
|
||||
f.sender.h_c = true;
|
||||
}
|
||||
AX25Frame.initializeCmd(f);
|
||||
f.sourcePort = this;
|
||||
rcvdFrame = f;
|
||||
|
||||
// ensure we flush this packet in a reasonable amount of time, instead
|
||||
// of waiting for the next packet to push it along
|
||||
if (serialPort != null) { // only do this for real-time serial data, not file playback
|
||||
yaesuTimerTask.resched(yaesuTimer, (int) ((256 * RF_SEND_TIME_PER_BYTE_9600 * currentCfg.baudRate) / 7680)); // pad for async stop bits
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.out.println(new Date().toString() + ": unable to handle " + (wPtr - 1) + "-byte frame:");
|
||||
StringBuilder b = new StringBuilder(wPtr * 3);
|
||||
for (int i = 0; i < wPtr; i++) {
|
||||
b.append(' ').append(Integer.toString(rcvBuf[i] & 0xFF, 16));
|
||||
}
|
||||
System.out.println(b.toString());
|
||||
e.printStackTrace(System.out);
|
||||
}
|
||||
} else {
|
||||
// finish filling in the frame body
|
||||
if (line.length() > 0) {
|
||||
if (rcvdFrame.body == null) {
|
||||
rcvdFrame.body = line.getBytes(StandardCharsets.UTF_8);
|
||||
} else {
|
||||
// some dingbat sent an APRS packet with an embedded line break
|
||||
|
||||
// problem with this is that last packet will be delayed until
|
||||
// first line of next packet arrives, so we have the timer to
|
||||
// encourage flushing, since there is no reason for the radio to
|
||||
// delay later lines in the same packet
|
||||
byte[] extension = line.getBytes(StandardCharsets.UTF_8);
|
||||
int oldBodyLen = rcvdFrame.body.length;
|
||||
byte[] tmp = new byte[oldBodyLen + 1 + extension.length];
|
||||
System.arraycopy(rcvdFrame.body, 0, tmp, 0, oldBodyLen);
|
||||
tmp[oldBodyLen] = '\r';
|
||||
System.arraycopy(extension, 0, tmp, oldBodyLen + 1, extension.length);
|
||||
rcvdFrame.body = tmp;
|
||||
}
|
||||
} else {
|
||||
// pass built frame to AX25Stack for further analysis
|
||||
flushFrame();
|
||||
fireReceiving(false);
|
||||
}
|
||||
}
|
||||
wPtr = 0;
|
||||
break;
|
||||
default:
|
||||
if (wPtr == 0) {
|
||||
fireReceiving(true);
|
||||
}
|
||||
rcvBuf[wPtr++] = (byte) newData;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
stats.numBadRcvFrames++;
|
||||
wPtr = 0;
|
||||
}
|
||||
} else {
|
||||
System.out.println("other SerialPortEvent=" + event.getEventType());
|
||||
}
|
||||
}
|
||||
|
||||
void flushFrame() {
|
||||
// flush the previous frame, we've gotten to the end of the previous frame's body
|
||||
// pass built frame to AX25Stack for further analysis
|
||||
fireConsumeFrame(rcvdFrame, rcvdFrame.rcptTime);
|
||||
stats.numRcvFrames++;
|
||||
stats.numRcvBytes += rcvdFrame.getEstimatedBitCount() / 8;
|
||||
yaesuTimerTask.cancel();
|
||||
rcvdFrame = null;
|
||||
}
|
||||
|
||||
private static boolean looksLikeFirstLine(String line) {
|
||||
int dstPos = line.indexOf('>');
|
||||
int datePos = line.indexOf('[', dstPos + 1);
|
||||
int endDatePos = line.indexOf(']', datePos + 10);
|
||||
int pidPos = line.indexOf('<', endDatePos + 1);
|
||||
return (dstPos > 0 && dstPos <= 9 && datePos > dstPos && endDatePos == datePos + 18 && pidPos > datePos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the object.
|
||||
*
|
||||
* @return a string representation of the object.
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
if (cachedToString == null) {
|
||||
cachedToString = TYPE_NAME + ": " + (serialPort == null ? currentCfg.deviceName : serialPort.getPortName()) + '[' + ']';
|
||||
}
|
||||
return cachedToString;
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 222 B |
|
@ -0,0 +1,36 @@
|
|||
package org.ka2ddo.yaac.aprs;
|
||||
/*
|
||||
* Copyright (C) 2011-2019 Andrew Pavlin, KA2DDO
|
||||
* This file is part of YAAC (Yet Another APRS Client).
|
||||
*
|
||||
* YAAC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* YAAC 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
|
||||
* and GNU Lesser General Public License along with YAAC. If not,
|
||||
* see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
/**
|
||||
* This interface defines events that can occur when the StationTracker class is
|
||||
* updated with digipeater information.
|
||||
* @author Andrew Pavlin, KA2DDO
|
||||
*/
|
||||
public interface DigipeatListener {
|
||||
/**
|
||||
* Report when a new digipeater is identified.
|
||||
* @param digipeater String callsign of digipeater
|
||||
*/
|
||||
void digipeaterAdded(String digipeater);
|
||||
/**
|
||||
* Report when a digipeater is used again.
|
||||
* @param digipeater String callsign of digipeater
|
||||
*/
|
||||
void digipeaterUsedAgain(String digipeater);
|
||||
}
|
|
@ -0,0 +1,456 @@
|
|||
package org.ka2ddo.yaac.gui;
|
||||
/*
|
||||
* Copyright (C) 2011-2020 Andrew Pavlin, KA2DDO
|
||||
* This file is part of YAAC (Yet Another APRS Client).
|
||||
*
|
||||
* YAAC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* YAAC 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
|
||||
* and GNU Lesser General Public License along with YAAC. If not,
|
||||
* see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import com.bbn.openmap.gui.MapPanelChild;
|
||||
import com.bbn.openmap.MapBean;
|
||||
import com.bbn.openmap.proj.coords.LatLonPoint;
|
||||
import com.bbn.openmap.proj.Projection;
|
||||
import com.bbn.openmap.event.ProjectionListener;
|
||||
import com.bbn.openmap.event.ProjectionEvent;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
import org.ka2ddo.ax25.AX25Stack;
|
||||
import org.ka2ddo.yaac.YAAC;
|
||||
import org.ka2ddo.yaac.gps.GPSDistributor;
|
||||
import org.ka2ddo.gps.GpsFix;
|
||||
import org.ka2ddo.yaac.io.BeaconData;
|
||||
import org.ka2ddo.util.DistanceUnit;
|
||||
|
||||
/**
|
||||
* This panel of widgets is a replacement for the default zoom controls
|
||||
* provided with OpenMap. It is specific to YAAC because it taps into the
|
||||
* GPSDistributor and BeaconData.
|
||||
* @see GPSDistributor
|
||||
* @see BeaconData
|
||||
*
|
||||
* @author Andrew Pavlin, KA2DDO
|
||||
*/
|
||||
public class ZoomControl extends JPanel implements MapPanelChild {
|
||||
private static final Font BUTTON_FONT = new Font("Sanserif", Font.BOLD, 15);
|
||||
private final JComponent preferredParent;
|
||||
private JPanel pBlinkenLights;
|
||||
private MemoryGCPanel memoryGCPanel;
|
||||
JLabel lFreezeBacklog;
|
||||
Timer freezeRefreshTimer = null;
|
||||
JButton bBeaconNow;
|
||||
Scale scale;
|
||||
JToggleButton bThaw;
|
||||
JToggleButton bFreeze;
|
||||
MapBean mapBean;
|
||||
|
||||
/**
|
||||
* Create a ZoomControl widget for the MapBean's toolbar.
|
||||
*
|
||||
* @param mapBean MapBean to attach the widget to
|
||||
* @param showMem boolean true if heap consumption widget should be displayed on the toolbar
|
||||
* @param showFreezeThaw boolean true if freeze and thaw toggle buttons should be added to toolbar
|
||||
* @see MemoryGCPanel
|
||||
*/
|
||||
public ZoomControl(MapBean mapBean, boolean showMem, boolean showFreezeThaw) {
|
||||
super(new FlowLayout(FlowLayout.LEADING));
|
||||
this.mapBean = mapBean;
|
||||
ResourceBundle msgBundle = YAAC.getMsgBundle();
|
||||
JPanel pZoomButtons = new JPanel(new GridLayout(1, 0, 5, 0));
|
||||
JButton bZoomIn = new JButton("+");
|
||||
bZoomIn.setToolTipText(msgBundle.getString("button.ZoomIn"));
|
||||
bZoomIn.setFont(BUTTON_FONT);
|
||||
pZoomButtons.add(bZoomIn);
|
||||
bZoomIn.addActionListener(new ActionListener() {
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
zoomIn(2.0f);
|
||||
ZoomControl.this.mapBean.requestFocusInWindow();
|
||||
}
|
||||
});
|
||||
JButton bZoomOut = new JButton("-");
|
||||
bZoomOut.setToolTipText(msgBundle.getString("button.ZoomOut"));
|
||||
bZoomOut.setFont(BUTTON_FONT);
|
||||
pZoomButtons.add(bZoomOut);
|
||||
bZoomOut.addActionListener(new ActionListener() {
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
zoomOut(2.0f);
|
||||
ZoomControl.this.mapBean.requestFocusInWindow();
|
||||
}
|
||||
});
|
||||
JButton bPanHome = new JButton(GuiSymbols.getStationIcon('/', '-'));
|
||||
bPanHome.setToolTipText(msgBundle.getString("button.PanHome"));
|
||||
pZoomButtons.add(bPanHome);
|
||||
bPanHome.addActionListener(new ActionListener() {
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
home();
|
||||
ZoomControl.this.mapBean.requestFocusInWindow();
|
||||
}
|
||||
});
|
||||
JButton bPan00 = new JButton(GuiSymbols.getStationIcon('/', '0'));
|
||||
bPan00.setToolTipText(msgBundle.getString("button.Pan00"));
|
||||
pZoomButtons.add(bPan00);
|
||||
bPan00.addActionListener(new ActionListener(){
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
ZoomControl.this.mapBean.setCenter(0.0F, 0.0F);
|
||||
ZoomControl.this.mapBean.requestFocusInWindow();
|
||||
}
|
||||
});
|
||||
bBeaconNow = new JButton(new BeaconNowAction());
|
||||
bBeaconNow.setToolTipText(msgBundle.getString("button.BeaconNow"));
|
||||
pZoomButtons.add(bBeaconNow);
|
||||
Timer beaconButtonColorTimer = new Timer(5000, new BeaconNowTimerListener(bBeaconNow));
|
||||
beaconButtonColorTimer.setInitialDelay(1000);
|
||||
beaconButtonColorTimer.setRepeats(true);
|
||||
beaconButtonColorTimer.start();
|
||||
System.out.println(new Date().toString() + ": adding map scale widget...");
|
||||
add(pZoomButtons);
|
||||
add(scale = new Scale(mapBean));
|
||||
// add blinkenlights panel to indicate various status changes in YAAC
|
||||
System.out.println(new Date().toString() + ": adding blinkenlight panel...");
|
||||
add(pBlinkenLights = new JPanel(new FlowLayout(FlowLayout.LEADING, 3, 5)));
|
||||
add(memoryGCPanel = new MemoryGCPanel());
|
||||
setMemoryGCPanelVisible(showMem);
|
||||
if (showFreezeThaw) {
|
||||
ButtonGroup bgFreeze = new ButtonGroup();
|
||||
bThaw = new JToggleButton(new ImageIcon(IconLoader.loadImage("images/go.png")));
|
||||
bThaw.setSelectedIcon(new ImageIcon(IconLoader.loadImage("images/go_selected.png")));
|
||||
String s = msgBundle.getString("button.thaw");
|
||||
bThaw.setToolTipText(s);
|
||||
bThaw.getAccessibleContext().setAccessibleDescription(s);
|
||||
bThaw.setSelected(true);
|
||||
bThaw.addItemListener(new ItemListener() {
|
||||
public void itemStateChanged(ItemEvent e) {
|
||||
if (bThaw.isSelected()) {
|
||||
AX25Stack.getInstance().setFrozen(false);
|
||||
}
|
||||
ZoomControl.this.mapBean.requestFocusInWindow();
|
||||
}
|
||||
});
|
||||
add(bThaw);
|
||||
bgFreeze.add(bThaw);
|
||||
bFreeze = new JToggleButton(new ImageIcon(IconLoader.loadImage("images/stop.png")));
|
||||
bFreeze.setSelectedIcon(new ImageIcon(IconLoader.loadImage("images/stop_selected.png")));
|
||||
s = msgBundle.getString("button.freeze");
|
||||
bFreeze.setToolTipText(s);
|
||||
bFreeze.getAccessibleContext().setAccessibleDescription(s);
|
||||
bFreeze.addItemListener(new ItemListener() {
|
||||
public void itemStateChanged(ItemEvent e) {
|
||||
if (bFreeze.isSelected()) {
|
||||
AX25Stack.getInstance().setFrozen(true);
|
||||
// start refresh timer for stack queue backlog
|
||||
freezeRefreshTimer = new Timer(3000, new ActionListener() {
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
AX25Stack ax25Stack = AX25Stack.getInstance();
|
||||
if (!ax25Stack.isFrozen()) {
|
||||
lFreezeBacklog.setText(" ");
|
||||
freezeRefreshTimer.stop();
|
||||
freezeRefreshTimer = null;
|
||||
} else {
|
||||
int currentBacklog = ax25Stack.getCurrentBacklog();
|
||||
int maxQueueCapacity = ax25Stack.getMaxQueueCapacity();
|
||||
lFreezeBacklog.setText(MessageFormat.format("{0} of {1}", currentBacklog, maxQueueCapacity));
|
||||
lFreezeBacklog.setForeground(currentBacklog > (maxQueueCapacity * 3) / 4 ? Color.RED : Color.BLACK);
|
||||
}
|
||||
}
|
||||
});
|
||||
freezeRefreshTimer.setRepeats(true);
|
||||
freezeRefreshTimer.start();
|
||||
}
|
||||
ZoomControl.this.mapBean.requestFocusInWindow();
|
||||
}
|
||||
});
|
||||
add(bFreeze);
|
||||
bgFreeze.add(bFreeze);
|
||||
lFreezeBacklog = new JLabel(" ");
|
||||
add(lFreezeBacklog);
|
||||
}
|
||||
preferredParent = (JComponent)mapBean.getParent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pan the map so the local station is at the center.
|
||||
*/
|
||||
public void home() {
|
||||
BeaconData beaconData = YAAC.getBeaconData();
|
||||
GpsFix fix = GPSDistributor.getInstance().getCurrentFix();
|
||||
if (!beaconData.useGpsForPosition ||
|
||||
fix.timestamp < System.currentTimeMillis() - (60 * 60 * 1000L)) {
|
||||
// too old to look at
|
||||
fix = null;
|
||||
}
|
||||
if (null != fix) {
|
||||
mapBean.setCenter((float) fix.latitude, (float) fix.longitude);
|
||||
} else {
|
||||
if (beaconData.latitude != 0 && beaconData.longitude != 0) {
|
||||
mapBean.setCenter((float) beaconData.latitude, (float) beaconData.longitude);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduce the map scale by the specified zoom factor.
|
||||
* @param zoomFactor ratio to divide the current map scale by
|
||||
*/
|
||||
public void zoomIn(float zoomFactor) {
|
||||
mapBean.setScale(mapBean.getScale() / zoomFactor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enlarge the map scale by the specified zoom factor.
|
||||
* @param zoomFactor ratio to multiply the current map scale by
|
||||
*/
|
||||
public void zoomOut(float zoomFactor) {
|
||||
mapBean.setScale(mapBean.getScale() * zoomFactor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get where on the map bean this control widget wants to be added.
|
||||
* @return always on the North side
|
||||
*/
|
||||
public String getPreferredLocation() {
|
||||
return BorderLayout.NORTH;
|
||||
}
|
||||
|
||||
/**
|
||||
* No-op, because this widget is hard-wired to want to be at the top edge of the map.
|
||||
* @param string ignored name of relative position on MapBean to place this widget
|
||||
*/
|
||||
public void setPreferredLocation(String string) {
|
||||
}
|
||||
|
||||
public String getParentName() {
|
||||
return preferredParent.getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current value of the map scale display on this widget.
|
||||
* @return float length of map scale
|
||||
*/
|
||||
public float getScale() {
|
||||
return scale.getScale();
|
||||
}
|
||||
|
||||
private static class Scale extends JComponent implements ProjectionListener, ComponentListener {
|
||||
private final MapBean mapBean;
|
||||
private transient Projection projection = null;
|
||||
private final Font LABEL_FONT = new Font("Sanserif", Font.PLAIN, 10);
|
||||
private final DecimalFormat NUMBER_FORMAT = new DecimalFormat("0");
|
||||
private final DecimalFormat SMALL_NUMBER_FORMAT = new DecimalFormat("0.0");
|
||||
private final DecimalFormat VERY_SMALL_NUMBER_FORMAT = new DecimalFormat("0.00");
|
||||
private float dist = 0.1f;
|
||||
|
||||
Scale(MapBean mapBean) {
|
||||
this.mapBean = mapBean;
|
||||
mapBean.addProjectionListener(this);
|
||||
setFont(LABEL_FONT);
|
||||
mapBean.addComponentListener(this);
|
||||
setMinimumSize(new Dimension(50, 18));
|
||||
}
|
||||
|
||||
public void projectionChanged(ProjectionEvent e) {
|
||||
this.projection = e.getProjection();
|
||||
recalculateScale();
|
||||
}
|
||||
|
||||
public void componentHidden(ComponentEvent e) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
public void componentMoved(ComponentEvent e) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
public void componentResized(ComponentEvent e) {
|
||||
if (projection != null) {
|
||||
recalculateScale();
|
||||
}
|
||||
}
|
||||
|
||||
public void componentShown(ComponentEvent e) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
private void recalculateScale() {
|
||||
Point2D llp = mapBean.getCenter();
|
||||
Dimension mapSize = mapBean.getSize();
|
||||
int y = mapSize.height / 2;
|
||||
int maxDelta = Math.min(mapSize.width, mapSize.height) / 3;
|
||||
|
||||
// figure out reasonably visible map scale in statute miles or kilometers as specified by preferences
|
||||
LatLonPoint llp2 = projection.inverse(mapSize.width / 2, y - maxDelta);
|
||||
dist = (llp2.getLatitude() - (float)llp.getY()) * YAAC.getDistanceUnit().from(DistanceUnit.DEG_LAT);
|
||||
|
||||
/*
|
||||
* figure out how much padding will be needed for the numbers on the scale:
|
||||
* width specified for scale plus half the string width of the scale number,
|
||||
* height specified as font height plus same for scale bar.
|
||||
*/
|
||||
FontMetrics fm = getFontMetrics(LABEL_FONT);
|
||||
Dimension d = new Dimension(maxDelta + 2, fm.getHeight() * 2);
|
||||
setPreferredSize(d);
|
||||
setMaximumSize(d);
|
||||
revalidate();
|
||||
repaint();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void paintComponent(Graphics g) {
|
||||
if (projection != null) {
|
||||
g.setColor(getBackground());
|
||||
Dimension d = getSize();
|
||||
g.fillRect(0, 0, d.width, d.height);
|
||||
Point2D llp = mapBean.getCenter();
|
||||
Dimension mapSize = mapBean.getSize();
|
||||
int y = mapSize.height / 2;
|
||||
int maxDelta = Math.min(mapSize.width, mapSize.height) / 3 - 2;
|
||||
|
||||
LatLonPoint llp2 = projection.inverse(mapSize.width / 2, y - maxDelta);
|
||||
dist = (llp2.getLatitude() - (float)llp.getY()) * YAAC.getDistanceUnit().from(DistanceUnit.DEG_LAT);
|
||||
|
||||
double exp = Math.log10(dist);
|
||||
int powerOf10 = (int)Math.floor(exp);
|
||||
double idealDist = Math.pow(10.0, powerOf10);
|
||||
if (idealDist < dist / 2) {
|
||||
// too short, try a longer scale of 2 or 5 times the ideal
|
||||
if (5 * idealDist > dist) {
|
||||
idealDist *= 2;
|
||||
} else {
|
||||
idealDist *= 5;
|
||||
}
|
||||
}
|
||||
int delta = (int)Math.round(maxDelta * (idealDist / dist));
|
||||
|
||||
g.setColor(getForeground());
|
||||
FontMetrics fm = getFontMetrics(LABEL_FONT);
|
||||
g.drawRect(1, fm.getHeight() / 2, delta, fm.getHeight() / 2);
|
||||
g.fillRect(1, fm.getHeight() / 2, delta / 2, fm.getHeight() / 2);
|
||||
g.drawString("0", 0, fm.getHeight() * 2);
|
||||
String label;
|
||||
if (powerOf10 >= 1) {
|
||||
label = NUMBER_FORMAT.format(idealDist) + ' ' + YAAC.getDistanceUnit().name().toLowerCase(Locale.getDefault());
|
||||
} else if (powerOf10 == 0) {
|
||||
label = SMALL_NUMBER_FORMAT.format(idealDist) + ' ' + YAAC.getDistanceUnit().name().toLowerCase(Locale.getDefault());
|
||||
} else {
|
||||
label = VERY_SMALL_NUMBER_FORMAT.format(idealDist) + ' ' + YAAC.getDistanceUnit().name().toLowerCase(Locale.getDefault());
|
||||
}
|
||||
g.drawString(label, 1 + delta - fm.stringWidth(label), fm.getHeight() * 2);
|
||||
}
|
||||
}
|
||||
|
||||
public float getScale() {
|
||||
return dist;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the small panel in the north toolbar for displaying blinkenlights (small status lights).
|
||||
* @return blinkenlights JPanel
|
||||
*/
|
||||
public JPanel getBlinkenLightsPanel() {
|
||||
return pBlinkenLights;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the visibility of the memory usage status monitor.
|
||||
* @param visible boolean true if memory status monitor should be visible
|
||||
*/
|
||||
public void setMemoryGCPanelVisible(boolean visible) {
|
||||
memoryGCPanel.setVisible(visible);
|
||||
}
|
||||
|
||||
/**
|
||||
* Report the current visibility of the MemoryGCPanel.
|
||||
* @return boolean true if panel is visible
|
||||
*/
|
||||
public boolean isMemoryGCPanelVisible() {
|
||||
return memoryGCPanel.isVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Timer listener to update the appearance of the BCN ("Beacon Now") button depending on how long
|
||||
* since a beacon has been transmitted, just as a hint to the user to not overdo it.
|
||||
* @author Andrew Pavlin, KA2DDO
|
||||
*/
|
||||
private static class BeaconNowTimerListener implements ActionListener {
|
||||
private final JButton bBeaconNow;
|
||||
private final Map<String, BeaconData> beaconMap = YAAC.getBeaconDataMap();
|
||||
private final Color quarterWayColor;
|
||||
private final Color halfWayColor;
|
||||
private final Color quarter3WayColor;
|
||||
|
||||
BeaconNowTimerListener(JButton bBeaconNow) {
|
||||
this.bBeaconNow = bBeaconNow;
|
||||
UIDefaults uiDefaults = UIManager.getLookAndFeel().getDefaults();
|
||||
Color controlBkgrndColor = uiDefaults.getColor("control");
|
||||
if (controlBkgrndColor == null) {
|
||||
controlBkgrndColor = SystemColor.control;
|
||||
}
|
||||
Color controlTextColor = uiDefaults.getColor("controlText");
|
||||
if (controlTextColor == null) {
|
||||
controlTextColor = SystemColor.controlText;
|
||||
}
|
||||
quarterWayColor = new Color((7 * controlBkgrndColor.getRed() + controlTextColor.getRed()) / 8,
|
||||
(7 * controlBkgrndColor.getGreen() + controlTextColor.getGreen()) / 8,
|
||||
(7 * controlBkgrndColor.getBlue() + controlTextColor.getBlue()) / 8,
|
||||
(7 * controlBkgrndColor.getAlpha() + controlTextColor.getAlpha()) / 8);
|
||||
halfWayColor = new Color((3 * controlBkgrndColor.getRed() + controlTextColor.getRed()) / 4,
|
||||
(3 * controlBkgrndColor.getGreen() + controlTextColor.getGreen()) / 4,
|
||||
(3 * controlBkgrndColor.getBlue() + controlTextColor.getBlue()) / 4,
|
||||
(3 * controlBkgrndColor.getAlpha() + controlTextColor.getAlpha()) / 4);
|
||||
quarter3WayColor = new Color((controlBkgrndColor.getRed() + controlTextColor.getRed()) / 2,
|
||||
(controlBkgrndColor.getGreen() + controlTextColor.getGreen()) / 2,
|
||||
(controlBkgrndColor.getBlue() + controlTextColor.getBlue()) / 2,
|
||||
(controlBkgrndColor.getAlpha() + controlTextColor.getAlpha()) / 2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
long mostRecentBeaconTime = 0L;
|
||||
boolean foundEnabledBeacon = false;
|
||||
for (BeaconData bd : beaconMap.values()) {
|
||||
if (bd.isEnabled()) {
|
||||
foundEnabledBeacon = true;
|
||||
long lastBeaconTime = bd.getLastSendTime();
|
||||
if (lastBeaconTime > mostRecentBeaconTime) {
|
||||
mostRecentBeaconTime = lastBeaconTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (foundEnabledBeacon) {
|
||||
if (!bBeaconNow.isEnabled()) {
|
||||
bBeaconNow.setEnabled(true);
|
||||
}
|
||||
long delta = System.currentTimeMillis() - mostRecentBeaconTime;
|
||||
bBeaconNow.setForeground(delta < 5000L ? SystemColor.control :
|
||||
(delta < 15000L ? quarterWayColor :
|
||||
(delta < 30000L ? halfWayColor :
|
||||
(delta < 60000L ? quarter3WayColor : SystemColor.controlText))));
|
||||
} else {
|
||||
if (bBeaconNow.isEnabled()) {
|
||||
bBeaconNow.setEnabled(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package org.ka2ddo.yaac.webserver;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2011-2020 Andrew Pavlin, KA2DDO
|
||||
* This file is part of YAAC (Yet Another APRS Client).
|
||||
*
|
||||
* YAAC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* YAAC 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
|
||||
* and GNU Lesser General Public License along with YAAC. If not,
|
||||
* see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import org.ka2ddo.util.UTF8StreamWriter;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
/**
|
||||
* This page produces a robots.txt file contents to try to prevent web spiders
|
||||
* from navigating the entire website in YAAC.
|
||||
* @author Andrew Pavlin, KA2DDO
|
||||
*/
|
||||
public class RobotsTxtPage extends PathHandler implements CacheablePage {
|
||||
/**
|
||||
* Given an inbound HTTP request, generate a page using its parameters.
|
||||
*
|
||||
* @param hct HttpConnectionThread issuing the call
|
||||
* @param requestHeaders the HTTP request headers the browser sent
|
||||
* @param in DataInput for reading the body of the HTTP request
|
||||
* @param out OutputStream to write the generated page to
|
||||
* @param path the remainder of the URL following the prefix identifying this particular page, in case the user is providing parameters to the page
|
||||
* @return the Content-Type header value to return to tell the remote browser how to interpret the page
|
||||
* @throws java.io.IOException if the page could not be generated for some reason
|
||||
*/
|
||||
@Override
|
||||
public String processPage(HttpConnectionThread hct, LinkedHashMap<String, String> requestHeaders, DataInput in, OutputStream out, String path) throws IOException {
|
||||
PrintWriter pOut = new PrintWriter(new UTF8StreamWriter(out));
|
||||
pOut.print("User-agent: *"); pOut.print(HttpConnectionThread.CRLF);
|
||||
HttpServer server = hct.getServerInstance();
|
||||
for (Iterator<String> it = server.getPathIterator(); it.hasNext(); ) {
|
||||
String page = it.next();
|
||||
if (!"/".equals(page) && !page.startsWith("/index.") && !"/robots.txt".equals(page)) {
|
||||
pOut.print("Disallow: "); pOut.print(page); pOut.print(HttpConnectionThread.CRLF);
|
||||
}
|
||||
}
|
||||
pOut.flush();
|
||||
return "text/plain; charset=UTF-8";
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 353 B |
After Width: | Height: | Size: 920 B |
|
@ -0,0 +1,27 @@
|
|||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<title>View GPS Status</title>
|
||||
</head>
|
||||
<body lang="en-US" dir="LTR">
|
||||
<h1>View GPS Status</h1>
|
||||
<p>YAAC allows you to monitor the current status of your GPS
|
||||
receiver. Select the menu item View -> Show GPS Status. It will
|
||||
display a popup window like this, reporting the current status of
|
||||
your received data. If the GPS receiver is not working, all of the
|
||||
fields will be blank.</p>
|
||||
<p><img src="./gpsstatus1.png" alt="GPS Position report"/>
|
||||
</p>
|
||||
<p>If your GPS receiver also provides $GPGSA and/or $GPGSV records,
|
||||
you can also look at a current sky view of the visible satellite
|
||||
constellation by selecting the Visibility tab of the popup window.</p>
|
||||
<p><img src="./gpsstatus2.png" alt="GPS Sky View report"/>
|
||||
</p>
|
||||
<p>If the $GPGSA record is provided by your GPS receiver, YAAC will
|
||||
indicate the satellites are reported as currently being used by the
|
||||
GPS receiver for location processing by highlighting them in green.</p>
|
||||
<p>Close the window with the normal title bar controls to get rid of
|
||||
it.</p>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,64 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
contributor license agreements. See the NOTICE file distributed with
|
||||
this work for additional information regarding copyright ownership.
|
||||
The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
(the "License"); you may not use this file except in compliance with
|
||||
the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>commons-math3</groupId>
|
||||
<artifactId>commons-math3</artifactId>
|
||||
<version>3.6.1</version>
|
||||
<name>Commons Math3</name>
|
||||
|
||||
<description>
|
||||
Commons Math3 is a library of lightweight, self-contained mathematics and statistics components addressing
|
||||
the most common problems not available in the Java programming language or Commons Lang.
|
||||
</description>
|
||||
|
||||
<url>http://commons.apache.org/math/</url>
|
||||
|
||||
<issueManagement>
|
||||
<system>jira</system>
|
||||
<url>http://issues.apache.org/jira/browse/math</url>
|
||||
</issueManagement>
|
||||
|
||||
<scm>
|
||||
<connection>scm:svn:http://svn.apache.org/repos/asf/commons/proper/math/branches/math-3.x/</connection>
|
||||
<developerConnection>scm:svn:https://svn.apache.org/repos/asf/commons/proper/math/branches/math-3.x/</developerConnection>
|
||||
<url>http://svn.apache.org/viewvc/commons/proper/math/branches/math-3.x/</url>
|
||||
</scm>
|
||||
|
||||
<dependencies>
|
||||
<!-- used for unit tests -->
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>3.8.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<properties>
|
||||
<maven.compile.source>1.4</maven.compile.source>
|
||||
<maven.compile.target>1.4</maven.compile.target>
|
||||
<commons.componentid>math3</commons.componentid>
|
||||
<commons.release.version>3.6.1</commons.release.version>
|
||||
<commons.release.name>commons-math3-3.6.1</commons.release.name>
|
||||
<commons.osgi.symbolicName>org.apache.commons.math3</commons.osgi.symbolicName>
|
||||
<commons.deployment.protocol>svn</commons.deployment.protocol>
|
||||
<commons.binary.suffix />
|
||||
<commons.jira.id>math</commons.jira.id>
|
||||
</properties>
|
||||
</project>
|
|
@ -0,0 +1,48 @@
|
|||
package org.ka2ddo.yaac.gui.config;
|
||||
/*
|
||||
* Copyright (C) 2011-2017 Andrew Pavlin, KA2DDO
|
||||
* This file is part of YAAC (Yet Another APRS Client).
|
||||
*
|
||||
* YAAC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* YAAC 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
|
||||
* and GNU Lesser General Public License along with YAAC. If not,
|
||||
* see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import org.ka2ddo.yaac.gui.FontCache;
|
||||
import org.ka2ddo.yaac.io.PortConnector;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.table.TableCellRenderer;
|
||||
import java.awt.*;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This class renders a port name in a JTable.
|
||||
*/
|
||||
class PortCellRenderer extends JLabel implements TableCellRenderer {
|
||||
private final List<PortConnector> portList;
|
||||
|
||||
PortCellRenderer(List<PortConnector> portList) {
|
||||
this.portList = portList;
|
||||
setOpaque(true);
|
||||
}
|
||||
|
||||
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
|
||||
setFont(FontCache.getInstance().getFont(Font.DIALOG));
|
||||
setText((String) value);
|
||||
setBackground(isSelected ? table.getSelectionBackground() : table.getBackground());
|
||||
PortConnector connector = portList.get(row);
|
||||
setForeground((connector == null || !connector.isOpen()) ? Color.RED : (isSelected ? table.getSelectionForeground() : table.getForeground()));
|
||||
return this;
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 16 KiB |
|
@ -0,0 +1,45 @@
|
|||
package org.ka2ddo.yaac.io;
|
||||
/*
|
||||
* Copyright (C) 2011-2012 Andrew Pavlin, KA2DDO
|
||||
* This file is part of YAAC (Yet Another APRS Client).
|
||||
*
|
||||
* YAAC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* YAAC 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
|
||||
* and GNU Lesser General Public License along with YAAC. If not,
|
||||
* see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This interface defines a listener that wants to know the details of when a
|
||||
* port is receiving or sending frames of data.
|
||||
*/
|
||||
public interface PortEventListener {
|
||||
/**
|
||||
* Reports when the specified port starts or stops transmitting a message.
|
||||
* @param port PortConnector of relevant port
|
||||
* @param isTransmitting boolean true if transmitting is starting, false if transmitting is ending
|
||||
*/
|
||||
public void portTransmitting(PortConnector port, boolean isTransmitting);
|
||||
|
||||
/**
|
||||
* Reports when the specified port starts or stops receiving a message.
|
||||
* @param port PortConnector of relevant port
|
||||
* @param isReceiving boolean true if receiving is starting, false if receiving is ending
|
||||
*/
|
||||
public void portReceiving(PortConnector port, boolean isReceiving);
|
||||
|
||||
/**
|
||||
* Reports when the port fails for some reason.
|
||||
* @param port PortConnector of relevant port
|
||||
*/
|
||||
public void portFailed(PortConnector port);
|
||||
}
|
After Width: | Height: | Size: 993 B |
|
@ -0,0 +1,119 @@
|
|||
package org.ka2ddo.yaac.gui.table;
|
||||
/*
|
||||
* Copyright (C) 2011-2018 Andrew Pavlin, KA2DDO
|
||||
* This file is part of YAAC (Yet Another APRS Client).
|
||||
*
|
||||
* YAAC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* YAAC 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
|
||||
* and GNU Lesser General Public License along with YAAC. If not,
|
||||
* see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import org.ka2ddo.aprs.Symbols;
|
||||
import org.ka2ddo.yaac.gui.GuiSymbols;
|
||||
import org.ka2ddo.yaac.gui.SelectableSymbol;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.table.TableCellRenderer;
|
||||
import java.awt.*;
|
||||
|
||||
/**
|
||||
* This class implements a renderer for SelectableSymbols that shows both the icon and
|
||||
* the characters representing the icon in the network packets.
|
||||
*/
|
||||
public class SelectableSymbolRenderer extends JLabel implements TableCellRenderer {
|
||||
private final boolean alwaysShowSymbolChars;
|
||||
|
||||
/**
|
||||
* Create a SelectableSymbolRenderer that only shows the symbol characters when it is an overlay.
|
||||
*/
|
||||
public SelectableSymbolRenderer() {
|
||||
setOpaque(true);
|
||||
alwaysShowSymbolChars = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a SelectableSymbolRenderer that selectively decides whether to show the symbol characters.
|
||||
* @param alwaysShowSymbolChars boolean true to always show symbol characters, else only if overlay
|
||||
*/
|
||||
public SelectableSymbolRenderer(boolean alwaysShowSymbolChars) {
|
||||
setOpaque(true);
|
||||
this.alwaysShowSymbolChars = alwaysShowSymbolChars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the component used for drawing the cell. This method is
|
||||
* used to configure the renderer appropriately before drawing.
|
||||
*
|
||||
* @param table the <code>JTable</code> that is asking the
|
||||
* renderer to draw; can be <code>null</code>
|
||||
* @param value the value of the cell to be rendered. It is
|
||||
* up to the specific renderer to interpret
|
||||
* and draw the value. For example, if
|
||||
* <code>value</code>
|
||||
* is the string "true", it could be rendered as a
|
||||
* string or it could be rendered as a check
|
||||
* box that is checked. <code>null</code> is a
|
||||
* valid value
|
||||
* @param isSelected true if the cell is to be rendered with the
|
||||
* selection highlighted; otherwise false
|
||||
* @param hasFocus if true, render cell appropriately. For
|
||||
* example, put a special border on the cell, if
|
||||
* the cell can be edited, render in the color used
|
||||
* to indicate editing
|
||||
* @param row the row index of the cell being drawn. When
|
||||
* drawing the header, the value of
|
||||
* <code>row</code> is -1
|
||||
* @param column the column index of the cell being drawn
|
||||
*/
|
||||
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
|
||||
SelectableSymbol ss = (SelectableSymbol)value;
|
||||
Symbols.SymbolAttr symbolAttr = null;
|
||||
if (ss != null) {
|
||||
symbolAttr = Symbols.getSymbolAttr(ss.symTableId, ss.symbolCode);
|
||||
}
|
||||
if (ss != null && alwaysShowSymbolChars) {
|
||||
setText(new String(new char[]{ss.symTableId, ss.symbolCode}));
|
||||
if (symbolAttr != null) {
|
||||
if (symbolAttr.img != null) {
|
||||
setIcon(((GuiSymbols.GuiSymbolImage)symbolAttr.img).getIcon());
|
||||
} else {
|
||||
setIcon(GuiSymbols.getStationIcon('\\', ss.symbolCode));
|
||||
}
|
||||
setToolTipText(symbolAttr.getTypeName());
|
||||
} else {
|
||||
setIcon(null);
|
||||
setToolTipText("");
|
||||
}
|
||||
} else if (symbolAttr != null) {
|
||||
if (ss.symTableId != symbolAttr.getAprsSymTableId()) {
|
||||
setText(new String(new char[]{ss.symTableId}));
|
||||
} else {
|
||||
setText("");
|
||||
}
|
||||
setIcon(GuiSymbols.getStationIcon(ss.symTableId, ss.symbolCode));
|
||||
setToolTipText(symbolAttr.getTypeName());
|
||||
} else {
|
||||
setText("");
|
||||
setIcon(null);
|
||||
setToolTipText("");
|
||||
}
|
||||
if (isSelected) {
|
||||
setBackground(table.getSelectionBackground());
|
||||
setForeground(table.getSelectionForeground());
|
||||
} else {
|
||||
setBackground(table.getBackground());
|
||||
setForeground(table.getForeground());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<title>View Tx I-Gated Stations</title>
|
||||
</head>
|
||||
<body lang="en-US" dir="LTR">
|
||||
<h1>View Tx I-Gated Stations</h1>
|
||||
<p>This view reports the stations that YAAC is I-gating to the local
|
||||
RF network from the Internet.</p>
|
||||
<img src="./viewtxigate.png"/>
|
||||
<p>For each station, the view reports the Internet sender's callsign,
|
||||
the last time a message was forwarded for the sender, the addressee
|
||||
of the last such message (which is for some local RF station),
|
||||
the last time a position message of some sort was forwarded from the
|
||||
sender, and the last time any other traffic was forwarded on behalf
|
||||
of the Internet sender due to <a href="./config_transmit.html#supplementalTxIgate">supplemental Tx I-gate filter settings</a>.</p>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,29 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE index
|
||||
PUBLIC "-//Sun Microsystems Inc.//DTD JavaHelp Index Version 2.0//EN"
|
||||
"http://java.sun.com/products/javahelp/index_2_0.dtd">
|
||||
<!--
|
||||
Copyright (C) 2011-2021 Andrew Pavlin, KA2DDO
|
||||
This file is part of YAAC (Yet Another APRS Client).
|
||||
|
||||
YAAC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
YAAC 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
|
||||
and GNU Lesser General Public License along with YAAC. If not,
|
||||
see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<index version="2.0">
|
||||
<indexitem text="AREDN, configuring" target="configure.aredn"/>
|
||||
<indexitem text="configuring YAAC">
|
||||
<indexitem text="AREDN" target="configure.aredn"/>
|
||||
</indexitem>
|
||||
</index>
|
|
@ -0,0 +1,147 @@
|
|||
package org.ka2ddo.yaac.gui;
|
||||
/*
|
||||
* Copyright (C) 2011-2020 Andrew Pavlin, KA2DDO
|
||||
* This file is part of YAAC.
|
||||
*
|
||||
* YAAC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* YAAC 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
|
||||
* and GNU Lesser General Public License along with YAAC. If not,
|
||||
* see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import org.ka2ddo.yaac.ax25.Age;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
/**
|
||||
* This model allows spinning for a specific Age value.
|
||||
* @author Andrew Pavlin, KA2DDO
|
||||
*/
|
||||
public class AgeSpinnerModel extends AbstractSpinnerModel {
|
||||
private transient long value;
|
||||
private long stepSize = 5000L;
|
||||
|
||||
/**
|
||||
* Create an AgeSpinnerModel with the specified initial value and step size.
|
||||
* @param value long value in milliseconds
|
||||
* @param stepSize long step size in milliseconds
|
||||
* @throws IllegalArgumentException if value or step size are not valid
|
||||
*/
|
||||
public AgeSpinnerModel(long value, long stepSize) throws IllegalArgumentException {
|
||||
setStepSize(stepSize);
|
||||
setValue(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an AgeSpinnerModel with the specified initial value and default step size of 5 seconds.
|
||||
* @param value Age value in milliseconds
|
||||
* @throws IllegalArgumentException if value is not valid
|
||||
*/
|
||||
public AgeSpinnerModel(Age value) throws IllegalArgumentException {
|
||||
setValue(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the step size of the model.
|
||||
* @param stepSize long step size in milliseconds
|
||||
* @throws IllegalArgumentException if step size is not valid or existing value not consistent with new step size
|
||||
*/
|
||||
public void setStepSize(long stepSize) throws IllegalArgumentException {
|
||||
if (stepSize < 1000L || (stepSize % 1000L) != 0) {
|
||||
throw new IllegalArgumentException("step size=" + stepSize + " not a positive multiple of 1000 milliseconds");
|
||||
}
|
||||
if ((value % stepSize) != 0) {
|
||||
throw new IllegalArgumentException("old value=" + value + " not a multiple of new stepSize=" + stepSize);
|
||||
}
|
||||
this.stepSize = stepSize;
|
||||
}
|
||||
/**
|
||||
* The <i>current element</i> of the sequence. This element is usually
|
||||
* displayed by the <code>editor</code> part of a <code>JSpinner</code>.
|
||||
*
|
||||
* @return the current spinner value.
|
||||
* @see #setValue
|
||||
*/
|
||||
@Override
|
||||
public Object getValue() {
|
||||
return new Age(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes current value of the model, typically this value is displayed
|
||||
* by the <code>editor</code> part of a <code>JSpinner</code>.
|
||||
* If the <code>SpinnerModel</code> implementation doesn't support
|
||||
* the specified value then an <code>IllegalArgumentException</code>
|
||||
* is thrown. For example a <code>SpinnerModel</code> for numbers might
|
||||
* only support values that are integer multiples of ten. In
|
||||
* that case, <code>model.setValue(new Number(11))</code>
|
||||
* would throw an exception.
|
||||
*
|
||||
* @param value new value for the model
|
||||
* @throws IllegalArgumentException if <code>value</code> isn't allowed
|
||||
* @see #getValue
|
||||
*/
|
||||
@Override
|
||||
public void setValue(Object value) {
|
||||
long tmp;
|
||||
if (value instanceof Long) {
|
||||
tmp = ((Long)value).longValue();
|
||||
} else if (value instanceof String) {
|
||||
tmp = Age.parseAge((String) value);
|
||||
} else if (value instanceof Age) {
|
||||
tmp = ((Age)value).getAge();
|
||||
} else {
|
||||
throw new IllegalArgumentException("unrecognized value class " + value.getClass().getName());
|
||||
}
|
||||
if (tmp < 0L) {
|
||||
throw new IllegalArgumentException("new value=" + tmp + " is negative");
|
||||
}
|
||||
if ((tmp % stepSize) != 0) {
|
||||
throw new IllegalArgumentException("new value=" + tmp + " not a multiple of stepSize=" + stepSize);
|
||||
}
|
||||
if (this.value != tmp) {
|
||||
this.value = tmp;
|
||||
fireStateChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the object in the sequence that comes after the object returned
|
||||
* by <code>getValue()</code>. If the end of the sequence has been reached
|
||||
* then return null. Calling this method does not effect <code>value</code>.
|
||||
*
|
||||
* @return the next legal value or null if one doesn't exist
|
||||
* @see #getValue
|
||||
* @see #getPreviousValue
|
||||
*/
|
||||
@Override
|
||||
public Object getNextValue() {
|
||||
return new Age(this.value + stepSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the object in the sequence that comes before the object returned
|
||||
* by <code>getValue()</code>. If the end of the sequence has been reached then
|
||||
* return null. Calling this method does not effect <code>value</code>.
|
||||
*
|
||||
* @return the previous legal value or null if one doesn't exist
|
||||
* @see #getValue
|
||||
* @see #getNextValue
|
||||
*/
|
||||
@Override
|
||||
public Object getPreviousValue() {
|
||||
if (value > 0L) {
|
||||
return new Age(Math.max(this.value - stepSize, 0L));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 13 KiB |
|
@ -0,0 +1,119 @@
|
|||
package org.ka2ddo.yaac.gui.filter;
|
||||
/*
|
||||
* Copyright (C) 2011-2019 Andrew Pavlin, KA2DDO
|
||||
* This file is part of YAAC (Yet Another APRS Client).
|
||||
*
|
||||
* YAAC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* YAAC 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
|
||||
* and GNU Lesser General Public License along with YAAC. If not,
|
||||
* see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import org.ka2ddo.yaac.YAAC;
|
||||
import org.ka2ddo.yaac.filter.Filter;
|
||||
import org.ka2ddo.yaac.filter.FilterChangeListener;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.event.ItemEvent;
|
||||
import java.awt.event.ItemListener;
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
/**
|
||||
* This Filter selects viewing messages that are only valid or only invalid.
|
||||
* @author Andrew Pavlin, KA2DDO
|
||||
*/
|
||||
public class ValidInvalidFilter extends JPanel implements FilterChangeListener, Closeable {
|
||||
transient final org.ka2ddo.yaac.filter.ValidInvalidFilter filter;
|
||||
final JRadioButton rbAll;
|
||||
final JRadioButton rbValid;
|
||||
final JRadioButton rbInvalid;
|
||||
|
||||
/**
|
||||
* Create a UI for controlling a ValidInvalidFilter.
|
||||
* @param myFilter the ValidInvalidFilter to control
|
||||
* @see org.ka2ddo.yaac.filter.ValidInvalidFilter
|
||||
*/
|
||||
public ValidInvalidFilter(org.ka2ddo.yaac.filter.ValidInvalidFilter myFilter) {
|
||||
this.filter = myFilter;
|
||||
ResourceBundle msgBundle = YAAC.getMsgBundle();
|
||||
ButtonGroup bg = new ButtonGroup();
|
||||
rbAll = new JRadioButton(msgBundle.getString("filter.ValidInvalid.all"));
|
||||
add(rbAll);
|
||||
bg.add(rbAll);
|
||||
if (org.ka2ddo.yaac.filter.ValidInvalidFilter.STATE_ALL == filter.getState()) {
|
||||
rbAll.setSelected(true);
|
||||
}
|
||||
rbAll.addItemListener(new ItemListener() {
|
||||
public void itemStateChanged(ItemEvent e) {
|
||||
if (rbAll.isSelected()) {
|
||||
filter.setState(org.ka2ddo.yaac.filter.ValidInvalidFilter.STATE_ALL);
|
||||
}
|
||||
}
|
||||
});
|
||||
rbValid = new JRadioButton(msgBundle.getString("filter.ValidInvalid.valid"));
|
||||
add(rbValid);
|
||||
bg.add(rbValid);
|
||||
if (org.ka2ddo.yaac.filter.ValidInvalidFilter.STATE_VALID == filter.getState()) {
|
||||
rbValid.setSelected(true);
|
||||
}
|
||||
rbValid.addItemListener(new ItemListener() {
|
||||
public void itemStateChanged(ItemEvent e) {
|
||||
if (rbValid.isSelected()) {
|
||||
filter.setState(org.ka2ddo.yaac.filter.ValidInvalidFilter.STATE_VALID);
|
||||
}
|
||||
}
|
||||
});
|
||||
rbInvalid = new JRadioButton(msgBundle.getString("filter.ValidInvalid.invalid"));
|
||||
add(rbInvalid);
|
||||
bg.add(rbInvalid);
|
||||
if (org.ka2ddo.yaac.filter.ValidInvalidFilter.STATE_INVALID == filter.getState()) {
|
||||
rbInvalid.setSelected(true);
|
||||
}
|
||||
rbInvalid.addItemListener(new ItemListener() {
|
||||
public void itemStateChanged(ItemEvent e) {
|
||||
if (rbInvalid.isSelected()) {
|
||||
filter.setState(org.ka2ddo.yaac.filter.ValidInvalidFilter.STATE_INVALID);
|
||||
}
|
||||
}
|
||||
});
|
||||
filter.addFilterChangeListener(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes this stream and releases any system resources associated
|
||||
* with it. If the stream is already closed then invoking this
|
||||
* method has no effect.
|
||||
*
|
||||
* @throws java.io.IOException if an I/O error occurs
|
||||
*/
|
||||
public void close() throws IOException {
|
||||
filter.removeFilterChangeListener(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the specified Filter's matching criteria have been changed.
|
||||
*
|
||||
* @param changedFilter Filter that has changed
|
||||
* @param changedByUser boolean true if change was manually made by user, false if
|
||||
* change was made automatically by dynamic filter logic
|
||||
*/
|
||||
public void filterSettingsChanged(Filter changedFilter, boolean changedByUser) {
|
||||
if (changedFilter == filter) {
|
||||
int state = filter.getState();
|
||||
rbAll.setSelected(org.ka2ddo.yaac.filter.ValidInvalidFilter.STATE_ALL == state);
|
||||
rbValid.setSelected(org.ka2ddo.yaac.filter.ValidInvalidFilter.STATE_VALID == state);
|
||||
rbInvalid.setSelected(org.ka2ddo.yaac.filter.ValidInvalidFilter.STATE_INVALID == state);
|
||||
}
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 1.3 KiB |
|
@ -0,0 +1,112 @@
|
|||
package org.ka2ddo.yaac.healthmonitor;
|
||||
/*
|
||||
* Copyright (C) 2011-2020 Andrew Pavlin, KA2DDO
|
||||
* This file is part of YAAC (Yet Another APRS Client).
|
||||
*
|
||||
* YAAC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* YAAC 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
|
||||
* and GNU Lesser General Public License along with YAAC. If not,
|
||||
* see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import org.ka2ddo.ax25.AX25Callsign;
|
||||
import org.ka2ddo.yaac.YAAC;
|
||||
import org.ka2ddo.yaac.ax25.StationState;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Set;
|
||||
import java.util.prefs.BackingStoreException;
|
||||
import java.util.prefs.Preferences;
|
||||
|
||||
/**
|
||||
* This class contains the control information for specifying one remote
|
||||
* station to monitor.
|
||||
* @author Andrew Pavlin, KA2DDO
|
||||
*/
|
||||
public class MonitoredStation {
|
||||
/**
|
||||
* Set of all the types of monitoring the health monitor should perform.
|
||||
*/
|
||||
public final Set<MonitorType> types = EnumSet.noneOf(MonitorType.class);
|
||||
AX25Callsign stationCallsign;
|
||||
/**
|
||||
* The StationState object for the monitored station.
|
||||
*/
|
||||
public transient StationState state;
|
||||
/**
|
||||
* The list of stations digipeated by this station. May be null if this station is not a digipeater or I-gate.
|
||||
*/
|
||||
public transient ArrayList<String> digipeatedStations = null;
|
||||
|
||||
/**
|
||||
* Create an empty MonitoredStation record.
|
||||
*/
|
||||
public MonitoredStation() {}
|
||||
|
||||
/**
|
||||
* Create a MonitoredStation record for the specified callsign-SSID.
|
||||
* @param callsign String callsign-SSID of the station to monitor
|
||||
*/
|
||||
public MonitoredStation(String callsign) {
|
||||
stationCallsign = new AX25Callsign(callsign);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the callsign-SSID of this MonitoredStation.
|
||||
* @return AX25Callsign of the station callsign-SSID
|
||||
*/
|
||||
public AX25Callsign getStationCallsign() {
|
||||
return stationCallsign;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the callsign for an empty MonitoredStation record.
|
||||
* @param stationCallsign AX25Callsign of the station callsign-SSID
|
||||
* @throws IllegalStateException if callsign was already set for this record
|
||||
*/
|
||||
public void setStationCallsign(AX25Callsign stationCallsign) throws IllegalStateException {
|
||||
if (this.stationCallsign != null) {
|
||||
throw new IllegalStateException("callsign already set for this MonitoredStation");
|
||||
}
|
||||
MonitoredStationList monitoredStationList = MonitoredStationList.getInstance();
|
||||
if (monitoredStationList.findStation(stationCallsign.toString()) != null) {
|
||||
throw new IllegalArgumentException("duplicate callsign already in list");
|
||||
}
|
||||
this.stationCallsign = stationCallsign;
|
||||
int index = monitoredStationList.indexOf(this);
|
||||
if (index >= 0) {
|
||||
monitoredStationList.addToMap(this);
|
||||
monitoredStationList.fireMonitoredStationUpdated(index);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist this MonitoredStation to Java Preferences.
|
||||
*/
|
||||
public void writeToPreferences() {
|
||||
Preferences prefs = YAAC.getPreferences().node(MonitoredStationList.MONITORED_STATIONS);
|
||||
StringBuilder b = new StringBuilder();
|
||||
for (MonitorType t : types) {
|
||||
if (b.length() > 0) {
|
||||
b.append(',');
|
||||
}
|
||||
b.append(t.name());
|
||||
}
|
||||
prefs.put(stationCallsign.toString(), b.toString());
|
||||
try {
|
||||
prefs.flush();
|
||||
} catch (BackingStoreException e) {
|
||||
e.printStackTrace(System.out);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package org.ka2ddo.yaac.adsbdecoder;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2011-2015 Andrew Pavlin, KA2DDO
|
||||
* This file is part of YAAC (Yet Another APRS Client).
|
||||
*
|
||||
* YAAC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* YAAC 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
|
||||
* and GNU Lesser General Public License along with YAAC. If not,
|
||||
* see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This interface defines callbacks so a listener can be informed of changes to the ADS-B aircraft cache.
|
||||
*/
|
||||
public interface AdsbCacheListener {
|
||||
public void aircraftAdded(int index, AircraftState aircraftState);
|
||||
public void aircraftUpdated(int index, AircraftState aircraftState);
|
||||
public void aircraftRemoved(int index, AircraftState aircraftState);
|
||||
}
|
After Width: | Height: | Size: 154 B |
|
@ -0,0 +1,41 @@
|
|||
package org.ka2ddo.ax25;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2011-2018 Andrew Pavlin, KA2DDO
|
||||
* This file is part of YAAC (Yet Another APRS Client).
|
||||
*
|
||||
* YAAC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* YAAC 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
|
||||
* and GNU Lesser General Public License along with YAAC. If not,
|
||||
* see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This interface defines a means by which an arbitrary handler can choose to accept
|
||||
* an inbound AX.25 connected-mode session request.
|
||||
* @author Andrew Pavlin, KA2DDO
|
||||
*/
|
||||
public interface ConnectionRequestListener {
|
||||
/**
|
||||
* Decide whether to accept the specified inbound AX.25 connected-mode session request. Note
|
||||
* that the state is not fully connected at the point of this call (so the called code can choose
|
||||
* to reject it), so the called code should register a {@link ConnectionEstablishmentListener ConnectionEstablishmentListener} on the
|
||||
* ConnState to be informed when the connection is fully established if the called code chooses to
|
||||
* accept the connection request.
|
||||
* @param state ConnState object describing the session being built
|
||||
* @param originator AX25Callsign of the originating station
|
||||
* @param port Connector through which the request was received
|
||||
* @return boolean true if request should be accepted, false if not
|
||||
* @see ConnState#listener
|
||||
*/
|
||||
public boolean acceptInbound(ConnState state, AX25Callsign originator, Connector port);
|
||||
}
|
After Width: | Height: | Size: 7.4 KiB |
|
@ -0,0 +1,7 @@
|
|||
/**
|
||||
* This package provides various useful utility classes that otherwise mostly have no relationship
|
||||
* with each other. These classes should not depend on any libraries other than those
|
||||
* provided in the core Java Runtime Environment; any change creating such dependencies
|
||||
* is a scope violation for this package.
|
||||
*/
|
||||
package org.ka2ddo.util;
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -0,0 +1,83 @@
|
|||
package org.ka2ddo.yaac.core.queries;
|
||||
/*
|
||||
* Copyright (C) 2011-2021 Andrew Pavlin, KA2DDO
|
||||
* This file is part of YAAC (Yet Another APRS Client).
|
||||
*
|
||||
* YAAC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* YAAC 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
|
||||
* and GNU Lesser General Public License along with YAAC. If not,
|
||||
* see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import org.ka2ddo.aprs.Message;
|
||||
import org.ka2ddo.ax25.Connector;
|
||||
import org.ka2ddo.yaac.ax25.StationTracker;
|
||||
import org.ka2ddo.yaac.io.PortManager;
|
||||
import org.ka2ddo.yaac.pluginapi.AbstractQueryHandler;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* This class handles queries for the list of stations directly reachable by this station over RF.
|
||||
* @author Andrew Pavlin, KA2DDO
|
||||
*/
|
||||
public class DirectQueryHandler extends AbstractQueryHandler {
|
||||
/**
|
||||
* Handle the passed in message and generate whatever appropriate response should be made.
|
||||
*
|
||||
* @param mm MessageMessage addressed explicitly to local station whose content begins with
|
||||
* one of the prefixes specified by the subclass.
|
||||
*/
|
||||
public void handleQuery(Message mm) {
|
||||
long now = System.currentTimeMillis();
|
||||
String[] directStations = StationTracker.getInstance().getDirectStationList(now);
|
||||
if (directStations != null && directStations.length > 0) {
|
||||
Arrays.sort(directStations);
|
||||
int totalCharLen = 0;
|
||||
for (String direct : directStations) {
|
||||
totalCharLen += 1 + direct.length();
|
||||
}
|
||||
if (totalCharLen < 59) {
|
||||
// can send answer in one piece without exceeding maximum message length
|
||||
StringBuilder b = new StringBuilder(totalCharLen+8).append("Directs=");
|
||||
for (String s : directStations) {
|
||||
b.append(' ').append(s);
|
||||
}
|
||||
transmitReply(mm, b.toString());
|
||||
} else {
|
||||
int numDirectResponses = 0;
|
||||
int index = 0;
|
||||
StringBuilder b = new StringBuilder("Directs(");
|
||||
while (index < directStations.length) {
|
||||
b.append(++numDirectResponses).append(")=");
|
||||
while (index < directStations.length && b.length() < 66) {
|
||||
String station = directStations[index];
|
||||
if (b.length() + station.length() + 1 < 67) {
|
||||
b.append(' ').append(station);
|
||||
index++;
|
||||
} else {
|
||||
break; // can't fit any more in this line
|
||||
}
|
||||
}
|
||||
transmitReply(mm, b.toString());
|
||||
b.setLength(8); // reset to use the prefix again
|
||||
}
|
||||
}
|
||||
} else if (PortManager.getPreferredConnector(Connector.CAP_RF) == null) {
|
||||
// must be only an I-Gate connection with no "locals" because no RF
|
||||
transmitReply(mm, "Directs=none (APRS-IS only)");
|
||||
} else {
|
||||
// we have RF with no "locals"
|
||||
transmitReply(mm, "Directs=none (with RF)");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
package org.ka2ddo.yaac.gui.pluginadapter;
|
||||
/*
|
||||
* Copyright (C) 2011-2019 Andrew Pavlin, KA2DDO
|
||||
* This file is part of YAAC (Yet Another APRS Client).
|
||||
*
|
||||
* YAAC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* YAAC 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
|
||||
* and GNU Lesser General Public License along with YAAC. If not,
|
||||
* see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import org.ka2ddo.yaac.pluginapi.AbstractMenuAction;
|
||||
import org.ka2ddo.yaac.pluginapi.AbstractMenuActionPropertyListener;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ItemEvent;
|
||||
import java.awt.event.ItemListener;
|
||||
import java.beans.PropertyChangeEvent;
|
||||
import java.beans.PropertyChangeListener;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* This class defines a Java Swing specific wrapper around the now GUI-independent
|
||||
* AbstractMenuAction class in the plugin API package.
|
||||
* @author Andrew Pavlin, KA2DDO
|
||||
*/
|
||||
public class MenuAction implements Action, AbstractMenuActionPropertyListener, ItemListener {
|
||||
private final AbstractMenuAction ama;
|
||||
private ArrayList<PropertyChangeListener> listeners;
|
||||
|
||||
/**
|
||||
* Create the AWT/Swing-specific wrapper around an AbstractMenuAction.
|
||||
* @param abstractMenuAction AbstractMenuAction to instantiate in Java Swing
|
||||
*/
|
||||
public MenuAction(AbstractMenuAction abstractMenuAction) {
|
||||
this.ama = abstractMenuAction;
|
||||
abstractMenuAction.setPropertyListener(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a <code>PropertyChange</code> listener. Containers and attached
|
||||
* components use these methods to register interest in this
|
||||
* <code>Action</code> object. When its enabled state or other property
|
||||
* changes, the registered listeners are informed of the change.
|
||||
*
|
||||
* @param listener a <code>PropertyChangeListener</code> object
|
||||
*/
|
||||
public void addPropertyChangeListener(PropertyChangeListener listener) {
|
||||
if (listeners == null) {
|
||||
listeners = new ArrayList<PropertyChangeListener>();
|
||||
}
|
||||
if (!listeners.contains(listener)) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets one of this object's properties
|
||||
* using the associated key.
|
||||
*
|
||||
* @see #putValue
|
||||
*/
|
||||
public Object getValue(String key) {
|
||||
return ama.getValue(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets one of this object's properties
|
||||
* using the associated key. If the value has
|
||||
* changed, a <code>PropertyChangeEvent</code> is sent
|
||||
* to listeners.
|
||||
*
|
||||
* @param key a <code>String</code> containing the key
|
||||
* @param value an <code>Object</code> value
|
||||
*/
|
||||
public void putValue(String key, Object value) {
|
||||
ama.putValue(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the enabled state of the <code>Action</code>. When enabled,
|
||||
* any component associated with this object is active and
|
||||
* able to fire this object's <code>actionPerformed</code> method.
|
||||
* If the value has changed, a <code>PropertyChangeEvent</code> is sent
|
||||
* to listeners.
|
||||
*
|
||||
* @param b true to enable this <code>Action</code>, false to disable it
|
||||
*/
|
||||
public void setEnabled(boolean b) {
|
||||
ama.setEnabled(b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the enabled state of the <code>Action</code>. When enabled,
|
||||
* any component associated with this object is active and
|
||||
* able to fire this object's <code>actionPerformed</code> method.
|
||||
*
|
||||
* @return true if this <code>Action</code> is enabled
|
||||
*/
|
||||
public boolean isEnabled() {
|
||||
return ama.isEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a <code>PropertyChange</code> listener.
|
||||
*
|
||||
* @param listener a <code>PropertyChangeListener</code> object
|
||||
* @see #addPropertyChangeListener
|
||||
*/
|
||||
public void removePropertyChangeListener(PropertyChangeListener listener) {
|
||||
if (listeners != null) {
|
||||
listeners.remove(listener);
|
||||
if (listeners.size() == 0) {
|
||||
listeners = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when an action occurs.
|
||||
*/
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
ama.actionPerformed(e.getSource());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell the listener that a property has changed.
|
||||
*
|
||||
* @param propertyName String name of property
|
||||
* @param oldValue Object value before the change
|
||||
* @param newValue Object value after the change
|
||||
*/
|
||||
public void menuPropertyChanged(String propertyName, Object oldValue, Object newValue) {
|
||||
ArrayList<PropertyChangeListener> listeners;
|
||||
if ((listeners = this.listeners) != null) { // avoid getfield code and possible NPE
|
||||
PropertyChangeEvent pce = new PropertyChangeEvent(this, propertyName, oldValue, newValue);
|
||||
for (int i = listeners.size() - 1; i >= 0; i--) {
|
||||
listeners.get(i).propertyChange(pce);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DO NOT CALL. Handles association of JMenuItemCheckBox with MenuAction.
|
||||
* @param e ItemEvent reporting JMenuItemCheckBox state change
|
||||
*/
|
||||
public void itemStateChanged(ItemEvent e) {
|
||||
ama.actionPerformed(e.getSource());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,898 @@
|
|||
package org.ka2ddo.yaac.io;
|
||||
/*
|
||||
* Copyright (C) 2011-2021 Andrew Pavlin, KA2DDO
|
||||
* This file is part of YAAC (Yet Another APRS Client).
|
||||
*
|
||||
* YAAC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* YAAC 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
|
||||
* and GNU Lesser General Public License along with YAAC. If not,
|
||||
* see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import org.ka2ddo.ax25.AX25Message;
|
||||
import org.ka2ddo.ax25.Connector;
|
||||
import org.ka2ddo.yaac.util.Localizer;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.prefs.BackingStoreException;
|
||||
import java.util.prefs.Preferences;
|
||||
|
||||
/**
|
||||
* This class contains all the parameters for configuring a PortConnector in
|
||||
* this application, and the encoding and decoding code for saving its value
|
||||
* in the persisted configuration data.
|
||||
* @author Andrew Pavlin, KA2DDO
|
||||
*/
|
||||
public class PortConfig implements Serializable, Comparable<PortConfig> {
|
||||
private static final long serialVersionUID = -162577201270002899L;
|
||||
/**
|
||||
* Empty array for ports that have no supported aliases.
|
||||
* @see Cfg#digiAliases
|
||||
*/
|
||||
public static final String[] NO_ALIASES = new String[0];
|
||||
/**
|
||||
* Flag bit indicating this port allows transmitting APRS packets.
|
||||
* @see Cfg#acceptableProtocolsMask
|
||||
*/
|
||||
public static final int PROTOCOL_APRS = 1;
|
||||
/**
|
||||
* Flag bit indicating this port allows transmitting OpenTRAC packets.
|
||||
* @see Cfg#acceptableProtocolsMask
|
||||
*/
|
||||
public static final int PROTOCOL_OPENTRAC = 2;
|
||||
/**
|
||||
* Flag bit indicating this port allows transmitting AX.25 frames containing traffic of types
|
||||
* other than APRS or OpenTRAC UI frames.
|
||||
* @see #PROTOCOL_APRS
|
||||
* @see #PROTOCOL_OPENTRAC
|
||||
* @see Cfg#acceptableProtocolsMask
|
||||
*/
|
||||
public static final int PROTOCOL_AX25 = 4;
|
||||
/**
|
||||
* Highest bit number supported in protocol bit mask.
|
||||
* @see Cfg#acceptableProtocolsMask
|
||||
*/
|
||||
public static final int MAX_PROTOCOL_BIT = 2;
|
||||
/**
|
||||
* No waypoint sentences sent to GPS on this port.
|
||||
*/
|
||||
public static final int SENTENCE_NONE = 0;
|
||||
/**
|
||||
* Send NMEA standard waypoint message back to GPS.
|
||||
*/
|
||||
public static final int SENTENCE_GPWPL = 1;
|
||||
/**
|
||||
* Send Kenwood-specific waypoint message back to GPS.
|
||||
*/
|
||||
public static final int SENTENCE_PKWDWPL = 2;
|
||||
/**
|
||||
* Indicates this is a TNC port over HF (low bandwidth, wide geographic coverage).
|
||||
* @see Cfg#flags
|
||||
*/
|
||||
public static final int FLAGS_HF = 1;
|
||||
/**
|
||||
* Indicates this is a UDP socket (not TCP).
|
||||
* @see Cfg#flags
|
||||
*/
|
||||
public static final int FLAGS_UDP = 2;
|
||||
/**
|
||||
* Indicates that this data should be consumed locally as well as transmitted.
|
||||
* @see Cfg#flags
|
||||
*/
|
||||
public static final int FLAGS_LOCAL = 4;
|
||||
/**
|
||||
* Indicates that non-flow-control serial ports should not raise the DTR and RTS signals.
|
||||
* @see Cfg#flags
|
||||
*/
|
||||
public static final int FLAGS_NODTR = 8;
|
||||
/**
|
||||
* Indicates that port opening should always retry if it fails, even on initial opening.
|
||||
* Meant for APRS-IS port driver when used with flaky and/or intermittent Internet connection.
|
||||
* @see Cfg#flags
|
||||
*/
|
||||
public static final int FLAGS_ALWAYS_RETRY = 16;
|
||||
/**
|
||||
* Indicates that work-around for strange device features should be used on this port.
|
||||
* For example, the KISS protocol ports use this to control the work-around for Kenwood
|
||||
* APRS/TNC radios that could interpret certain characters in a KISS packet as commands
|
||||
* from the memory programming application and thereby screw up the radio's settings. However,
|
||||
* other TNCs do not accept the work-around.
|
||||
* @see Cfg#flags
|
||||
*/
|
||||
public static final int FLAGS_USE_WORKAROUND = 32;
|
||||
/**
|
||||
* Indicates that this port does not require the port driver to wait for the external
|
||||
* device to finish processing the last outbound traffic before sending more traffic.
|
||||
* This is for ports such as Serial_TNC, which could be connected to quarter-duplex devices
|
||||
* like old TNCs that can't handle receiving another outbound packet from the computer
|
||||
* while still transmitting the last packet to RF.
|
||||
* @see Cfg#flags
|
||||
*/
|
||||
public static final int FLAGS_PIPELINED = 64;
|
||||
/**
|
||||
* When using timeslotting on a TNC port, don't coalesce duplicate packets (such as beacons or
|
||||
* status frames). This is useful for cases such as meteor-scatter operation. Not all TNC port types
|
||||
* may support this capability. Note this is coded as a negative value to preserve backwards compatibility
|
||||
* with YAAC installations configured at an earlier build.
|
||||
* @see Cfg#flags
|
||||
*/
|
||||
public static final int FLAGS_DONT_COALESCE = 128;
|
||||
/**
|
||||
* These four bits contain the KISS device ID to be used in KISS frames sent through this port.
|
||||
* This supports the {@link KissOverTcpConnector} port type when talking to the DireWolf
|
||||
* software TNC which can support up to 6 audio devices (and therefore up to 6 device IDs in
|
||||
* KISS frames). Conveniently, since these bits weren't used before, the backwards-compatible
|
||||
* default KISS device ID is zero.
|
||||
* @see Cfg#flags
|
||||
*/
|
||||
public static final int FLAGS_MASK_KISSPORT = 0xF00;
|
||||
/**
|
||||
* This constant gets the number of bits to shift the above {@link #FLAGS_MASK_KISSPORT} bits right
|
||||
* to put them in the least significant bits of an integer value.
|
||||
* @see Cfg#flags
|
||||
*/
|
||||
public static final int FLAGS_SHIFT_KISSPORT = 8;
|
||||
/**
|
||||
* When looking up a TCP/IP network object, the {@link Cfg#deviceName deviceName} is not a fully-qualified domain name (FQDN)
|
||||
* or numeric IP address of the target, but rather a service instance name for some service supported
|
||||
* by the port per Internet RFC 6763 (DNS-Based Service Discovery), such that the code needs to do a
|
||||
* service discovery to get the correct host name prior to using DNS to translate the host name.
|
||||
* @see Cfg#flags
|
||||
*/
|
||||
public static final int FLAGS_IS_SRV_INST_NAME = 0x1000;
|
||||
/**
|
||||
* Indicate that {@link Cfg#deviceName} is of a hardware device rather than
|
||||
* some software concept (like a network port).
|
||||
*/
|
||||
public static final int FLAGS_HARDWARE = 0x2000;
|
||||
|
||||
/**
|
||||
* Enumeration identifying all the fields in a Cfg sub-record. Used by port driver classes to support
|
||||
* configuration transfer by XML to indicate which fields are required and also which are to be suppressed (to
|
||||
* block duplicate station callsigns, etc.).
|
||||
* @see PortConnector
|
||||
* @see ConfigImporter
|
||||
* @author Andrew Pavlin, KA2DDO
|
||||
*/
|
||||
public enum Fields {
|
||||
/** @see Cfg#enabled */
|
||||
enabled,
|
||||
/** @see Cfg#deviceName */
|
||||
deviceName,
|
||||
/** @see Cfg#baudRate */
|
||||
baudRate,
|
||||
/** @see Cfg#callsign */
|
||||
callsign,
|
||||
/** @see Cfg#passcode */
|
||||
passcode,
|
||||
/** @see Cfg#filter */
|
||||
filter,
|
||||
/** @see Cfg#transmitAllowed */
|
||||
transmitAllowed,
|
||||
/** @see Cfg#flowControlled */
|
||||
flowControlled,
|
||||
/** @see Cfg#flags */
|
||||
flags,
|
||||
/** @see Cfg#digiAliases */
|
||||
digiAliases,
|
||||
/** @see Cfg#acceptableProtocolsMask */
|
||||
acceptableProtocolsMask,
|
||||
/** @see Cfg#timeslotCycleLength */
|
||||
timeslotCycleLength,
|
||||
/** @see Cfg#timeslotOffset */
|
||||
timeslotOffset,
|
||||
/** @see Cfg#beaconNames */
|
||||
beaconNames,
|
||||
/** @see Cfg#timeslotLength */
|
||||
timeslotLength }
|
||||
|
||||
/**
|
||||
* For the {@link ConfigImporter}, provide hints of how required but non-transferable config fields
|
||||
* should be asked for.
|
||||
* @author Andrew Pavlin, KA2DDO
|
||||
* @see RequireHints
|
||||
*/
|
||||
public enum HintType {
|
||||
/**
|
||||
* Just prompt with a plain text edit field.
|
||||
*/
|
||||
TEXT,
|
||||
/**
|
||||
* Just prompt with a plain text edit field, but only accept uppercase.
|
||||
*/
|
||||
TEXT_UC,
|
||||
/**
|
||||
* Prompt with a combo box pre-loaded with available serial ports.
|
||||
*/
|
||||
SERIAL_PORTS,
|
||||
/**
|
||||
* Prompt with a combo box pre-loaded with standard APRS-IS Tier 2 rotator hostnames.
|
||||
*/
|
||||
APRSIS_HOSTS,
|
||||
/**
|
||||
* Prompt with a combo box pre-loaded with the localhost name.
|
||||
*/
|
||||
LOCALHOST,
|
||||
/**
|
||||
* Prompt with a file chooser. {@link RequireHints} for this type must be a {@link RequireFile}.
|
||||
*/
|
||||
FILE_CHOOSER,
|
||||
/**
|
||||
* Prompt with a combo box loaded with a String array obtained from a static
|
||||
* getDeviceNames() method provided by the {@link PortConnector} subclass.
|
||||
*/
|
||||
GENERIC_CALL_FOR_NAMELIST,
|
||||
/**
|
||||
* Just prompt with a plain text edit field, but only accept characters legal in a callsign-SSID.
|
||||
*/
|
||||
CALLSIGN_SSID
|
||||
}
|
||||
|
||||
/**
|
||||
* Data structure describing how the ConfigImporter should ask for missing (or
|
||||
* blanked-out) required port configuration parameters.
|
||||
* @author Andrew Pavlin, KA2DDO
|
||||
* @see ConfigImporter
|
||||
*/
|
||||
public static class RequireHints {
|
||||
/**
|
||||
* The ResourceBundle key for a localized prompt string to display in a dialog box.
|
||||
*/
|
||||
public final String promptTag;
|
||||
/**
|
||||
* The data entry field type to be used in the prompting dialog box.
|
||||
*/
|
||||
public final HintType typeOfDialogPrompt;
|
||||
|
||||
/**
|
||||
* Create a RequireHints object for the specified prompt string and a default text field.
|
||||
* @param promptTag String key to a ResourceBundle localized prompt text
|
||||
*/
|
||||
public RequireHints(String promptTag) {
|
||||
this.promptTag = promptTag;
|
||||
this.typeOfDialogPrompt = HintType.TEXT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a RequireHints object for the specified prompt string and the specified type
|
||||
* of data entry field.
|
||||
* @param promptTag String key to a ResourceBundle localized prompt text
|
||||
* @param typeOfDialogPrompt HintType enum for the type of data entry to use
|
||||
*/
|
||||
public RequireHints(String promptTag, HintType typeOfDialogPrompt) {
|
||||
this.promptTag = promptTag;
|
||||
this.typeOfDialogPrompt = typeOfDialogPrompt;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data structure describing how the ConfigImporter should ask for missing (or
|
||||
* blanked-out) required port configuration parameters when they are a file in the
|
||||
* filesystem.
|
||||
* @author Andrew Pavlin, KA2DDO
|
||||
* @see ConfigImporter
|
||||
* @see HintType#FILE_CHOOSER
|
||||
*/
|
||||
public static class RequireFile extends RequireHints {
|
||||
/**
|
||||
* ResourceBundle key to localized text to display on file chooser's select button.
|
||||
*/
|
||||
public final String fileSelectTag;
|
||||
/**
|
||||
* Boolean true indicates chooser should select a directory, false for files.
|
||||
*/
|
||||
public final boolean isDirectory;
|
||||
|
||||
/**
|
||||
* Create a RequireFile object for the specified prompt string and a file chooser.
|
||||
* @param promptTag String key to a ResourceBundle localized prompt text
|
||||
* @param fileSelectTag String text to a ResourceBundle localized text to display on the file chooser's select button
|
||||
* @param isDirectory boolean true if user must select directory, false for file
|
||||
*/
|
||||
public RequireFile(String promptTag, String fileSelectTag, boolean isDirectory) {
|
||||
super(promptTag, HintType.FILE_CHOOSER);
|
||||
this.fileSelectTag = fileSelectTag;
|
||||
this.isDirectory = isDirectory;
|
||||
}
|
||||
}
|
||||
|
||||
private String displayName = "";
|
||||
private int portNumber = -1;
|
||||
/**
|
||||
* Type of PortConnector used to implement this port, generally defined as a String constant
|
||||
* named PORT_TYPE on the PortConnector subclass.
|
||||
*/
|
||||
public String portType = "";
|
||||
|
||||
/**
|
||||
* Whether port should be opened automatically upon startup.
|
||||
*/
|
||||
public boolean enabled = true;
|
||||
|
||||
/**
|
||||
* Port-type-specific configuration parameters for a PortConnector.
|
||||
* @author Andrew Pavlin, KA2DDO
|
||||
*/
|
||||
public static class Cfg implements Serializable, Comparable<Cfg> {
|
||||
private static final long serialVersionUID = 885437494458611609L;
|
||||
|
||||
/**
|
||||
* The device name or network host name/address associated with this port.
|
||||
*/
|
||||
public String deviceName = "";
|
||||
/**
|
||||
* The baud rate used for the port, if needed, or port number for TCP and UDP socket connections.
|
||||
*/
|
||||
public int baudRate = 0;
|
||||
/**
|
||||
* The amateur radio station callsign associated with this port, if needed.
|
||||
* Also used for weather station model name.
|
||||
*/
|
||||
public String callsign = "";
|
||||
/**
|
||||
* The authentication passcode associated with this port, if needed.
|
||||
*/
|
||||
public String passcode = "";
|
||||
/**
|
||||
* Any filter expression associated with this port, if needed.
|
||||
*/
|
||||
public String filter = "";
|
||||
/**
|
||||
* Indicates whether messages can be transmitted from YAAC via this port.
|
||||
*/
|
||||
public boolean transmitAllowed;
|
||||
/**
|
||||
* Indicates whether flow control is enabled on this port.
|
||||
*/
|
||||
public boolean flowControlled;
|
||||
/**
|
||||
* A collection of flag bits indicating other attributes of the port configuration.
|
||||
* @see #FLAGS_HF
|
||||
* @see #FLAGS_UDP
|
||||
* @see #FLAGS_LOCAL
|
||||
* @see #FLAGS_NODTR
|
||||
* @see #FLAGS_ALWAYS_RETRY
|
||||
* @see #FLAGS_USE_WORKAROUND
|
||||
* @see #FLAGS_PIPELINED
|
||||
* @see #FLAGS_DONT_COALESCE
|
||||
* @see #FLAGS_MASK_KISSPORT
|
||||
* @see #FLAGS_IS_SRV_INST_NAME
|
||||
* @see #FLAGS_HARDWARE
|
||||
*/
|
||||
public int flags;
|
||||
/**
|
||||
* Array of digipeat alias Strings for which this port will digipeat (if transmitAllowed is true).
|
||||
*
|
||||
* @see #transmitAllowed
|
||||
*/
|
||||
public String[] digiAliases = NO_ALIASES;
|
||||
/**
|
||||
* Bit mask or enum number of protocols that can be transmitted through this port. Only meaningful for
|
||||
* ports with CAP_XMT_PACKET_DATA set (for protocol bitmask) or CAP_WAYPOINT_SENDER (for enum).
|
||||
* @see Connector#CAP_XMT_PACKET_DATA
|
||||
* @see Connector#CAP_WAYPOINT_SENDER
|
||||
* @see #PROTOCOL_APRS
|
||||
* @see #PROTOCOL_OPENTRAC
|
||||
* @see #PROTOCOL_AX25
|
||||
* @see #SENTENCE_NONE
|
||||
* @see #SENTENCE_GPWPL
|
||||
* @see #SENTENCE_PKWDWPL
|
||||
*/
|
||||
public int acceptableProtocolsMask = PROTOCOL_APRS;
|
||||
/**
|
||||
* Number of seconds in a timeslot cycle, which will be aligned to UTC and the Unix epoch time
|
||||
* (if some weird prime number is used). Negative values means timeslotting is disabled, but is
|
||||
* preserving the last-used cycle length in case it gets re-enabled.
|
||||
*/
|
||||
public int timeslotCycleLength = -120;
|
||||
/**
|
||||
* Number of seconds since the start of a cycle when this port is allowed to transmit.
|
||||
* This should be an integer multiple of {@link #timeslotLength} greater than or equal to
|
||||
* zero, but less than the {@link #timeslotCycleLength}.
|
||||
*/
|
||||
public int timeslotOffset = 0;
|
||||
/**
|
||||
* Number of seconds in a timeslot. This should be a positive integer fraction of the
|
||||
* {@link #timeslotCycleLength}. Zero or negative means that once a station enters its
|
||||
* timeslot, it can transmit until its queue for the port is empty, rather than having
|
||||
* to stop because another station's timeslot has started.
|
||||
*/
|
||||
public int timeslotLength = 10;
|
||||
/**
|
||||
* Names of beacon instances to send through this port (zero-length array means only default beacon).
|
||||
* @see BeaconData
|
||||
* @see BeaconData#MYCALL
|
||||
* @see BeaconData#beaconName
|
||||
*/
|
||||
public String[] beaconNames = new String[0];
|
||||
|
||||
/**
|
||||
* Make a deep copy of this Cfg object.
|
||||
*
|
||||
* @return duplicate Cfg
|
||||
*/
|
||||
public Cfg dup() {
|
||||
Cfg cfg = new Cfg();
|
||||
cfg.deviceName = deviceName;
|
||||
cfg.baudRate = baudRate;
|
||||
cfg.callsign = callsign;
|
||||
cfg.passcode = passcode;
|
||||
cfg.transmitAllowed = transmitAllowed;
|
||||
cfg.digiAliases = new String[digiAliases.length];
|
||||
System.arraycopy(digiAliases, 0, cfg.digiAliases, 0, digiAliases.length);
|
||||
cfg.filter = filter;
|
||||
cfg.flowControlled = flowControlled;
|
||||
cfg.acceptableProtocolsMask = acceptableProtocolsMask;
|
||||
cfg.timeslotCycleLength = timeslotCycleLength;
|
||||
cfg.timeslotOffset = timeslotOffset;
|
||||
cfg.timeslotLength = timeslotLength;
|
||||
cfg.flags = flags;
|
||||
cfg.beaconNames = Arrays.copyOf(beaconNames, beaconNames.length);
|
||||
return cfg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares this object with the specified object for order. Returns a
|
||||
* negative integer, zero, or a positive integer as this object is less
|
||||
* than, equal to, or greater than the specified object.
|
||||
*
|
||||
* @param o the object to be compared.
|
||||
* @return a negative integer, zero, or a positive integer as this object
|
||||
* is less than, equal to, or greater than the specified object.
|
||||
*
|
||||
* @throws ClassCastException if the specified object's type prevents it
|
||||
* from being compared to this object.
|
||||
*/
|
||||
public int compareTo(Cfg o) {
|
||||
return deviceName.compareTo(o.deviceName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether some other object is "equal to" this one.
|
||||
*
|
||||
* @param obj the reference object with which to compare.
|
||||
* @return {@code true} if this object is the same as the obj
|
||||
* argument; {@code false} otherwise.
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof Cfg) {
|
||||
Cfg other = (Cfg)obj;
|
||||
return other.deviceName.equals(deviceName) &&
|
||||
other.baudRate == baudRate &&
|
||||
other.callsign.equals(callsign) &&
|
||||
other.passcode.equals(passcode) &&
|
||||
other.transmitAllowed == transmitAllowed &&
|
||||
Arrays.equals(digiAliases, other.digiAliases) &&
|
||||
other.filter.equals(filter) &&
|
||||
other.flowControlled == flowControlled &&
|
||||
other.acceptableProtocolsMask == acceptableProtocolsMask &&
|
||||
other.timeslotCycleLength == timeslotCycleLength &&
|
||||
other.timeslotOffset == timeslotOffset &&
|
||||
other.timeslotLength == timeslotLength &&
|
||||
other.flags == flags &&
|
||||
Arrays.equals(other.beaconNames, beaconNames);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a hash code value for the object.
|
||||
*
|
||||
* @return a hash code value for this object.
|
||||
* @see #equals(Object)
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return deviceName.hashCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the object.
|
||||
* @return a string representation of the object.
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PortConfig.Cfg[" + deviceName + ',' + baudRate + ',' + callsign + ',' + (transmitAllowed ? "xmt" : "") +
|
||||
",digi=" + Arrays.toString(digiAliases) + ',' + (null != filter && filter.length() > 0 ? "filt=\"" + filter + '"' : "") +
|
||||
(flags != 0 ? ",flags=0x" + Integer.toHexString(flags) : "") +
|
||||
(timeslotCycleLength > 0 ? ",ts=" + timeslotOffset + '(' + timeslotLength + ")/" + timeslotCycleLength : "") + ']';
|
||||
}
|
||||
}
|
||||
|
||||
private final HashMap<String, Cfg> variations = new HashMap<String, Cfg>(2);
|
||||
|
||||
/**
|
||||
* Get the port-type-specific configuration parameters for the currently specified port type.
|
||||
* @return type-specific Cfg record
|
||||
*/
|
||||
public final Cfg current() {
|
||||
Cfg cfg;
|
||||
if ((cfg = variations.get(portType)) == null) {
|
||||
variations.put(portType, cfg = new Cfg());
|
||||
}
|
||||
return cfg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the port-type-specific configuration parameters for a specific port type.
|
||||
* @param portType the name of the port type whose configuration parameters should be obtained
|
||||
* @return type-specific Cfg record (created if it did not previously exist)
|
||||
*/
|
||||
public final Cfg specific(String portType) {
|
||||
Cfg cfg;
|
||||
if ((cfg = variations.get(portType)) == null) {
|
||||
variations.put(portType, cfg = new Cfg());
|
||||
}
|
||||
return cfg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new PortConfig record with a new port instance identifier.
|
||||
*/
|
||||
public PortConfig() {}
|
||||
|
||||
/**
|
||||
* Allocate a new port number for this PortConfig object.
|
||||
*/
|
||||
public void assignPortNumber() {
|
||||
portNumber = PortManager.getNextAvailablePortNumber();
|
||||
displayName = "Port" + portNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Craete a PortConfig record with the specified port instance identifier.
|
||||
* @param displayName port identifier name to use
|
||||
*/
|
||||
public PortConfig(String displayName) {
|
||||
this.displayName = displayName;
|
||||
portNumber = Integer.parseInt(displayName.substring(4).trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether some other object is "equal to" this one.
|
||||
*
|
||||
* @param obj the reference object with which to compare.
|
||||
* @return <code>true</code> if this object is the same as the obj
|
||||
* argument; <code>false</code> otherwise.
|
||||
* @see #hashCode()
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof PortConfig) {
|
||||
PortConfig o = (PortConfig)obj;
|
||||
return displayName.equals(o.displayName);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a hash code value for the object. This method is
|
||||
* supported for the benefit of hashtables such as those provided by
|
||||
* <code>java.util.Hashtable</code>.
|
||||
*
|
||||
* @return a hash code value for this object.
|
||||
* @see #equals(Object)
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return displayName.hashCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name by which this port will be identified.
|
||||
* @return String port name
|
||||
*/
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the arbitrary sequence port number assigned to this configuration record.
|
||||
* @return port number
|
||||
*/
|
||||
public int getPortNumber() {
|
||||
return portNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the object.
|
||||
*
|
||||
* @return a string representation of the object.
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
Cfg cfg = current();
|
||||
return "PortConfig[" + displayName + '>' + portType + ',' + cfg.deviceName + ',' + cfg.baudRate + ',' + cfg.callsign + ']';
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a deep copy of this PortConfig object.
|
||||
* @return duplicate PortConfig
|
||||
*/
|
||||
public PortConfig dup() {
|
||||
PortConfig cfg = new PortConfig(displayName);
|
||||
cfg.portType = portType;
|
||||
cfg.enabled = enabled;
|
||||
for (Map.Entry<String, Cfg> entry : variations.entrySet()) {
|
||||
cfg.variations.put(entry.getKey(), entry.getValue().dup());
|
||||
}
|
||||
return cfg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do a deep copy of this PortConfig object into another object..
|
||||
* @param dest destination PortConfig
|
||||
*/
|
||||
public void copyInto(PortConfig dest) {
|
||||
dest.portType = portType;
|
||||
dest.enabled = enabled;
|
||||
dest.variations.clear();
|
||||
dest.variations.put(portType, variations.get(portType));
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares this object with the specified object for order. Returns a
|
||||
* negative integer, zero, or a positive integer as this object is less
|
||||
* than, equal to, or greater than the specified object.
|
||||
*
|
||||
* @param other the object to be compared.
|
||||
* @return a negative integer, zero, or a positive integer as this object
|
||||
* is less than, equal to, or greater than the specified object.
|
||||
*
|
||||
* @throws ClassCastException if the specified object's type prevents it
|
||||
* from being compared to this object.
|
||||
*/
|
||||
public int compareTo(PortConfig other) {
|
||||
int diff = portType.compareTo(other.portType);
|
||||
if (0 == diff) {
|
||||
diff = current().compareTo(other.current());
|
||||
}
|
||||
return diff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write this PortConfig object to Java Preferences on the Ports sub-node of the
|
||||
* specified Preferences node.
|
||||
* @param root Preferences node to use to store the Ports sub-node
|
||||
*/
|
||||
public void writeToPreferences(Preferences root) {
|
||||
// write or overwrite preferences
|
||||
Preferences portNode = root.node("Ports");
|
||||
portNode.put(displayName, generatePrefsString());
|
||||
try {
|
||||
portNode.flush();
|
||||
} catch (BackingStoreException e) {
|
||||
e.printStackTrace(System.out);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Generate the encoded String for storing the PortConfig into Java Preferences.
|
||||
* The preferences String consists of multiple expressions separated by semicolon ";"
|
||||
* characters. The first expression (required) will have the following format:</p>
|
||||
* <code><i>portType</i>,<i>deviceName</i>,<i>baudRateOrPortNumber</i>,<i>callsign</i>,<i>passcode</i>,<i>transmitEnabled</i>[,<i>digipeatAlias[,...]]</i></code>
|
||||
* <p>where <i>deviceName</i> and <i>passcode</i> are escaped to be legitimate text in an XML tag body,
|
||||
* <i>transmitEnabled</i> is either "true" or "false", and there can be any number of digipeat aliases
|
||||
* in the syntax of display-format AX.25 callsign-SSID values.</p>
|
||||
* <p>The second expression is a APRS-IS filter expression.</p>
|
||||
* <p>The third expression is "true" or "false" for whether this port is enabled for operation.</p>
|
||||
* <p>The fourth expression is "true" or "false" for whether flow control (or something overloading
|
||||
* the meaning of this field) should be enabled or not.</p>
|
||||
* <p>The fifth expression is a decimal number of the value of the {@link Cfg#acceptableProtocolsMask} bitmask.</p>
|
||||
* <p>The sixth expression is two decimal numbers separated by a slash "/" character representing
|
||||
* the timeslot offset (relative to the top of the cycle) in seconds and the length of the timeslot cycle in seconds.
|
||||
* Negative cycle length indicates that timeslotting is not used, but the values are preserved
|
||||
* in case the user wants to turn timeslotting back on.</p>
|
||||
* <p>The seventh expression is a decimal number of the value of the {@link Cfg#flags} bitmask.
|
||||
* Bit meanings are portType-specific.</p>
|
||||
* <p>The eighth expression is a pipe "|" separated list of names of {@link BeaconData}
|
||||
* definitions that should be transmitted through this port. Only meaningful for ports
|
||||
* capable of transmitting APRS or OpenTRAC packets.</p>
|
||||
* @return encoded String
|
||||
* @see #decodePreferenceValue(String)
|
||||
*/
|
||||
public String generatePrefsString() {
|
||||
Cfg cfg = current();
|
||||
StringBuilder b = new StringBuilder(portType).append(',').append(passcodeEncode(cfg.deviceName)).append(',').
|
||||
append(cfg.baudRate).append(',').append(cfg.callsign).append(',').append(passcodeEncode(cfg.passcode)).append(',').append(cfg.transmitAllowed);
|
||||
for (String r : cfg.digiAliases) {
|
||||
b.append(',').append(r);
|
||||
}
|
||||
b.append(';').append(cfg.filter).append(';').append(enabled).append(';').append(cfg.flowControlled).append(';').append(cfg.acceptableProtocolsMask);
|
||||
b.append(';').append(cfg.timeslotOffset).append('/').append(cfg.timeslotCycleLength).append('/').append(cfg.timeslotLength);
|
||||
b.append(';').append(cfg.flags).append(';');
|
||||
for (int i = 0; i < cfg.beaconNames.length; i++) {
|
||||
if (0 != i) {
|
||||
b.append('|');
|
||||
}
|
||||
b.append(cfg.beaconNames[i]);
|
||||
}
|
||||
return b.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode the passcode so characters that would break XML or our field concatenation are escaped.
|
||||
* @param passcode String containing passcode
|
||||
* @return escaped passcode
|
||||
*/
|
||||
private static String passcodeEncode(String passcode) {
|
||||
boolean needsEscape = false;
|
||||
int len = passcode.length();
|
||||
for (int i = 0; i < len; i++) {
|
||||
char ch = passcode.charAt(i);
|
||||
if (ch < ' ' || ch >= '\u007F' || '&' == ch || '"' == ch || '\\' == ch || ',' == ch || ';' == ch) {
|
||||
needsEscape = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (needsEscape) {
|
||||
StringBuilder b = new StringBuilder(len + 20);
|
||||
for (int i = 0; i < len; i++) {
|
||||
char ch = passcode.charAt(i);
|
||||
if (ch < ' ' || ch >= '\u007F' || '&' == ch || '"' == ch || '\\' == ch || ',' == ch || ';' == ch) {
|
||||
b.append("\\u").append(Integer.toHexString(0x10000 + (int)ch).substring(1));
|
||||
} else {
|
||||
b.append(ch);
|
||||
}
|
||||
}
|
||||
passcode = b.toString();
|
||||
}
|
||||
return passcode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete this PortConfig from the Java Preferences backing store.
|
||||
* @param root Preferences node used to store the Ports sub-node
|
||||
*/
|
||||
public void removeFromPreferences(Preferences root) {
|
||||
// remove this PortConfig from the Preferences
|
||||
Preferences portNode = root.node("Ports");
|
||||
portNode.remove(displayName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a PortConfig object from Java Preferences.
|
||||
* @param root Preferences node used to store the Ports sub-node
|
||||
* @param displayName the String port identifier name to read
|
||||
* @return a populated PortConfig object for the specified name, or null if no such record
|
||||
* exists in the specified node of Preferences
|
||||
*/
|
||||
public static PortConfig readFromPreferences(Preferences root, String displayName) {
|
||||
PortConfig config = null;
|
||||
Preferences portNode = root.node("Ports");
|
||||
String values = portNode.get(displayName, null);
|
||||
if (values != null) {
|
||||
config = new PortConfig(displayName);
|
||||
config.decodePreferenceValue(values);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the storage format used to store a PortConfig in Java Preferences into
|
||||
* this PortConfig object.
|
||||
* @param values String of encoded values
|
||||
* @see #generatePrefsString()
|
||||
*/
|
||||
public void decodePreferenceValue(String values) {
|
||||
String[] prePostFilter = AX25Message.split(values, ';');
|
||||
String[] tk = AX25Message.split(prePostFilter[0], ',');
|
||||
portType = tk[0];
|
||||
if (portType.equals("I-Gate")) {
|
||||
portType = "APRS-IS";
|
||||
}
|
||||
Cfg cfg = current();
|
||||
cfg.deviceName = passcodeDecode(tk[1]);
|
||||
if (tk.length >= 3) {
|
||||
cfg.baudRate = Integer.parseInt(tk[2]);
|
||||
if (tk.length >= 4) {
|
||||
cfg.callsign = tk[3];
|
||||
if (tk.length >= 5) {
|
||||
cfg.passcode = passcodeDecode(tk[4]);
|
||||
if (tk.length >= 6) {
|
||||
cfg.transmitAllowed = Boolean.parseBoolean(tk[5]);
|
||||
if (tk.length >= 7) {
|
||||
cfg.digiAliases = new String[tk.length - 6];
|
||||
System.arraycopy(tk, 6, cfg.digiAliases, 0, cfg.digiAliases.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (prePostFilter.length > 1) {
|
||||
cfg.filter = prePostFilter[1];
|
||||
if (prePostFilter.length > 2) {
|
||||
if (prePostFilter[2].length() > 0) {
|
||||
enabled = Boolean.parseBoolean(prePostFilter[2]);
|
||||
}
|
||||
if (prePostFilter.length > 3) {
|
||||
if (prePostFilter[3].length() > 0) {
|
||||
cfg.flowControlled = Boolean.parseBoolean(prePostFilter[3]);
|
||||
}
|
||||
if (prePostFilter.length > 4) {
|
||||
if (prePostFilter[4].length() > 0) {
|
||||
cfg.acceptableProtocolsMask = Integer.parseInt(prePostFilter[4]);
|
||||
}
|
||||
if (prePostFilter.length > 5) {
|
||||
if (prePostFilter[5].length() > 0) {
|
||||
String[] timeslotParams = AX25Message.split(prePostFilter[5], '/');
|
||||
cfg.timeslotOffset = Integer.parseInt(timeslotParams[0]);
|
||||
cfg.timeslotCycleLength = Integer.parseInt(timeslotParams[1]);
|
||||
if (timeslotParams.length >= 3) {
|
||||
cfg.timeslotLength = Integer.parseInt(timeslotParams[2]);
|
||||
} else {
|
||||
cfg.timeslotLength = 0; // for backwards compatibility
|
||||
}
|
||||
}
|
||||
if (prePostFilter.length > 6) {
|
||||
if (prePostFilter[6].length() > 0) {
|
||||
cfg.flags = Integer.parseInt(prePostFilter[6]);
|
||||
}
|
||||
if (prePostFilter.length > 7) {
|
||||
if (prePostFilter[7].length() > 0) {
|
||||
cfg.beaconNames = AX25Message.split(prePostFilter[7], '\u001E');
|
||||
//TODO: temporary repair until damage from build#107 is removed
|
||||
String defaultBeaconDisplayName = Localizer.getMsg("configure.tab.Beacon.DefaultBeaconName");
|
||||
for (int i = 0; i < cfg.beaconNames.length; i++) {
|
||||
if (cfg.beaconNames[i].equals(defaultBeaconDisplayName)) {
|
||||
cfg.beaconNames[i] = BeaconData.MYCALL;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cfg.beaconNames = new String[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the passcode so characters that would break XML or our field concatenation are un-escaped.
|
||||
* @param passcode String containing escaped passcode
|
||||
* @return unescaped passcode
|
||||
*/
|
||||
private static String passcodeDecode(String passcode) {
|
||||
boolean hasEscape = false;
|
||||
int len = passcode.length();
|
||||
for (int i = 0; i < len; i++) {
|
||||
char ch = passcode.charAt(i);
|
||||
if ('\\' == ch) {
|
||||
hasEscape = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hasEscape) {
|
||||
StringBuilder b = new StringBuilder(len);
|
||||
for (int i = 0; i < len; i++) {
|
||||
char ch = passcode.charAt(i);
|
||||
if ('\\' == ch) {
|
||||
if (passcode.charAt(i+1) != 'u' || i + 6 > len) {
|
||||
throw new IllegalArgumentException("invalid encoded string \"" + passcode + '"');
|
||||
}
|
||||
ch = (char)Integer.parseInt(passcode.substring(i+2,i+6), 16);
|
||||
i += 5;
|
||||
}
|
||||
b.append(ch);
|
||||
}
|
||||
passcode = b.toString();
|
||||
}
|
||||
return passcode;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>RXTXcomm</groupId>
|
||||
<artifactId>RXTXcomm</artifactId>
|
||||
<version>2.2pre2</version>
|
||||
<name>RXTXcomm</name>
|
||||
|
||||
<description>
|
||||
RXTXcomm implements the Java Serial Communications API with native libraries for Microsoft Windows (32-bit and 64-bit),
|
||||
Linux (i386 and x86_64 architectures), and Macintosh OS X on Intel hardware.
|
||||
</description>
|
||||
|
||||
<url>http://rxtx.qbang.org</url>
|
||||
|
||||
<scm>
|
||||
<connection>cvs:pserver:anonymous@qbang.org:/var/cvs/cvsroot</connection>
|
||||
<developerConnection>scm:cvs:pserver:anonymous@qbang.org:/var/cvs/cvsroot</developerConnection>
|
||||
</scm>
|
||||
|
||||
<dependencies>
|
||||
<!-- used for unit tests -->
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>3.8.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<properties>
|
||||
<maven.compile.source>1.4</maven.compile.source>
|
||||
<maven.compile.target>1.4</maven.compile.target>
|
||||
<commons.componentid>compress</commons.componentid>
|
||||
<commons.release.version>1.4</commons.release.version>
|
||||
<commons.release.name>RXTXcomm-2.2pre2</commons.release.name>
|
||||
<commons.osgi.symbolicName>gnu.io</commons.osgi.symbolicName>
|
||||
<commons.deployment.protocol>cvs</commons.deployment.protocol>
|
||||
<commons.binary.suffix />
|
||||
</properties>
|
||||
</project>
|
|
@ -0,0 +1,112 @@
|
|||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<title>Querying Stations</title>
|
||||
</head>
|
||||
<body lang="en-US" dir="LTR">
|
||||
<h1>Querying Stations</h1>
|
||||
<p>There are several types of queries one transmit-capable APRS station can make to
|
||||
another. The most common, requesting an updated position report, is
|
||||
automatically issued on a <a href="./locatestation.html">Locate->Station</a> request if YAAC doesn't have
|
||||
recent position information about the requested station. Other common
|
||||
queries supported by YAAC are:</p>
|
||||
<ul>
|
||||
<li>Query->Station Direct: The list of stations heard directly (not via digipeaters or
|
||||
IGates) by the queried station
|
||||
<li>Query->Station Heard: How many messages have been heard by the queried station from
|
||||
a third station
|
||||
<li>Query->Station Ping and Query->Station Trace: The path from the queried station to the querying station
|
||||
(using either the ?PING? or ?APRST queries)
|
||||
<li>Query->Station Status: What is the remote station's status message?</li>
|
||||
<li>Query->Station SW Version: a description of the software being executed on the queried station.
|
||||
Note this query is presently supported only by YAAC, and KJ4ERJ's APRSIS/32 and APRSIS/CE, and will probably
|
||||
be ignored by other stations.</li>
|
||||
</ul>
|
||||
<p><b>NOTE</b> None of these commands will be available until after you have <a href="./FAQ.html#noquery">set up a
|
||||
transmit-capable APRS port</a> in YAAC. You may need to restart YAAC so that the menu bar will notice
|
||||
the new capability and add the extra menu options.</p>
|
||||
<p>In addition, YAAC can make broadcast queries to search for RF-local stations, either any APRS station, or
|
||||
those performing I-gate or weather station functions. These broadcast queries can only be used when YAAC
|
||||
has a RF connection and will only be transmitted out RF ports.</p>
|
||||
<p>Also, YAAC can broadcast <a name="queryQRU">QRU requests</a> to find out about local resources of various types.
|
||||
Some of the standard QRU groups that are commonly used are:</p>
|
||||
<table border="1"><tbody>
|
||||
<tr><th>group name</th><th>meaning</th></tr>
|
||||
<tr><td>AMBU</td><td>ambulance</td></tr>
|
||||
<tr><td>CLUB</td><td>ham radio club</td></tr>
|
||||
<tr><td>ECHO</td><td>Echolink</td></tr>
|
||||
<tr><td>FIRE</td><td>fire station</td></tr>
|
||||
<tr><td>FOOD</td><td>restaurants</td></tr>
|
||||
<tr><td>FUEL</td><td>gas/petrol stations</td></tr>
|
||||
<tr><td>HOSP</td><td>hospitals</td></tr>
|
||||
<tr><td>LIFEBOAT</td><td>lifeboats</td></tr>
|
||||
<tr><td>LTHS</td><td>lighthouses</td></tr>
|
||||
<tr><td>POLI</td><td>police stations</td></tr>
|
||||
<tr><td>POST</td><td>post offices</td></tr>
|
||||
<tr><td>RD13</td><td>D-Star 13cm repeaters</td></tr>
|
||||
<tr><td>RD23</td><td>D-Star 23cm repeaters</td></tr>
|
||||
<tr><td>RD2M</td><td>D-Star 2m repeaters</td></tr>
|
||||
<tr><td>RD3C</td><td>D-Star 3cm repeaters</td></tr>
|
||||
<tr><td>RD70</td><td>D-Star 70cm repeaters</td></tr>
|
||||
<tr><td>RP10</td><td>analog 10m repeaters</td></tr>
|
||||
<tr><td>RP13</td><td>analog 13cm repeaters</td></tr>
|
||||
<tr><td>RP23</td><td>analog 23cm repeaters</td></tr>
|
||||
<tr><td>RP2M</td><td>analog 2m repeaters</td></tr>
|
||||
<tr><td>RP3C</td><td>analog 3cm repeaters</td></tr>
|
||||
<tr><td>RP6M</td><td>analog 6m repeaters</td></tr>
|
||||
<tr><td>RP70</td><td>analog 70cm repeaters</td></tr>
|
||||
<tr><td>RT13</td><td>television 13cm repeaters</td></tr>
|
||||
<tr><td>RT23</td><td>television 23cm repeaters</td></tr>
|
||||
<tr><td>RT3C</td><td>television 3cm repeaters</td></tr>
|
||||
<tr><td>SRAIL</td><td>steam railroad</td></tr>
|
||||
<tr><td>STOR</td><td>Amateur Radio stores</td></tr>
|
||||
<tr><td>T2SRV</td><td>approximate locations of Tier 2 APRS-IS servers</td></tr>
|
||||
<tr><td>VETE</td><td>veterinarians</td></tr>
|
||||
<tr><td>WOTA</td><td>Wainwrights On The Air peaks</td></tr>
|
||||
</tbody></table>
|
||||
<p>To find out what QRU codes are in use in your area, send the default query INFO; every QRU server station in range will
|
||||
report the QRU codes they serve and their maximum range.</p>
|
||||
<p>YAAC can also function as a QRU server, based on the default beacon position. Objects and Items announced by
|
||||
YAAC can be <a href="./objecteditor.html#defineQRU">associated with a QRU group</a>, and will be automatically retransmitted when a corresponding in-range
|
||||
QRU request is received.</p>
|
||||
<p>YAAC can also send and receive lists of <a name="tactical">tactical callsigns (aliases)</a> in use by RF-local
|
||||
stations and objects; this message interchange is compatible with Xastir and APRS+SA.</p>
|
||||
<p>The APRS-standard and custom queries that YAAC will respond to are:</p>
|
||||
<table border="1"><tbody>
|
||||
<tr><th>Directed query code</th><th>Response</th></tr>
|
||||
<tr><td>??</td><td>This list of query commands recognized by YAAC.</td></tr>
|
||||
<tr><td>?ABOUT</td><td>The version of YAAC software being used.</td></tr>
|
||||
<tr><td>?ALOHA</td><td>The current radius of the Aloha circle for this YAAC station.</td></tr>
|
||||
<tr><td>?APRSD</td><td>The list of directly heard stations.</td></tr>
|
||||
<tr><td>?APRSH <I>callsign</I></td><td>The number of messages heard from the specified station for each of the last 8 hours.</td></tr>
|
||||
<tr><td>?APRSL</td><td>The list of stations considered local for I-Gating by this station.</td></tr>
|
||||
<tr><td>?APRSM</td><td>Immediately retransmit all unacknowledged messages transmitted to the requesting station.</td></tr>
|
||||
<tr><td>?APRSO</td><td>Immediately transmit all objects and items controlled by this station.</td></tr>
|
||||
<tr><td>?APRSP</td><td>Immediately transmit the current position of this station.</td></tr>
|
||||
<tr><td>?APRSS</td><td>Immediately transmit the current status message of this station.</td></tr>
|
||||
<tr><td>?APRST</td><td>A message with the path from the requesting station to the queried station.</td></tr>
|
||||
<tr><td>?APRSV</td><td>The version of YAAC software being used.</td></tr>
|
||||
<tr><td>?IGATE</td><td>If this station is functioning as an I-Gate, the I-Gate statistics for this station.</td></tr>
|
||||
<tr><td>?PING?</td><td>A message with the path from the requesting station to the queried station.</td></tr>
|
||||
<tr><td>?QUERIES</td><td>This list of query commands recognized by YAAC.</td></tr>
|
||||
<tr><td>?UP</td><td>Immediately transmit the duration of time this instance of YAAC has been running.</td></tr>
|
||||
<tr><td>?VER</td><td>The version of software being used.</td></tr>
|
||||
<tr><td>?WX</td><td>If this station has a weather station attached, immediately send the station position and a positionless weather message.</td></tr>
|
||||
<tr><td>?YDBG</td><td>Report debugging information about the local YAAC station, including memory consumption, station/object quantity being monitored, current map pan and zoom, and thread status.</td></tr>
|
||||
</tbody></table>
|
||||
<a name="broadcast"><table border="1"><tbody>
|
||||
<tr><th>Broadcast query code</th><th>Response</th></tr>
|
||||
<tr><td>?APRS?</td><td>Immediately transmit the current position of this station.</td></tr>
|
||||
<tr><td>?IGATE?</td><td>If this station is functioning as an I-Gate, the I-Gate statistics for this station.</td></tr>
|
||||
<tr><td>?WX?</td><td>If this station has a weather station attached, immediately send the station position and a positionless weather message.</td></tr>
|
||||
</tbody></table></a>
|
||||
<p>Note that the above broadcasts are only permitted on RF, as every station on the planet would be expected to
|
||||
respond if these were sent over the APRS-IS Internet servers.</p>
|
||||
<table border="1"><tbody>
|
||||
<tr><th>Broadcast message destination</th><th>Meaning</th></tr>
|
||||
<tr><td>QRU</td><td>ask for local resources</td></tr>
|
||||
<tr><td>TACTICAL</td><td>send or receive tactical callsign aliases for real callsign-SSIDs</td></tr>
|
||||
</tbody></table>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,716 @@
|
|||
package org.ka2ddo.yaac.legacyrxtx.io;
|
||||
/*
|
||||
* Copyright (C) 2011-2020 Andrew Pavlin, KA2DDO
|
||||
* This file is part of YAAC (Yet Another APRS Client).
|
||||
*
|
||||
* YAAC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* YAAC 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
|
||||
* and GNU Lesser General Public License along with YAAC. If not,
|
||||
* see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import gnu.io.CommPortIdentifier;
|
||||
import gnu.io.SerialPort;
|
||||
import gnu.io.SerialPortEvent;
|
||||
import gnu.io.SerialPortEventListener;
|
||||
import gnu.io.UnsupportedCommOperationException;
|
||||
import org.ka2ddo.aprs.PositionlessWeatherReport;
|
||||
import org.ka2ddo.ax25.AX25Callsign;
|
||||
import org.ka2ddo.ax25.AX25Frame;
|
||||
import org.ka2ddo.ax25.TransmittingConnector;
|
||||
import org.ka2ddo.gps.GpsFix;
|
||||
import org.ka2ddo.gps.GpsFixQuality;
|
||||
import org.ka2ddo.util.FastBlockingQueue;
|
||||
import org.ka2ddo.util.ReschedulableTimerTask;
|
||||
import org.ka2ddo.yaac.YAAC;
|
||||
import org.ka2ddo.yaac.core.HelpTaggedIOException;
|
||||
import org.ka2ddo.yaac.gps.GPSDistributor;
|
||||
import org.ka2ddo.yaac.io.KissEscapeOutputStream;
|
||||
import org.ka2ddo.yaac.io.NonshareableBufferedDataOutputStream;
|
||||
import org.ka2ddo.yaac.io.PortConfig;
|
||||
import org.ka2ddo.yaac.io.PortManager;
|
||||
import org.ka2ddo.yaac.io.TNCConnector;
|
||||
import org.ka2ddo.yaac.io.Transmitter;
|
||||
import org.ka2ddo.yaac.weather.WeatherDistributor;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* This class implements a port to a TNC connected by a serial port using KISS
|
||||
* over asynchronous serial data via RS-232C or USB. Note this can only handle one TNC
|
||||
* per serial port; it does not support the multiplexing with port numbers in the
|
||||
* high nibble of the KISS command byte, but only expects port#0, although it does
|
||||
* support other device numbers in the KISS packets to support the Byonics TinyTrak 4's
|
||||
* GPS feed-through multiplexing (which ensures the GPS data doesn't get mixed with
|
||||
* the AX.25 packet data).
|
||||
* @author Andrew Pavlin, KA2DDO
|
||||
*/
|
||||
public class SerialTNCConnector extends TNCConnector implements SerialPortEventListener, TransmittingConnector {
|
||||
/**
|
||||
* The PortConnector type name for SerialTNCConnector.
|
||||
*/
|
||||
public static final String TYPE_NAME = "Serial_TNC";
|
||||
/**
|
||||
* The fully-qualified platform-wildcarded class name for the configuration editor for SerialTNCConnector.
|
||||
*/
|
||||
public static final String CONFIG_GUI = "org.ka2ddo.yaac.legacyrxtx.*.SerialTNCPortEditor";
|
||||
private static final EnumSet<PortConfig.Fields> x_BLANK_FIELDS = EnumSet.noneOf(PortConfig.Fields.class);
|
||||
/**
|
||||
* Set of PortConfig field identifiers for fields that should not be copied from another system and should therefore be
|
||||
* blanked out when copying configuration files.
|
||||
*/
|
||||
public static final Set<PortConfig.Fields> BLANK_FIELDS = Collections.unmodifiableSet(x_BLANK_FIELDS);
|
||||
private static final IdentityHashMap<PortConfig.Fields, PortConfig.RequireHints> x_REQ_FIELDS = new IdentityHashMap<PortConfig.Fields, PortConfig.RequireHints>();
|
||||
/**
|
||||
* List of PortConfig field identifiers that are mandated for this port type to work, and the ResourceBundle key to
|
||||
* prompt for a value if missing.
|
||||
*/
|
||||
public static final Map<PortConfig.Fields, PortConfig.RequireHints> REQ_FIELDS = Collections.unmodifiableMap(x_REQ_FIELDS);
|
||||
static {
|
||||
x_BLANK_FIELDS.add(PortConfig.Fields.deviceName);
|
||||
x_REQ_FIELDS.put(PortConfig.Fields.deviceName, new PortConfig.RequireHints("ConfigImporter.serialPort", PortConfig.HintType.SERIAL_PORTS));
|
||||
x_BLANK_FIELDS.add(PortConfig.Fields.callsign);
|
||||
x_REQ_FIELDS.put(PortConfig.Fields.callsign, new PortConfig.RequireHints("ConfigImporter.callsign", PortConfig.HintType.CALLSIGN_SSID));
|
||||
}
|
||||
|
||||
private CommPortIdentifier serialPortId;
|
||||
private SerialPort serialPort;
|
||||
private transient String cachedToString = null;
|
||||
private NonshareableBufferedDataOutputStream os = null;
|
||||
private KissEscapeOutputStream kos = null;
|
||||
private InputStream is;
|
||||
private byte[] rcvBuf = new byte[2048];
|
||||
private int wEnd = 0;
|
||||
private int cmdPromptCheck = 0;
|
||||
private int kissForceCRCount = 0;
|
||||
private transient long frameStartTime = -1L;
|
||||
private transient int fixedPriority = Thread.MIN_PRIORITY - 1;
|
||||
private transient int numBytesPerEvent = 1;
|
||||
private transient ReschedulableTimerTask timeslotTask = null;
|
||||
private transient FastBlockingQueue<AX25Frame> timeslotQueue = null;
|
||||
private transient long nextAllowableTransmitTime = -1L;
|
||||
private transient boolean rcvdGPSOverKISS = false;
|
||||
private transient boolean rcvdWxOverKISS = false;
|
||||
private static final char[] CMD_MODE_TNC_PROMPT = { 'c', 'm', 'd', ':' };
|
||||
private static final HashMap<Character,Character> ESCAPED_CHAR = new HashMap<Character, Character>();
|
||||
static {
|
||||
ESCAPED_CHAR.put('r', '\r');
|
||||
ESCAPED_CHAR.put('n', '\n');
|
||||
ESCAPED_CHAR.put('t', '\t');
|
||||
ESCAPED_CHAR.put('0', '\0');
|
||||
ESCAPED_CHAR.put('\\', '\\');
|
||||
ESCAPED_CHAR.put('f', '\f');
|
||||
ESCAPED_CHAR.put('b', '\b');
|
||||
ESCAPED_CHAR.put('\'', '\'');
|
||||
ESCAPED_CHAR.put('e', '\033');
|
||||
ESCAPED_CHAR.put('"', '"');
|
||||
ESCAPED_CHAR.put('w', null);
|
||||
}
|
||||
|
||||
private transient KissEscapeOutputStream.RcvState curState = KissEscapeOutputStream.RcvState.IDLE;
|
||||
|
||||
/**
|
||||
* Create an uninitialized SerialTNCConnector instance, to be configured with a later call to configure().
|
||||
* @see #configure(PortConfig)
|
||||
*/
|
||||
public SerialTNCConnector() {}
|
||||
|
||||
/**
|
||||
* Set the correct defaults for a port in process of being created.
|
||||
* @param cfg PortConfig.Cfg whose defaults should be adjusted
|
||||
*/
|
||||
public static void fillinConfigDefaults(PortConfig.Cfg cfg) {
|
||||
if (cfg.deviceName.length() == 0) {
|
||||
// new config, fill with defaults
|
||||
cfg.digiAliases = new String[]{"TEMP1"};
|
||||
cfg.baudRate = 9600;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the configuration of the connector to match the updated
|
||||
* setup.
|
||||
* @param portConfig PortConfig defining new port settings
|
||||
* @throws IOException if interface changes could not be applied
|
||||
* @throws IllegalArgumentException if type information is invalid for
|
||||
* changing the settings of this PortConnector
|
||||
*/
|
||||
public void configure(PortConfig portConfig) throws IOException, IllegalArgumentException {
|
||||
if (!TYPE_NAME.equals(portConfig.portType)) {
|
||||
throw new IllegalArgumentException("invalid portType=" + portConfig.portType);
|
||||
}
|
||||
setPortConfig(portConfig);
|
||||
if (serialPortId != null) {
|
||||
if (!portConfig.enabled || !serialPortId.getName().equals(currentCfg.deviceName)) {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (portConfig.enabled) {
|
||||
if (serialPort == null) {
|
||||
serialPortId = CommPortIdentifier.getPortIdentifier(currentCfg.deviceName);
|
||||
serialPort = (SerialPort)serialPortId.open("YAAC(SerialTNC," + currentCfg.deviceName + ')', 1000);
|
||||
System.out.println(new Date().toString() + ": opened SerialTNC " + currentCfg.deviceName + " default input buf size=" + serialPort.getInputBufferSize());
|
||||
is = serialPort.getInputStream();
|
||||
os = new NonshareableBufferedDataOutputStream(serialPort.getOutputStream(), 4096);
|
||||
kos = new KissEscapeOutputStream((OutputStream)os, (currentCfg.flags & PortConfig.FLAGS_USE_WORKAROUND) != 0);
|
||||
curState = KissEscapeOutputStream.RcvState.IDLE;
|
||||
serialPort.addEventListener(this);
|
||||
serialPort.notifyOnDataAvailable(true);
|
||||
serialPort.notifyOnOverrunError(true);
|
||||
}
|
||||
if (currentCfg.baudRate != 0) {
|
||||
serialPort.setSerialPortParams(currentCfg.baudRate,
|
||||
SerialPort.DATABITS_8,
|
||||
SerialPort.STOPBITS_1,
|
||||
SerialPort.PARITY_NONE);
|
||||
}
|
||||
if (currentCfg.flowControlled) {
|
||||
if (!serialPort.isDSR() && (currentCfg.flags & PortConfig.FLAGS_USE_WORKAROUND) == 0) {
|
||||
// only do this if not a Kenwood radio, which isn't wired to provide DSR
|
||||
throw new HelpTaggedIOException(MessageFormat.format(YAAC.getMsg("dialog.IOError.checkModem"), TYPE_NAME, currentCfg.deviceName), "FAQ.oldTNC");
|
||||
}
|
||||
boolean modemOn = (currentCfg.flags & (PortConfig.FLAGS_LOCAL|PortConfig.FLAGS_NODTR)) == 0; // LOCAL is for backward compatibility
|
||||
serialPort.setDTR(modemOn);
|
||||
serialPort.setFlowControlMode(SerialPort.FLOWCONTROL_RTSCTS_IN | SerialPort.FLOWCONTROL_RTSCTS_OUT);
|
||||
} else {
|
||||
serialPort.setFlowControlMode(SerialPort.FLOWCONTROL_NONE);
|
||||
boolean modemOn = (currentCfg.flags & (PortConfig.FLAGS_LOCAL|PortConfig.FLAGS_NODTR)) == 0; // LOCAL is for backward compatibility
|
||||
serialPort.setDTR(modemOn);
|
||||
serialPort.setRTS(modemOn);
|
||||
}
|
||||
if (currentCfg.callsign.length() > 0) {
|
||||
setPortAddress(new AX25Callsign(currentCfg.callsign));
|
||||
} else {
|
||||
throw new HelpTaggedIOException(MessageFormat.format(YAAC.getMsg("dialog.IOError.cantOpenNoCallsign"), TYPE_NAME, currentCfg.deviceName), "FAQ.oldTNC");
|
||||
}
|
||||
forceToKissMode();
|
||||
cachedToString = null;
|
||||
rcvdGPSOverKISS = false;
|
||||
rcvdWxOverKISS = false;
|
||||
PortManager.firePortOpened(this);
|
||||
// set up transmission queue
|
||||
timeslotQueue = new FastBlockingQueue<>(16);
|
||||
|
||||
// set up queue-draining timer
|
||||
timeslotTask = new ReschedulableTimerTask() {
|
||||
public void run() {
|
||||
long endOfSlot = Long.MAX_VALUE;
|
||||
if (currentCfg.timeslotLength > 0) {
|
||||
endOfSlot = System.currentTimeMillis() + currentCfg.timeslotLength * 1000L;
|
||||
}
|
||||
while (true) {
|
||||
AX25Frame drain = null;
|
||||
synchronized (SerialTNCConnector.this) {
|
||||
if (timeslotQueue != null && timeslotQueue.size() > 0) {
|
||||
drain = timeslotQueue.poll();
|
||||
}
|
||||
}
|
||||
if (drain != null) {
|
||||
try {
|
||||
synchronized (SerialTNCConnector.this) {
|
||||
if (os == null) {
|
||||
// abort if closed in middle of write loop
|
||||
return;
|
||||
}
|
||||
sendFrameNow(drain);
|
||||
if (endOfSlot < nextAllowableTransmitTime) {
|
||||
break; // can't send any more until next assigned timeslot
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace(System.out);
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
if (currentCfg.timeslotCycleLength > 0) {
|
||||
scheduleTimeslotTimer();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.out.println(new Date().toString() + ": failed to open SerialTNC " + currentCfg.deviceName + ": ");
|
||||
e.printStackTrace(System.out);
|
||||
throw new HelpTaggedIOException(MessageFormat.format(YAAC.getMsg("dialog.IOError.unableToOpen"), TYPE_NAME, currentCfg.deviceName), e, "FAQ.oldTNC");
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleTimeslotTimer() {
|
||||
GpsFix fix = GPSDistributor.getInstance().getCurrentFix();
|
||||
long skewCorr = 0;
|
||||
if (fix != null && (fix.quality == GpsFixQuality.DGPS || fix.quality == GpsFixQuality.GPS_SPS || fix.quality == GpsFixQuality.PPS)) {
|
||||
skewCorr = -fix.localToGpsClockDiff;
|
||||
}
|
||||
long wakeup;
|
||||
if (currentCfg.timeslotCycleLength > 0) {
|
||||
long now = System.currentTimeMillis();
|
||||
// figure the next cycle startup time
|
||||
long timeslotCycleLengthMsec = currentCfg.timeslotCycleLength * 1000L;
|
||||
wakeup = (now + skewCorr + timeslotCycleLengthMsec) / timeslotCycleLengthMsec;
|
||||
wakeup *= timeslotCycleLengthMsec;
|
||||
// add our offset
|
||||
wakeup += currentCfg.timeslotOffset * 1000L;
|
||||
// back up one cycle if we have time
|
||||
if (wakeup - now > timeslotCycleLengthMsec + 1000L) {
|
||||
wakeup -= timeslotCycleLengthMsec;
|
||||
}
|
||||
timeslotTask.resched(timeslotTimer, new Date(wakeup), currentCfg.timeslotCycleLength);
|
||||
} else {
|
||||
wakeup = nextAllowableTransmitTime;
|
||||
timeslotTask.resched(timeslotTimer, new Date(wakeup));
|
||||
}
|
||||
}
|
||||
|
||||
private void forceToKissMode() throws IOException {
|
||||
curState = KissEscapeOutputStream.RcvState.IDLE;
|
||||
wEnd = 0; // discard everything in the receive buffer
|
||||
if (currentCfg.filter.length() > 0) {
|
||||
synchronized (this) {
|
||||
kissForceCRCount = 1; // account for one extra CR, to prevent thrashing
|
||||
for (int i = 0; i < currentCfg.filter.length(); i++) {
|
||||
char ch = currentCfg.filter.charAt(i);
|
||||
if ('\\' == ch && i < currentCfg.filter.length() - 1) {
|
||||
Character c = ESCAPED_CHAR.get(currentCfg.filter.charAt(++i));
|
||||
if (c != null) {
|
||||
os.write(c);
|
||||
os.flush();
|
||||
if ('\r' == c) {
|
||||
kissForceCRCount += 2; // assume it will be echoed back plus a response line
|
||||
try {
|
||||
wait(300L);
|
||||
} catch (InterruptedException e) {
|
||||
// do nothing, we're just stalling to let the TNC operate
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
wait(100L);
|
||||
} catch (InterruptedException e) {
|
||||
// do nothing, we're just stalling to let the TNC operate
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
wait(1000L);
|
||||
} catch (InterruptedException e) {
|
||||
// do nothing, we're just stalling to let the TNC operate
|
||||
}
|
||||
}
|
||||
} else {
|
||||
os.write(ch);
|
||||
}
|
||||
}
|
||||
os.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports whether this PortConnector has an open connection to its port.
|
||||
*
|
||||
* @return boolean true if PortConnector is open
|
||||
*/
|
||||
public boolean isOpen() {
|
||||
return serialPort != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transmit an AX.25 frame through this port. May delay packet if using timeslotting or
|
||||
* still transmitting another packet.
|
||||
*
|
||||
* @param frame AX25Frame object to transmit
|
||||
* @throws IOException if transmit failed for any reason other than a receive-only port
|
||||
*/
|
||||
public synchronized void sendFrame(AX25Frame frame) throws IOException {
|
||||
if (os != null && currentCfg.transmitAllowed) {
|
||||
if (currentCfg.timeslotCycleLength <= 0 && System.currentTimeMillis() >= nextAllowableTransmitTime) {
|
||||
sendFrameNow(frame);
|
||||
} else {
|
||||
// queue outbound traffic for timeslot transmit task (coalescing duplicates such as the beacon)
|
||||
if (!timeslotQueue.contains(frame)) {
|
||||
timeslotQueue.add(frame);
|
||||
scheduleTimeslotTimer();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transmit one frame through this port. Must be called from a method synchronized on the SerialTNCConnector
|
||||
* object, and caller is responsible for determining whether this port is enabled for transmissions.
|
||||
* @param frame AX25Frame to send; if sender is missing, the port's callsign is inserted
|
||||
* @throws IOException if transmit fails for any reason
|
||||
*/
|
||||
private void sendFrameNow(AX25Frame frame) throws IOException {
|
||||
fireTransmitting(true);
|
||||
long startTime = System.currentTimeMillis();
|
||||
if (frame.sender == null || (frame.sourcePort != null && frame.sourcePort.hasCapability(CAP_IGATE))) {
|
||||
frame.sender = new AX25Callsign(currentCfg.callsign);
|
||||
frame.sender.h_c = !frame.dest.h_c;
|
||||
}
|
||||
kos.resetByteCount();
|
||||
os.write(KissEscapeOutputStream.FEND);
|
||||
kos.write(0); // data frame to TNC port 0
|
||||
frame.write(kos);
|
||||
os.write(KissEscapeOutputStream.FEND);
|
||||
os.flush();
|
||||
int byteCount = kos.getByteCount();
|
||||
stats.numXmtBytes += byteCount;
|
||||
stats.numXmtFrames++;
|
||||
long now = System.currentTimeMillis();
|
||||
Transmitter.getInstance().logTransmittedPacket(frame, now);
|
||||
int estElapsedTimeUntilPacketisTransmitted = (int) ((getRFSendTimePerByte() * (byteCount + 10)) +
|
||||
// next component is guesstimate of how long until packet is actually started transmitting
|
||||
// based on serial port to TNC transmission time
|
||||
((byteCount * 10.0F / currentCfg.baudRate) - (now - startTime)));
|
||||
nextAllowableTransmitTime = now + estElapsedTimeUntilPacketisTransmitted;
|
||||
fireTransmitting(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process incoming serial port event.
|
||||
* @param event SerialPortEvent describing the handling needed
|
||||
*/
|
||||
public void serialEvent(SerialPortEvent event) {
|
||||
// make this more real-time
|
||||
Thread thread = Thread.currentThread();
|
||||
if (thread.getPriority() <= Thread.NORM_PRIORITY) {
|
||||
if (fixedPriority < Thread.MIN_PRIORITY) {
|
||||
thread.setPriority(Thread.NORM_PRIORITY + 1);
|
||||
fixedPriority = thread.getPriority();
|
||||
}
|
||||
}
|
||||
if (is == null) {
|
||||
return; // avoid race conditions on port close
|
||||
}
|
||||
switch (event.getEventType()) {
|
||||
case SerialPortEvent.DATA_AVAILABLE:
|
||||
int newData;
|
||||
long lastRcvByteCount = stats.numRcvBytes;
|
||||
try {
|
||||
int availBytes;
|
||||
while (is != null && (availBytes = is.available()) > 0) {
|
||||
for (int count = 0; count < availBytes; count++) {
|
||||
if (is == null) {
|
||||
break;
|
||||
}
|
||||
newData = is.read();
|
||||
switch (curState) {
|
||||
case IDLE:
|
||||
if (KissEscapeOutputStream.FEND == newData) {
|
||||
curState = KissEscapeOutputStream.RcvState.IN_FRAME;
|
||||
wEnd = 0;
|
||||
kissForceCRCount = 0;
|
||||
} else if ('\r' == newData) {
|
||||
if (currentCfg.filter.length() > 0) { // has a command for switching to KISS mode?
|
||||
if (kissForceCRCount <= 0) {
|
||||
// try forcing TNC into KISS mode again
|
||||
System.out.println(new Date().toString() + ": trying to force echoing TNC back into KISS mode again");
|
||||
kissForceCRCount = 0; // in case it got screwed up somehow
|
||||
forceToKissMode();
|
||||
} else {
|
||||
kissForceCRCount--;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case IN_FRAME:
|
||||
switch (newData) {
|
||||
case KissEscapeOutputStream.FEND:
|
||||
// send the just-finished frame up to the next layer
|
||||
if (wEnd > 1) {
|
||||
if (CMD_MODE_TNC_PROMPT.length == cmdPromptCheck) {
|
||||
// try forcing TNC into KISS mode again
|
||||
System.out.println(new Date().toString() + ": " + currentCfg.deviceName + ": bogusframe, trying to force TNC back into KISS mode again");
|
||||
curState = KissEscapeOutputStream.RcvState.IDLE;
|
||||
forceToKissMode();
|
||||
cmdPromptCheck = 0;
|
||||
break; // don't waste any more time on processing a "frame" we're discarding
|
||||
|
||||
// not just a stream of frame borders....
|
||||
} else if ((rcvBuf[0] & 0x0F) == 0) { // KISS data frame?
|
||||
long now = System.currentTimeMillis();
|
||||
long transTime = now - frameStartTime;
|
||||
if (transTime > wEnd * 5L) {
|
||||
System.out.println(new Date(now).toString() + ":" + currentCfg.deviceName + ": excessively long time " + transTime + "msec to receive " + wEnd + "-byte frame");
|
||||
}
|
||||
AX25Frame frame = null;
|
||||
try {
|
||||
if (rcvBuf[1] == '$' && (rcvBuf[0] & 0xF0) != 0) {
|
||||
// KISS deviceID >= 1 and it looks like GPS sentence (AX.25 frame can't start with 0x24 as first byte of dest addr)
|
||||
String line = new String(rcvBuf, 1, wEnd);
|
||||
int delim = line.indexOf('\r');
|
||||
if (delim > 0) {
|
||||
line = line.substring(0, delim);
|
||||
}
|
||||
if (line.startsWith("$PKWDWXI")) {
|
||||
if (!rcvdWxOverKISS) {
|
||||
rcvdWxOverKISS = true;
|
||||
PortManager.firePortStatusChanged(this);
|
||||
}
|
||||
PositionlessWeatherReport pwr = new PositionlessWeatherReport(line, 8);
|
||||
Map<Enum, Object> readOnlyExtensionMap = pwr.getReadOnlyExtensionMap();
|
||||
if (readOnlyExtensionMap.size() > 0) {
|
||||
WeatherDistributor.getInstance().loadCurrentWeather(readOnlyExtensionMap);
|
||||
}
|
||||
} else {
|
||||
if (!rcvdGPSOverKISS) {
|
||||
rcvdGPSOverKISS = true;
|
||||
PortManager.firePortStatusChanged(this);
|
||||
}
|
||||
// GPS forwarded by Byonics TinyTrak4???
|
||||
GPSDistributor.getInstance().parseNMEA0183DataLine(line, this, true, currentCfg.callsign);
|
||||
}
|
||||
} else {
|
||||
frame = fireConsumeFrame(rcvBuf, 1, wEnd - 1, frameStartTime);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.out.println(new Date().toString() + ": SerialTNCConnector unable to handle " + (wEnd - 1) + "-byte frame:");
|
||||
StringBuilder b = new StringBuilder(wEnd * 2);
|
||||
for (int i = 1; i < wEnd; i++) {
|
||||
byte ch = rcvBuf[i];
|
||||
b.append(' ').append(Integer.toHexString(ch & 0xFF));
|
||||
}
|
||||
System.out.println(b.toString());
|
||||
e.printStackTrace(System.out);
|
||||
stats.numBadRcvFrames++;
|
||||
}
|
||||
long afterConsume = System.currentTimeMillis();
|
||||
long consumeTime = afterConsume - now;
|
||||
if (consumeTime > 50) {
|
||||
System.out.println(new Date(afterConsume).toString() + ":" + currentCfg.deviceName + ": excessively long time " + consumeTime + "msec to queue " + wEnd + "-byte frame " + frame);
|
||||
}
|
||||
} else {
|
||||
System.out.println(new Date().toString() + ": Serial_TNC[" + currentCfg.deviceName + "]: discarding type=" + Integer.toHexString((int) rcvBuf[0] & 0xFF) + " KISS frame len=" + (wEnd - 1));
|
||||
}
|
||||
stats.numRcvFrames++;
|
||||
stats.numRcvBytes += wEnd - 1;
|
||||
}
|
||||
// reset the receive buffer for the next frame
|
||||
wEnd = 0;
|
||||
frameStartTime = -1L;
|
||||
fireReceiving(false);
|
||||
break;
|
||||
case KissEscapeOutputStream.FESC:
|
||||
if (-1L == frameStartTime) {
|
||||
frameStartTime = System.currentTimeMillis() - 20000 / currentCfg.baudRate - 1; // account for transmission time of first byte and frame byte
|
||||
fireReceiving(true);
|
||||
cmdPromptCheck = 0; // obviously can't be in TNC prompt sequence
|
||||
}
|
||||
curState = KissEscapeOutputStream.RcvState.IN_ESC;
|
||||
break;
|
||||
default:
|
||||
long now = System.currentTimeMillis();
|
||||
if (-1L == frameStartTime) {
|
||||
frameStartTime = now - 20000 / currentCfg.baudRate - 1; // account for transmission time of first byte and frame byte
|
||||
fireReceiving(true);
|
||||
} else if (now - frameStartTime > 5000L) {
|
||||
if (cmdPromptCheck == CMD_MODE_TNC_PROMPT.length) {
|
||||
// try forcing TNC into KISS mode again
|
||||
System.out.println(new Date().toString() + ": " + currentCfg.deviceName + ": trying to force TNC back into KISS mode again");
|
||||
forceToKissMode();
|
||||
cmdPromptCheck = 0;
|
||||
break; // don't waste any more time on processing a "frame" we're discarding
|
||||
}
|
||||
}
|
||||
if (cmdPromptCheck < CMD_MODE_TNC_PROMPT.length && newData == CMD_MODE_TNC_PROMPT[cmdPromptCheck]) {
|
||||
cmdPromptCheck++;
|
||||
} else {
|
||||
cmdPromptCheck = 0;
|
||||
}
|
||||
if (wEnd == rcvBuf.length / 2) {
|
||||
System.out.println(new Date().toString() + ": " + currentCfg.deviceName + ": excessively long packet, must be mode garbling, force into KISS again");
|
||||
forceToKissMode();
|
||||
}
|
||||
if (wEnd < rcvBuf.length) {
|
||||
rcvBuf[wEnd++] = (byte) newData;
|
||||
} else {
|
||||
// some kind of protocol error, so reset and start over
|
||||
System.out.println(new Date().toString() + ": " + currentCfg.deviceName + ": receive buffer overflow, must be mode garbling, reset protocol");
|
||||
wEnd = 0;
|
||||
fireReceiving(false);
|
||||
curState = KissEscapeOutputStream.RcvState.IDLE;
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case IN_ESC:
|
||||
stats.numRcvBytes++;
|
||||
switch (newData) {
|
||||
case KissEscapeOutputStream.TFEND:
|
||||
rcvBuf[wEnd++] = (byte) KissEscapeOutputStream.FEND;
|
||||
break;
|
||||
case KissEscapeOutputStream.TFESC:
|
||||
rcvBuf[wEnd++] = (byte) KissEscapeOutputStream.FESC;
|
||||
break;
|
||||
default:
|
||||
rcvBuf[wEnd++] = (byte) newData;
|
||||
break;
|
||||
}
|
||||
curState = KissEscapeOutputStream.RcvState.IN_FRAME;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
stats.numBadRcvFrames++;
|
||||
System.out.println(new Date().toString() + ": IOException in SerialTNCConnector[" + currentCfg.deviceName + "], closing port");
|
||||
e.printStackTrace(System.out);
|
||||
// discard this frame
|
||||
curState = KissEscapeOutputStream.RcvState.IDLE;
|
||||
fireFailed();
|
||||
close();
|
||||
} catch (Throwable e) {
|
||||
stats.numBadRcvFrames++;
|
||||
System.out.println(new Date().toString() + ": unhandled exception in SerialTNCConnector[" + currentCfg.deviceName + ']');
|
||||
e.printStackTrace(System.out);
|
||||
// discard this frame
|
||||
curState = KissEscapeOutputStream.RcvState.IDLE;
|
||||
}
|
||||
int numNewBytes = (int)(stats.numRcvBytes - lastRcvByteCount);
|
||||
if (numNewBytes > numBytesPerEvent) {
|
||||
numBytesPerEvent = numNewBytes;
|
||||
System.out.println(new Date().toString() + ": " + currentCfg.deviceName + ": got as many as " + numBytesPerEvent + " bytes in one chunk");
|
||||
}
|
||||
break;
|
||||
case SerialPortEvent.OE:
|
||||
System.out.println(new Date().toString() + ": Serial_TNC: receive data overrun on " + portConfig.portType + ' ' + currentCfg.deviceName);
|
||||
stats.numDataOverrunLosses++;
|
||||
stats.numRcvBytes++; // at least one lost
|
||||
break;
|
||||
case SerialPortEvent.CTS:
|
||||
if (event.getOldValue() && !event.getNewValue()) {
|
||||
try {
|
||||
serialPort.setFlowControlMode(SerialPort.FLOWCONTROL_RTSCTS_OUT|SerialPort.FLOWCONTROL_RTSCTS_IN);
|
||||
System.out.println(new Date().toString() + ": " + currentCfg.deviceName + ": switching to hardware flow control...");
|
||||
} catch (UnsupportedCommOperationException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case SerialPortEvent.DSR:
|
||||
if (event.getOldValue() && !event.getNewValue()) {
|
||||
System.out.println(new Date().toString() + ": " + currentCfg.deviceName + ": lost DSR");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
System.out.println("other SerialPortEvent=" + event.getEventType());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shut down this port connection.
|
||||
*/
|
||||
@Override
|
||||
public void close() {
|
||||
if (serialPort != null) {
|
||||
if (timeslotTask != null) {
|
||||
timeslotTask.cancel();
|
||||
timeslotTask = null;
|
||||
}
|
||||
if (timeslotQueue != null) {
|
||||
// any pending messages just get lost, sorry.
|
||||
timeslotQueue = null;
|
||||
}
|
||||
if (os != null) {
|
||||
try {
|
||||
if (currentCfg.filter.length() > 0) {
|
||||
// we switched into KISS mode, so get us out
|
||||
os.write(KissEscapeOutputStream.FEND);
|
||||
os.write(0xFF);
|
||||
os.write(KissEscapeOutputStream.FEND);
|
||||
os.flush();
|
||||
}
|
||||
os.close();
|
||||
os = null;
|
||||
kos = null;
|
||||
} catch (IOException e) {
|
||||
System.out.print("error switching TNC out of KISS mode: ");
|
||||
e.printStackTrace(System.out);
|
||||
}
|
||||
}
|
||||
if (is != null) {
|
||||
try {
|
||||
is.close();
|
||||
is = null;
|
||||
} catch (IOException e) {
|
||||
System.out.print("error closing TNC input stream: ");
|
||||
e.printStackTrace(System.out);
|
||||
}
|
||||
}
|
||||
serialPort.close();
|
||||
serialPort = null;
|
||||
serialPortId = null;
|
||||
cachedToString = null;
|
||||
fireReceiving(false);
|
||||
fireTransmitting(false);
|
||||
PortManager.firePortClosed(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the object.
|
||||
*
|
||||
* @return a string representation of the object.
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
if (cachedToString == null) {
|
||||
cachedToString = TYPE_NAME + ": " + (serialPortId == null ? "???" : serialPortId.getName()) + '[' + getCallsign() + ']';
|
||||
}
|
||||
return cachedToString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify what capabilities a port of this type has.
|
||||
* @return bitmask of capability flags
|
||||
*/
|
||||
@Override
|
||||
public int getCapabilities() {
|
||||
int cap = CAP_RCV_PACKET_DATA|CAP_RF;
|
||||
PortConfig.Cfg currentCfg;
|
||||
if ((currentCfg = this.currentCfg) != null) {
|
||||
if (currentCfg.transmitAllowed) {
|
||||
cap |= CAP_XMT_PACKET_DATA;
|
||||
}
|
||||
if ((currentCfg.acceptableProtocolsMask & PortConfig.PROTOCOL_OPENTRAC) != 0) {
|
||||
cap |= CAP_OPENTRAC;
|
||||
}
|
||||
if ((currentCfg.acceptableProtocolsMask & PortConfig.PROTOCOL_AX25) != 0) {
|
||||
cap |= CAP_RAW_AX25;
|
||||
}
|
||||
if ((currentCfg.flags & PortConfig.FLAGS_HF) != 0) {
|
||||
cap |= CAP_HF;
|
||||
}
|
||||
}
|
||||
if (rcvdGPSOverKISS) {
|
||||
cap |= CAP_GPS_DATA;
|
||||
}
|
||||
if (rcvdWxOverKISS) {
|
||||
cap |= CAP_WEATHER;
|
||||
}
|
||||
return cap;
|
||||
}
|
||||
}
|