// License: Public Domain. For details, see LICENSE file. package livegps; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.Color; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; import java.awt.geom.Point2D; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.Map; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.data.Bounds; import org.openstreetmap.josm.data.coor.LatLon; import org.openstreetmap.josm.data.gpx.GpxData; import org.openstreetmap.josm.data.gpx.GpxTrack; import org.openstreetmap.josm.data.gpx.WayPoint; import org.openstreetmap.josm.data.preferences.CachingProperty; import org.openstreetmap.josm.data.preferences.ColorProperty; import org.openstreetmap.josm.gui.MapView; import org.openstreetmap.josm.gui.layer.GpxLayer; public class LiveGpsLayer extends GpxLayer implements PropertyChangeListener { public static final String LAYER_NAME = tr("LiveGPS layer"); public static final String C_LIVEGPS_COLOR_POSITION = "color.livegps.position"; public static final String C_LIVEGPS_COLOR_POSITION_ESTIMATE = "color.livegps.position_estimate"; private static final CachingProperty<Color> COLOR_POSITION = new ColorProperty(C_LIVEGPS_COLOR_POSITION, Color.RED).cached(); private static final CachingProperty<Color> COLOR_POSITION_ESTIMATE = new ColorProperty(C_LIVEGPS_COLOR_POSITION_ESTIMATE, Color.CYAN).cached(); private static final int DEFAULT_REFRESH_INTERVAL = 250; private static final int DEFAULT_CENTER_INTERVAL = 5000; private static final int DEFAULT_CENTER_FACTOR = 80; private static final String oldC_REFRESH_INTERVAL = "livegps.refreshinterval"; /* in seconds */ private static final String C_REFRESH_INTERVAL = "livegps.refresh_interval_msec"; /* in msec */ private static final String C_CENTER_INTERVAL = "livegps.center_interval_msec"; /* in msec */ private static final String C_CENTER_FACTOR = "livegps.center_factor" /* in percent */; private static final String C_CURSOR_H = "livegps.cursor_height"; /* in pixels */ private static final String C_CURSOR_W = "livegps.cursor_width"; /* in pixels */ private static final String C_CURSOR_T = "livegps.cursor_thickness"; /* in pixels */ private int refreshInterval; private int centerInterval; private double centerFactor; private long lastRedraw = 0; private long lastCenter = 0; LiveGpsData lastData; LatLon lastPos; WayPoint lastPoint; private final AppendableGpxTrackSegment trackSegment; boolean autocenter; private SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"); public LiveGpsLayer(GpxData data) { super(data, LAYER_NAME); trackSegment = new AppendableGpxTrackSegment(); Map<String, Object> attr = new HashMap<>(); attr.put("desc", "josm live gps"); GpxTrack trackBeingWritten = new SingleSegmentGpxTrack(trackSegment, attr); data.tracks.add(trackBeingWritten); initIntervals(); } void setCurrentPosition(double lat, double lon) { LatLon thisPos = new LatLon(lat, lon); if ((lastPos != null) && (thisPos.equalsEpsilon(lastPos))) // no change in position // maybe show a "paused" cursor or some such return; lastPos = thisPos; lastPoint = new WayPoint(thisPos); lastPoint.attr.put("time", dateFormat.format(new Date())); trackSegment.addWaypoint(lastPoint); if (autocenter) conditionalCenter(thisPos); } public void center() { if (lastPoint != null) Main.map.mapView.zoomTo(lastPoint.getCoor()); } public void conditionalCenter(LatLon Pos) { Point2D P = Main.map.mapView.getPoint2D(Pos); Rectangle rv = Main.map.mapView.getBounds(null); Date date = new Date(); long current = date.getTime(); rv.grow(-(int) (rv.getHeight() * centerFactor), -(int) (rv.getWidth() * centerFactor)); if (!rv.contains(P) || (centerInterval > 0 && current - lastCenter >= centerInterval)) { Main.map.mapView.zoomTo(Pos); lastCenter = current; } } public void setAutoCenter(boolean ac) { autocenter = ac; } @Override public void paint(Graphics2D g, MapView mv, Bounds bounds) { super.paint(g, mv, bounds); if (lastPoint == null) return; Point screen = mv.getPoint(lastPoint.getCoor()); int TriaHeight = Main.pref.getInteger(C_CURSOR_H, 20); int TriaWidth = Main.pref.getInteger(C_CURSOR_W, 10); int TriaThick = Main.pref.getInteger(C_CURSOR_T, 4); /* * Draw a bold triangle. * In case of deep zoom draw also a thin DOP oval. */ g.setColor(COLOR_POSITION_ESTIMATE.get()); int w, h; double ppm = 100 / mv.getDist100Pixel(); /* pixels per metre */ w = (int) Math.round(lastData.getEpx() * ppm); h = (int) Math.round(lastData.getEpy() * ppm); if (w > TriaWidth || h > TriaWidth) { int xo, yo; yo = screen.y - Math.round(h/2); xo = screen.x - Math.round(w/2); g.drawOval(xo, yo, w, h); } int[] x = new int[4]; int[] y = new int[4]; float course = lastData.getCourse(); float csin = (float) Math.sin(Math.toRadians(course)); float ccos = (float) Math.cos(Math.toRadians(course)); float csin120 = (float) Math.sin(Math.toRadians(course + 120)); float ccos120 = (float) Math.cos(Math.toRadians(course + 120)); float csin240 = (float) Math.sin(Math.toRadians(course + 240)); float ccos240 = (float) Math.cos(Math.toRadians(course + 240)); g.setColor(COLOR_POSITION.get()); for (int i = 0; i < TriaThick; i++, TriaHeight--, TriaWidth--) { x[0] = screen.x + Math.round(TriaHeight * csin); y[0] = screen.y - Math.round(TriaHeight * ccos); x[1] = screen.x + Math.round(TriaWidth * csin120); y[1] = screen.y - Math.round(TriaWidth * ccos120); x[2] = screen.x; y[2] = screen.y; x[3] = screen.x + Math.round(TriaWidth * csin240); y[3] = screen.y - Math.round(TriaWidth * ccos240); g.drawPolygon(x, y, 4); } } @Override public void propertyChange(PropertyChangeEvent evt) { if (!isVisible()) { return; } if ("gpsdata".equals(evt.getPropertyName())) { lastData = (LiveGpsData) evt.getNewValue(); if (lastData.isFix()) { setCurrentPosition(lastData.getLatitude(), lastData.getLongitude()); if (allowRedraw()) Main.map.repaint(); } } } /** * Check, if a redraw is currently allowed. * * @return true, if a redraw is permitted, false, if a re-draw * should be suppressed. */ private boolean allowRedraw() { Date date = new Date(); long current = date.getTime(); if (current - lastRedraw >= refreshInterval) { lastRedraw = current; return true; } else return false; } /** * Retrieve the refreshInterval and centerInterval from the configuration. Be compatible * with old version that stored refreshInterval in seconds. If no such configuration key * exists, it will be initialized here. */ private void initIntervals() { if ((refreshInterval = Main.pref.getInteger(oldC_REFRESH_INTERVAL, 0)) != 0) { refreshInterval *= 1000; Main.pref.put(oldC_REFRESH_INTERVAL, null); } else refreshInterval = Main.pref.getInteger(C_REFRESH_INTERVAL, DEFAULT_REFRESH_INTERVAL); centerInterval = Main.pref.getInteger(C_CENTER_INTERVAL, DEFAULT_CENTER_INTERVAL); centerFactor = Main.pref.getInteger(C_CENTER_FACTOR, DEFAULT_CENTER_FACTOR); if (centerFactor <= 1 || centerFactor >= 99) centerFactor = DEFAULT_CENTER_FACTOR; Main.pref.putInteger(C_REFRESH_INTERVAL, refreshInterval); Main.pref.putInteger(C_CENTER_INTERVAL, centerInterval); Main.pref.putInteger(C_CENTER_FACTOR, (int) centerFactor); /* * Do one time conversion of factor: user value means "how big is inner rectangle * comparing to screen in percent", machine value means "what is the shrink ratio * for each dimension on _both_ sides". */ centerFactor = (100 - centerFactor) / 2 / 100; } }