package nanolog; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.Color; import java.awt.Graphics2D; import java.awt.Point; import java.awt.event.ActionEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.swing.Action; import javax.swing.Icon; import javax.swing.JOptionPane; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.actions.JosmAction; import org.openstreetmap.josm.actions.RenameLayerAction; import org.openstreetmap.josm.data.Bounds; import org.openstreetmap.josm.data.coor.EastNorth; import org.openstreetmap.josm.data.coor.LatLon; import org.openstreetmap.josm.data.gpx.WayPoint; import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor; import org.openstreetmap.josm.gui.MapView; import org.openstreetmap.josm.gui.dialogs.LayerListDialog; import org.openstreetmap.josm.gui.dialogs.LayerListPopup; import org.openstreetmap.josm.gui.layer.GpxLayer; import org.openstreetmap.josm.gui.layer.JumpToMarkerActions; import org.openstreetmap.josm.gui.layer.Layer; import org.openstreetmap.josm.tools.ImageProvider; /** * NanoLog layer: a set of points that can be georeferenced. * * @author zverik */ public class NanoLogLayer extends Layer implements JumpToMarkerActions.JumpToMarkerLayer { private List<NanoLogEntry> log; private int selectedEntry; private final Set<NanoLogLayerListener> listeners = new HashSet<>(); private NLLMouseAdapter mouseListener; public NanoLogLayer(List<NanoLogEntry> entries) { super(tr("NanoLog")); log = new ArrayList<>(entries); selectedEntry = -1; mouseListener = new NLLMouseAdapter(); } public void setupListeners() { Main.map.mapView.addMouseListener(mouseListener); Main.map.mapView.addMouseMotionListener(mouseListener); } @Override public void destroy() { Main.map.mapView.removeMouseListener(mouseListener); Main.map.mapView.removeMouseMotionListener(mouseListener); super.destroy(); } public NanoLogLayer(File file) throws IOException { this(readNanoLog(file)); } public void addListener(NanoLogLayerListener listener) { listeners.add(listener); } public void removeListener(NanoLogLayerListener listener) { listeners.remove(listener); } protected void fireMarkersChanged() { for (NanoLogLayerListener listener : listeners) { listener.markersUpdated(this); } } protected void fireMarkerSelected() { for (NanoLogLayerListener listener : listeners) { listener.markerActivated(this, selectedEntry < 0 ? null : log.get(selectedEntry)); } } public List<NanoLogEntry> getEntries() { return Collections.unmodifiableList(log); } public static List<NanoLogEntry> readNanoLog(File file) throws IOException { final Pattern NANOLOG_LINE = Pattern.compile("(.+?)\\t(.+?)(?:\\s*\\{\\{(-?\\d+\\.\\d+),\\s*(-?\\d+\\.\\d+)(?:,\\s*(\\d+))?\\}\\})?"); final SimpleDateFormat fmt = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss.SS"); List<NanoLogEntry> result = new ArrayList<>(); try (BufferedReader r = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF8"))) { while (r.ready()) { String line = r.readLine(); if (line != null) { Matcher m = NANOLOG_LINE.matcher(line); if (m.matches()) { String time = m.group(1); String message = m.group(2); String lat = m.group(3); String lon = m.group(4); String dir = m.group(5); Date timeDate = null; try { timeDate = fmt.parse(time); } catch (ParseException e) { Main.warn(e); } if (message == null || message.length() == 0 || timeDate == null) continue; LatLon pos = null; Integer direction = null; if (lat != null && lon != null) { try { pos = new LatLon(Double.parseDouble(lat), Double.parseDouble(lon)); direction = new Integer(dir); } catch (NumberFormatException e) { Main.trace(e); } } NanoLogEntry entry = new NanoLogEntry(timeDate, message, pos, direction); result.add(entry); } } } } return result; } @Override public void paint(Graphics2D g, MapView mv, Bounds box) { // todo for (int i = 0; i < log.size(); i++) { NanoLogEntry entry = log.get(i); int radius = 4; if (entry.getPos() != null) { Point p = mv.getPoint(entry.getPos()); g.setColor(selectedEntry == i ? Color.red : Color.yellow); g.fillOval(p.x - radius, p.y - radius, radius * 2, radius * 2); } } } @Override public Icon getIcon() { return ImageProvider.get("nanolog.png"); } @Override public String getToolTipText() { return tr("NanoLog of {0} entries", log.size()); } @Override public void mergeFrom(Layer from) { // todo } @Override public boolean isMergable(Layer other) { return other instanceof NanoLogLayer; } @Override public void visitBoundingBox(BoundingXYVisitor v) { for (NanoLogEntry entry : log) { v.visit(entry.getPos()); } } @Override public Object getInfoComponent() { StringBuilder b = new StringBuilder(); int cnt = 0; for (NanoLogEntry e : log) { if (e.getPos() != null) cnt++; } b.append(tr("NanoLog of {0} lines, {1} of them with coordinates.", log.size(), cnt)); return b.toString(); } @Override public Action[] getMenuEntries() { return new Action[] { LayerListDialog.getInstance().createShowHideLayerAction(), LayerListDialog.getInstance().createDeleteLayerAction(), new RenameLayerAction(null, this), SeparatorLayerAction.INSTANCE, new CorrelateEntries(true), new CorrelateEntries(false), new SaveLayer(), SeparatorLayerAction.INSTANCE, new LayerListPopup.InfoAction(this) }; } @Override public void jumpToNextMarker() { selectedEntry++; if (selectedEntry < 0) selectedEntry = 0; else if (selectedEntry >= log.size()) selectedEntry = log.size() - 1; Main.map.repaint(); } @Override public void jumpToPreviousMarker() { selectedEntry--; if (selectedEntry < 0) selectedEntry = 0; else if (selectedEntry >= log.size()) selectedEntry = log.size() - 1; Main.map.repaint(); } protected void setSelected(int i) { int newSelected = i >= 0 && i < log.size() ? i : -1; if (newSelected != selectedEntry) { // System.out.println("selected: " + log.get(newSelected).getMessage()); selectedEntry = newSelected; fireMarkerSelected(); Main.map.mapView.repaint(); } } public void setSelected(NanoLogEntry entry) { if (entry == null) setSelected(-1); else { for (int i = 0; i < log.size(); i++) { if (entry.equals(log.get(i))) { setSelected(i); break; } } } } private class NLLMouseAdapter extends MouseAdapter { private int dragging; public int nearestEntry(MouseEvent e) { LatLon ll = Main.map.mapView.getLatLon(e.getX(), e.getY()); int radius = 8; if (ll != null) { LatLon lld = Main.map.mapView.getLatLon(e.getX() + radius, e.getY() + radius); double distance = Math.max(lld.lat() - ll.lat(), lld.lon() - ll.lon()); boolean selectedIsSelected = false; int newSelected = -1; for (int i = 0; i < log.size(); i++) { if (log.get(i).getPos() != null && log.get(i).getPos().distance(ll) < distance) { newSelected = i; if (i == selectedEntry) selectedIsSelected = true; } } if (newSelected >= 0) return selectedIsSelected ? selectedEntry : newSelected; } return -1; } @Override public void mouseMoved(MouseEvent e) { int nearest = nearestEntry(e); if (nearest > 0) setSelected(nearest); } @Override public void mouseDragged(MouseEvent e) { doDrag(e); } @Override public void mouseReleased(MouseEvent e) { if (dragging > 0) { dragging = 0; } } @Override public void mousePressed(MouseEvent e) { int nearest = nearestEntry(e); if (nearest > 0 && Main.getLayerManager().getActiveLayer() == NanoLogLayer.this) { dragging = nearest; doDrag(e); } } private void doDrag(MouseEvent e) { if (dragging > 0) dragTo(dragging, e.getX(), e.getY()); } } protected void dragTo(int entry, int x, int y) { GpxLayer gpx = GPXChooser.topLayer(); if (gpx == null) return; EastNorth eastNorth = Main.map.mapView.getEastNorth(x, y); double tolerance = eastNorth.distance(Main.map.mapView.getEastNorth(x + 300, y)); WayPoint wp = gpx.data.nearestPointOnTrack(eastNorth, tolerance); if (wp == null) return; long newTime = Correlator.getGpxDate(gpx.data, wp.getCoor()); if (newTime <= 0) return; Correlator.revertPos(log); Correlator.correlate(log, gpx.data, log.get(entry).getTime().getTime() - newTime); Main.map.mapView.repaint(); } private class CorrelateEntries extends JosmAction { private boolean toZero; CorrelateEntries(boolean toZero) { super(toZero ? tr("Correlate with GPX...") : tr("Put on GPX..."), "nanolog/correlate", tr("Correlate entries with GPS trace"), null, false); this.toZero = toZero; } @Override public void actionPerformed(ActionEvent e) { // 1. Select GPX trace or display message to load one // (better yet, disable when no GPX traces) GpxLayer layer = GPXChooser.chooseLayer(); // 2. Correlate by default, sticking by date // (if does not match, shift so hours-minutes stay) if (layer != null) { long offset = toZero ? 0 : Correlator.crudeMatch(log, layer.data); Correlator.revertPos(log); Correlator.correlate(log, layer.data, offset); fireMarkersChanged(); Main.map.mapView.repaint(); } // 3. Show non-modal (?) window with a slider and a text input // (todo: better slider, like in blender) } } private class SaveLayer extends JosmAction { SaveLayer() { super(tr("Save layer..."), "nanolog/save", tr("Save NanoLog layer"), null, false); } @Override public void actionPerformed(ActionEvent e) { // todo JOptionPane.showMessageDialog(Main.parent, "Sorry, no saving yet", "NanoLog", JOptionPane.ERROR_MESSAGE); } } public interface NanoLogLayerListener { void markersUpdated(NanoLogLayer layer); void markerActivated(NanoLogLayer layer, NanoLogEntry entry); } }