// License: Public Domain. For details, see LICENSE file.
package livegps;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.net.InetAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import javax.json.Json;
import javax.json.JsonException;
import javax.json.JsonNumber;
import javax.json.JsonObject;
import org.openstreetmap.josm.Main;
public class LiveGpsAcquirer implements Runnable {
private static final String DEFAULT_HOST = "localhost";
private static final int DEFAULT_PORT = 2947;
private static final String C_HOST = "livegps.gpsd.host";
private static final String C_PORT = "livegps.gpsd.port";
private String gpsdHost;
private int gpsdPort;
private Socket gpsdSocket;
private BufferedReader gpsdReader;
private boolean connected = false;
private boolean shutdownFlag = false;
private boolean JSONProtocol = true;
private final List<PropertyChangeListener> propertyChangeListener = new ArrayList<>();
private PropertyChangeEvent lastStatusEvent;
private PropertyChangeEvent lastDataEvent;
/**
* Constructor, initializes the configurable settings.
*/
public LiveGpsAcquirer() {
gpsdHost = Main.pref.get(C_HOST, DEFAULT_HOST);
gpsdPort = Main.pref.getInteger(C_PORT, DEFAULT_PORT);
// put the settings back in to the preferences, makes keys appear.
Main.pref.put(C_HOST, gpsdHost);
Main.pref.putInteger(C_PORT, gpsdPort);
}
/**
* Adds a property change listener to the acquirer.
* @param listener the new listener
*/
public void addPropertyChangeListener(PropertyChangeListener listener) {
if (!propertyChangeListener.contains(listener)) {
propertyChangeListener.add(listener);
}
}
/**
* Remove a property change listener from the acquirer.
* @param listener the new listener
*/
public void removePropertyChangeListener(PropertyChangeListener listener) {
if (propertyChangeListener.contains(listener)) {
propertyChangeListener.remove(listener);
}
}
/**
* Fire a gps status change event. Fires events with key "gpsstatus" and a {@link LiveGpsStatus}
* object as value.
* The status event may be sent any time.
* @param status the status.
* @param statusMessage the status message.
*/
public void fireGpsStatusChangeEvent(LiveGpsStatus.GpsStatus status, String statusMessage) {
PropertyChangeEvent event = new PropertyChangeEvent(this, "gpsstatus",
null, new LiveGpsStatus(status, statusMessage));
if (!event.equals(lastStatusEvent)) {
firePropertyChangeEvent(event);
lastStatusEvent = event;
}
}
/**
* Fire a gps data change event to all listeners. Fires events with key "gpsdata" and a
* {@link LiveGpsData} object as values.
* This event is only sent, when the suppressor permits it. This
* event will cause the UI to re-draw itself, which has some performance penalty,
* @param oldData the old gps data.
* @param newData the new gps data.
*/
public void fireGpsDataChangeEvent(LiveGpsData oldData, LiveGpsData newData) {
PropertyChangeEvent event = new PropertyChangeEvent(this, "gpsdata", oldData, newData);
if (!event.equals(lastDataEvent)) {
firePropertyChangeEvent(event);
lastDataEvent = event;
}
}
/**
* Fires the given event to all listeners.
* @param event the event to fire.
*/
protected void firePropertyChangeEvent(PropertyChangeEvent event) {
for (PropertyChangeListener listener : propertyChangeListener) {
listener.propertyChange(event);
}
}
@Override
public void run() {
LiveGpsData oldGpsData = null;
LiveGpsData gpsData = null;
shutdownFlag = false;
while (!shutdownFlag) {
while (!connected) {
try {
connect();
} catch (IOException iox) {
fireGpsStatusChangeEvent(LiveGpsStatus.GpsStatus.CONNECTION_FAILED, tr("Connection Failed"));
try {
Thread.sleep(1000);
} catch (InterruptedException ignore) {
Main.trace(ignore);
}
}
}
assert (connected);
try {
String line;
// <FIXXME date="23.06.2007" author="cdaller">
// TODO this read is blocking if gps is connected but has no
// fix, so gpsd does not send positions
line = gpsdReader.readLine();
// </FIXXME>
if (line == null)
throw new IOException();
if (JSONProtocol == true)
gpsData = ParseJSON(line);
else
gpsData = ParseOld(line);
if (gpsData == null)
continue;
fireGpsDataChangeEvent(oldGpsData, gpsData);
oldGpsData = gpsData;
} catch (IOException iox) {
Main.warn(iox, "LiveGps: lost connection to gpsd");
fireGpsStatusChangeEvent(
LiveGpsStatus.GpsStatus.CONNECTION_FAILED,
tr("Connection Failed"));
disconnect();
try {
Thread.sleep(1000);
} catch (InterruptedException ignore) {
Main.trace(ignore);
}
// send warning to layer
}
}
Main.info("LiveGps: Disconnected from gpsd");
fireGpsStatusChangeEvent(LiveGpsStatus.GpsStatus.DISCONNECTED,
tr("Not connected"));
disconnect();
}
public void shutdown() {
shutdownFlag = true;
}
private void connect() throws IOException {
JsonObject greeting;
String line, type, release;
Main.info("LiveGps: trying to connect to gpsd at " + gpsdHost + ":" + gpsdPort);
fireGpsStatusChangeEvent(LiveGpsStatus.GpsStatus.CONNECTING, tr("Connecting"));
InetAddress[] addrs = InetAddress.getAllByName(gpsdHost);
for (int i = 0; i < addrs.length && gpsdSocket == null; i++) {
try {
gpsdSocket = new Socket(addrs[i], gpsdPort);
break;
} catch (IOException e) {
Main.warn("LiveGps: Could not open connection to gpsd: " + e);
gpsdSocket = null;
}
}
if (gpsdSocket == null || gpsdSocket.isConnected() == false)
throw new IOException();
/*
* First emit the "w" symbol. The older version will activate, the newer one will ignore it.
*/
gpsdSocket.getOutputStream().write(new byte[] {'w', 13, 10});
gpsdReader = new BufferedReader(new InputStreamReader(gpsdSocket.getInputStream(), StandardCharsets.UTF_8));
line = gpsdReader.readLine();
if (line == null)
return;
try {
greeting = Json.createReader(new StringReader(line)).readObject();
type = greeting.getString("class");
if (type.equals("VERSION")) {
release = greeting.getString("release");
Main.info("LiveGps: Connected to gpsd " + release);
} else
Main.info("LiveGps: Unexpected JSON in gpsd greeting: " + line);
} catch (JsonException jex) {
if (line.startsWith("GPSD,")) {
connected = true;
JSONProtocol = false;
Main.info("LiveGps: Connected to old gpsd protocol version.");
fireGpsStatusChangeEvent(LiveGpsStatus.GpsStatus.CONNECTED, tr("Connected"));
}
}
if (JSONProtocol == true) {
JsonObject watch = Json.createObjectBuilder()
.add("enable", true)
.add("json", true)
.build();
String request = "?WATCH=" + watch.toString() + ";\n";
gpsdSocket.getOutputStream().write(request.getBytes(StandardCharsets.UTF_8));
connected = true;
fireGpsStatusChangeEvent(LiveGpsStatus.GpsStatus.CONNECTED, tr("Connected"));
}
}
private void disconnect() {
assert gpsdSocket != null;
connected = false;
try {
gpsdSocket.close();
gpsdSocket = null;
} catch (Exception e) {
Main.warn("LiveGps: Unable to close socket; reconnection may not be possible");
}
}
private LiveGpsData ParseJSON(String line) {
JsonObject report;
double lat, lon;
float speed = 0;
float course = 0;
float epx = 0;
float epy = 0;
try {
report = Json.createReader(new StringReader(line)).readObject();
} catch (JsonException jex) {
Main.warn("LiveGps: line read from gpsd is not a JSON object:" + line);
return null;
}
if (!report.getString("class").equals("TPV") || report.getInt("mode") < 2)
return null;
lat = report.getJsonNumber("lat").doubleValue();
lon = report.getJsonNumber("lon").doubleValue();
JsonNumber epxJson = report.getJsonNumber("epx");
JsonNumber epyJson = report.getJsonNumber("epy");
JsonNumber speedJson = report.getJsonNumber("speed");
JsonNumber trackJson = report.getJsonNumber("track");
if (speedJson != null)
speed = (float) speedJson.doubleValue();
if (trackJson != null)
course = (float) trackJson.doubleValue();
if (epxJson != null)
epx = (float) epxJson.doubleValue();
if (epyJson != null)
epy = (float) epyJson.doubleValue();
return new LiveGpsData(lat, lon, course, speed, epx, epy);
}
private LiveGpsData ParseOld(String line) {
String[] words;
double lat = 0;
double lon = 0;
float speed = 0;
float course = 0;
words = line.split(",");
if ((words.length == 0) || (!words[0].equals("GPSD")))
return null;
for (int i = 1; i < words.length; i++) {
if ((words[i].length() < 2) || (words[i].charAt(1) != '=')) {
// unexpected response.
continue;
}
char what = words[i].charAt(0);
String value = words[i].substring(2);
switch (what) {
case 'O':
// full report, tab delimited.
String[] status = value.split("\\s+");
if (status.length >= 5) {
lat = Double.parseDouble(status[3]);
lon = Double.parseDouble(status[4]);
try {
speed = Float.parseFloat(status[9]);
course = Float.parseFloat(status[8]);
} catch (NumberFormatException nex) {
Main.debug(nex);
}
return new LiveGpsData(lat, lon, course, speed);
}
break;
case 'P':
// position report, tab delimited.
String[] pos = value.split("\\s+");
if (pos.length >= 2) {
lat = Double.parseDouble(pos[0]);
lon = Double.parseDouble(pos[1]);
speed = Float.NaN;
course = Float.NaN;
return new LiveGpsData(lat, lon, course, speed);
}
break;
default:
// not interested
}
}
return null;
}
}