From 88b034207a64c052c891a2507c045492206695ec Mon Sep 17 00:00:00 2001 From: Douglas Gillespie <50671166+douggillespie@users.noreply.github.com> Date: Mon, 8 May 2023 15:44:26 +0100 Subject: [PATCH 1/8] Merge pull request #42 from PAMGuard/main (#106) From bf5f0daca547f33acc228596168bb87ac25639fe Mon Sep 17 00:00:00 2001 From: Douglas Gillespie <50671166+douggillespie@users.noreply.github.com> Date: Fri, 12 May 2023 10:03:30 +0100 Subject: [PATCH 2/8] NI Playback bug Fix bug in NI playback which threw an exception if no devices were present and configured in NI-MAX --- src/nidaqdev/NIFilePlayback.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/nidaqdev/NIFilePlayback.java b/src/nidaqdev/NIFilePlayback.java index f87a35ef..3b8b42ed 100644 --- a/src/nidaqdev/NIFilePlayback.java +++ b/src/nidaqdev/NIFilePlayback.java @@ -127,7 +127,8 @@ public class NIFilePlayback implements FilePlaybackDevice, PamSettings { @Override public boolean preparePlayback(PlaybackParameters playbackParameters) { - if (niDeviceLUT == null || niDeviceLUT.length <= playbackParameters.deviceNumber) { + if (niDeviceLUT == null || niDeviceLUT.length <= playbackParameters.deviceNumber + || playbackParameters.deviceNumber < 0) { return false; } int bn = niDeviceLUT[playbackParameters.deviceNumber]; From db037c6d2936148e0b4fcbba79e1d9f79a3ad51f Mon Sep 17 00:00:00 2001 From: Douglas Gillespie <50671166+douggillespie@users.noreply.github.com> Date: Fri, 12 May 2023 15:37:21 +0100 Subject: [PATCH 3/8] Fixing SUD order --- src/Acquisition/FolderInputSystem.java | 12 ++++++++++++ src/PamController/InputStoreInfo.java | 21 +++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/Acquisition/FolderInputSystem.java b/src/Acquisition/FolderInputSystem.java index 731c3439..3eed96ec 100644 --- a/src/Acquisition/FolderInputSystem.java +++ b/src/Acquisition/FolderInputSystem.java @@ -861,7 +861,19 @@ public class FolderInputSystem extends FileInputSystem implements PamSettings, D long[] allFileStarts = new long[allFiles.size()]; for (int i = 0; i < allFiles.size(); i++) { allFileStarts[i] = getFileStartTime(allFiles.get(i).getAbsoluteFile()); + if (allFileStarts[i] < firstFileStart) { +// System.out.printf("Swap first file from %s to %s\n", firstFile.getName(), allFiles.get(i).getName()); + firstFile = allFiles.get(i); + firstFileStart = allFileStarts[i]; + } + if (allFileStarts[i] > lastFileEnd) { +// System.out.printf("Swap last file from %s to %s\n", lastFile.getName(), allFiles.get(i).getName()); + lastFile = allFiles.get(i); + lastFileEnd = allFileStarts[i] + (long) (lastFile.getDurationInSeconds()*1000.); + } } + storeInfo.setFirstFileStart(firstFileStart); // just incase changed. + storeInfo.setLastFileEnd(lastFileEnd); // just incase changed storeInfo.setFileStartTimes(allFileStarts); } return storeInfo; diff --git a/src/PamController/InputStoreInfo.java b/src/PamController/InputStoreInfo.java index 265ed9d2..cde04c8c 100644 --- a/src/PamController/InputStoreInfo.java +++ b/src/PamController/InputStoreInfo.java @@ -75,6 +75,27 @@ public class InputStoreInfo { public long[] getFileStartTimes() { return fileStartTimes; } + + /** + * @param firstFileStart the firstFileStart to set + */ + public void setFirstFileStart(long firstFileStart) { + this.firstFileStart = firstFileStart; + } + + /** + * @param lastFileStart the lastFileStart to set + */ + public void setLastFileStart(long lastFileStart) { + this.lastFileStart = lastFileStart; + } + + /** + * @param lastFileEnd the lastFileEnd to set + */ + public void setLastFileEnd(long lastFileEnd) { + this.lastFileEnd = lastFileEnd; + } From c17208d315585fddfa0f62392435ceeca61b6c59 Mon Sep 17 00:00:00 2001 From: Douglas Gillespie <50671166+douggillespie@users.noreply.github.com> Date: Tue, 23 May 2023 15:45:44 +0100 Subject: [PATCH 4/8] Map file errors Trap errors in corrupt map files and deal with a null array error in analogue sensors readout. --- dependency-reduced-pom.xml | 2 +- pom.xml | 2 +- src/Map/GebcoMapFile.java | 26 +++++++++++++++++-- src/PamController/PamSettingManager.java | 12 ++++++--- src/PamController/PamguardVersionInfo.java | 4 +-- src/PamView/dialog/warn/WarnOnce.java | 1 + src/PamView/dialog/warn/WarnOnceDialog.java | 7 ++++- .../AnalogArraySensorDataUnit.java | 2 +- .../swing/AnalogDiagnosticsDisplay.java | 3 ++- src/modbustcp/brainbox/BBED549.java | 2 +- 10 files changed, 48 insertions(+), 13 deletions(-) diff --git a/dependency-reduced-pom.xml b/dependency-reduced-pom.xml index c62a41fe..a0157ae4 100644 --- a/dependency-reduced-pom.xml +++ b/dependency-reduced-pom.xml @@ -4,7 +4,7 @@ org.pamguard Pamguard Pamguard Java12+ - 2.02.07ab + 2.02.08 Pamguard for Java 12+, using Maven to control dependcies www.pamguard.org diff --git a/pom.xml b/pom.xml index d91d13ce..41f6aa34 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 org.pamguard Pamguard - 2.02.07ab + 2.02.08 Pamguard Java12+ Pamguard for Java 12+, using Maven to control dependcies www.pamguard.org diff --git a/src/Map/GebcoMapFile.java b/src/Map/GebcoMapFile.java index f33474db..befe7d3a 100644 --- a/src/Map/GebcoMapFile.java +++ b/src/Map/GebcoMapFile.java @@ -11,8 +11,10 @@ import java.util.Vector; import javax.swing.JFileChooser; import javax.swing.JOptionPane; +import PamController.PamController; import PamUtils.LatLong; import PamUtils.PamFileChooser; +import PamView.dialog.warn.WarnOnce; public class GebcoMapFile implements MapFileManager { @@ -109,10 +111,19 @@ public class GebcoMapFile implements MapFileManager { int pointCount; int depth; MapContour mapContour; + boolean error = false; try { + int iLine = 0; while((line = reader.readLine())!=null){ + iLine++; line = line.trim(); spaceIndex = line.indexOf(' '); + if (spaceIndex < 0) { + String msg = String.format("Error in map file at line %d \"%s\"", iLine, line); + WarnOnce.showNamedWarning("Gebco Map File Warning", PamController.getMainFrame(), gebcoFile.getName(), msg, WarnOnce.WARNING_MESSAGE); + error = true; + break; + } num1 = line.substring(0,spaceIndex).trim(); num2 = line.substring(spaceIndex).trim(); depth = Integer.valueOf(num1); @@ -143,11 +154,22 @@ public class GebcoMapFile implements MapFileManager { } catch (NumberFormatException nex) { nex.printStackTrace(); - return false; + error = true; + } + catch (IndexOutOfBoundsException iex) { + iex.printStackTrace(); + error = true; + } + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + e.printStackTrace(); + } } Collections.sort(mapContours); Collections.sort(availableContours); - return true; + return !error; } diff --git a/src/PamController/PamSettingManager.java b/src/PamController/PamSettingManager.java index 42488e1d..d101fad7 100644 --- a/src/PamController/PamSettingManager.java +++ b/src/PamController/PamSettingManager.java @@ -519,9 +519,15 @@ public class PamSettingManager { if (initializationComplete == false) { // if PAMGAURD hasn't finished loading, then don't save the settings - // or the file will get wrecked (bug tracker 2269579) - System.out.println("Settings have not yet loaded. Don't save file"); - return false; + // or the file will get wrecked (bug tracker 2269579) + String msg = "There was an error loading settings from this configuration, so the configuration" + + " may be incomplete.

