diff --git a/src/main/java/io/github/dsheirer/gui/SDRTrunk.java b/src/main/java/io/github/dsheirer/gui/SDRTrunk.java
index 7d5504f33..0ffd851c6 100644
--- a/src/main/java/io/github/dsheirer/gui/SDRTrunk.java
+++ b/src/main/java/io/github/dsheirer/gui/SDRTrunk.java
@@ -222,7 +222,7 @@ public SDRTrunk()
mPlaylistManager.getChannelProcessingManager().addAudioSegmentListener(mAudioRecordingManager);
mPlaylistManager.getChannelProcessingManager().addAudioSegmentListener(mAudioStreamingManager);
- MapService mapService = new MapService(mIconModel);
+ MapService mapService = new MapService(aliasModel, mIconModel);
mPlaylistManager.getChannelProcessingManager().addDecodeEventListener(mapService);
mNowPlayingDetailsVisible = mPreferences.getBoolean(PREFERENCE_NOW_PLAYING_DETAILS_VISIBLE, true);
diff --git a/src/main/java/io/github/dsheirer/map/MapPanel.java b/src/main/java/io/github/dsheirer/map/MapPanel.java
index a4847e13a..e3bd1c2e6 100644
--- a/src/main/java/io/github/dsheirer/map/MapPanel.java
+++ b/src/main/java/io/github/dsheirer/map/MapPanel.java
@@ -1,6 +1,6 @@
-/*******************************************************************************
- * sdrtrunk
- * Copyright (C) 2014-2017 Dennis Sheirer
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -14,12 +14,16 @@
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see
- *
- ******************************************************************************/
+ * ****************************************************************************
+ */
package io.github.dsheirer.map;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.icon.IconModel;
+import io.github.dsheirer.identifier.Form;
+import io.github.dsheirer.identifier.Identifier;
+import io.github.dsheirer.identifier.IdentifierClass;
+import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.settings.MapViewSetting;
import io.github.dsheirer.settings.SettingsManager;
import net.miginfocom.swing.MigLayout;
@@ -31,18 +35,67 @@
import org.jdesktop.swingx.mapviewer.GeoPosition;
import org.jdesktop.swingx.mapviewer.TileFactoryInfo;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JSpinner;
+import javax.swing.JSplitPane;
+import javax.swing.JTable;
+import javax.swing.JToggleButton;
+import javax.swing.SpinnerNumberModel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.event.TableModelEvent;
import java.awt.EventQueue;
+import java.util.ArrayList;
+import java.util.List;
+/**
+ * Swing map panel.
+ */
public class MapPanel extends JPanel implements IPlottableUpdateListener
{
private static final long serialVersionUID = 1L;
+ private static final int ZOOM_MINIMUM = 1;
+ private static final int ZOOM_MAXIMUM = 16;
+
+ private static final String FOLLOW = "Follow";
+ private static final String UNFOLLOW = "Unfollow";
+ private static final String SELECT_A_TRACK = "(select a track)";
+ private static final String NO_SYSTEM_NAME = "(no system name)";
private SettingsManager mSettingsManager;
private MapService mMapService;
- private JXMapViewer mMapViewer = new JXMapViewer();
+ private JXMapViewer mMapViewer;
private PlottableEntityPainter mMapPainter;
+ private TrackGenerator mTrackGenerator;
+ private JToggleButton mTrackGeneratorToggle;
+ private JTable mPlottedTracksTable;
+ private JButton mClearMapButton;
+ private JButton mReplotAllTracksButton;
+ private JButton mDeleteAllTracksButton;
+ private JButton mDeleteTrackButton;
+ private JButton mFollowButton;
+ private JLabel mFollowedEntityLabel;
+ private JCheckBox mCenterOnSelectedCheckBox;
+ private PlottableEntityHistory mFollowedTrack;
+ private JComboBox mTrackHistoryLengthComboBox;
+ private JSpinner mMapZoomSpinner;
+ private SpinnerNumberModel mMapZoomSpinnerModel;
+ private TrackHistoryModel mTrackHistoryModel = new TrackHistoryModel();
+ private JTable mTrackHistoryTable;
+ private JLabel mSelectedTrackSystemLabel;
+ /**
+ * Constructs an instance
+ * @param mapService for accessing entities to plot
+ * @param aliasModel for alias lookup
+ * @param iconModel for icon lookup
+ * @param settingsManager for user specified options/settings.
+ */
public MapPanel(MapService mapService, AliasModel aliasModel, IconModel iconModel, SettingsManager settingsManager)
{
mSettingsManager = settingsManager;
@@ -55,62 +108,548 @@ public MapPanel(MapService mapService, AliasModel aliasModel, IconModel iconMode
private void init()
{
setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill]"));
-
- /**
- * Set the entity painter as the overlay painter and register this panel
- * to receive new messages (plots)
- */
- mMapViewer.setOverlayPainter(mMapPainter);
mMapService.addListener(this);
- /**
- * Map image source
- */
- TileFactoryInfo info = new OSMTileFactoryInfo();
- DefaultTileFactory tileFactory = new DefaultTileFactory(info);
- mMapViewer.setTileFactory(tileFactory);
-
- /**
- * Defines how many threads will be used to fetch the background map
- * tiles (graphics)
- */
- tileFactory.setThreadPoolSize(8);
-
- /**
- * Set initial location and zoom for the map upon display
- */
- GeoPosition syracuse = new GeoPosition(43.048, -76.147);
- int zoom = 7;
-
- MapViewSetting view = mSettingsManager.getMapViewSetting("Default", syracuse, zoom);
-
- mMapViewer.setAddressLocation(view.getGeoPosition());
- mMapViewer.setZoom(view.getZoom());
-
- /**
- * Add a mouse adapter for panning and scrolling
- */
- MapMouseListener listener = new MapMouseListener(mMapViewer, mSettingsManager);
- mMapViewer.addMouseListener(listener);
- mMapViewer.addMouseMotionListener(listener);
-
- /* Map zoom listener */
- mMapViewer.addMouseWheelListener(new ZoomMouseWheelListenerCursor(mMapViewer));
-
- /* Keyboard panning listener */
- mMapViewer.addKeyListener(new PanKeyListener(mMapViewer));
-
- /**
- * Add a selection listener
- */
- SelectionAdapter sa = new SelectionAdapter(mMapViewer);
- mMapViewer.addMouseListener(sa);
- mMapViewer.addMouseMotionListener(sa);
-
- /**
- * Map component
- */
- add(mMapViewer, "span");
+ JPanel cp = new JPanel();
+ cp.setLayout(new MigLayout("insets 5", "[grow,fill][grow,fill][grow,fill]",
+ "[grow,fill][][][][][][grow,fill][]"));
+ cp.add(new JScrollPane(getPlottedTracksTable()), "span 3,wrap");
+
+ cp.add(getMapZoomSpinner());
+ JPanel zoomCenterPanel = new JPanel();
+ zoomCenterPanel.setLayout(new MigLayout("insets 0", "[]5[grow,fill]", ""));
+ zoomCenterPanel.add(new JLabel("Zoom"));
+ zoomCenterPanel.add(getCenterOnSelectedCheckBox());
+ cp.add(zoomCenterPanel, "span 2, wrap");
+
+ cp.add(getDeleteTrackButton());
+ cp.add(getDeleteAllTracksButton());
+ cp.add(getClearMapButton(), "wrap");
+
+ cp.add(getReplotAllTracksButton());
+ cp.add(getFollowButton());
+ cp.add(getFollowedEntityLabel(), "wrap");
+
+ cp.add(getTrackHistoryLengthComboBox());
+ cp.add(new JLabel("Track History Length"), "span 2, wrap");
+
+ cp.add(new JLabel("Selected System:"), "align right");
+ cp.add(getSelectedTrackSystemLabel(), "span 2, wrap");
+
+ cp.add(new JScrollPane(getTrackHistoryTable()), "span 3,wrap");
+
+// cp.add(getTrackGeneratorToggle(), "span 3,wrap");
+
+ JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
+ splitPane.setDividerLocation(300);
+ splitPane.add(cp);
+ splitPane.add(getMapViewer());
+ add(splitPane, "span");
+ }
+
+ private void setSelected(PlottableEntityHistory selected)
+ {
+ mTrackHistoryModel.load(selected);
+
+ if(selected != null)
+ {
+ Identifier system = selected.getIdentifierCollection().getIdentifier(IdentifierClass.CONFIGURATION, Form.SYSTEM, Role.ANY);
+
+ if(system != null)
+ {
+ getSelectedTrackSystemLabel().setText(system.toString());
+ }
+ else
+ {
+ getSelectedTrackSystemLabel().setText(NO_SYSTEM_NAME);
+ }
+
+ if(mMapPainter.addEntity(getSelected()))
+ {
+ getMapViewer().repaint();
+ }
+ }
+ else
+ {
+ getSelectedTrackSystemLabel().setText(SELECT_A_TRACK);
+ }
+
+ if(getCenterOnSelectedCheckBox().isSelected())
+ {
+ centerOn(selected);
+ }
+ }
+
+ /**
+ * Replots all tracks to the map (after a clear operation).
+ * @return button
+ */
+ private JButton getReplotAllTracksButton()
+ {
+ if(mReplotAllTracksButton == null)
+ {
+ mReplotAllTracksButton = new JButton("Replot All");
+ mReplotAllTracksButton.addActionListener(e ->
+ {
+ boolean added = mMapPainter.addAll(mMapService.getPlottableEntityModel().getAll());
+
+ if(added)
+ {
+ getMapViewer().repaint();
+ }
+ });
+ }
+
+ return mReplotAllTracksButton;
+ }
+
+ private JLabel getSelectedTrackSystemLabel()
+ {
+ if(mSelectedTrackSystemLabel == null)
+ {
+ mSelectedTrackSystemLabel = new JLabel(SELECT_A_TRACK);
+ }
+
+ return mSelectedTrackSystemLabel;
+ }
+
+ private JTable getTrackHistoryTable()
+ {
+ if(mTrackHistoryTable == null)
+ {
+ mTrackHistoryTable = new JTable(mTrackHistoryModel);
+// mTrackHistoryTable.getSelectionModel().addListSelectionListener(e ->
+// {
+// if(getCenterOnSelectedCheckBox().isSelected())
+// {
+// int modelIndex = getTrackHistoryTable().convertRowIndexToModel(getTrackHistoryTable().getSelectedRow());
+// TimestampedGeoPosition geo = mTrackHistoryModel.get(modelIndex);
+//
+// if(geo != null)
+// {
+// mMapViewer.setCenterPosition(geo);
+// }
+// }
+// });
+ }
+
+ return mTrackHistoryTable;
+ }
+
+ /**
+ * Map zoom level combo box
+ */
+ private JSpinner getMapZoomSpinner()
+ {
+ if(mMapZoomSpinner == null)
+ {
+ mMapZoomSpinnerModel = new SpinnerNumberModel(2, ZOOM_MINIMUM, ZOOM_MAXIMUM, 1);
+ mMapZoomSpinnerModel.addChangeListener(new ChangeListener()
+ {
+ @Override
+ public void stateChanged(ChangeEvent e)
+ {
+ Number number = mMapZoomSpinnerModel.getNumber();
+ mMapViewer.setZoom(number.intValue());
+ }
+ });
+
+ mMapZoomSpinner = new JSpinner(mMapZoomSpinnerModel);
+ }
+
+ return mMapZoomSpinner;
+ }
+
+ /**
+ * Plotted track history trail length selection combo box.
+ */
+ private JComboBox getTrackHistoryLengthComboBox()
+ {
+ if(mTrackHistoryLengthComboBox == null)
+ {
+ List lengths = new ArrayList<>();
+ for(int length = 1; length <= 10; length++)
+ {
+ lengths.add(length);
+ }
+
+ mTrackHistoryLengthComboBox = new JComboBox<>(lengths.toArray(new Integer[]{lengths.size()}));
+ mTrackHistoryLengthComboBox.setSelectedItem(mMapPainter.getTrackHistoryLength());
+
+ mTrackHistoryLengthComboBox.addActionListener(e ->
+ {
+ int length = (int)getTrackHistoryLengthComboBox().getSelectedItem();
+ mMapPainter.setTrackHistoryLength(length);
+ });
+ }
+
+ return mTrackHistoryLengthComboBox;
+ }
+
+ /**
+ * Label to show the followed entity
+ */
+ private JLabel getFollowedEntityLabel()
+ {
+ if(mFollowedEntityLabel == null)
+ {
+ mFollowedEntityLabel = new JLabel(" ");
+ }
+
+ return mFollowedEntityLabel;
+ }
+
+ /**
+ * Toggles the following state for an entity.
+ */
+ private JButton getFollowButton()
+ {
+ if(mFollowButton == null)
+ {
+ mFollowButton = new JButton(FOLLOW);
+ mFollowButton.setEnabled(false);
+ mFollowButton.addActionListener(e ->
+ {
+ if(getFollowButton().getText().equals(FOLLOW))
+ {
+ follow(getSelected());
+ }
+ else
+ {
+ follow(null);
+ }
+ });
+ }
+
+ return mFollowButton;
+ }
+
+ private JButton getClearMapButton()
+ {
+ if(mClearMapButton == null)
+ {
+ mClearMapButton = new JButton("Clear Map");
+ mClearMapButton.addActionListener(e ->
+ {
+ mMapPainter.clearAllEntities();
+ repaint();
+ });
+ }
+
+ return mClearMapButton;
+ }
+
+ /**
+ * Toggles the behavior of centering on the selected track when a user selects a track in the table.
+ * @return check box.
+ */
+ private JToggleButton getCenterOnSelectedCheckBox()
+ {
+ if(mCenterOnSelectedCheckBox == null)
+ {
+ mCenterOnSelectedCheckBox = new JCheckBox("Center on Selection");
+ mCenterOnSelectedCheckBox.setSelected(true);
+ }
+
+ return mCenterOnSelectedCheckBox;
+ }
+
+ private JButton getDeleteAllTracksButton()
+ {
+ if(mDeleteAllTracksButton == null)
+ {
+ mDeleteAllTracksButton = new JButton("Delete All");
+ mDeleteAllTracksButton.addActionListener(e -> {
+ mMapService.getPlottableEntityModel().deleteAllTracks();
+ mMapPainter.clearAllEntities();
+ //Clear followed entity
+ follow(null);
+ getMapViewer().repaint();
+ });
+ }
+
+ return mDeleteAllTracksButton;
+ }
+
+ private JButton getDeleteTrackButton()
+ {
+ if(mDeleteTrackButton == null)
+ {
+ mDeleteTrackButton = new JButton("Delete");
+ mDeleteTrackButton.setEnabled(false);
+ mDeleteTrackButton.addActionListener(e -> {
+
+ List toDelete = new ArrayList<>();
+ int[] selectedIndices = getPlottedTracksTable().getSelectionModel().getSelectedIndices();
+
+ for(int selectedIndex : selectedIndices)
+ {
+ int modelIndex = getPlottedTracksTable().convertRowIndexToModel(selectedIndex);
+ PlottableEntityHistory entity = mMapService.getPlottableEntityModel().get(modelIndex);
+ if(entity != null)
+ {
+ toDelete.add(entity);
+
+ //Clear followed entity if it's being deleted
+ if(entity.equals(mFollowedTrack))
+ {
+ follow(null);
+ }
+ }
+ }
+ mMapService.getPlottableEntityModel().delete(toDelete);
+ mMapPainter.clearEntities(toDelete);
+ getMapViewer().repaint();
+ });
+ }
+
+ return mDeleteTrackButton;
+ }
+
+ /**
+ * Access the selected entity history.
+ * @return selected entity or null of one is not selected.
+ */
+ private PlottableEntityHistory getSelected()
+ {
+ if(getPlottedTracksTable().getSelectedRow() >= 0)
+ {
+ int modelIndex = getPlottedTracksTable().convertRowIndexToModel(getPlottedTracksTable().getSelectedRow());
+ return mMapService.getPlottableEntityModel().get(modelIndex);
+ }
+
+ return null;
+ }
+
+ /**
+ * Centers on the plottable entity.
+ * @param entityHistory to center on
+ */
+ private void centerOn(PlottableEntityHistory entityHistory)
+ {
+ if(entityHistory != null)
+ {
+ GeoPosition geoPosition = entityHistory.getLatestPosition();
+
+ if(geoPosition != null)
+ {
+ mMapViewer.setCenterPosition(geoPosition);
+ }
+ }
+ }
+
+ /**
+ * Follow or unfollow an entity.
+ * @param entityHistory to follow or null to unfollow.
+ */
+ private void follow(PlottableEntityHistory entityHistory)
+ {
+ mFollowedTrack = entityHistory;
+
+ if(mFollowedTrack != null)
+ {
+ centerOn(mFollowedTrack);
+ getFollowButton().setText(UNFOLLOW);
+ getFollowButton().setEnabled(true);
+ getFollowedEntityLabel().setText("Following: " + mFollowedTrack.getIdentifier());
+ getCenterOnSelectedCheckBox().setEnabled(false); //Disabled while we're following
+ }
+ else
+ {
+ getFollowButton().setText(FOLLOW);
+ getFollowButton().setEnabled(getSelected() != null);
+ getFollowedEntityLabel().setText(null);
+ getCenterOnSelectedCheckBox().setEnabled(true);
+ }
+ }
+
+ private JTable getPlottedTracksTable()
+ {
+ if(mPlottedTracksTable == null)
+ {
+ mPlottedTracksTable = new JTable(mMapService.getPlottableEntityModel());
+
+ mMapService.getPlottableEntityModel().addTableModelListener(e ->
+ {
+ //Update the followed entity for DELETE/UPDATE operations
+ if(mFollowedTrack != null)
+ {
+ if(e.getType() == TableModelEvent.DELETE)
+ {
+ for(int x = e.getFirstRow(); x <= e.getLastRow(); x++)
+ {
+ if(mMapService.getPlottableEntityModel().get(x).equals(mFollowedTrack))
+ {
+ follow(null);
+ return;
+ }
+ }
+ }
+ else if(e.getType() == TableModelEvent.UPDATE)
+ {
+ if(e.getFirstRow() == 0 && e.getLastRow() == Integer.MAX_VALUE)
+ {
+ return;
+ }
+
+ for(int x = e.getFirstRow(); x <= e.getLastRow(); x++)
+ {
+ PlottableEntityHistory entity = mMapService.getPlottableEntityModel().get(x);
+
+ if(entity.equals(getSelected()))
+ {
+ mTrackHistoryModel.update();
+ }
+
+ if(entity != null && entity.equals(mFollowedTrack))
+ {
+ centerOn(mMapService.getPlottableEntityModel().get(x));
+ return;
+ }
+ }
+ }
+ }
+
+ if(e.getType() == TableModelEvent.UPDATE && !(e.getFirstRow() == 0 && e.getLastRow() == Integer.MAX_VALUE))
+ {
+ for(int x = e.getFirstRow(); x <= e.getLastRow(); x++)
+ {
+ PlottableEntityHistory entity = mMapService.getPlottableEntityModel().get(x);
+
+ if(entity != null && entity.equals(getSelected()))
+ {
+ mTrackHistoryModel.update();
+ }
+ }
+ }
+ else if(e.getType() == TableModelEvent.DELETE && getSelected() == null)
+ {
+ mTrackHistoryModel.load(null);
+ }
+ });
+
+ //Register selection listener to update button/label states
+ mPlottedTracksTable.getSelectionModel().addListSelectionListener(e ->
+ {
+ //Toggle the enabled state of the delete (single) track button
+ int count = mPlottedTracksTable.getSelectionModel().getSelectedItemsCount();
+ getDeleteTrackButton().setEnabled(count > 0);
+
+ PlottableEntityHistory selected = getSelected();
+ setSelected(selected);
+
+ //Refresh the followed entity button/label states
+ follow(mFollowedTrack);
+ });
+ }
+
+ return mPlottedTracksTable;
+ }
+
+ private JToggleButton getTrackGeneratorToggle()
+ {
+ if(mTrackGeneratorToggle == null)
+ {
+ mTrackGeneratorToggle = new JToggleButton("Track Generator");
+ mTrackGeneratorToggle.addActionListener(e -> {
+ if(mTrackGeneratorToggle.isSelected())
+ {
+ getTrackGenerator().start();
+ }
+ else
+ {
+ getTrackGenerator().stop();
+ }
+ });
+ }
+
+ return mTrackGeneratorToggle;
+ }
+
+ /**
+ * Optional test track generator
+ */
+ private TrackGenerator getTrackGenerator()
+ {
+ if(mTrackGenerator == null)
+ {
+ mTrackGenerator = new TrackGenerator(mMapService);
+ }
+
+ return mTrackGenerator;
+ }
+
+ public JXMapViewer getMapViewer()
+ {
+ if(mMapViewer == null)
+ {
+ mMapViewer = new JXMapViewer();
+
+ /**
+ * Set the entity painter as the overlay painter and register this panel to receive new messages (plots)
+ */
+ mMapViewer.setOverlayPainter(mMapPainter);
+
+ /**
+ * Map image source
+ */
+ TileFactoryInfo info = new OSMTileFactoryInfo();
+ DefaultTileFactory tileFactory = new DefaultTileFactory(info);
+ mMapViewer.setTileFactory(tileFactory);
+
+ /**
+ * Defines how many threads will be used to fetch the background map tiles (graphics)
+ */
+ tileFactory.setThreadPoolSize(8);
+
+ /**
+ * Set initial location and zoom for the map upon display
+ */
+ GeoPosition syracuse = new GeoPosition(43.048, -76.147);
+ int zoom = 7;
+
+ MapViewSetting view = mSettingsManager.getMapViewSetting("Default", syracuse, zoom);
+
+ mMapViewer.setAddressLocation(view.getGeoPosition());
+ mMapZoomSpinnerModel.setValue(view.getZoom());
+
+ /**
+ * Add a mouse adapter for panning and scrolling
+ */
+ MapMouseListener listener = new MapMouseListener(mMapViewer, mSettingsManager);
+ mMapViewer.addMouseListener(listener);
+ mMapViewer.addMouseMotionListener(listener);
+
+ /* Map zoom listener */
+ mMapViewer.addMouseWheelListener(new ZoomMouseWheelListenerCursor(this));
+
+ /* Keyboard panning listener */
+ mMapViewer.addKeyListener(new PanKeyListener(mMapViewer));
+
+ /**
+ * Add a selection listener
+ */
+ SelectionAdapter sa = new SelectionAdapter(mMapViewer);
+ mMapViewer.addMouseListener(sa);
+ mMapViewer.addMouseMotionListener(sa);
+ }
+
+ return mMapViewer;
+ }
+
+ /**
+ * Changes the zoom level by the specified value.
+ * @param adjustment zoom value.
+ */
+ public void adjustZoom(int adjustment)
+ {
+ Number currentZoom = mMapZoomSpinnerModel.getNumber();
+ int updatedZoom = currentZoom.intValue() + adjustment;
+
+ if(ZOOM_MINIMUM <= updatedZoom && updatedZoom <= ZOOM_MAXIMUM)
+ {
+ mMapZoomSpinnerModel.setValue(currentZoom.intValue() + adjustment);
+ }
}
@Override
diff --git a/src/main/java/io/github/dsheirer/map/MapService.java b/src/main/java/io/github/dsheirer/map/MapService.java
index ba6ec5517..49b0e99cb 100644
--- a/src/main/java/io/github/dsheirer/map/MapService.java
+++ b/src/main/java/io/github/dsheirer/map/MapService.java
@@ -1,7 +1,6 @@
/*
- * ******************************************************************************
- * sdrtrunk
- * Copyright (C) 2014-2018 Dennis Sheirer
+ * *****************************************************************************
+ * Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,78 +14,59 @@
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see
- * *****************************************************************************
+ * ****************************************************************************
*/
package io.github.dsheirer.map;
+import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.icon.IconModel;
-import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.event.IDecodeEvent;
import io.github.dsheirer.module.decode.event.PlottableDecodeEvent;
import io.github.dsheirer.sample.Listener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
public class MapService implements Listener
{
private final static Logger mLog = LoggerFactory.getLogger(MapService.class);
-
- private int mMaxHistory = 2;
- private List mListeners = new ArrayList();
- private Map mEntityHistories = new HashMap<>();
private IconModel mIconModel;
+ private PlottableEntityModel mPlottableEntityModel;
+
+ /**
+ * Constructs an instance
+ * @param aliasModel to lookup aliases
+ * @param iconModel to lookup icons from entity aliases.
+ */
+ public MapService(AliasModel aliasModel, IconModel iconModel)
+ {
+ mPlottableEntityModel = new PlottableEntityModel(aliasModel);
+ mIconModel = iconModel;
+ }
- public MapService(IconModel resourceManager)
+ /**
+ * Table model that holds the plottable entity history
+ */
+ public PlottableEntityModel getPlottableEntityModel()
{
- mIconModel = resourceManager;
+ return mPlottableEntityModel;
}
@Override
public void receive(IDecodeEvent decodeEvent)
{
- if(decodeEvent instanceof PlottableDecodeEvent)
+ if(decodeEvent instanceof PlottableDecodeEvent plottableDecodeEvent)
{
- PlottableDecodeEvent plottableDecodeEvent = (PlottableDecodeEvent)decodeEvent;
- Identifier from = plottableDecodeEvent.getIdentifierCollection().getFromIdentifier();
-
- if(from != null)
- {
- PlottableEntityHistory entityHistory = mEntityHistories.get(from);
-
- if(entityHistory == null)
- {
- entityHistory = new PlottableEntityHistory(from, plottableDecodeEvent);
- mEntityHistories.put(from, entityHistory);
- }
- else
- {
- entityHistory.add(plottableDecodeEvent);
- }
-
- for(IPlottableUpdateListener listener : mListeners)
- {
- listener.addPlottableEntity(entityHistory);
- }
- }
- else
- {
- mLog.warn("Received plottable decode event that does not contain a FROM identifier - cannot plot");
- }
+ mPlottableEntityModel.receive(plottableDecodeEvent);
}
}
public void addListener(IPlottableUpdateListener listener)
{
- mListeners.add(listener);
+ mPlottableEntityModel.addListener(listener);
}
public void removeListener(IPlottableUpdateListener listener)
{
- mListeners.remove(listener);
+ mPlottableEntityModel.removeListener(listener);
}
}
diff --git a/src/main/java/io/github/dsheirer/map/PlottableEntityHistory.java b/src/main/java/io/github/dsheirer/map/PlottableEntityHistory.java
index 301bfa824..85fdacf69 100644
--- a/src/main/java/io/github/dsheirer/map/PlottableEntityHistory.java
+++ b/src/main/java/io/github/dsheirer/map/PlottableEntityHistory.java
@@ -31,7 +31,8 @@
*/
public class PlottableEntityHistory
{
- private List mLocationHistory = new ArrayList<>();
+ public static final int MAX_LOCATION_HISTORY = 10;
+ private List mLocationHistory = new ArrayList<>();
private PlottableDecodeEvent mCurrentEvent;
private Identifier mIdentifier;
@@ -47,11 +48,24 @@ public PlottableEntityHistory(Identifier identifier, PlottableDecodeEvent event)
/**
* Location history for this entity
*/
- public List getLocationHistory()
+ public List getLocationHistory()
{
return new ArrayList<>(mLocationHistory);
}
+ /**
+ * Latest position for this entity.
+ */
+ public TimestampedGeoPosition getLatestPosition()
+ {
+ if(mLocationHistory.size() > 0)
+ {
+ return mLocationHistory.get(0);
+ }
+
+ return null;
+ }
+
/**
* Identifier for this plottable
*/
@@ -74,6 +88,11 @@ public IdentifierCollection getIdentifierCollection()
public void add(PlottableDecodeEvent event)
{
mCurrentEvent = event;
- mLocationHistory.add(event.getLocation());
+ mLocationHistory.add(0, new TimestampedGeoPosition(event.getLocation(), event.getTimeStart()));
+
+ while(mLocationHistory.size() > MAX_LOCATION_HISTORY)
+ {
+ mLocationHistory.remove(mLocationHistory.size() - 1);
+ }
}
}
diff --git a/src/main/java/io/github/dsheirer/map/PlottableEntityModel.java b/src/main/java/io/github/dsheirer/map/PlottableEntityModel.java
new file mode 100644
index 000000000..0b54ee411
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/map/PlottableEntityModel.java
@@ -0,0 +1,251 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2024 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.map;
+
+import io.github.dsheirer.alias.Alias;
+import io.github.dsheirer.alias.AliasList;
+import io.github.dsheirer.alias.AliasModel;
+import io.github.dsheirer.identifier.Identifier;
+import io.github.dsheirer.identifier.configuration.AliasListConfigurationIdentifier;
+import io.github.dsheirer.module.decode.event.PlottableDecodeEvent;
+import io.github.dsheirer.sample.Listener;
+import java.awt.EventQueue;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.swing.table.AbstractTableModel;
+
+/**
+ * Table model for plottable entity history elements.
+ */
+public class PlottableEntityModel extends AbstractTableModel implements Listener
+{
+ private final static Logger LOGGER = LoggerFactory.getLogger(PlottableEntityModel.class);
+ private static final int COLUMN_ID = 0;
+ private static final int COLUMN_ALIAS = 1;
+ private static final int COLUMN_ALIAS_LIST = 2;
+ private static final String[] COLUMN_NAMES = {"ID", "Alias", "List"};
+ private static final String KEY_NO_ALIAS_LIST = "(no alias list)";
+ private Map mEntityHistoryMap = new HashMap();
+ private List mEntityHistories = new ArrayList<>();
+ private List mPlottableUpdateListeners = new ArrayList<>();
+ private AliasModel mAliasModel;
+
+ /**
+ * Constructs an instance
+ * @param aliasModel to lookup aliases
+ */
+ public PlottableEntityModel(AliasModel aliasModel)
+ {
+ mAliasModel = aliasModel;
+ }
+
+ /**
+ * Deletes all tracks and histories.
+ */
+ public void deleteAllTracks()
+ {
+ EventQueue.invokeLater(() -> {
+ if(mEntityHistories.size() > 0)
+ {
+ int firstRow = 0;
+ int lastRow = mEntityHistories.size() - 1;
+ mEntityHistoryMap.clear();
+ mEntityHistories.clear();
+ fireTableRowsDeleted(firstRow, lastRow);
+ }
+ });
+ }
+
+ /**
+ * Deletes the tracks from both the map and entity histories and fires a table model changed event.
+ * @param tracksToDelete to delete
+ */
+ public void delete(List tracksToDelete)
+ {
+ EventQueue.invokeLater(() ->
+ {
+ for(PlottableEntityHistory track : tracksToDelete)
+ {
+ int index = mEntityHistories.indexOf(track);
+ mEntityHistories.remove(track);
+ mEntityHistoryMap.entrySet().removeIf(entry -> entry.getValue() == track);
+ fireTableRowsDeleted(index, index);
+ }
+ });
+ }
+
+ @Override
+ public void receive(PlottableDecodeEvent plottableDecodeEvent)
+ {
+ //Add or update the event on the swing event thread
+ EventQueue.invokeLater(() -> {
+ Identifier from = plottableDecodeEvent.getIdentifierCollection().getFromIdentifier();
+
+ if(from != null)
+ {
+ AliasListConfigurationIdentifier aliasList = plottableDecodeEvent.getIdentifierCollection().getAliasListConfiguration();
+ String key = (aliasList != null ? aliasList.toString() : KEY_NO_ALIAS_LIST) + from;
+
+ PlottableEntityHistory entityHistory = mEntityHistoryMap.get(key);
+
+ if(entityHistory == null)
+ {
+ entityHistory = new PlottableEntityHistory(from, plottableDecodeEvent);
+ mEntityHistories.add(entityHistory);
+ mEntityHistoryMap.put(key, entityHistory);
+ int index = mEntityHistories.indexOf(entityHistory);
+ fireTableRowsInserted(index, index);
+ }
+ else
+ {
+ entityHistory.add(plottableDecodeEvent);
+ int index = mEntityHistories.indexOf(entityHistory);
+ fireTableRowsUpdated(index, index);
+ }
+
+ for(IPlottableUpdateListener listener : mPlottableUpdateListeners)
+ {
+ listener.addPlottableEntity(entityHistory);
+ }
+ }
+ else
+ {
+ LOGGER.warn("Received plottable decode event that does not contain a FROM identifier - cannot plot");
+ }
+ });
+ }
+
+ @Override
+ public String getColumnName(int column)
+ {
+ if(0 <= column && column < COLUMN_NAMES.length)
+ {
+ return COLUMN_NAMES[column];
+ }
+
+ return "error!";
+ }
+
+ @Override
+ public int getRowCount()
+ {
+ return mEntityHistories.size();
+ }
+
+ @Override
+ public int getColumnCount()
+ {
+ return COLUMN_NAMES.length;
+ }
+
+ /**
+ * Get the plottable entity history for the specified model index
+ * @param index in the model
+ * @return entity history or null.
+ */
+ public PlottableEntityHistory get(int index)
+ {
+ if(index >= 0 && index < mEntityHistories.size())
+ {
+ return mEntityHistories.get(index);
+ }
+
+ return null;
+ }
+
+ /**
+ * Get all plottable entity histories.
+ */
+ public List getAll()
+ {
+ return new ArrayList<>(mEntityHistories);
+ }
+
+ @Override
+ public Object getValueAt(int rowIndex, int columnIndex)
+ {
+ PlottableEntityHistory history = mEntityHistories.get(rowIndex);
+
+ if(history != null)
+ {
+ switch(columnIndex)
+ {
+ case COLUMN_ID:
+ Identifier identifier = history.getIdentifier();
+
+ if(identifier != null)
+ {
+ return identifier.toString();
+ }
+ else
+ {
+ return "(no ID)";
+ }
+ case COLUMN_ALIAS:
+ Identifier aliasListConfig = history.getIdentifierCollection().getAliasListConfiguration();
+ if(aliasListConfig != null)
+ {
+ AliasList al = mAliasModel.getAliasList(aliasListConfig.toString());
+
+ if(al != null)
+ {
+ List aliases = al.getAliases(history.getIdentifier());
+
+ if(!aliases.isEmpty())
+ {
+ return aliases.get(0).getName();
+ }
+ }
+ }
+ break;
+ case COLUMN_ALIAS_LIST:
+ Identifier aliasList = history.getIdentifierCollection().getAliasListConfiguration();
+ if(aliasList != null)
+ {
+ return aliasList.toString();
+ }
+ else
+ {
+ return "(no alias list)";
+ }
+ default:
+ throw new IllegalArgumentException("Unexpected column index");
+ }
+ }
+
+ return null;
+ }
+
+ public void addListener(IPlottableUpdateListener listener)
+ {
+ mPlottableUpdateListeners.add(listener);
+ }
+
+ public void removeListener(IPlottableUpdateListener listener)
+ {
+ mPlottableUpdateListeners.remove(listener);
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/map/PlottableEntityPainter.java b/src/main/java/io/github/dsheirer/map/PlottableEntityPainter.java
index 705765438..74333e502 100644
--- a/src/main/java/io/github/dsheirer/map/PlottableEntityPainter.java
+++ b/src/main/java/io/github/dsheirer/map/PlottableEntityPainter.java
@@ -1,8 +1,7 @@
/*
- * ******************************************************************************
- * sdrtrunk
- * Copyright (C) 2014-2018 Dennis Sheirer
+ * *****************************************************************************
+ * Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -16,26 +15,33 @@
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see
- * *****************************************************************************
+ * ****************************************************************************
*/
package io.github.dsheirer.map;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.icon.IconModel;
-import org.jdesktop.swingx.JXMapViewer;
-import org.jdesktop.swingx.painter.AbstractPainter;
-
import java.awt.Graphics2D;
import java.awt.Rectangle;
-import java.util.Collections;
import java.util.HashSet;
+import java.util.List;
import java.util.Set;
+import org.jdesktop.swingx.JXMapViewer;
+import org.jdesktop.swingx.painter.AbstractPainter;
+/**
+ * Paints plottable entities to the map.
+ */
public class PlottableEntityPainter extends AbstractPainter
{
private PlottableEntityRenderer mRenderer;
private Set mEntities = new HashSet<>();
+ /**
+ * Constructs an instance
+ * @param aliasModel to lookup alias for entities
+ * @param iconModel to lookup icon from alias.
+ */
public PlottableEntityPainter(AliasModel aliasModel, IconModel iconModel)
{
mRenderer = new PlottableEntityRenderer(aliasModel, iconModel);
@@ -43,24 +49,77 @@ public PlottableEntityPainter(AliasModel aliasModel, IconModel iconModel)
setCacheable(false);
}
- public void addEntity(PlottableEntityHistory entity)
+ /**
+ * Sets the length of the plotted history trails.
+ * @param length of history trails
+ */
+ public void setTrackHistoryLength(int length)
+ {
+ mRenderer.setTrackHistoryLength(length);
+ }
+
+ /**
+ * Current size of the history trail length.
+ */
+ public int getTrackHistoryLength()
+ {
+ return mRenderer.getTrackHistoryLength();
+ }
+
+ /**
+ * Adds an entity to the map
+ * @param entity to add
+ */
+ public boolean addEntity(PlottableEntityHistory entity)
{
- mEntities.add(entity);
+ if(entity != null && !mEntities.contains(entity))
+ {
+ mEntities.add(entity);
+ return true;
+ }
+
+ return false;
}
+ /**
+ * Adds all entities to this painter
+ * @param entities to add.
+ */
+ public boolean addAll(List entities)
+ {
+ boolean added = false;
+ for(PlottableEntityHistory entity : entities)
+ {
+ added |= addEntity(entity);
+ }
+
+ return added;
+ }
+
+ /**
+ * Removes an entity from the map
+ * @param entity to remove
+ */
public void removeEntity(PlottableEntityHistory entity)
{
mEntities.remove(entity);
}
- public void clearEntities()
+ /**
+ * Clears all entities from the map.
+ */
+ public void clearAllEntities()
{
mEntities.clear();
}
- private Set getEntities()
+ /**
+ * Clears teh specified entities from the map
+ * @param toDelete to delete
+ */
+ public void clearEntities(List toDelete)
{
- return Collections.unmodifiableSet(mEntities);
+ mEntities.removeAll(toDelete);
}
@Override
@@ -70,9 +129,7 @@ protected void doPaint(Graphics2D g, JXMapViewer map, int width, int height)
g.translate(-viewportBounds.getX(), -viewportBounds.getY());
- Set entities = getEntities();
-
- for(PlottableEntityHistory entity : entities)
+ for(PlottableEntityHistory entity : mEntities)
{
mRenderer.paintPlottableEntity(g, map, entity, true);
}
diff --git a/src/main/java/io/github/dsheirer/map/PlottableEntityRenderer.java b/src/main/java/io/github/dsheirer/map/PlottableEntityRenderer.java
index 6556f6838..338022935 100644
--- a/src/main/java/io/github/dsheirer/map/PlottableEntityRenderer.java
+++ b/src/main/java/io/github/dsheirer/map/PlottableEntityRenderer.java
@@ -1,7 +1,6 @@
/*
- * ******************************************************************************
- * sdrtrunk
- * Copyright (C) 2014-2018 Dennis Sheirer
+ * *****************************************************************************
+ * Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,7 +14,7 @@
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see
- * *****************************************************************************
+ * ****************************************************************************
*/
package io.github.dsheirer.map;
@@ -23,10 +22,6 @@
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.icon.IconModel;
-import org.jdesktop.swingx.JXMapViewer;
-import org.jdesktop.swingx.mapviewer.GeoPosition;
-
-import javax.swing.ImageIcon;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
@@ -34,21 +29,58 @@
import java.awt.geom.Point2D;
import java.util.Collections;
import java.util.List;
+import org.jdesktop.swingx.JXMapViewer;
+import org.jdesktop.swingx.mapviewer.GeoPosition;
+
+import javax.swing.ImageIcon;
+/**
+ * Paints a single plottable entity to the map.
+ */
public class PlottableEntityRenderer
{
private AliasModel mAliasModel;
private IconModel mIconModel;
+ private int mTrackHistoryLength = 3;
+ /**
+ * Constructs an instance
+ * @param aliasModel for alias lookup for the entity to determine plot color and icon
+ * @param iconModel to retrieve icon for plotting.
+ */
public PlottableEntityRenderer(AliasModel aliasModel, IconModel iconModel)
{
mAliasModel = aliasModel;
mIconModel = iconModel;
}
+ /**
+ * Sets the length of the plotted history trails.
+ * @param length of history trails
+ */
+ public void setTrackHistoryLength(int length)
+ {
+ mTrackHistoryLength = length;
+ }
+
+ /**
+ * Current size of the history trail length.
+ */
+ public int getTrackHistoryLength()
+ {
+ return mTrackHistoryLength;
+ }
+
+ /**
+ * Requests the plottable entity be painted to the map viewer.
+ * @param g graphics
+ * @param viewer requesting the painting
+ * @param entity to be painted
+ * @param antiAliasing for rendering.
+ */
public void paintPlottableEntity(Graphics2D g, JXMapViewer viewer, PlottableEntityHistory entity, boolean antiAliasing)
{
- List locationHistory = entity.getLocationHistory();
+ List locationHistory = entity.getLocationHistory();
if(!locationHistory.isEmpty() && locationHistory.get(locationHistory.size() - 1).isValid())
{
@@ -66,7 +98,7 @@ public void paintPlottableEntity(Graphics2D g, JXMapViewer viewer, PlottableEnti
/**
* Use the entity's preferred color for lines and labels
*/
- Color color = (alias != null ? alias.getDisplayColor() : Color.YELLOW);
+ Color color = (alias != null ? alias.getDisplayColor() : Color.BLUE);
graphics.setColor(color);
/**
@@ -79,7 +111,7 @@ public void paintPlottableEntity(Graphics2D g, JXMapViewer viewer, PlottableEnti
* Convert the lat/long geoposition to an x/y point on the viewer
*/
- Point2D point = viewer.getTileFactory().geoToPixel(locationHistory.get(locationHistory.size() - 1), viewer.getZoom());
+ Point2D point = viewer.getTileFactory().geoToPixel(entity.getLatestPosition(), viewer.getZoom());
/**
* Paint the icon at the current location
@@ -139,7 +171,7 @@ private void paintLabel(Graphics2D graphics, Point2D point, String label, int xO
*/
private void paintRoute(Graphics2D graphics, JXMapViewer viewer, PlottableEntityHistory entity, Color color)
{
- List locations = entity.getLocationHistory();
+ List locations = entity.getLocationHistory();
if(!locations.isEmpty())
{
@@ -160,21 +192,26 @@ private void paintRoute(Graphics2D graphics, JXMapViewer viewer, PlottableEntity
/**
* Draws a route from a list of plottables
*/
- private void drawRoute(List locations, Graphics2D g, JXMapViewer viewer)
+ private void drawRoute(List locations, Graphics2D g, JXMapViewer viewer)
{
- Point2D lastPoint = null;
+ Point2D previousPoint = null;
- for(GeoPosition location : locations)
+ int length = Math.min(locations.size(), mTrackHistoryLength);
+
+ for(int x = 0; x < length; x++)
{
+ GeoPosition location = locations.get(x);
+
// convert geo-coordinate to world bitmap pixel
Point2D currentPoint = viewer.getTileFactory().geoToPixel(location, viewer.getZoom());
- if(lastPoint != null)
+ if(previousPoint != null)
{
- g.drawLine((int)lastPoint.getX(), (int)lastPoint.getY(), (int)currentPoint.getX(), (int)currentPoint.getY());
+ g.drawLine((int)previousPoint.getX(), (int)previousPoint.getY(), (int)currentPoint.getX(), (int)currentPoint.getY());
}
- lastPoint = currentPoint;
+ previousPoint = currentPoint;
+
}
}
}
diff --git a/src/main/java/io/github/dsheirer/map/TimestampedGeoPosition.java b/src/main/java/io/github/dsheirer/map/TimestampedGeoPosition.java
new file mode 100644
index 000000000..dd56a03bb
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/map/TimestampedGeoPosition.java
@@ -0,0 +1,32 @@
+package io.github.dsheirer.map;
+
+import org.jdesktop.swingx.mapviewer.GeoPosition;
+
+/**
+ * Timestamped geo position
+ */
+public class TimestampedGeoPosition extends GeoPosition
+{
+ private long mTimestamp;
+
+ /**
+ * Constructs an instance
+ * @param latitude for the position
+ * @param longitude for the position
+ * @param timestamp in milliseconds
+ */
+ public TimestampedGeoPosition(GeoPosition position, long timestamp)
+ {
+ super(position.getLatitude(), position.getLongitude());
+ mTimestamp = timestamp;
+ }
+
+ /**
+ * Timestamp for the geo position
+ * @return timestamp in milliseconds.
+ */
+ public long getTimestamp()
+ {
+ return mTimestamp;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/map/TrackGenerator.java b/src/main/java/io/github/dsheirer/map/TrackGenerator.java
new file mode 100644
index 000000000..3fc8196e9
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/map/TrackGenerator.java
@@ -0,0 +1,174 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2024 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.map;
+
+import io.github.dsheirer.identifier.IdentifierCollection;
+import io.github.dsheirer.identifier.MutableIdentifierCollection;
+import io.github.dsheirer.identifier.configuration.AliasListConfigurationIdentifier;
+import io.github.dsheirer.identifier.configuration.FrequencyConfigurationIdentifier;
+import io.github.dsheirer.identifier.configuration.SiteConfigurationIdentifier;
+import io.github.dsheirer.identifier.configuration.SystemConfigurationIdentifier;
+import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio;
+import io.github.dsheirer.module.decode.event.DecodeEventType;
+import io.github.dsheirer.module.decode.event.PlottableDecodeEvent;
+import io.github.dsheirer.protocol.Protocol;
+import io.github.dsheirer.util.ThreadPool;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import org.jdesktop.swingx.mapviewer.GeoPosition;
+
+/**
+ * Map test track data generator.
+ *
+ * Creates plottable events and publishes them to the map service. Issues periodic updates to the plottable events.
+ */
+public class TrackGenerator
+{
+ private static GeoPosition DEFAULT_START_POSITION = new GeoPosition(43.048, -76.147);
+ private static final String ALIAS_LIST_NAME = "DMR Test Alias List";
+ private MapService mMapService;
+ private List mTrackElementGenerators = new ArrayList<>();
+ private double mBaseSpeedKPH = 40.0;
+ private int mTrackCount = 25;
+ private ScheduledFuture> mGeneratorFuture;
+
+ /**
+ * Constructs an instance to publish tracks to the specified map service
+ * @param mapService to receive test tracks.
+ */
+ public TrackGenerator(MapService mapService)
+ {
+ mMapService = mapService;
+
+ MutableIdentifierCollection base = new MutableIdentifierCollection();
+ base.update(SystemConfigurationIdentifier.create("Test System"));
+ base.update(SiteConfigurationIdentifier.create("Test Site"));
+ base.update(FrequencyConfigurationIdentifier.create(155000000l));
+ base.update(AliasListConfigurationIdentifier.create(ALIAS_LIST_NAME));
+
+ Random random = new Random();
+
+ for(int x = 0; x < mTrackCount; x++)
+ {
+ MutableIdentifierCollection mic = new MutableIdentifierCollection(base.getIdentifiers());
+ mic.update(DMRRadio.createFrom(x + 1));
+ double speedKPH = mBaseSpeedKPH + (random.nextDouble() * 15);
+ mTrackElementGenerators.add(new TrackElementGenerator(speedKPH, mic));
+ }
+ }
+
+ /**
+ * Starts the test generator
+ */
+ public void start()
+ {
+ if(mGeneratorFuture == null)
+ {
+ mGeneratorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(() -> update(), 0, 1, TimeUnit.SECONDS);
+ }
+ }
+
+ /**
+ * Stops the test generator
+ */
+ public void stop()
+ {
+ if(mGeneratorFuture != null)
+ {
+ mGeneratorFuture.cancel(true);
+ mGeneratorFuture = null;
+ }
+ }
+
+ /**
+ * Updates each of the track generates causing them to dispatch a new decode event to the map service.
+ *
+ * Invoke this method once per second.
+ */
+ private void update()
+ {
+ for(TrackElementGenerator tg: mTrackElementGenerators)
+ {
+ mMapService.receive(tg.update());
+ }
+ }
+
+ /**
+ * Test track element generator
+ */
+ public static class TrackElementGenerator
+ {
+ public static double EARTH_RADIUS_KM = 6378.137;
+ public static double ONE_SECOND = 1.0 / 60.0 / 60.0; //1 hour divided by 60 minutes divided by 60 seconds.
+ private IdentifierCollection mIdentifierCollection;
+ private double mSpeedKPH;
+ private GeoPosition mPosition = new GeoPosition(DEFAULT_START_POSITION.getLatitude(), DEFAULT_START_POSITION.getLongitude());
+ private Random mRandom = new Random();
+ private double mHeading = 360.0 * mRandom.nextDouble();
+
+ /**
+ * Constructs an instance
+ * @param trackId for the track
+ * @param speedKPH speed in KPH
+ */
+ public TrackElementGenerator(double speedKPH, IdentifierCollection identifierCollection)
+ {
+ mSpeedKPH = speedKPH;
+ mIdentifierCollection = identifierCollection;
+ }
+
+ /**
+ *
+ * @param heading 0-360 degrees
+ */
+ public PlottableDecodeEvent update()
+ {
+ mHeading = mHeading + (15.0 - (mRandom.nextDouble() * 30.0));
+ mHeading = Math.max(mHeading, 0);
+ mHeading = Math.min(mHeading, 360.0);
+ double distanceKM = mSpeedKPH * ONE_SECOND;
+ double headingRadians = Math.toRadians(mHeading);
+
+ double angularDistance = distanceKM / EARTH_RADIUS_KM;
+ double latRadians = Math.toRadians(mPosition.getLatitude());
+ double lonRadians = Math.toRadians(mPosition.getLongitude());
+ double latitude = Math.asin((Math.sin(latRadians) * Math.cos(angularDistance)) +
+ (Math.cos(latRadians) * Math.sin(angularDistance) * Math.cos(headingRadians)));
+ double longitude = lonRadians + Math.atan2(Math.sin(headingRadians) * Math.sin(angularDistance) *
+ Math.cos(latRadians), Math.cos(angularDistance) - Math.sin(latRadians) * Math.sin(latitude));
+
+ latitude = Math.toDegrees(latitude);
+ longitude = Math.toDegrees(longitude);
+ mPosition = new GeoPosition(latitude, longitude);
+ PlottableDecodeEvent event =
+ new PlottableDecodeEvent.PlottableDecodeEventBuilder(DecodeEventType.GPS, System.currentTimeMillis())
+ .heading(mHeading)
+ .speed(mSpeedKPH)
+ .location(mPosition)
+ .protocol(Protocol.DMR)
+ .identifiers(mIdentifierCollection)
+ .build();
+ return event;
+ }
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/map/TrackHistoryModel.java b/src/main/java/io/github/dsheirer/map/TrackHistoryModel.java
new file mode 100644
index 000000000..44dd93d72
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/map/TrackHistoryModel.java
@@ -0,0 +1,126 @@
+package io.github.dsheirer.map;
+
+import javax.swing.table.AbstractTableModel;
+import java.text.DecimalFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.function.ToLongFunction;
+
+/**
+ * Table model for track history.
+ */
+public class TrackHistoryModel extends AbstractTableModel
+{
+ private static final String[] COLUMNS = new String[]{"Time", "Latitude", "Longitude"};
+ private SimpleDateFormat mSimpleDateFormat = new SimpleDateFormat("HH:mm:ss");
+ private DecimalFormat mDegreeFormat = new DecimalFormat("0.00000");
+ private List mTimestampedGeoPositions = new ArrayList<>();
+ private PlottableEntityHistory mPlottableEntityHistory;
+
+ /**
+ * Constructs an instance
+ */
+ public TrackHistoryModel()
+ {
+ }
+
+ /**
+ * Get the geo position at the specified index
+ * @param index to retrieve
+ * @return geo or null.
+ */
+ public TimestampedGeoPosition get(int index)
+ {
+ if(index < mTimestampedGeoPositions.size())
+ {
+ return mTimestampedGeoPositions.get(index);
+ }
+
+ return null;
+ }
+
+ /**
+ * Loads the plottable entity history into this model
+ * @param plottableEntityHistory to load
+ */
+ public void load(PlottableEntityHistory plottableEntityHistory)
+ {
+ mPlottableEntityHistory = plottableEntityHistory;
+
+ if(mTimestampedGeoPositions.size() > 0)
+ {
+ int lastRow = mTimestampedGeoPositions.size() - 1;
+ mTimestampedGeoPositions.clear();
+ fireTableRowsDeleted(0, lastRow);
+ }
+
+ if(mPlottableEntityHistory != null)
+ {
+ mTimestampedGeoPositions.addAll(mPlottableEntityHistory.getLocationHistory());
+
+ if(mTimestampedGeoPositions.size() > 0)
+ {
+ fireTableRowsInserted(0, mTimestampedGeoPositions.size() - 1);
+ }
+ }
+ }
+
+ /**
+ * Updates or refreshes the track history from the current plottable entity.
+ */
+ public void update()
+ {
+ if(mPlottableEntityHistory != null)
+ {
+ List geos = mPlottableEntityHistory.getLocationHistory();
+ Collections.reverse(geos);
+
+ for(TimestampedGeoPosition geo: geos)
+ {
+ if(!mTimestampedGeoPositions.contains(geo))
+ {
+ mTimestampedGeoPositions.add(0, geo);
+ fireTableRowsInserted(0, 0);
+ }
+ }
+ }
+ }
+
+ @Override
+ public int getRowCount()
+ {
+ return mTimestampedGeoPositions.size();
+ }
+
+ @Override
+ public int getColumnCount()
+ {
+ return COLUMNS.length;
+ }
+
+ @Override
+ public String getColumnName(int column)
+ {
+ return COLUMNS[column];
+ }
+
+ @Override
+ public Object getValueAt(int rowIndex, int columnIndex)
+ {
+ TimestampedGeoPosition geoPosition = mTimestampedGeoPositions.get(rowIndex);
+ switch (columnIndex)
+ {
+ case 0:
+ return mSimpleDateFormat.format(geoPosition.getTimestamp());
+ case 1:
+ return mDegreeFormat.format(geoPosition.getLatitude());
+ case 2:
+ return mDegreeFormat.format(geoPosition.getLongitude());
+ }
+
+ return null;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/LinearFeedbackShiftRegister.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/LinearFeedbackShiftRegister.java
index 06b1e0d13..4d017d5ed 100644
--- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/LinearFeedbackShiftRegister.java
+++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/timeslot/LinearFeedbackShiftRegister.java
@@ -1,23 +1,20 @@
/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2024 Dennis Sheirer
*
- * * ******************************************************************************
- * * Copyright (C) 2014-2019 Dennis Sheirer
- * *
- * * This program is free software: you can redistribute it and/or modify
- * * it under the terms of the GNU General Public License as published by
- * * the Free Software Foundation, either version 3 of the License, or
- * * (at your option) any later version.
- * *
- * * This program is distributed in the hope that it will be useful,
- * * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * * GNU General Public License for more details.
- * *
- * * You should have received a copy of the GNU General Public License
- * * along with this program. If not, see
- * * *****************************************************************************
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
*
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
*
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
*/
package io.github.dsheirer.module.decode.p25.phase2.timeslot;
@@ -63,9 +60,6 @@ public void updateSeed(int wacn, int system, int nac)
mSystem = system;
mNac = nac;
- int temp = 0xFFFFF & 1;
- long tempShift = temp << 24;
-
mRegisters = (long)(0xFFFFF & wacn) << 24;
mRegisters += (0xFFF & system) << 12;
mRegisters += (0xFFF & nac);
@@ -78,6 +72,16 @@ public void updateSeed(int wacn, int system, int nac)
mCurrentOutput = getTap(TAP_43);
}
+ /**
+ * Loads the seed value directly into the registers.
+ * @param seed for the lfsr
+ */
+ public void updateSeed(long seed)
+ {
+ mRegisters = seed;
+ mCurrentOutput = getTap(TAP_43);
+ }
+
/**
* Indicates if the shift register is currently configured for the argument values (and doesn't need updating).
*
@@ -120,6 +124,32 @@ public BinaryMessage generateScramblingSequence(int wacn, int system, int nac)
return sequence;
}
+ /**
+ * Generates a (de)scrambling sequence for the specified seed and length
+ * @param seed value to use
+ * @param length of the generated sequence
+ * @return scrambling sequence in a binary message
+ */
+ public BinaryMessage generateScramblingSequence(long seed, int length)
+ {
+ updateSeed(seed);
+ BinaryMessage sequence = new BinaryMessage(length);
+
+ try
+ {
+ for(int x = 0; x < length; x++)
+ {
+ sequence.add(next());
+ }
+ }
+ catch(BitSetFullException e)
+ {
+ //This shouldn't happen
+ }
+
+ return sequence;
+ }
+
/**
* Provides the next output bit from the LFSR
*/
@@ -152,4 +182,39 @@ private boolean getTap(long tap)
{
return (mRegisters & tap) == tap;
}
+
+ public static void main(String[] args)
+ {
+ long seed = 0;
+ int wacn = 0xBEE00;
+ int system = 0x1C7;
+ int nac = 0x1C1;
+
+ seed = (long)(0xFFFFF & wacn) << 24;
+ seed += (0xFFF & system) << 12;
+ seed += (0xFFF & nac);
+
+ seed = 0xBEE001C7013l;
+
+ System.out.println("Seed: " + Long.toHexString(seed).toUpperCase());
+
+ LinearFeedbackShiftRegister lfsr = new LinearFeedbackShiftRegister();
+// BinaryMessage scramble = lfsr.generateScramblingSequence(0xBEE07, 0x40F, 0x04E);
+ BinaryMessage scramble = lfsr.generateScramblingSequence(seed, 400);
+ System.out.println("SCRAM: " + scramble.toHexString());
+// scramble.rotateRight(64, 0, 401);
+ System.out.println("SCRAM: " + scramble.toHexString());
+
+ BinaryMessage raw1 = BinaryMessage.loadHex("BEE001C70139CB7D5F2D4823695F7ED499EA998F8748E6DAB167FAC15EC2C6222E");
+// BinaryMessage raw1 = BinaryMessage.loadHex("BEE0740F04E0172D21681A1B52FFBFBFEE53D2A5ADB9561CADF4D955EBF1CB0000");
+// BinaryMessage raw2 = BinaryMessage.loadHex("BEE0740F04E0DD2D21681A1B52FFBFBFEE53FE86BEF78FD5AB910B2376F9D80000");
+ System.out.println(" RAW: " + raw1.toHexString());
+ System.out.println(" xxx: BEE001C70139CB");
+ int length = raw1.length();
+
+
+ raw1.xor(scramble);
+ BinaryMessage descrambled = raw1.get(0, length);
+ System.out.println("DESCR: " + descrambled.toHexString());
+ }
}
diff --git a/src/main/java/org/jdesktop/swingx/JXMapViewer.java b/src/main/java/org/jdesktop/swingx/JXMapViewer.java
index 6a174b7cf..ef77d0c56 100644
--- a/src/main/java/org/jdesktop/swingx/JXMapViewer.java
+++ b/src/main/java/org/jdesktop/swingx/JXMapViewer.java
@@ -1,23 +1,20 @@
/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2024 Dennis Sheirer
*
- * * ******************************************************************************
- * * Copyright (C) 2014-2019 Dennis Sheirer
- * *
- * * This program is free software: you can redistribute it and/or modify
- * * it under the terms of the GNU General Public License as published by
- * * the Free Software Foundation, either version 3 of the License, or
- * * (at your option) any later version.
- * *
- * * This program is distributed in the hope that it will be useful,
- * * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * * GNU General Public License for more details.
- * *
- * * You should have received a copy of the GNU General Public License
- * * along with this program. If not, see
- * * *****************************************************************************
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
*
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
*
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
*/
/*
@@ -31,6 +28,20 @@
package org.jdesktop.swingx;
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Image;
+import java.awt.Insets;
+import java.awt.Rectangle;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.awt.image.BufferedImage;
+import java.beans.DesignMode;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.util.Set;
import jiconfont.icons.font_awesome.FontAwesome;
import jiconfont.swing.IconFontSwing;
import org.apache.commons.math3.util.FastMath;
@@ -46,20 +57,6 @@
import org.slf4j.LoggerFactory;
import javax.swing.JPanel;
-import java.awt.Color;
-import java.awt.Dimension;
-import java.awt.Graphics;
-import java.awt.Graphics2D;
-import java.awt.Image;
-import java.awt.Insets;
-import java.awt.Rectangle;
-import java.awt.geom.Point2D;
-import java.awt.geom.Rectangle2D;
-import java.awt.image.BufferedImage;
-import java.beans.DesignMode;
-import java.beans.PropertyChangeEvent;
-import java.beans.PropertyChangeListener;
-import java.util.Set;
/**
* A tile oriented map component that can easily be used with tile sources
@@ -87,8 +84,7 @@ public class JXMapViewer extends JPanel implements DesignMode
{
private static final long serialVersionUID = -3530746298586937321L;
- private final static Logger mLog =
- LoggerFactory.getLogger( JXMapViewer.class );
+ private final static Logger mLog = LoggerFactory.getLogger(JXMapViewer.class);
private final boolean isNegativeYAllowed = true; // maybe rename to isNorthBounded and isSouthBounded?
@@ -96,25 +92,25 @@ public class JXMapViewer extends JPanel implements DesignMode
* The zoom level. Generally a value between 1 and 15 (TODO Is this true for all the mapping worlds? What does this
* mean if some mapping system doesn't support the zoom level?
*/
- private int zoomLevel = 1;
+ private int mZoomLevel = 1;
/**
* The position, in map coordinates of the center point. This is defined as the distance from the top and
* left edges of the map in pixels. Dragging the map component will change the center position. Zooming in/out will
* cause the center to be recalculated so as to remain in the center of the new "map".
*/
- private Point2D center = new Point2D.Double(0, 0);
+ private Point2D mCenter = new Point2D.Double(0, 0);
/**
* Indicates whether or not to draw the borders between tiles. Defaults to false. TODO Generally not very nice
* looking, very much a product of testing Consider whether this should really be a property or not.
*/
- private boolean drawTileBorders = false;
+ private boolean mDrawTileBorders = false;
/**
* Factory used by this component to grab the tiles necessary for painting the map.
*/
- private TileFactory factory;
+ private TileFactory mTileFactory;
/**
* The position in latitude/longitude of the "address" being mapped. This is a special coordinate that, when moved,
@@ -122,27 +118,27 @@ public class JXMapViewer extends JPanel implements DesignMode
* (in pixels) of the viewport whereas this will not change when panning or zooming. Whenever the addressLocation is
* changed, however, the map will be repositioned.
*/
- private GeoPosition addressLocation;
+ private GeoPosition mAddressLocation;
/**
* The overlay to delegate to for painting the "foreground" of the map component. This would include painting
* waypoints, day/night, etc. Also receives mouse events.
*/
- private Painter super JXMapViewer> overlay;
+ private Painter super JXMapViewer> mOverlay;
- private boolean designTime;
+ private boolean mDesignTime;
- private Image loadingImage;
+ private Image mLoadingImage;
- private boolean restrictOutsidePanning = true;
- private boolean horizontalWrapped = true;
+ private boolean mRestrictOutsidePanning = true;
+ private boolean mHorizontalWrapped = true;
/**
* Create a new JXMapViewer. By default it will use the EmptyTileFactory
*/
public JXMapViewer()
{
- factory = new EmptyTileFactory();
+ mTileFactory = new EmptyTileFactory();
// setTileFactory(new GoogleTileFactory());
// make a dummy loading image
@@ -153,9 +149,7 @@ public JXMapViewer()
}
catch (Throwable ex)
{
-
mLog.error( "JXMapViewer could not load default 'loading.png'" );
-
BufferedImage img = new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = img.createGraphics();
g2.setColor(Color.black);
@@ -203,7 +197,7 @@ private void doPaintComponent(Graphics g)
@Override
public void setDesignTime(boolean b)
{
- this.designTime = b;
+ this.mDesignTime = b;
}
/**
@@ -213,7 +207,7 @@ public void setDesignTime(boolean b)
@Override
public boolean isDesignTime()
{
- return designTime;
+ return mDesignTime;
}
/**
@@ -313,9 +307,9 @@ else if (tile.isLoaded())
@SuppressWarnings("unused")
private void drawOverlays(final int zoom, final Graphics g, final Rectangle viewportBounds)
{
- if (overlay != null)
+ if (mOverlay != null)
{
- overlay.paint((Graphics2D) g, this, getWidth(), getHeight());
+ mOverlay.paint((Graphics2D) g, this, getWidth(), getHeight());
}
}
@@ -333,7 +327,7 @@ private boolean isTileOnMap(int x, int y, Dimension mapSize)
public void setOverlayPainter(Painter super JXMapViewer> overlay)
{
Painter super JXMapViewer> old = getOverlayPainter();
- this.overlay = overlay;
+ this.mOverlay = overlay;
PropertyChangeListener listener = new PropertyChangeListener()
{
@@ -369,7 +363,7 @@ public void propertyChange(PropertyChangeEvent evt)
*/
public Painter super JXMapViewer> getOverlayPainter()
{
- return overlay;
+ return mOverlay;
}
/**
@@ -399,7 +393,7 @@ private Rectangle calculateViewportBounds(Point2D centr)
*/
public void setZoom(int zoom)
{
- if (zoom == this.zoomLevel)
+ if (zoom == this.mZoomLevel)
{
return;
}
@@ -412,10 +406,10 @@ public void setZoom(int zoom)
}
// if(zoom >= 0 && zoom <= 15 && zoom != this.zoom) {
- int oldzoom = this.zoomLevel;
+ int oldzoom = this.mZoomLevel;
Point2D oldCenter = getCenter();
Dimension oldMapSize = getTileFactory().getMapSize(oldzoom);
- this.zoomLevel = zoom;
+ this.mZoomLevel = zoom;
this.firePropertyChange("zoom", oldzoom, zoom);
Dimension mapSize = getTileFactory().getMapSize(zoom);
@@ -432,7 +426,7 @@ public void setZoom(int zoom)
*/
public int getZoom()
{
- return this.zoomLevel;
+ return this.mZoomLevel;
}
/**
@@ -442,7 +436,7 @@ public int getZoom()
*/
public GeoPosition getAddressLocation()
{
- return addressLocation;
+ return mAddressLocation;
}
/**
@@ -452,7 +446,7 @@ public GeoPosition getAddressLocation()
public void setAddressLocation(GeoPosition addressLocation)
{
GeoPosition old = getAddressLocation();
- this.addressLocation = addressLocation;
+ this.mAddressLocation = addressLocation;
setCenter(getTileFactory().geoToPixel(addressLocation, getZoom()));
firePropertyChange("addressLocation", old, getAddressLocation());
@@ -475,7 +469,7 @@ public void recenterToAddressLocation()
*/
public boolean isDrawTileBorders()
{
- return drawTileBorders;
+ return mDrawTileBorders;
}
/**
@@ -485,7 +479,7 @@ public boolean isDrawTileBorders()
public void setDrawTileBorders(boolean drawTileBorders)
{
boolean old = isDrawTileBorders();
- this.drawTileBorders = drawTileBorders;
+ this.mDrawTileBorders = drawTileBorders;
firePropertyChange("drawTileBorders", old, isDrawTileBorders());
repaint();
}
@@ -497,7 +491,7 @@ public void setDrawTileBorders(boolean drawTileBorders)
public void setCenterPosition(GeoPosition geoPosition)
{
GeoPosition oldVal = getCenterPosition();
- setCenter(getTileFactory().geoToPixel(geoPosition, zoomLevel));
+ setCenter(getTileFactory().geoToPixel(geoPosition, mZoomLevel));
repaint();
GeoPosition newVal = getCenterPosition();
firePropertyChange("centerPosition", oldVal, newVal);
@@ -509,7 +503,7 @@ public void setCenterPosition(GeoPosition geoPosition)
*/
public GeoPosition getCenterPosition()
{
- return getTileFactory().pixelToGeo(getCenter(), zoomLevel);
+ return getTileFactory().pixelToGeo(getCenter(), mZoomLevel);
}
/**
@@ -518,7 +512,7 @@ public GeoPosition getCenterPosition()
*/
public TileFactory getTileFactory()
{
- return factory;
+ return mTileFactory;
}
/**
@@ -530,10 +524,10 @@ public void setTileFactory(TileFactory factory)
if (factory == null)
throw new NullPointerException("factory must not be null");
- this.factory.removeTileListener(tileLoadListener);
- this.factory.dispose();
+ this.mTileFactory.removeTileListener(tileLoadListener);
+ this.mTileFactory.dispose();
- this.factory = factory;
+ this.mTileFactory = factory;
this.setZoom(factory.getInfo().getDefaultZoomLevel());
factory.addTileListener(tileLoadListener);
@@ -547,7 +541,7 @@ public void setTileFactory(TileFactory factory)
*/
public Image getLoadingImage()
{
- return loadingImage;
+ return mLoadingImage;
}
/**
@@ -556,7 +550,7 @@ public Image getLoadingImage()
*/
public void setLoadingImage(Image loadingImage)
{
- this.loadingImage = loadingImage;
+ this.mLoadingImage = loadingImage;
}
/**
@@ -565,7 +559,7 @@ public void setLoadingImage(Image loadingImage)
*/
public Point2D getCenter()
{
- return center;
+ return mCenter;
}
/**
@@ -641,8 +635,8 @@ public void setCenter(Point2D center)
}
GeoPosition oldGP = this.getCenterPosition();
- this.center = new Point2D.Double(centerX, centerY);
- firePropertyChange("center", old, this.center);
+ this.mCenter = new Point2D.Double(centerX, centerY);
+ firePropertyChange("center", old, this.mCenter);
firePropertyChange("centerPosition", oldGP, this.getCenterPosition());
repaint();
}
@@ -739,7 +733,7 @@ public void tileLoaded(Tile tile)
*/
public boolean isRestrictOutsidePanning()
{
- return restrictOutsidePanning;
+ return mRestrictOutsidePanning;
}
/**
@@ -747,7 +741,7 @@ public boolean isRestrictOutsidePanning()
*/
public void setRestrictOutsidePanning(boolean restrictOutsidePanning)
{
- this.restrictOutsidePanning = restrictOutsidePanning;
+ this.mRestrictOutsidePanning = restrictOutsidePanning;
}
/**
@@ -755,7 +749,7 @@ public void setRestrictOutsidePanning(boolean restrictOutsidePanning)
*/
public boolean isHorizontalWrapped()
{
- return horizontalWrapped;
+ return mHorizontalWrapped;
}
/**
@@ -763,7 +757,7 @@ public boolean isHorizontalWrapped()
*/
public void setHorizontalWrapped(boolean horizontalWrapped)
{
- this.horizontalWrapped = horizontalWrapped;
+ this.mHorizontalWrapped = horizontalWrapped;
}
/**
diff --git a/src/main/java/org/jdesktop/swingx/input/ZoomMouseWheelListenerCursor.java b/src/main/java/org/jdesktop/swingx/input/ZoomMouseWheelListenerCursor.java
index cda6cdabf..140e6e4b5 100644
--- a/src/main/java/org/jdesktop/swingx/input/ZoomMouseWheelListenerCursor.java
+++ b/src/main/java/org/jdesktop/swingx/input/ZoomMouseWheelListenerCursor.java
@@ -17,6 +17,7 @@
******************************************************************************/
package org.jdesktop.swingx.input;
+import io.github.dsheirer.map.MapPanel;
import org.jdesktop.swingx.JXMapViewer;
import java.awt.*;
@@ -31,32 +32,34 @@
*/
public class ZoomMouseWheelListenerCursor implements MouseWheelListener
{
- private JXMapViewer viewer;
-
+ private MapPanel mMapPanel;
+ private JXMapViewer mViewer;
+
/**
* @param viewer the jxmapviewer
*/
- public ZoomMouseWheelListenerCursor(JXMapViewer viewer)
+ public ZoomMouseWheelListenerCursor(MapPanel mapPanel)
{
- this.viewer = viewer;
+ mMapPanel = mapPanel;
+ mViewer = mMapPanel.getMapViewer();
}
@Override
public void mouseWheelMoved(MouseWheelEvent evt)
{
Point current = evt.getPoint();
- Rectangle bound = viewer.getViewportBounds();
+ Rectangle bound = mViewer.getViewportBounds();
double dx = current.x - bound.width / 2;
double dy = current.y - bound.height / 2;
- Dimension oldMapSize = viewer.getTileFactory().getMapSize(viewer.getZoom());
+ Dimension oldMapSize = mViewer.getTileFactory().getMapSize(mViewer.getZoom());
- viewer.setZoom(viewer.getZoom() + evt.getWheelRotation());
-
- Dimension mapSize = viewer.getTileFactory().getMapSize(viewer.getZoom());
+ mMapPanel.adjustZoom(evt.getWheelRotation());
+
+ Dimension mapSize = mViewer.getTileFactory().getMapSize(mViewer.getZoom());
- Point2D center = viewer.getCenter();
+ Point2D center = mViewer.getCenter();
double dzw = (mapSize.getWidth() / oldMapSize.getWidth());
double dzh = (mapSize.getHeight() / oldMapSize.getHeight());
@@ -64,6 +67,6 @@ public void mouseWheelMoved(MouseWheelEvent evt)
double x = center.getX() + dx * (dzw - 1);
double y = center.getY() + dy * (dzh - 1);
- viewer.setCenter(new Point2D.Double(x, y));
+ mViewer.setCenter(new Point2D.Double(x, y));
}
}
diff --git a/src/main/java/org/jdesktop/swingx/mapviewer/GeoPosition.java b/src/main/java/org/jdesktop/swingx/mapviewer/GeoPosition.java
index d85c8c027..f907e8ae8 100644
--- a/src/main/java/org/jdesktop/swingx/mapviewer/GeoPosition.java
+++ b/src/main/java/org/jdesktop/swingx/mapviewer/GeoPosition.java
@@ -1,3 +1,22 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2024 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
/*
* GeoPosition.java
*
@@ -33,7 +52,6 @@ public GeoPosition(double latitude, double longitude)
this.latitude = latitude;
this.longitude = longitude;
}
- // must be an array of length two containing lat then long in that order.
/**
* Creates a new instance of GeoPosition from the specified
@@ -89,6 +107,36 @@ public double getLongitude()
return longitude;
}
+ /**
+ * Formats the position as Degrees Minutes Seconds (DMS)
+ */
+ public String toDMS()
+ {
+ int latDegrees = (int)Math.floor(Math.abs(latitude));
+ double latMinutes = (Math.abs(latitude) - latDegrees) * 60.0;
+ int latMinutesInt = (int)Math.floor(latMinutes);
+ double latSeconds = (latMinutes - latMinutesInt) * 60.0;
+ int latSecondsInt = (int)latSeconds;
+
+ int lonDegrees = (int)Math.floor(Math.abs(longitude));
+ double lonMinutes = (Math.abs(longitude) - lonDegrees) * 60.0;
+ int lonMinutesInt = (int)Math.floor(lonMinutes);
+ double lonSeconds = (lonMinutes - lonMinutesInt) * 60.0;
+ int lonSecondsInt = (int)lonSeconds;
+
+ StringBuilder sb = new StringBuilder();
+ sb.append(latDegrees).append("°");
+ sb.append(latMinutesInt).append("'");
+ sb.append(latSecondsInt).append("");
+ sb.append(latitude >= 0 ? "N" : "S");
+ sb.append(", ");
+ sb.append(lonDegrees).append("°");
+ sb.append(lonMinutesInt).append("'");
+ sb.append(lonSecondsInt).append("");
+ sb.append(longitude >= 0 ? "E" : "W");
+ return sb.toString();
+ }
+
@Override
public int hashCode()
{