/*****************************************************************************
* Limpet - the Lightweight InforMation ProcEssing Toolkit
* http://limpet.info
*
* (C) 2015-2016, Deep Blue C Technologies Ltd
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the Eclipse Public License v1.0
* (http://www.eclipse.org/legal/epl-v10.html)
*
* This library 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.
*****************************************************************************/
package info.limpet.data.operations.spatial;
import static javax.measure.unit.SI.METRE;
import static javax.measure.unit.SI.RADIAN;
import static javax.measure.unit.SI.SECOND;
import info.limpet.IBaseTemporalCollection;
import info.limpet.ICollection;
import info.limpet.ICommand;
import info.limpet.IContext;
import info.limpet.IOperation;
import info.limpet.IQuantityCollection;
import info.limpet.IStore;
import info.limpet.IStoreGroup;
import info.limpet.IStoreItem;
import info.limpet.data.commands.AbstractCommand;
import info.limpet.data.impl.samples.StockTypes;
import info.limpet.data.impl.samples.StockTypes.NonTemporal.Location;
import info.limpet.data.impl.samples.StockTypes.Temporal.FrequencyHz;
import info.limpet.data.impl.samples.TemporalLocation;
import info.limpet.data.operations.CollectionComplianceTests;
import info.limpet.data.operations.CollectionComplianceTests.TimePeriod;
import info.limpet.data.store.StoreGroup;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import javax.measure.quantity.Angle;
import javax.measure.quantity.Frequency;
import javax.measure.quantity.Velocity;
import javax.measure.unit.SI;
public class DopplerShiftBetweenTracksOperation implements
IOperation<IStoreItem>
{
public static class DSOperation extends AbstractCommand<IStoreItem>
{
private static final String SOUND_SPEED = "SOUND_SPEED";
private static final String LOC = "LOC";
private static final String SPEED = "SPEED";
private static final String COURSE = "COURSE";
private static final String FREQ = "FREQ";
private static final String TX = "TX_";
/**
* let the class organise a tidy set of data, to collate the assorted datasets
*
*/
private transient HashMap<String, ICollection> _data;
/**
* nominated transmitter
*
*/
private final StoreGroup _tx;
/**
* nominated receivers
*
*/
private final List<TrackProvider> _allTracks;
private final CollectionComplianceTests aTests =
new CollectionComplianceTests();
/**
* way to make singleton locations (that don't have course/speed) look like tracks
*
* @author ian
*
*/
public interface TrackProvider
{
Point2D getLocationAt(long time);
double getCourseAt(long time);
double getSpeedAt(long time);
String getName();
/**
* @param dsOperation
*/
void addDependent(ICommand<IStoreItem> dsOperation);
}
protected static class SingletonWrapper implements TrackProvider
{
private final String _name;
private Location _dataset;
public SingletonWrapper(String name, Location loc)
{
_name = name;
_dataset = loc;
}
@Override
public Point2D getLocationAt(long time)
{
return _dataset.getValues().iterator().next();
}
@Override
public double getCourseAt(long time)
{
return 0;
}
@Override
public double getSpeedAt(long time)
{
return 0;
}
@Override
public String getName()
{
return _name;
}
/*
* (non-Javadoc)
*
* @see info.limpet.data.operations.spatial.DopplerShiftBetweenTracksOperation
* .DSOperation.TrackProvider#addDependent(info.limpet.IOperation)
*/
@Override
public void addDependent(ICommand<IStoreItem> operation)
{
_dataset.addDependent(operation);
}
}
protected static class CompositeTrackWrapper implements TrackProvider
{
private final String _name;
private CollectionComplianceTests aTests =
new CollectionComplianceTests();
private IQuantityCollection<?> _course;
private IQuantityCollection<?> _speed;
private TemporalLocation _location;
public CompositeTrackWrapper(IStoreGroup track, String name)
{
_name = name;
// assign the components
_course =
aTests.collectionWith(track, Angle.UNIT.getDimension(), false);
_speed =
aTests.collectionWith(track, Velocity.UNIT.getDimension(), false);
Iterator<IStoreItem> iter = track.iterator();
while (iter.hasNext())
{
IStoreItem iStoreItem = (IStoreItem) iter.next();
if (iStoreItem instanceof TemporalLocation)
{
_location = (TemporalLocation) iStoreItem;
}
}
}
/*
* (non-Javadoc)
*
* @see info.limpet.data.operations.spatial.DopplerShiftBetweenTracksOperation
* .DSOperation.TrackProvider#addDependent(info.limpet.IOperation)
*/
@Override
public void addDependent(ICommand<IStoreItem> operation)
{
_course.addDependent(operation);
_speed.addDependent(operation);
_location.addDependent(operation);
}
@Override
public Point2D getLocationAt(long time)
{
return aTests.locationFor((ICollection) _location, time);
}
@Override
public double getCourseAt(long time)
{
return aTests.valueAt(_course, time, RADIAN.asType(Angle.class));
}
@Override
public double getSpeedAt(long time)
{
return aTests.valueAt(_speed, time, METRE.divide(SECOND).asType(
Velocity.class));
}
@Override
public String getName()
{
return _name;
}
}
/**
* find any selection items that we can use as tracks
*
* @param ignoreMe
* @param selection
* @param aTests
* @return
*/
public static List<TrackProvider> getTracks(IStoreGroup ignoreMe,
List<IStoreItem> selection, CollectionComplianceTests aTests)
{
List<TrackProvider> res = new ArrayList<TrackProvider>();
Iterator<IStoreItem> iter = selection.iterator();
while (iter.hasNext())
{
IStoreItem item = iter.next();
if (item != ignoreMe)
{
if (item instanceof Location)
{
Location loc = (Location) item;
res.add(new SingletonWrapper(loc.getName(), loc));
}
else if (item instanceof IStoreGroup)
{
// CHECK IF IT'S SUITABLE AS A TRACK. IF NOT, SEE IF IT JUST
// CONTAINS LOCATIONS
// - THEN ADD THEM ALL
//
IStoreGroup grp = (IStoreGroup) item;
// see if this is a composite track
// or, is this a conventional track
if (aTests.isATrack(grp))
{
res.add(new CompositeTrackWrapper(grp, grp.getName()));
}
else
{
// see if this is a group of non-temporal locations
Iterator<IStoreItem> iter2 = grp.iterator();
while (iter2.hasNext())
{
IStoreItem iStoreItem = (IStoreItem) iter2.next();
if (iStoreItem instanceof ICollection)
{
ICollection coll = (ICollection) iStoreItem;
if (coll.getValuesCount() == 1)
{
if (coll instanceof Location)
{
final Location loc = (Location) coll;
res.add(new SingletonWrapper(coll.getName(), loc));
}
}
}
}
}
}
}
}
return res;
}
public DSOperation(final StoreGroup tx, final IStore store,
final String title, final String description,
final List<IStoreItem> selection, IContext context)
{
super(title, description, store, true, true, selection, context);
_tx = tx;
// create the list of non-tx tracks
_allTracks = getTracks(tx, selection, aTests);
}
@Override
public void execute()
{
// store the data in an accessible way
organiseData();
// get the unit
final List<IStoreItem> outputs = new ArrayList<IStoreItem>();
// create the output dataset
Iterator<TrackProvider> oIter = _allTracks.iterator();
while (oIter.hasNext())
{
TrackProvider storeGroup = oIter.next();
if (storeGroup != _tx)
{
// put the names into a string
final String title =
getOutputNameFor(_tx.getName(), storeGroup.getName());
// ok, generate the new series
final IQuantityCollection<?> target = getOutputCollection(title);
outputs.add(target);
// store the output
super.addOutput(target);
}
}
// start adding values.
performCalc(outputs);
// tell each series that we're a dependent
final Iterator<ICollection> iter = _data.values().iterator();
while (iter.hasNext())
{
final ICollection iCollection = iter.next();
// sometimes a dataset is optional, so double-check we aren't
// looking at a null dataset
if (iCollection != null)
{
iCollection.addDependent(this);
}
}
// and for the receiver tracks.
oIter = _allTracks.iterator();
while (oIter.hasNext())
{
TrackProvider track = (TrackProvider) oIter.next();
track.addDependent(this);
}
// ok, done
getStore().addAll(super.getOutputs());
}
@Override
protected String getOutputName()
{
throw new RuntimeException("Get output name not implemented for Doppler");
// return getContext().getInput("Doppler shift between tracks",
// NEW_DATASET_MESSAGE,
// "Doppler shift between " + _tx.getName() + " and " + _rx.getName());
}
@Override
public void undo()
{
// ok, remove the calculated dataset
IStoreItem results = getOutputs().iterator().next();
IStore store = getStore();
if (store instanceof StoreGroup)
{
StoreGroup im = (StoreGroup) store;
im.remove(results);
}
}
@Override
public void redo()
{
IStoreItem results = getOutputs().iterator().next();
IStore store = getStore();
if (store instanceof StoreGroup)
{
StoreGroup im = (StoreGroup) store;
im.add(results);
}
}
public HashMap<String, ICollection> getDataMap()
{
return _data;
}
protected IQuantityCollection<?> getOutputCollection(final String title)
{
return new StockTypes.Temporal.FrequencyHz(title, this);
}
protected String getOutputNameFor(final String tx, String rx)
{
return "Doppler shift between " + tx + " and " + rx;
}
public void organiseData()
{
if (_data == null)
{
// ok, we need to collate the data
_data = new HashMap<String, ICollection>();
final CollectionComplianceTests tests = new CollectionComplianceTests();
// ok, transmitter data
_data.put(TX + FREQ, tests.collectionWith(_tx, Frequency.UNIT
.getDimension(), true));
_data.put(TX + COURSE, tests.collectionWith(_tx, SI.RADIAN
.getDimension(), true));
_data.put(TX + SPEED, tests.collectionWith(_tx, METRE.divide(SECOND)
.getDimension(), true));
_data.put(TX + LOC, tests.someHaveLocation(_tx));
// and the sound speed
_data.put(SOUND_SPEED, tests.collectionWith(getInputs(), METRE.divide(
SECOND).getDimension(), false));
}
}
/**
* wrap the actual operation. We're doing this since we need to separate it from the core
* "execute" operation in order to support dynamic updates
*
* @param unit
* @param outputs
*/
private void performCalc(final List<IStoreItem> outputs)
{
// just check we've been organised (if we've been loaded from persistent
// storage)
organiseData();
// and the bounding period
final TimePeriod period = aTests.getBoundingTime(_data.values());
// check it's valid
if (period.invalid())
{
System.err.println("Insufficient coverage for datasets");
return;
}
// ok, let's start by finding our time sync
final IBaseTemporalCollection times =
aTests.getOptimalTimes(period, _data.values());
// check we were able to find some times
if (times == null)
{
System.err.println("Unable to find time source dataset");
return;
}
// keep a list of updated tracks
List<ICollection> updated = new ArrayList<ICollection>();
final IGeoCalculator calc = GeoSupport.getCalculator();
// ok, now loop through the receivers
Iterator<TrackProvider> rIter = _allTracks.iterator();
while (rIter.hasNext())
{
TrackProvider trackProvider = (TrackProvider) rIter.next();
// find the relevant outputs dataset
String thisOutName =
getOutputNameFor(_tx.getName(), trackProvider.getName());
Iterator<IStoreItem> oIter = getOutputs().iterator();
FrequencyHz thisOutput = null;
while (oIter.hasNext() && thisOutput == null)
{
FrequencyHz tmpOutput = (FrequencyHz) oIter.next();
if (tmpOutput.getName().equals(thisOutName)
&& (tmpOutput.getValuesCount() == 0))
{
thisOutput = tmpOutput;
}
}
if (thisOutput == null)
{
continue;
}
// and now we can start looping through
final Iterator<Long> tIter = times.getTimes().iterator();
while (tIter.hasNext())
{
final long thisTime = tIter.next();
if (thisTime >= period.getStartTime()
&& thisTime <= period.getEndTime())
{
// ok, now collate our data
final Point2D txLoc =
aTests.locationFor(_data.get(TX + LOC), thisTime);
final double txCourseRads =
aTests.valueAt(_data.get(TX + COURSE), thisTime, SI.RADIAN);
final double txSpeedMSec =
aTests.valueAt(_data.get(TX + SPEED), thisTime,
SI.METERS_PER_SECOND);
final double freq =
aTests.valueAt(_data.get(TX + FREQ), thisTime, SI.HERTZ);
final double soundSpeed =
aTests.valueAt(_data.get(SOUND_SPEED), thisTime,
SI.METERS_PER_SECOND);
final Point2D rxLoc = trackProvider.getLocationAt(thisTime);
final double rxCourseRads = trackProvider.getCourseAt(thisTime);
final double rxSpeedMSec = trackProvider.getSpeedAt(thisTime);
// check we have locations. During some property editing we receive
// recalc call
// after old value is removed, and before new value is added.
if (txLoc != null && rxLoc != null)
{
// now find the bearing between them
double angleDegs = calc.getAngleBetween(txLoc, rxLoc);
if (angleDegs < 0)
{
angleDegs += 360;
}
final double angleRads = Math.toRadians(angleDegs);
// ok, and the calculation
final double shifted =
calcPredictedFreqSI(soundSpeed, txCourseRads, rxCourseRads,
txSpeedMSec, rxSpeedMSec, angleRads, freq);
// see if we have an output collection for this input one.
thisOutput.add(thisTime, shifted);
if (!updated.contains(thisOutput))
{
updated.add(thisOutput);
}
}
}
}
}
Iterator<ICollection> updates = updated.iterator();
while (updates.hasNext())
{
ICollection iCollection = (ICollection) updates.next();
iCollection.fireDataChanged();
}
}
@Override
protected void recalculate(IStoreItem subject)
{
// do we know which subject this relates to?
// just one of our input datasets has changed
boolean handled = false;
final Iterator<IStoreItem> iter = getOutputs().iterator();
final String nameToRemove =
getOutputNameFor(_tx.getName(), subject.getName());
while (iter.hasNext())
{
final IQuantityCollection<?> qC =
(IQuantityCollection<?>) iter.next();
if (qC.getName().equals(nameToRemove))
{
qC.clearQuiet();
handled = true;
break;
}
}
// did we manage a precision surgical removal?
if (!handled)
{
// clear out all the lists, first
Iterator<IStoreItem> iter2 = getOutputs().iterator();
while (iter2.hasNext())
{
final IQuantityCollection<?> qC =
(IQuantityCollection<?>) iter2.next();
qC.clearQuiet();
}
}
// update the results
performCalc(getOutputs());
}
}
private final CollectionComplianceTests aTests =
new CollectionComplianceTests();
@Override
public Collection<ICommand<IStoreItem>> actionsFor(
final List<IStoreItem> selection, final IStore destination,
IContext context)
{
final Collection<ICommand<IStoreItem>> res =
new ArrayList<ICommand<IStoreItem>>();
if (appliesTo(selection))
{
// get the list of tracks
ArrayList<StoreGroup> trackList = aTests.getChildTrackGroups(selection);
// ok, loop through them
Iterator<StoreGroup> iter = trackList.iterator();
while (iter.hasNext())
{
StoreGroup thisG = (StoreGroup) iter.next();
final boolean hasFrequency =
aTests.collectionWith(thisG, Frequency.UNIT.getDimension(), true) != null;
if (hasFrequency)
{
final ICommand<IStoreItem> newC =
new DSOperation(thisG, destination,
"Doppler between tracks (from " + thisG.getName() + ")",
"Calculate doppler between two tracks", selection, context);
res.add(newC);
}
}
}
return res;
}
protected boolean appliesTo(final List<IStoreItem> selection)
{
// ok, check we have two collections
final boolean allTracks = aTests.getNumberOfTracks(selection) >= 2;
final boolean someHaveFreq =
aTests.collectionWith(selection, Frequency.UNIT.getDimension(), true) != null;
final boolean topLevelSpeed =
aTests.collectionWith(selection, METRE.divide(SECOND).getDimension(),
false) != null;
return allTracks && someHaveFreq && topLevelSpeed;
}
/**
*
* @param speedOfSound
* @param osHeadingRads
* @param tgtHeadingRads
* @param osSpeed
* @param tgtSpeed
* @param bearing
* @param fNought
* @return
*/
private static double calcPredictedFreqSI(final double speedOfSound,
final double osHeadingRads, final double tgtHeadingRads,
final double osSpeed, final double tgtSpeed, final double bearing,
final double fNought)
{
final double relB = bearing - osHeadingRads;
// note - contrary to some publications TSL uses the
// angle along the bearing, not the angle back down the bearing (ATB).
final double angleOffTheOtherB = tgtHeadingRads - bearing;
final double valOSL = Math.cos(relB) * osSpeed;
final double valTSL = Math.cos(angleOffTheOtherB) * tgtSpeed;
final double freq =
fNought * (speedOfSound + valOSL) / (speedOfSound + valTSL);
return freq;
}
}