Do you want to save anyway ?

" + + " If you have added new modules, the answer is probably \"Yes\""; + int ans = WarnOnce.showWarning("Confuguration file warning", msg, WarnOnce.YES_NO_OPTION); + if (ans == WarnOnce.CANCEL_OPTION) { + System.out.println("Settings have not yet loaded. Don't save file"); + return false; + } } saveGlobalSettings(); diff --git a/src/PamController/PamguardVersionInfo.java b/src/PamController/PamguardVersionInfo.java index 791c56a3..b83f6404 100644 --- a/src/PamController/PamguardVersionInfo.java +++ b/src/PamController/PamguardVersionInfo.java @@ -31,12 +31,12 @@ public class PamguardVersionInfo { * Version number, major version.minorversion.sub-release. * Note: can't go higher than sub-release 'f' */ - static public final String version = "2.02.07f"; + static public final String version = "2.02.08"; /** * Release date */ - static public final String date = "4 April 2023"; + static public final String date = "9 May 2023"; // /** // * Release type - Beta or Core diff --git a/src/PamView/dialog/warn/WarnOnce.java b/src/PamView/dialog/warn/WarnOnce.java index bee48997..93ed4618 100644 --- a/src/PamView/dialog/warn/WarnOnce.java +++ b/src/PamView/dialog/warn/WarnOnce.java @@ -24,6 +24,7 @@ import javafx.scene.control.Alert.AlertType; public class WarnOnce implements PamSettings { public static final int OK_CANCEL_OPTION = JOptionPane.OK_CANCEL_OPTION; + public static final int YES_NO_OPTION = JOptionPane.YES_NO_OPTION; public static final int WARNING_MESSAGE = JOptionPane.DEFAULT_OPTION; public static final int OK_OPTION = JOptionPane.OK_OPTION; public static final int CANCEL_OPTION = JOptionPane.CANCEL_OPTION; diff --git a/src/PamView/dialog/warn/WarnOnceDialog.java b/src/PamView/dialog/warn/WarnOnceDialog.java index 48d90d87..2f211919 100644 --- a/src/PamView/dialog/warn/WarnOnceDialog.java +++ b/src/PamView/dialog/warn/WarnOnceDialog.java @@ -92,12 +92,17 @@ public class WarnOnceDialog extends PamDialog { } // if the message type is OK_OPTION, hide the cancel button - if (messageType == WarnOnce.OK_OPTION) { + if (messageType == WarnOnce.YES_NO_OPTION) { + getOkButton().setText("Yes"); + getCancelButton().setText("No"); + } + else if (messageType == WarnOnce.OK_OPTION) { getCancelButton().setVisible(false); } else { getCancelButton().setVisible(true); } + // change the button text to custom text, if needed if (okButtonText!=null) { getOkButton().setText(okButtonText); diff --git a/src/analogarraysensor/AnalogArraySensorDataUnit.java b/src/analogarraysensor/AnalogArraySensorDataUnit.java index c9d3ec79..201d3355 100644 --- a/src/analogarraysensor/AnalogArraySensorDataUnit.java +++ b/src/analogarraysensor/AnalogArraySensorDataUnit.java @@ -60,7 +60,7 @@ public class AnalogArraySensorDataUnit extends PamDataUnit implements Array.sens } private Double getFieldVal(int iVal) { - if (sensorData == null || sensorData.length <= iVal) { + if (sensorData == null || sensorData.length <= iVal || iVal < 0) { return null; } AnalogSensorData sensDat = sensorData[iVal]; diff --git a/src/analoginput/swing/AnalogDiagnosticsDisplay.java b/src/analoginput/swing/AnalogDiagnosticsDisplay.java index 0a0d1bbd..d4a19d5a 100644 --- a/src/analoginput/swing/AnalogDiagnosticsDisplay.java +++ b/src/analoginput/swing/AnalogDiagnosticsDisplay.java @@ -225,7 +225,8 @@ public class AnalogDiagnosticsDisplay extends UserDisplayComponentAdapter implem } break; case 4: - if (allItemData[rowIndex] != null) { + ItemAllData data = allItemData[rowIndex]; + if (data != null) { return allItemData[rowIndex].getIntValue(); } break; diff --git a/src/modbustcp/brainbox/BBED549.java b/src/modbustcp/brainbox/BBED549.java index 394891fe..754e1e77 100644 --- a/src/modbustcp/brainbox/BBED549.java +++ b/src/modbustcp/brainbox/BBED549.java @@ -140,7 +140,7 @@ public class BBED549 { public static double hexToEngineering(int range, int data) { /** * convert integer data to engineering units. - * @param range range on device, asumed the same for all channels. + * @param range range on device, assumed the same for all channels. * @param data data array * @return */ From ee153d6ffedce11b3118b472adfb06a4c848cd54 Mon Sep 17 00:00:00 2001 From: Douglas Gillespie <50671166+douggillespie@users.noreply.github.com> Date: Thu, 25 May 2023 09:10:51 +0100 Subject: [PATCH 5/8] Update OfflineFileList.java Sort function in offline file list --- src/fileOfflineData/OfflineFileList.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/fileOfflineData/OfflineFileList.java b/src/fileOfflineData/OfflineFileList.java index 5395c230..cece0092 100644 --- a/src/fileOfflineData/OfflineFileList.java +++ b/src/fileOfflineData/OfflineFileList.java @@ -3,6 +3,8 @@ package fileOfflineData; import java.io.File; import java.io.FileFilter; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; /** * Make a list of files with the given file filter. @@ -29,6 +31,21 @@ public class OfflineFileList { addFiles(current); return files.size(); } + + public void sortByFileName() { + if (files == null || files.size() == 0) { + return; + } + Collections.sort(files, new Comparator() { + + @Override + public int compare(File file1, File file2) { + String n1 = file1.getName(); + String n2 = file2.getName(); + return n1.compareTo(n2); + } + }); + } private void addFiles(File current) { if (current.exists() == false) { From d074d40a509b9fcb8ac98d38efbdbce17ca76499 Mon Sep 17 00:00:00 2001 From: Douglas Gillespie <50671166+douggillespie@users.noreply.github.com> Date: Thu, 25 May 2023 11:42:05 +0100 Subject: [PATCH 6/8] Offline bearing localiser Fixed some issues with making sure correct raw or fft data are loaded for offline bearing calculations. --- src/PamguardMVC/PamDataBlock.java | 27 ++++++++++ .../dataOffline/OfflineDataLoading.java | 5 +- .../offline/BLOfflineTask.java | 28 ++++++---- .../fftorganiser/FFTDataOrganiser.java | 52 +++++++++++++++++++ 4 files changed, 102 insertions(+), 10 deletions(-) diff --git a/src/PamguardMVC/PamDataBlock.java b/src/PamguardMVC/PamDataBlock.java index 2ad1d09e..97bd709c 100644 --- a/src/PamguardMVC/PamDataBlock.java +++ b/src/PamguardMVC/PamDataBlock.java @@ -843,6 +843,30 @@ public class PamDataBlock extends PamObservable { return unitsInInterval; } + + /** + * Do data exist which cover the given time range ? + * @param startMillis + * @param endMillis + * @return true if data exist covering that time range. + */ + public boolean hasDataRange(long startMillis, long endMillis) { + PamDataUnit first = null, last = null; + synchronized (synchronizationLock) { + first = getFirstUnit(); + last = getLastUnit(); + } + if (first == null || last == null) { + return false; + } + if (first.getTimeMilliseconds() > startMillis) { + return false; + } + if (last.getEndTimeInMilliseconds() < endMillis) { + return false; + } + return true; + } // recursive search for the correct unit // private Tunit searchFirstUnitAfter(int i1, int i2, long timems) { @@ -1014,6 +1038,9 @@ public class PamDataBlock extends PamObservable { * @return true if we need to reload offline data. */ public boolean needViewerDataLoad(OfflineDataLoadInfo offlineDataLoadInfo) { + if (pamDataUnits.size() == 0) { + return true; + } if (offlineDataLoadInfo.getStartMillis() == currentViewDataStart && offlineDataLoadInfo.getEndMillis() == currentViewDataEnd) { return false; diff --git a/src/PamguardMVC/dataOffline/OfflineDataLoading.java b/src/PamguardMVC/dataOffline/OfflineDataLoading.java index a342f357..c10a716c 100644 --- a/src/PamguardMVC/dataOffline/OfflineDataLoading.java +++ b/src/PamguardMVC/dataOffline/OfflineDataLoading.java @@ -527,7 +527,10 @@ public class OfflineDataLoading { public void notifyOfflineObservers(T pamDataUnit) { if (requestingObservers != null) { for (int i = 0; i < requestingObservers.size(); i++) { - requestingObservers.get(i).addData(pamDataBlock, pamDataUnit); + PamObserver obs = requestingObservers.get(i); + if (obs != null) { + obs.addData(pamDataBlock, pamDataUnit); + } } } diff --git a/src/bearinglocaliser/offline/BLOfflineTask.java b/src/bearinglocaliser/offline/BLOfflineTask.java index 2e0061f2..3f98c031 100644 --- a/src/bearinglocaliser/offline/BLOfflineTask.java +++ b/src/bearinglocaliser/offline/BLOfflineTask.java @@ -4,6 +4,7 @@ import java.util.ListIterator; import PamguardMVC.PamDataBlock; import PamguardMVC.PamDataUnit; +import PamguardMVC.dataOffline.OfflineDataLoadInfo; import bearinglocaliser.BearingLocaliserControl; import bearinglocaliser.BearingProcess; import dataMap.OfflineDataMapPoint; @@ -28,6 +29,9 @@ public class BLOfflineTask extends OfflineTask { @Override public String getName() { + if (bearingLocaliserControl == null) { + return null; + } return bearingLocaliserControl.getUnitName(); } @@ -67,15 +71,21 @@ public class BLOfflineTask extends OfflineTask { if (rawOrFFTBlock == null) { return; } - ListIterator it = rawOrFFTBlock.getListIterator(dataUnit.getTimeMilliseconds(), rawOrFFTBlock.getChannelMap(), - PamDataBlock.MATCH_BEFORE, PamDataBlock.POSITION_BEFORE); - if (it == null || it.hasNext() == false) { - long dataStart = dataUnit.getTimeMilliseconds(); - long dataEnd = dataUnit.getEndTimeInMilliseconds(); - if (dataEnd-dataStart <= 0) { - dataEnd = dataStart + 1000; - } - rawOrFFTBlock.loadViewerData(dataStart, dataEnd, null); + long dataStart = dataUnit.getTimeMilliseconds(); + long dataEnd = dataUnit.getEndTimeInMilliseconds(); + boolean haveData = rawOrFFTBlock.hasDataRange(dataStart, dataEnd); + if (haveData == false) { +// ListIterator it = rawOrFFTBlock.getListIterator(dataUnit.getTimeMilliseconds(), rawOrFFTBlock.getChannelMap(), +// PamDataBlock.MATCH_BEFORE, PamDataBlock.POSITION_BEFORE); +// if (it == null || it.hasNext() == false) { +// if (dataEnd-dataStart <= 0) { +// dataEnd = dataStart + 1000; +// } + OfflineDataLoadInfo offlineLoadInfo = new OfflineDataLoadInfo(dataStart, dataEnd); + offlineLoadInfo.setLoadKeepLayers(2); + rawOrFFTBlock.getOfflineData(offlineLoadInfo); +// System.out.printf("Loaded some FFT data I hope\n"); +// rawOrFFTBlock.loadViewerData(dataStart, dataEnd, null); } } diff --git a/src/fftManager/fftorganiser/FFTDataOrganiser.java b/src/fftManager/fftorganiser/FFTDataOrganiser.java index f32d6c8e..093f6f4f 100644 --- a/src/fftManager/fftorganiser/FFTDataOrganiser.java +++ b/src/fftManager/fftorganiser/FFTDataOrganiser.java @@ -4,6 +4,7 @@ import java.util.Arrays; import java.util.List; import java.util.ListIterator; +import PamController.PamController; import PamController.SettingsNameProvider; import PamDetection.RawDataUnit; import PamUtils.PamUtils; @@ -143,6 +144,11 @@ public class FFTDataOrganiser { if (rawOrFFTData == null) { return null; } + +// if (PamController.getInstance().getRunMode() == PamController.RUN_PAMVIEW) { +// checkOfflineDataLoad(rawOrFFTData, pamDataUnit.getTimeMilliseconds(), pamDataUnit.getEndTimeInMilliseconds()); +// } + switch (inputType) { case FFTData: return createFromFFTData(pamDataUnit, sampleRate, channelMap); @@ -157,6 +163,52 @@ public class FFTDataOrganiser { } } + /** + * Called when running offline to try to ensure required raw or fft data are in memory. + * @param sourceData + * @param timeMilliseconds + * @param endTimeInMilliseconds + */ + private boolean checkOfflineDataLoad(PamDataBlock sourceData, long startMilliseconds, long endMilliseconds) { + if (sourceData == null) { + return false; + } + boolean needData = needOfflineDataLoad(sourceData, startMilliseconds, endMilliseconds); + if (needData) { + sourceData.loadViewerData(startMilliseconds, endMilliseconds, null); + } + return needOfflineDataLoad(sourceData, startMilliseconds, endMilliseconds); + } + + /** + * Test to see if we still need to load offline data. + * @param sourceData + * @param startMilliseconds + * @param endMilliseconds + * @return + */ + private boolean needOfflineDataLoad(PamDataBlock sourceData, long startMilliseconds, long endMilliseconds) { + if (sourceData == null) { + return false; + } + synchronized (sourceData.getSynchLock()) { + PamDataUnit first = sourceData.getFirstUnit(); + PamDataUnit last = sourceData.getLastUnit(); + if (first == null || last == null) { + return true; + } + if (first.getTimeMilliseconds() > startMilliseconds) { + return true; + } + if (last.getEndTimeInMilliseconds() < endMilliseconds) { + return true; + } + } + return false; + } + + + /** * Get FFT data units matching in time from the source * @param pamDataUnit data unit we need FFT data for (can be anything, just needs it's times) From 397e7423581d226b571dad4f4ea32ac1639dcf5f Mon Sep 17 00:00:00 2001 From: Douglas Gillespie <50671166+douggillespie@users.noreply.github.com> Date: Fri, 26 May 2023 09:46:22 +0100 Subject: [PATCH 7/8] Update ScrollingDataPanel.java Fixed 'scroll to' bug in datamap --- src/dataMap/ScrollingDataPanel.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/dataMap/ScrollingDataPanel.java b/src/dataMap/ScrollingDataPanel.java index bf60c164..04b42aa8 100644 --- a/src/dataMap/ScrollingDataPanel.java +++ b/src/dataMap/ScrollingDataPanel.java @@ -391,6 +391,9 @@ public class ScrollingDataPanel extends PamBorderPanel { public void scrollToData(PamDataBlock dataBlock) { long startTime = dataBlock.getCurrentViewDataStart(); int val = (int) ((startTime - getScreenStartMillis())/1000 - getScreenSeconds()/5) ; + val += hScrollBar.getValue(); +// System.out.printf("Scroll bar %d to %d set %d\n", hScrollBar.getMinimum(), +// hScrollBar.getMaximum(), val); hScrollBar.setValue(val); } From 1b5454be9179f1ecce2e6b4796611f2fba5be79d Mon Sep 17 00:00:00 2001 From: Douglas Gillespie <50671166+douggillespie@users.noreply.github.com> Date: Fri, 26 May 2023 10:27:25 +0100 Subject: [PATCH 8/8] Stop command small change so command is available as a constant --- .classpath | 2 +- src/PamController/command/StopCommand.java | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.classpath b/.classpath index 0eb3c4fb..188c7291 100644 --- a/.classpath +++ b/.classpath @@ -6,7 +6,7 @@ - + diff --git a/src/PamController/command/StopCommand.java b/src/PamController/command/StopCommand.java index 59eb7da4..f21df863 100644 --- a/src/PamController/command/StopCommand.java +++ b/src/PamController/command/StopCommand.java @@ -3,9 +3,11 @@ package PamController.command; import PamController.PamController; public class StopCommand extends ExtCommand { + + public static final String commandId = "stop"; public StopCommand() { - super("stop", false); + super(commandId, false); } @Override