From a2509554db8c41cbca562dfb4451a474bc270413 Mon Sep 17 00:00:00 2001 From: Denny Sheirer Date: Fri, 26 Dec 2025 05:40:57 -0500 Subject: [PATCH] #2340 Update map plotting to address glitchy behavior when selecting entities and histories. (#2341) Co-authored-by: denny --- .../java/io/github/dsheirer/map/MapPanel.java | 71 +---------- .../dsheirer/map/PlottableEntityHistory.java | 62 ++------- .../dsheirer/map/PlottableEntityRenderer.java | 8 +- .../github/dsheirer/map/TrackGenerator.java | 119 ++++++++++------- .../dsheirer/map/TrackHistoryModel.java | 120 ++++++++++-------- .../lc/motorola/LCMotorolaUnitGPS.java | 13 -- 6 files changed, 160 insertions(+), 233 deletions(-) diff --git a/src/main/java/io/github/dsheirer/map/MapPanel.java b/src/main/java/io/github/dsheirer/map/MapPanel.java index 65eb2dcc..02ad0985 100644 --- a/src/main/java/io/github/dsheirer/map/MapPanel.java +++ b/src/main/java/io/github/dsheirer/map/MapPanel.java @@ -51,7 +51,6 @@ import javax.swing.JToggleButton; import javax.swing.SpinnerNumberModel; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; -import javax.swing.event.TableModelEvent; /** * Swing map panel. @@ -85,9 +84,9 @@ public class MapPanel extends JPanel implements IPlottableUpdateListener private JComboBox mTrackHistoryLengthComboBox; private JSpinner mMapZoomSpinner; private SpinnerNumberModel mMapZoomSpinnerModel; - private TrackHistoryModel mTrackHistoryModel = new TrackHistoryModel(); private JTable mTrackHistoryTable; private JLabel mSelectedTrackSystemLabel; + private final TrackHistoryModel EMPTY_HISTORY = new TrackHistoryModel(); /** * Constructs an instance @@ -138,6 +137,7 @@ public class MapPanel extends JPanel implements IPlottableUpdateListener cp.add(new JScrollPane(getTrackHistoryTable()), "span 3,wrap"); +//Uncomment this line to add a debug track generator button to the display // cp.add(getTrackGeneratorToggle(), "span 3,wrap"); JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); @@ -149,10 +149,10 @@ public class MapPanel extends JPanel implements IPlottableUpdateListener private void setSelected(PlottableEntityHistory selected) { - mTrackHistoryModel.load(selected); - if(selected != null) { + getTrackHistoryTable().setModel(selected.getTrackHistoryModel()); + Identifier system = selected.getIdentifierCollection().getIdentifier(IdentifierClass.CONFIGURATION, Form.SYSTEM, Role.ANY); if(system != null) @@ -171,6 +171,7 @@ public class MapPanel extends JPanel implements IPlottableUpdateListener } else { + getTrackHistoryTable().setModel(EMPTY_HISTORY); getSelectedTrackSystemLabel().setText(SELECT_A_TRACK); } @@ -217,13 +218,13 @@ public class MapPanel extends JPanel implements IPlottableUpdateListener { if(mTrackHistoryTable == null) { - mTrackHistoryTable = new JTable(mTrackHistoryModel); + mTrackHistoryTable = new JTable(EMPTY_HISTORY); mTrackHistoryTable.getSelectionModel().addListSelectionListener(e -> { if(getCenterOnSelectedCheckBox().isSelected()) { int modelIndex = getTrackHistoryTable().convertRowIndexToModel(getTrackHistoryTable().getSelectedRow()); - TimestampedGeoPosition geo = mTrackHistoryModel.get(modelIndex); + TimestampedGeoPosition geo = ((TrackHistoryModel)(getTrackHistoryTable().getModel())).get(modelIndex); if(geo != null) { @@ -469,64 +470,6 @@ public class MapPanel extends JPanel implements IPlottableUpdateListener { mPlottedTracksTable = new JTable(mMapService.getPlottableEntityModel()); mPlottedTracksTable.setAutoCreateRowSorter(true); - 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 -> diff --git a/src/main/java/io/github/dsheirer/map/PlottableEntityHistory.java b/src/main/java/io/github/dsheirer/map/PlottableEntityHistory.java index c25e4f68..2b86493a 100644 --- a/src/main/java/io/github/dsheirer/map/PlottableEntityHistory.java +++ b/src/main/java/io/github/dsheirer/map/PlottableEntityHistory.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * Copyright (C) 2014-2025 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 @@ -22,19 +22,15 @@ package io.github.dsheirer.map; import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.IdentifierCollection; import io.github.dsheirer.module.decode.event.PlottableDecodeEvent; -import java.util.ArrayList; -import java.util.List; -import org.jdesktop.swingx.mapviewer.GeoPosition; /** * Plottable entity history with location history. */ public class PlottableEntityHistory { - public static final int MAX_LOCATION_HISTORY = 10; - private List mLocationHistory = new ArrayList<>(); + private final TrackHistoryModel mTrackHistoryModel = new TrackHistoryModel(); private PlottableDecodeEvent mCurrentEvent; - private Identifier mIdentifier; + private final Identifier mIdentifier; /** * Constructs a plottable entity history @@ -42,28 +38,24 @@ public class PlottableEntityHistory public PlottableEntityHistory(Identifier identifier, PlottableDecodeEvent event) { mIdentifier = identifier; + mCurrentEvent = event; add(event); } /** * Location history for this entity */ - public List getLocationHistory() + public TrackHistoryModel getTrackHistoryModel() { - return new ArrayList<>(mLocationHistory); + return mTrackHistoryModel; } /** - * Latest position for this entity. + * Latest position for this history */ public TimestampedGeoPosition getLatestPosition() { - if(mLocationHistory.size() > 0) - { - return mLocationHistory.get(0); - } - - return null; + return mTrackHistoryModel.get(0); } /** @@ -89,43 +81,9 @@ public class PlottableEntityHistory { if(event.isValidLocation()) { - TimestampedGeoPosition mostRecentPosition = getLatestPosition(); - TimestampedGeoPosition latest = new TimestampedGeoPosition(event.getLocation(), event.getTimeStart()); - - if(isUnique(latest, mostRecentPosition)) - { - mCurrentEvent = event; - mLocationHistory.add(0, latest); - - while(mLocationHistory.size() > MAX_LOCATION_HISTORY) - { - mLocationHistory.remove(mLocationHistory.size() - 1); - } - } - } - } - - /** - * Indicates if the latest time and position is at least 30 seconds newer than the previous position and either the - * latitude or longitude differs by at least 0.00001 degrees. - * @param latest location - * @param previous location - * @return indication of uniqueness. - */ - private boolean isUnique(TimestampedGeoPosition latest, TimestampedGeoPosition previous) - { - if(latest != null && previous == null) - { - return true; + mTrackHistoryModel.add(new TimestampedGeoPosition(event.getLocation(), event.getTimeStart())); } - if(latest != null && previous != null) - { - return latest.getTimestamp() > (previous.getTimestamp() + 30_000) || - Math.abs(latest.getLatitude() - previous.getLatitude()) > 0.00001 || - Math.abs(latest.getLongitude() - previous.getLongitude()) > 0.00001; - } - - return false; + mCurrentEvent = event; } } diff --git a/src/main/java/io/github/dsheirer/map/PlottableEntityRenderer.java b/src/main/java/io/github/dsheirer/map/PlottableEntityRenderer.java index 33802293..b77e1297 100644 --- a/src/main/java/io/github/dsheirer/map/PlottableEntityRenderer.java +++ b/src/main/java/io/github/dsheirer/map/PlottableEntityRenderer.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * Copyright (C) 2014-2025 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 @@ -80,9 +80,9 @@ public class PlottableEntityRenderer */ public void paintPlottableEntity(Graphics2D g, JXMapViewer viewer, PlottableEntityHistory entity, boolean antiAliasing) { - List locationHistory = entity.getLocationHistory(); + List locationHistory = entity.getTrackHistoryModel().getTrackHistory(); - if(!locationHistory.isEmpty() && locationHistory.get(locationHistory.size() - 1).isValid()) + if(!locationHistory.isEmpty() && locationHistory.getLast().isValid()) { Graphics2D graphics = (Graphics2D)g.create(); @@ -171,7 +171,7 @@ public class PlottableEntityRenderer */ private void paintRoute(Graphics2D graphics, JXMapViewer viewer, PlottableEntityHistory entity, Color color) { - List locations = entity.getLocationHistory(); + List locations = entity.getTrackHistoryModel().getTrackHistory(); if(!locations.isEmpty()) { diff --git a/src/main/java/io/github/dsheirer/map/TrackGenerator.java b/src/main/java/io/github/dsheirer/map/TrackGenerator.java index 3fc8196e..81c1ebd7 100644 --- a/src/main/java/io/github/dsheirer/map/TrackGenerator.java +++ b/src/main/java/io/github/dsheirer/map/TrackGenerator.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * Copyright (C) 2014-2025 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 @@ -28,6 +28,7 @@ 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.module.decode.p25.identifier.radio.APCO25RadioIdentifier; import io.github.dsheirer.protocol.Protocol; import io.github.dsheirer.util.ThreadPool; import java.util.ArrayList; @@ -44,13 +45,9 @@ import org.jdesktop.swingx.mapviewer.GeoPosition; */ 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; + private static final GeoPosition DEFAULT_START_POSITION = new GeoPosition(43.048, -76.147); + private final MapService mMapService; + private final List mTrackElementGenerators = new ArrayList<>(); /** * Constructs an instance to publish tracks to the specified map service @@ -60,19 +57,27 @@ public class TrackGenerator { 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++) + int trackCount = 25; + for(int x = 0; x < trackCount; x++) { - MutableIdentifierCollection mic = new MutableIdentifierCollection(base.getIdentifiers()); - mic.update(DMRRadio.createFrom(x + 1)); - double speedKPH = mBaseSpeedKPH + (random.nextDouble() * 15); + MutableIdentifierCollection mic = new MutableIdentifierCollection(); + mic.update(SystemConfigurationIdentifier.create("Test System")); + mic.update(SiteConfigurationIdentifier.create("Test Site")); + mic.update(FrequencyConfigurationIdentifier.create(155000000l)); + if(x % 2 == 1) + { + mic.update(AliasListConfigurationIdentifier.create("DMR " + x + " LIST")); + mic.update(DMRRadio.createFrom(x + 1)); + } + else + { + mic.update(AliasListConfigurationIdentifier.create("P25 " + x + " LIST")); + mic.update(APCO25RadioIdentifier.createFrom(x + 1)); + } + double baseSpeedKPH = 20.0; + double speedKPH = baseSpeedKPH + random.nextDouble(15); mTrackElementGenerators.add(new TrackElementGenerator(speedKPH, mic)); } } @@ -82,9 +87,9 @@ public class TrackGenerator */ public void start() { - if(mGeneratorFuture == null) + for(TrackElementGenerator generator: mTrackElementGenerators) { - mGeneratorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(() -> update(), 0, 1, TimeUnit.SECONDS); + generator.start(); } } @@ -93,59 +98,81 @@ public class TrackGenerator */ public void stop() { - if(mGeneratorFuture != null) + for(TrackElementGenerator generator: mTrackElementGenerators) { - 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()); + generator.stop(); } } /** * Test track element generator */ - public static class TrackElementGenerator + public 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 GeoPosition mPosition; private Random mRandom = new Random(); private double mHeading = 360.0 * mRandom.nextDouble(); + private ScheduledFuture mGeneratorFuture; /** * Constructs an instance - * @param trackId for the track * @param speedKPH speed in KPH + * @param identifierCollection for the track */ public TrackElementGenerator(double speedKPH, IdentifierCollection identifierCollection) { mSpeedKPH = speedKPH; mIdentifierCollection = identifierCollection; + double latOffset = (1.0 - (mRandom.nextFloat() * 2)) / 800; + double lonOffset = (1.0 - (mRandom.nextFloat() * 2)) / 800; + mPosition = new GeoPosition(DEFAULT_START_POSITION.getLatitude() + latOffset, + DEFAULT_START_POSITION.getLongitude() + lonOffset); } /** - * - * @param heading 0-360 degrees + * Starts this track generator */ - public PlottableDecodeEvent update() + public void start() { - mHeading = mHeading + (15.0 - (mRandom.nextDouble() * 30.0)); - mHeading = Math.max(mHeading, 0); - mHeading = Math.min(mHeading, 360.0); + if(mGeneratorFuture == null) + { + long intervalMS = 500 + (mRandom.nextLong(1000)); + mSpeedKPH /= (intervalMS / 1000.0); + System.out.println("Speed: " + mSpeedKPH); + mGeneratorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(this::update, 0, intervalMS, TimeUnit.MILLISECONDS); + } + } + + /** + * Stops this track generator + */ + public void stop() + { + if(mGeneratorFuture != null) + { + mGeneratorFuture.cancel(true); + mGeneratorFuture = null; + } + } + + /** + * Pushes a new update to the map service. + */ + public void update() + { + mHeading = mHeading + (30.0 - (mRandom.nextDouble(60))); + if(mHeading < 0) + { + mHeading += 360; + } + else if(mHeading >= 360) + { + mHeading -= 360; + } double distanceKM = mSpeedKPH * ONE_SECOND; double headingRadians = Math.toRadians(mHeading); @@ -168,7 +195,7 @@ public class TrackGenerator .protocol(Protocol.DMR) .identifiers(mIdentifierCollection) .build(); - return event; + mMapService.receive(event); } } } diff --git a/src/main/java/io/github/dsheirer/map/TrackHistoryModel.java b/src/main/java/io/github/dsheirer/map/TrackHistoryModel.java index c1283f6a..28eba1d3 100644 --- a/src/main/java/io/github/dsheirer/map/TrackHistoryModel.java +++ b/src/main/java/io/github/dsheirer/map/TrackHistoryModel.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer + * Copyright (C) 2014-2025 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 @@ -28,15 +28,15 @@ import java.util.List; import javax.swing.table.AbstractTableModel; /** - * Table model for track history. + * Track history table model. */ public class TrackHistoryModel extends AbstractTableModel { + public static final int MAX_LOCATION_HISTORY = 10; private static final String[] COLUMNS = new String[]{"Time", "Latitude", "Longitude"}; - private SimpleDateFormat mSimpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - private DecimalFormat mDegreeFormat = new DecimalFormat("0.00000"); - private List mTimestampedGeoPositions = new ArrayList<>(); - private PlottableEntityHistory mPlottableEntityHistory; + private final SimpleDateFormat mSimpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + private final DecimalFormat mDegreeFormat = new DecimalFormat("0.00000"); + private final List mTimestampedGeoPositions = new ArrayList<>(); /** * Constructs an instance @@ -45,6 +45,65 @@ public class TrackHistoryModel extends AbstractTableModel { } + /** + * Complete history for this entity. + */ + public List getTrackHistory() + { + return Collections.unmodifiableList(mTimestampedGeoPositions); + } + + /** + * Adds the position to the history if it is unique and trims the history to MAX_LOCATION_HISTORY size. + * @param latest position to add. + */ + public void add(TimestampedGeoPosition latest) + { + TimestampedGeoPosition mostRecentPosition = null; + + if(!mTimestampedGeoPositions.isEmpty()) + { + mostRecentPosition = mTimestampedGeoPositions.getFirst(); + } + + if(isUnique(latest, mostRecentPosition)) + { + mTimestampedGeoPositions.addFirst(latest); + fireTableRowsInserted(0, 0); + + while(mTimestampedGeoPositions.size() > MAX_LOCATION_HISTORY) + { + int index = mTimestampedGeoPositions.size() - 1; + mTimestampedGeoPositions.removeLast(); + fireTableRowsDeleted(index, index); + } + } + } + + /** + * Indicates if the latest time and position is at least 30 seconds newer than the previous position and either the + * latitude or longitude differs by at least 0.00001 degrees. + * @param latest location + * @param previous location + * @return indication of uniqueness. + */ + private boolean isUnique(TimestampedGeoPosition latest, TimestampedGeoPosition previous) + { + if(latest != null && previous == null) + { + return true; + } + + if(latest != null) + { + return latest.getTimestamp() > (previous.getTimestamp() + 2_000) || + Math.abs(latest.getLatitude() - previous.getLatitude()) > 0.00001 || + Math.abs(latest.getLongitude() - previous.getLongitude()) > 0.00001; + } + + return false; + } + /** * Get the geo position at the specified index * @param index to retrieve @@ -52,7 +111,7 @@ public class TrackHistoryModel extends AbstractTableModel */ public TimestampedGeoPosition get(int index) { - if(index < mTimestampedGeoPositions.size()) + if(index >= 0 && index < mTimestampedGeoPositions.size()) { return mTimestampedGeoPositions.get(index); } @@ -60,53 +119,6 @@ public class TrackHistoryModel extends AbstractTableModel 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() { diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/motorola/LCMotorolaUnitGPS.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/motorola/LCMotorolaUnitGPS.java index 8c0da7b6..896fc96d 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/motorola/LCMotorolaUnitGPS.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/motorola/LCMotorolaUnitGPS.java @@ -19,7 +19,6 @@ package io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola; -import io.github.dsheirer.bits.BinaryMessage; import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.bits.IntField; import io.github.dsheirer.identifier.Identifier; @@ -123,16 +122,4 @@ public class LCMotorolaUnitGPS extends LinkControlWord return mIdentifiers; } - - static void main() - { - BinaryMessage mick = BinaryMessage.load("000001101001000000000000101100011010110101010011011000101001000001000010"); - BinaryMessage newj = BinaryMessage.load("000001101001000000000000001110010000011010111110110010110011100011000010"); - - LCMotorolaUnitGPS motoMick = new LCMotorolaUnitGPS(new CorrectedBinaryMessage(mick)); - LCMotorolaUnitGPS motoNewJ = new LCMotorolaUnitGPS(new CorrectedBinaryMessage(newj)); - - System.out.println("MICK: " + motoMick); - System.out.println("NEWJ: " + motoNewJ); - } }