/*
This file is part of RouteConverter.
RouteConverter 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 2 of the License, or
(at your option) any later version.
RouteConverter 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 RouteConverter; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
Copyright (C) 2007 Christian Pesch. All Rights Reserved.
*/
package slash.navigation.converter.gui.helpers;
import slash.common.type.CompactCalendar;
import slash.navigation.common.LongitudeAndLatitude;
import slash.navigation.common.NavigationPosition;
import slash.navigation.common.NumberPattern;
import slash.navigation.common.NumberingStrategy;
import slash.navigation.converter.gui.RouteConverter;
import slash.navigation.converter.gui.models.PositionColumnValues;
import slash.navigation.converter.gui.models.PositionsModel;
import slash.navigation.gui.Application;
import slash.navigation.gui.events.ContinousRange;
import slash.navigation.gui.events.RangeOperation;
import slash.navigation.gui.notifications.NotificationManager;
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.logging.Logger;
import static java.lang.Math.abs;
import static java.lang.Math.min;
import static java.lang.String.format;
import static java.util.Arrays.asList;
import static java.util.concurrent.Executors.newSingleThreadExecutor;
import static javax.swing.JOptionPane.ERROR_MESSAGE;
import static javax.swing.JOptionPane.showMessageDialog;
import static javax.swing.SwingUtilities.invokeLater;
import static javax.swing.event.TableModelEvent.ALL_COLUMNS;
import static slash.common.helpers.ExceptionHelper.getLocalizedMessage;
import static slash.common.io.Transfer.widthInDigits;
import static slash.common.type.CompactCalendar.fromMillis;
import static slash.navigation.base.RouteComments.formatNumberedPosition;
import static slash.navigation.base.RouteComments.getNumberedPosition;
import static slash.navigation.common.NumberingStrategy.Absolute_Position_Within_Position_List;
import static slash.navigation.converter.gui.helpers.PositionHelper.formatElevation;
import static slash.navigation.converter.gui.helpers.PositionHelper.formatSpeed;
import static slash.navigation.converter.gui.models.PositionColumns.DATE_TIME_COLUMN_INDEX;
import static slash.navigation.converter.gui.models.PositionColumns.DESCRIPTION_COLUMN_INDEX;
import static slash.navigation.converter.gui.models.PositionColumns.ELEVATION_COLUMN_INDEX;
import static slash.navigation.converter.gui.models.PositionColumns.LATITUDE_COLUMN_INDEX;
import static slash.navigation.converter.gui.models.PositionColumns.LONGITUDE_COLUMN_INDEX;
import static slash.navigation.converter.gui.models.PositionColumns.SPEED_COLUMN_INDEX;
import static slash.navigation.gui.helpers.JTableHelper.scrollToPosition;
/**
* Helps to augment a positions with coordinates, elevation, position number for its description,
* postal address, populated place and speed information.
*
* @author Christian Pesch
*/
public class PositionAugmenter {
private static final Logger log = Logger.getLogger(PositionAugmenter.class.getName());
private final JFrame frame;
private final JTable positionsView;
private final PositionsModel positionsModel;
private final ExecutorService executor = newSingleThreadExecutor();
private final ElevationServiceFacade elevationServiceFacade = RouteConverter.getInstance().getElevationServiceFacade();
private final GeocodingServiceFacade geocodingServiceFacade = RouteConverter.getInstance().getGeocodingServiceFacade();
private static final Object notificationMutex = new Object();
private boolean running = true;
public PositionAugmenter(JTable positionsView, PositionsModel positionsModel, JFrame frame) {
this.positionsView = positionsView;
this.positionsModel = positionsModel;
this.frame = frame;
}
public void interrupt() {
synchronized (notificationMutex) {
this.running = false;
}
}
public void dispose() {
interrupt();
executor.shutdownNow();
}
private interface OverwritePredicate {
boolean shouldOverwrite(NavigationPosition position);
}
private static final OverwritePredicate TAUTOLOGY_PREDICATE = new OverwritePredicate() {
public boolean shouldOverwrite(NavigationPosition position) {
return true;
}
};
private static final OverwritePredicate COORDINATE_PREDICATE = new OverwritePredicate() {
public boolean shouldOverwrite(NavigationPosition position) {
return position.hasCoordinates();
}
};
private interface Operation {
String getName();
int getColumnIndex();
void performOnStart();
boolean run(int index, NavigationPosition position) throws Exception;
String getMessagePrefix();
}
private NotificationManager getNotificationManager() {
return Application.getInstance().getContext().getNotificationManager();
}
private static class CancelAction extends AbstractAction {
private boolean canceled = false;
public boolean isCanceled() {
return canceled;
}
public void actionPerformed(ActionEvent e) {
this.canceled = true;
}
}
private void executeOperation(final JTable positionsTable,
final PositionsModel positionsModel,
final int[] rows,
final boolean slowOperation,
final OverwritePredicate predicate,
final Operation operation) {
synchronized (notificationMutex) {
this.running = true;
}
final CancelAction cancelAction = new CancelAction();
executor.execute(new Runnable() {
public void run() {
final int[] count = new int[1];
count[0] = 0;
try {
invokeLater(new Runnable() {
public void run() {
if (positionsTable != null && rows.length > 0)
scrollToPosition(positionsTable, rows[0]);
}
});
operation.performOnStart();
final Exception[] lastException = new Exception[1];
lastException[0] = null;
final int maximumRangeLength = rows.length > 99 ? rows.length / (slowOperation ? 100 : 10) : rows.length;
new ContinousRange(rows, new RangeOperation() {
public void performOnIndex(final int index) {
NavigationPosition position = positionsModel.getPosition(index);
if (predicate.shouldOverwrite(position)) {
try {
// ignoring the result since the performance boost of the continous
// range operations outweights the possible optimization
operation.run(index, position);
} catch (Exception e) {
log.warning(format("Error while running operation %s on position %d: %s", operation, index, e));
lastException[0] = e;
}
}
getNotificationManager().showNotification(MessageFormat.format(
RouteConverter.getBundle().getString("augmenting-progress"), count[0]++, rows.length), cancelAction);
}
public void performOnRange(final int firstIndex, final int lastIndex) {
invokeLater(new Runnable() {
public void run() {
positionsModel.fireTableRowsUpdated(firstIndex, lastIndex, operation.getColumnIndex());
if (positionsTable != null) {
scrollToPosition(positionsTable, min(lastIndex + maximumRangeLength, positionsModel.getRowCount() - 1));
}
}
});
}
public boolean isInterrupted() {
synchronized (notificationMutex) {
return cancelAction.isCanceled() || !running;
}
}
}).performMonotonicallyIncreasing(maximumRangeLength);
if (lastException[0] != null) {
String errorMessage = RouteConverter.getBundle().getString(operation.getMessagePrefix() + "error");
showMessageDialog(frame,
MessageFormat.format(errorMessage, getLocalizedMessage(lastException[0])), frame.getTitle(), ERROR_MESSAGE);
}
} finally {
invokeLater(new Runnable() {
public void run() {
getNotificationManager().showNotification(MessageFormat.format(
RouteConverter.getBundle().getString("augmenting-finished"), count[0]), null);
}
});
}
}
});
}
private void processCoordinates(final JTable positionsTable,
final PositionsModel positionsModel,
final int[] rows,
final OverwritePredicate predicate) {
executeOperation(positionsTable, positionsModel, rows, true, predicate,
new Operation() {
public String getName() {
return "CoordinatesPositionAugmenter";
}
public int getColumnIndex() {
return ALL_COLUMNS; // LONGITUDE_COLUMN_INDEX + LATITUDE_COLUMN_INDEX;
}
public void performOnStart() {
}
public boolean run(int index, NavigationPosition position) throws Exception {
NavigationPosition coordinates = RouteConverter.getInstance().getGeocodingServiceFacade().getPositionFor(position.getDescription());
if (coordinates != null)
positionsModel.edit(index,
new PositionColumnValues(asList(LONGITUDE_COLUMN_INDEX, LATITUDE_COLUMN_INDEX),
Arrays.<Object>asList(coordinates.getLongitude(), coordinates.getLatitude())), false, true);
return coordinates != null;
}
public String getMessagePrefix() {
return "add-coordinates-";
}
}
);
}
public void addCoordinates() {
int[] rows = positionsView.getSelectedRows();
if (rows.length > 0)
processCoordinates(positionsView, positionsModel, rows, TAUTOLOGY_PREDICATE);
}
private void processElevations(final JTable positionsTable,
final PositionsModel positionsModel,
final int[] rows,
final OverwritePredicate predicate) {
executeOperation(positionsTable, positionsModel, rows, true, predicate,
new Operation() {
public String getName() {
return "ElevationPositionAugmenter";
}
public int getColumnIndex() {
return ELEVATION_COLUMN_INDEX;
}
public void performOnStart() {
downloadElevationData(rows, true);
}
public boolean run(int index, NavigationPosition position) throws Exception {
String previousElevation = formatElevation(position.getElevation());
String nextElevation = getElevationFor(position);
boolean changed = nextElevation != null && !nextElevation.equals(previousElevation);
if (changed)
positionsModel.edit(index, new PositionColumnValues(ELEVATION_COLUMN_INDEX, nextElevation), false, true);
return changed;
}
public String getMessagePrefix() {
return "add-elevation-";
}
}
);
}
private String getElevationFor(NavigationPosition position) throws IOException {
if(!position.hasCoordinates())
return null;
Double elevation = elevationServiceFacade.getElevationFor(position.getLongitude(), position.getLatitude());
if(elevation == null)
return null;
return formatElevation(elevation);
}
private void downloadElevationData(int[] rows, boolean waitForDownload) {
if (!elevationServiceFacade.isDownload())
return;
List<LongitudeAndLatitude> longitudeAndLatitudes = new ArrayList<>();
for (int row : rows) {
NavigationPosition position = positionsModel.getPosition(row);
if (position.hasCoordinates())
longitudeAndLatitudes.add(new LongitudeAndLatitude(position.getLongitude(), position.getLatitude()));
}
elevationServiceFacade.downloadElevationDataFor(longitudeAndLatitudes, waitForDownload);
}
public void addElevations() {
int[] rows = positionsView.getSelectedRows();
if (rows.length > 0)
processElevations(positionsView, positionsModel, rows, COORDINATE_PREDICATE);
}
private void addAddresses(final JTable positionsTable,
final PositionsModel positionsModel,
final int[] rows,
final OverwritePredicate predicate) {
executeOperation(positionsTable, positionsModel, rows, true, predicate,
new Operation() {
public String getName() {
return "AddressPositionAugmenter";
}
public int getColumnIndex() {
return DESCRIPTION_COLUMN_INDEX;
}
public void performOnStart() {
}
public boolean run(int index, NavigationPosition position) throws Exception {
String description = geocodingServiceFacade.getAddressFor(position);
if (description != null)
positionsModel.edit(index, new PositionColumnValues(DESCRIPTION_COLUMN_INDEX, description), false, true);
return description != null;
}
public String getMessagePrefix() {
return "add-address-";
}
}
);
}
public void addAddresses() {
int[] rows = positionsView.getSelectedRows();
if (rows.length > 0)
addAddresses(positionsView, positionsModel, rows, COORDINATE_PREDICATE);
}
private void processSpeeds(final JTable positionsTable,
final PositionsModel positionsModel,
final int[] rows,
final OverwritePredicate predicate) {
executeOperation(positionsTable, positionsModel, rows, false, predicate,
new Operation() {
public String getName() {
return "SpeedPositionAugmenter";
}
public int getColumnIndex() {
return SPEED_COLUMN_INDEX;
}
public void performOnStart() {
}
public boolean run(int index, NavigationPosition position) throws Exception {
NavigationPosition predecessor = index > 0 && index < positionsModel.getRowCount() ? positionsModel.getPosition(index - 1) : null;
if (predecessor != null) {
String previousSpeed = formatSpeed(position.getSpeed());
String nextSpeed = formatSpeed(position.calculateSpeed(predecessor));
boolean changed = nextSpeed != null && !nextSpeed.equals(previousSpeed);
if (changed)
positionsModel.edit(index, new PositionColumnValues(SPEED_COLUMN_INDEX, nextSpeed), false, true);
return changed;
}
return false;
}
public String getMessagePrefix() {
return "add-speed-";
}
}
);
}
public void addSpeeds() {
int[] rows = positionsView.getSelectedRows();
if (rows.length > 0)
processSpeeds(positionsView, positionsModel, rows, COORDINATE_PREDICATE);
}
private int findPredecessorWithTime(PositionsModel positionsModel, int index) {
while (index > 0) {
NavigationPosition position = positionsModel.getPosition(index--);
if (position.hasTime())
return index;
}
return -1;
}
private int findSuccessorWithTime(PositionsModel positionsModel, int index) {
while (index < positionsModel.getRowCount() - 1) {
NavigationPosition position = positionsModel.getPosition(index++);
if (position.hasTime())
return index;
}
return -1;
}
private CompactCalendar interpolateTime(PositionsModel positionsModel, int positionIndex,
int predecessorIndex, int successorIndex) {
NavigationPosition predecessor = positionsModel.getPosition(predecessorIndex);
if (!predecessor.hasTime())
return null;
NavigationPosition successor = positionsModel.getPosition(successorIndex);
if (!successor.hasTime())
return null;
long timeDelta = abs(predecessor.calculateTime(successor));
double distanceToPredecessor = positionsModel.getRoute().getDistance(predecessorIndex, positionIndex);
double distanceToSuccessor = positionsModel.getRoute().getDistance(positionIndex, successorIndex);
double distanceRatio = distanceToPredecessor / (distanceToPredecessor + distanceToSuccessor);
long time = (long) (predecessor.getTime().getTimeInMillis() + (double) timeDelta * distanceRatio);
return fromMillis(time);
}
private void processTimes(final JTable positionsTable,
final PositionsModel positionsModel,
final int[] rows,
final OverwritePredicate predicate) {
executeOperation(positionsTable, positionsModel, rows, false, predicate,
new Operation() {
private int predecessorIndex, successorIndex;
public String getName() {
return "TimePositionAugmenter";
}
public int getColumnIndex() {
return DATE_TIME_COLUMN_INDEX;
}
public void performOnStart() {
predecessorIndex = findPredecessorWithTime(positionsModel, rows[0]);
successorIndex = findSuccessorWithTime(positionsModel, rows[rows.length-1]);
}
public boolean run(int index, NavigationPosition position) throws Exception {
if (predecessorIndex != -1 && successorIndex != -1) {
CompactCalendar previousTime = position.getTime();
CompactCalendar nextTime = interpolateTime(positionsModel, index, predecessorIndex, successorIndex);
boolean changed = nextTime != null && !nextTime.equals(previousTime);
if (changed)
positionsModel.edit(index, new PositionColumnValues(DATE_TIME_COLUMN_INDEX, nextTime), false, true);
return changed;
}
return false;
}
public String getMessagePrefix() {
return "add-time-";
}
}
);
}
public void addTimes() {
int[] rows = positionsView.getSelectedRows();
if (rows.length > 0)
processTimes(positionsView, positionsModel, rows, COORDINATE_PREDICATE);
}
private int findRelativeIndex(int[] selectedIndices, int indexToSearch) {
for (int i = 0; i < selectedIndices.length; i++)
if (selectedIndices[i] == indexToSearch)
return i;
return indexToSearch;
}
private void processNumbers(final JTable positionsTable,
final PositionsModel positionsModel,
final int[] rows,
final int digitCount,
final NumberPattern numberPattern,
final NumberingStrategy numberingStrategy,
final OverwritePredicate predicate) {
executeOperation(positionsTable, positionsModel, rows, false, predicate,
new Operation() {
public String getName() {
return "NumberPositionAugmenter";
}
public int getColumnIndex() {
return DESCRIPTION_COLUMN_INDEX;
}
public void performOnStart() {
}
public boolean run(int index, NavigationPosition position) throws Exception {
String previousDescription = position.getDescription();
int number = numberingStrategy.equals(Absolute_Position_Within_Position_List) ? index : findRelativeIndex(rows, index);
String nextDescription = getNumberedPosition(position, number, digitCount, numberPattern);
boolean changed = nextDescription != null && !nextDescription.equals(previousDescription);
if (changed)
positionsModel.edit(index, new PositionColumnValues(DESCRIPTION_COLUMN_INDEX, nextDescription), false, true);
return changed;
}
public String getMessagePrefix() {
return "add-number-";
}
}
);
}
public void addNumbers() {
int[] rows = positionsView.getSelectedRows();
if (rows.length == 0)
return;
int digitCount = widthInDigits(positionsModel.getRowCount() + 1);
NumberPattern numberPattern = RouteConverter.getInstance().getNumberPatternPreference();
NumberingStrategy numberingStrategy = RouteConverter.getInstance().getNumberingStrategyPreference();
processNumbers(positionsView, positionsModel, rows, digitCount, numberPattern, numberingStrategy, COORDINATE_PREDICATE);
}
private void addData(final JTable positionsTable,
final PositionsModel positionsModel,
final int[] rows,
final OverwritePredicate predicate,
final boolean complementDescription,
final boolean complementTime,
final boolean complementElevation,
final boolean waitForDownload,
final boolean trackUndo) {
executeOperation(positionsTable, positionsModel, rows, true, predicate,
new Operation() {
private int predecessorIndex, successorIndex;
public String getName() {
return "DataPositionAugmenter";
}
public int getColumnIndex() {
return ALL_COLUMNS; // might be DESCRIPTION_COLUMN_INDEX, ELEVATION_COLUMN_INDEX, DATE_TIME_COLUMN_INDEX
}
public void performOnStart() {
predecessorIndex = findPredecessorWithTime(positionsModel, rows[0]);
successorIndex = findSuccessorWithTime(positionsModel, rows[rows.length-1]);
downloadElevationData(rows, waitForDownload);
}
public boolean run(int index, NavigationPosition position) throws Exception {
List<Integer> columnIndices = new ArrayList<>(3);
List<Object> columnValues = new ArrayList<>(3);
if (complementDescription) {
String nextDescription = waitForDownload ? geocodingServiceFacade.getAddressFor(position) : null;
if (nextDescription != null)
nextDescription = createDescription(index + 1, nextDescription);
String previousDescription = position.getDescription();
boolean changed = nextDescription != null && !nextDescription.equals(previousDescription);
if (changed) {
columnIndices.add(DESCRIPTION_COLUMN_INDEX);
columnValues.add(nextDescription);
}
}
if (complementElevation) {
String previousElevation = formatElevation(position.getElevation());
String nextElevation = waitForDownload || elevationServiceFacade.isDownload() ?
getElevationFor(position) : null;
boolean changed = nextElevation != null && !nextElevation.equals(previousElevation);
if (changed) {
columnIndices.add(ELEVATION_COLUMN_INDEX);
columnValues.add(nextElevation);
}
}
if (complementTime) {
if (predecessorIndex != -1 && successorIndex != -1) {
CompactCalendar previousTime = position.getTime();
CompactCalendar nextTime = interpolateTime(positionsModel, index, predecessorIndex, successorIndex);
boolean changed = nextTime != null && !nextTime.equals(previousTime);
if (changed) {
columnIndices.add(DATE_TIME_COLUMN_INDEX);
columnValues.add(nextTime);
}
}
}
positionsModel.edit(index, new PositionColumnValues(columnIndices, columnValues), false, trackUndo);
return complementDescription && columnIndices.contains(DESCRIPTION_COLUMN_INDEX) &&
complementElevation && columnIndices.contains(ELEVATION_COLUMN_INDEX) &&
complementTime && columnIndices.contains(DATE_TIME_COLUMN_INDEX);
}
public String getMessagePrefix() {
String messageKey = "add-data-";
if (complementDescription)
messageKey = "add-description-";
else if (complementElevation)
messageKey = "add-elevation-";
else if (complementTime)
messageKey = "add-time-";
return messageKey;
}
}
);
}
public void addData(int[] rows, boolean description, boolean time, boolean elevation, boolean waitForDownload, boolean trackUndo) {
addData(positionsView, positionsModel, rows, COORDINATE_PREDICATE, description, time, elevation, waitForDownload, trackUndo);
}
public String createDescription(int index, String description) {
if (description == null)
description = RouteConverter.getBundle().getString("new-position-name");
NumberPattern numberPattern = RouteConverter.getInstance().getNumberPatternPreference();
String number = Integer.toString(index);
return formatNumberedPosition(numberPattern, number, description);
}
}