#1849 Map enhancements. Adds track list, zoom to selection, history trail size and other features. (#1920)

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
This commit is contained in:
Denny Sheirer
2024-05-19 07:06:44 -04:00
committed by GitHub
parent c50a914632
commit dddda052fa
14 changed files with 1566 additions and 241 deletions

View File

@@ -222,7 +222,7 @@ public class SDRTrunk implements Listener<TunerEvent>
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);

View File

@@ -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 <http://www.gnu.org/licenses/>
*
******************************************************************************/
* ****************************************************************************
*/
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.DefaultTileFactory;
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<Integer> 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 class MapPanel extends JPanel implements IPlottableUpdateListener
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);
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");
/**
* Defines how many threads will be used to fetch the background map
* tiles (graphics)
*/
tileFactory.setThreadPoolSize(8);
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");
/**
* Set initial location and zoom for the map upon display
*/
GeoPosition syracuse = new GeoPosition(43.048, -76.147);
int zoom = 7;
cp.add(getDeleteTrackButton());
cp.add(getDeleteAllTracksButton());
cp.add(getClearMapButton(), "wrap");
MapViewSetting view = mSettingsManager.getMapViewSetting("Default", syracuse, zoom);
cp.add(getReplotAllTracksButton());
cp.add(getFollowButton());
cp.add(getFollowedEntityLabel(), "wrap");
mMapViewer.setAddressLocation(view.getGeoPosition());
mMapViewer.setZoom(view.getZoom());
cp.add(getTrackHistoryLengthComboBox());
cp.add(new JLabel("Track History Length"), "span 2, wrap");
/**
* Add a mouse adapter for panning and scrolling
*/
MapMouseListener listener = new MapMouseListener(mMapViewer, mSettingsManager);
mMapViewer.addMouseListener(listener);
mMapViewer.addMouseMotionListener(listener);
cp.add(new JLabel("Selected System:"), "align right");
cp.add(getSelectedTrackSystemLabel(), "span 2, wrap");
/* Map zoom listener */
mMapViewer.addMouseWheelListener(new ZoomMouseWheelListenerCursor(mMapViewer));
cp.add(new JScrollPane(getTrackHistoryTable()), "span 3,wrap");
/* Keyboard panning listener */
mMapViewer.addKeyListener(new PanKeyListener(mMapViewer));
// cp.add(getTrackGeneratorToggle(), "span 3,wrap");
/**
* Add a selection listener
*/
SelectionAdapter sa = new SelectionAdapter(mMapViewer);
mMapViewer.addMouseListener(sa);
mMapViewer.addMouseMotionListener(sa);
JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
splitPane.setDividerLocation(300);
splitPane.add(cp);
splitPane.add(getMapViewer());
add(splitPane, "span");
}
/**
* Map component
*/
add(mMapViewer, "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<Integer> getTrackHistoryLengthComboBox()
{
if(mTrackHistoryLengthComboBox == null)
{
List<Integer> 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<PlottableEntityHistory> 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

View File

@@ -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 <http://www.gnu.org/licenses/>
* *****************************************************************************
* ****************************************************************************
*/
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<IDecodeEvent>
{
private final static Logger mLog = LoggerFactory.getLogger(MapService.class);
private int mMaxHistory = 2;
private List<IPlottableUpdateListener> mListeners = new ArrayList<IPlottableUpdateListener>();
private Map<Identifier,PlottableEntityHistory> mEntityHistories = new HashMap<>();
private IconModel mIconModel;
private PlottableEntityModel mPlottableEntityModel;
public MapService(IconModel resourceManager)
/**
* Constructs an instance
* @param aliasModel to lookup aliases
* @param iconModel to lookup icons from entity aliases.
*/
public MapService(AliasModel aliasModel, IconModel iconModel)
{
mIconModel = resourceManager;
mPlottableEntityModel = new PlottableEntityModel(aliasModel);
mIconModel = iconModel;
}
/**
* Table model that holds the plottable entity history
*/
public PlottableEntityModel getPlottableEntityModel()
{
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);
}
}

View File

@@ -31,7 +31,8 @@ import org.jdesktop.swingx.mapviewer.GeoPosition;
*/
public class PlottableEntityHistory
{
private List<GeoPosition> mLocationHistory = new ArrayList<>();
public static final int MAX_LOCATION_HISTORY = 10;
private List<TimestampedGeoPosition> mLocationHistory = new ArrayList<>();
private PlottableDecodeEvent mCurrentEvent;
private Identifier mIdentifier;
@@ -47,11 +48,24 @@ public class PlottableEntityHistory
/**
* Location history for this entity
*/
public List<GeoPosition> getLocationHistory()
public List<TimestampedGeoPosition> 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 class PlottableEntityHistory
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);
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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<PlottableDecodeEvent>
{
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<String,PlottableEntityHistory> mEntityHistoryMap = new HashMap();
private List<PlottableEntityHistory> mEntityHistories = new ArrayList<>();
private List<IPlottableUpdateListener> 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<PlottableEntityHistory> 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<PlottableEntityHistory> 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<Alias> 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);
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* *****************************************************************************
* ****************************************************************************
*/
package io.github.dsheirer.map;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.icon.IconModel;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
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.Set;
/**
* Paints plottable entities to the map.
*/
public class PlottableEntityPainter extends AbstractPainter<JXMapViewer>
{
private PlottableEntityRenderer mRenderer;
private Set<PlottableEntityHistory> 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 class PlottableEntityPainter extends AbstractPainter<JXMapViewer>
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)
{
mEntities.add(entity);
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)
{
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<PlottableEntityHistory> 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<PlottableEntityHistory> getEntities()
/**
* Clears teh specified entities from the map
* @param toDelete to delete
*/
public void clearEntities(List<PlottableEntityHistory> toDelete)
{
return Collections.unmodifiableSet(mEntities);
mEntities.removeAll(toDelete);
}
@Override
@@ -70,9 +129,7 @@ public class PlottableEntityPainter extends AbstractPainter<JXMapViewer>
g.translate(-viewportBounds.getX(), -viewportBounds.getY());
Set<PlottableEntityHistory> entities = getEntities();
for(PlottableEntityHistory entity : entities)
for(PlottableEntityHistory entity : mEntities)
{
mRenderer.paintPlottableEntity(g, map, entity, true);
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* *****************************************************************************
* ****************************************************************************
*/
package io.github.dsheirer.map;
@@ -23,10 +22,6 @@ import io.github.dsheirer.alias.Alias;
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.RenderingHints;
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<GeoPosition> locationHistory = entity.getLocationHistory();
List<TimestampedGeoPosition> locationHistory = entity.getLocationHistory();
if(!locationHistory.isEmpty() && locationHistory.get(locationHistory.size() - 1).isValid())
{
@@ -66,7 +98,7 @@ public class PlottableEntityRenderer
/**
* 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 class PlottableEntityRenderer
* 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 @@ public class PlottableEntityRenderer
*/
private void paintRoute(Graphics2D graphics, JXMapViewer viewer, PlottableEntityHistory entity, Color color)
{
List<GeoPosition> locations = entity.getLocationHistory();
List<TimestampedGeoPosition> locations = entity.getLocationHistory();
if(!locations.isEmpty())
{
@@ -160,21 +192,26 @@ public class PlottableEntityRenderer
/**
* Draws a route from a list of plottables
*/
private void drawRoute(List<GeoPosition> locations, Graphics2D g, JXMapViewer viewer)
private void drawRoute(List<TimestampedGeoPosition> 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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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<TrackElementGenerator> 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;
}
}
}

View File

@@ -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<TimestampedGeoPosition> 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<TimestampedGeoPosition> 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;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* * *****************************************************************************
* 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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.p25.phase2.timeslot;
@@ -63,9 +60,6 @@ public class LinearFeedbackShiftRegister
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 class LinearFeedbackShiftRegister
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 class LinearFeedbackShiftRegister
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 @@ public class LinearFeedbackShiftRegister
{
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());
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* * *****************************************************************************
* 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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
/*
@@ -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.Logger;
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 <I>map coordinates</I> 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 class JXMapViewer extends JPanel implements DesignMode
}
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 @@ public class JXMapViewer extends JPanel implements DesignMode
@Override
public void setDesignTime(boolean b)
{
this.designTime = b;
this.mDesignTime = b;
}
/**
@@ -213,7 +207,7 @@ public class JXMapViewer extends JPanel implements DesignMode
@Override
public boolean isDesignTime()
{
return designTime;
return mDesignTime;
}
/**
@@ -313,9 +307,9 @@ public class JXMapViewer extends JPanel implements DesignMode
@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 @@ public class JXMapViewer extends JPanel implements DesignMode
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 class JXMapViewer extends JPanel implements DesignMode
*/
public Painter<? super JXMapViewer> getOverlayPainter()
{
return overlay;
return mOverlay;
}
/**
@@ -399,7 +393,7 @@ public class JXMapViewer extends JPanel implements DesignMode
*/
public void setZoom(int zoom)
{
if (zoom == this.zoomLevel)
if (zoom == this.mZoomLevel)
{
return;
}
@@ -412,10 +406,10 @@ public class JXMapViewer extends JPanel implements DesignMode
}
// 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 class JXMapViewer extends JPanel implements DesignMode
*/
public int getZoom()
{
return this.zoomLevel;
return this.mZoomLevel;
}
/**
@@ -442,7 +436,7 @@ public class JXMapViewer extends JPanel implements DesignMode
*/
public GeoPosition getAddressLocation()
{
return addressLocation;
return mAddressLocation;
}
/**
@@ -452,7 +446,7 @@ public class JXMapViewer extends JPanel implements DesignMode
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 class JXMapViewer extends JPanel implements DesignMode
*/
public boolean isDrawTileBorders()
{
return drawTileBorders;
return mDrawTileBorders;
}
/**
@@ -485,7 +479,7 @@ public class JXMapViewer extends JPanel implements DesignMode
public void setDrawTileBorders(boolean drawTileBorders)
{
boolean old = isDrawTileBorders();
this.drawTileBorders = drawTileBorders;
this.mDrawTileBorders = drawTileBorders;
firePropertyChange("drawTileBorders", old, isDrawTileBorders());
repaint();
}
@@ -497,7 +491,7 @@ public class JXMapViewer extends JPanel implements DesignMode
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 class JXMapViewer extends JPanel implements DesignMode
*/
public GeoPosition getCenterPosition()
{
return getTileFactory().pixelToGeo(getCenter(), zoomLevel);
return getTileFactory().pixelToGeo(getCenter(), mZoomLevel);
}
/**
@@ -518,7 +512,7 @@ public class JXMapViewer extends JPanel implements DesignMode
*/
public TileFactory getTileFactory()
{
return factory;
return mTileFactory;
}
/**
@@ -530,10 +524,10 @@ public class JXMapViewer extends JPanel implements DesignMode
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 class JXMapViewer extends JPanel implements DesignMode
*/
public Image getLoadingImage()
{
return loadingImage;
return mLoadingImage;
}
/**
@@ -556,7 +550,7 @@ public class JXMapViewer extends JPanel implements DesignMode
*/
public void setLoadingImage(Image loadingImage)
{
this.loadingImage = loadingImage;
this.mLoadingImage = loadingImage;
}
/**
@@ -565,7 +559,7 @@ public class JXMapViewer extends JPanel implements DesignMode
*/
public Point2D getCenter()
{
return center;
return mCenter;
}
/**
@@ -641,8 +635,8 @@ public class JXMapViewer extends JPanel implements DesignMode
}
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 class JXMapViewer extends JPanel implements DesignMode
*/
public boolean isRestrictOutsidePanning()
{
return restrictOutsidePanning;
return mRestrictOutsidePanning;
}
/**
@@ -747,7 +741,7 @@ public class JXMapViewer extends JPanel implements DesignMode
*/
public void setRestrictOutsidePanning(boolean restrictOutsidePanning)
{
this.restrictOutsidePanning = restrictOutsidePanning;
this.mRestrictOutsidePanning = restrictOutsidePanning;
}
/**
@@ -755,7 +749,7 @@ public class JXMapViewer extends JPanel implements DesignMode
*/
public boolean isHorizontalWrapped()
{
return horizontalWrapped;
return mHorizontalWrapped;
}
/**
@@ -763,7 +757,7 @@ public class JXMapViewer extends JPanel implements DesignMode
*/
public void setHorizontalWrapped(boolean horizontalWrapped)
{
this.horizontalWrapped = horizontalWrapped;
this.mHorizontalWrapped = horizontalWrapped;
}
/**

View File

@@ -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 @@ import java.awt.geom.Point2D;
*/
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());
Point2D center = viewer.getCenter();
Dimension mapSize = mViewer.getTileFactory().getMapSize(mViewer.getZoom());
Point2D center = mViewer.getCenter();
double dzw = (mapSize.getWidth() / oldMapSize.getWidth());
double dzh = (mapSize.getHeight() / oldMapSize.getHeight());
@@ -64,6 +67,6 @@ public class ZoomMouseWheelListenerCursor implements MouseWheelListener
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));
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
/*
* GeoPosition.java
*
@@ -33,7 +52,6 @@ public class GeoPosition
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 class GeoPosition
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()
{