#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());
}
}