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