/*
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.mapview;
import slash.common.type.CompactCalendar;
import slash.navigation.base.BaseNavigationPosition;
import slash.navigation.base.BaseRoute;
import slash.navigation.base.RouteCharacteristics;
import slash.navigation.common.BoundingBox;
import slash.navigation.common.NavigationPosition;
import slash.navigation.common.PositionPair;
import slash.navigation.common.SimpleNavigationPosition;
import slash.navigation.converter.gui.models.*;
import slash.navigation.nmn.NavigatingPoiWarnerFormat;
import javax.swing.event.*;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.io.*;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.logging.Logger;
import java.util.prefs.Preferences;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.lang.Boolean.parseBoolean;
import static java.lang.Character.isLetterOrDigit;
import static java.lang.Character.isWhitespace;
import static java.lang.Math.max;
import static java.lang.Math.min;
import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.sleep;
import static java.util.Arrays.asList;
import static java.util.Calendar.SECOND;
import static java.util.concurrent.Executors.newCachedThreadPool;
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.ListDataEvent.CONTENTS_CHANGED;
import static javax.swing.event.TableModelEvent.*;
import static slash.common.helpers.ThreadHelper.safeJoin;
import static slash.common.io.Transfer.*;
import static slash.common.type.CompactCalendar.fromCalendar;
import static slash.navigation.base.RouteCharacteristics.*;
import static slash.navigation.converter.gui.models.CharacteristicsModel.IGNORE;
import static slash.navigation.converter.gui.models.PositionColumns.*;
import static slash.navigation.gui.events.Range.asRange;
import static slash.navigation.gui.helpers.JTableHelper.isFirstToLastRow;
/**
* Base implementation for a component that displays the positions of a position list on a map.
*
* @author Christian Pesch
*/
public abstract class BaseMapView implements MapView {
protected static final Preferences preferences = Preferences.userNodeForPackage(MapView.class);
protected static final Logger log = Logger.getLogger(MapView.class.getName());
protected static final String MAP_TYPE_PREFERENCE = "mapType";
private static final String CLEAN_ELEVATION_ON_MOVE_PREFERENCE = "cleanElevationOnMove";
private static final String COMPLEMENT_ELEVATION_ON_MOVE_PREFERENCE = "complementElevationOnMove";
private static final String CLEAN_TIME_ON_MOVE_PREFERENCE = "cleanTimeOnMove";
private static final String COMPLEMENT_TIME_ON_MOVE_PREFERENCE = "complementTimeOnMove";
private static final String MOVE_COMPLETE_SELECTION_PREFERENCE = "moveCompleteSelection";
private static final String CENTER_LATITUDE_PREFERENCE = "centerLatitude";
private static final String CENTER_LONGITUDE_PREFERENCE = "centerLongitude";
private static final String CENTER_ZOOM_PREFERENCE = "centerZoom";
private static final String RECENTER_MAP_PREFERENCE = "recenterMap";
private PositionsModel positionsModel;
private List<NavigationPosition> positions;
private PositionsSelectionModel positionsSelectionModel;
private List<NavigationPosition> lastSelectedPositions = new ArrayList<>();
private int[] selectedPositionIndices = new int[0];
private int lastZoom = -1;
private ServerSocket callbackListenerServerSocket;
private Thread positionListUpdater, selectionUpdater, callbackListener, callbackPoller;
protected final Object notificationMutex = new Object();
protected boolean initialized = false;
private boolean recenterAfterZooming, showCoordinates, showWaypointDescription, running = true,
haveToInitializeMapOnFirstStart = true, haveToRepaintSelectionImmediately = false,
haveToRepaintRouteImmediately = false, haveToRecenterMap = false,
haveToUpdateRoute = false, haveToReplaceRoute = false,
haveToRepaintSelection = false, ignoreNextZoomCallback = false;
private UnitSystemModel unitSystemModel;
private String routeUpdateReason = "?", selectionUpdateReason = "?";
private MapViewCallback mapViewCallback;
private PositionReducer positionReducer;
private final ExecutorService executor = newCachedThreadPool();
private int overQueryLimitCount = 0, zeroResultsCount = 0;
// initialization
public void initialize(PositionsModel positionsModel,
PositionsSelectionModel positionsSelectionModel,
CharacteristicsModel characteristicsModel,
MapViewCallback mapViewCallback,
boolean recenterAfterZooming,
boolean showCoordinates, boolean showWaypointDescription,
UnitSystemModel unitSystemModel) {
this.mapViewCallback = mapViewCallback;
initializeBrowser();
setModel(positionsModel, positionsSelectionModel, characteristicsModel, unitSystemModel);
this.recenterAfterZooming = recenterAfterZooming;
this.showCoordinates = showCoordinates;
this.showWaypointDescription = showWaypointDescription;
}
protected abstract void initializeBrowser();
protected void setModel(final PositionsModel positionsModel,
PositionsSelectionModel positionsSelectionModel,
CharacteristicsModel characteristicsModel,
final UnitSystemModel unitSystemModel) {
this.positionsModel = positionsModel;
this.positionsSelectionModel = positionsSelectionModel;
this.unitSystemModel = unitSystemModel;
positionsModel.addTableModelListener(new TableModelListener() {
public void tableChanged(TableModelEvent e) {
boolean insertOrDelete = e.getType() == INSERT || e.getType() == DELETE;
boolean allRowsChanged = isFirstToLastRow(e);
// used to be limited to single rows which did work reliably but with usabilty problems
// if (e.getFirstRow() == e.getLastRow() && insertOrDelete)
if (!allRowsChanged && insertOrDelete)
updateRouteButDontRecenter();
else {
// ignored updates on columns not displayed
if (e.getType() == UPDATE &&
!(e.getColumn() == DESCRIPTION_COLUMN_INDEX ||
e.getColumn() == LONGITUDE_COLUMN_INDEX ||
e.getColumn() == LATITUDE_COLUMN_INDEX ||
e.getColumn() == ALL_COLUMNS))
return;
update(allRowsChanged || insertOrDelete);
}
// update position marker on updates of longitude and latitude
if (e.getType() == UPDATE &&
(e.getColumn() == LONGITUDE_COLUMN_INDEX ||
e.getColumn() == LATITUDE_COLUMN_INDEX ||
e.getColumn() == DESCRIPTION_COLUMN_INDEX ||
e.getColumn() == ALL_COLUMNS)) {
for (int selectedPositionIndex : selectedPositionIndices) {
if (selectedPositionIndex >= e.getFirstRow() && selectedPositionIndex <= e.getLastRow()) {
updateSelection();
break;
}
}
}
}
});
characteristicsModel.addListDataListener(new ListDataListener() {
public void intervalAdded(ListDataEvent e) {
}
public void intervalRemoved(ListDataEvent e) {
}
public void contentsChanged(ListDataEvent e) {
// ignore events following setRoute()
if (e.getType() == CONTENTS_CHANGED && e.getIndex0() == IGNORE && e.getIndex1() == IGNORE)
return;
updateRouteButDontRecenter();
}
});
unitSystemModel.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent e) {
setDegreeFormat();
}
});
mapViewCallback.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent e) {
if (positionsModel.getRoute().getCharacteristics().equals(Route))
update(false);
}
});
positionReducer = new PositionReducer(new PositionReducer.Callback() {
public int getZoom() {
return BaseMapView.this.getZoom();
}
public NavigationPosition getNorthEastBounds() {
return BaseMapView.this.getNorthEastBounds();
}
public NavigationPosition getSouthWestBounds() {
return BaseMapView.this.getSouthWestBounds();
}
});
}
private Throwable initializationCause = null;
public Throwable getInitializationCause() {
return initializationCause;
}
protected void setInitializationCause(Throwable initializationCause) {
this.initializationCause = initializationCause;
}
public boolean isInitialized() {
synchronized (this) {
return initialized;
}
}
protected void initializeBrowserInteraction() {
getComponent().addComponentListener(new ComponentListener() {
public void componentResized(ComponentEvent e) {
resize();
}
public void componentMoved(ComponentEvent e) {
}
public void componentShown(ComponentEvent e) {
}
public void componentHidden(ComponentEvent e) {
}
});
positionListUpdater = new Thread(new Runnable() {
public void run() {
long lastTime = 0;
boolean recenter;
while (true) {
List<NavigationPosition> copiedPositions;
synchronized (notificationMutex) {
try {
notificationMutex.wait(1000);
} catch (InterruptedException e) {
// ignore this
}
if (!running)
return;
if (!hasPositions())
continue;
if (!isVisible())
continue;
/*
Update conditions:
- new route was loaded
- clear cache
- center map
- set zoom level according to route bounds
- repaint immediately
- user has moved position
- clear cache
- stay on current zoom level
- center map to position
- repaint
- user has removed position
- clear cache
- stay on current zoom level
- repaint
- user has zoomed map
- repaint if zooming into the map as it reveals more details
- user has moved map
- repaint if moved
*/
long currentTime = currentTimeMillis();
if (haveToRepaintRouteImmediately ||
haveToReplaceRoute ||
(haveToUpdateRoute && (currentTime - lastTime > 5 * 1000))) {
log.fine("Woke up to update route: " + routeUpdateReason +
" haveToUpdateRoute:" + haveToUpdateRoute +
" haveToReplaceRoute:" + haveToReplaceRoute +
" haveToRepaintRouteImmediately:" + haveToRepaintRouteImmediately);
copiedPositions = new ArrayList<>(positions);
recenter = isRecenteringMap() && haveToReplaceRoute;
haveToUpdateRoute = false;
haveToReplaceRoute = false;
haveToRepaintRouteImmediately = false;
} else
continue;
}
setCenterOfMap(copiedPositions, recenter);
RouteCharacteristics characteristics = positionsModel.getRoute().getCharacteristics();
List<NavigationPosition> render = positionReducer.reducePositions(copiedPositions, characteristics, showWaypointDescription);
switch (characteristics) {
case Route:
addDirectionsToMap(render);
break;
case Track:
addPolylinesToMap(render);
break;
case Waypoints:
addMarkersToMap(render);
break;
default:
throw new IllegalArgumentException("RouteCharacteristics " + characteristics + " is not supported");
}
log.info("Position list updated for " + render.size() + " positions of type " +
characteristics + ", recentering: " + recenter);
lastTime = currentTimeMillis();
}
}
}, "MapViewPositionListUpdater");
positionListUpdater.start();
selectionUpdater = new Thread(new Runnable() {
public void run() {
long lastTime = 0;
while (true) {
int[] copiedSelectedPositionIndices;
List<NavigationPosition> copiedPositions;
boolean recenter;
synchronized (notificationMutex) {
try {
notificationMutex.wait(250);
} catch (InterruptedException e) {
// ignore this
}
if (!running)
return;
if (!hasPositions())
continue;
if (!isVisible())
continue;
long currentTime = currentTimeMillis();
if (haveToRecenterMap || haveToRepaintSelectionImmediately ||
(haveToRepaintSelection && (currentTime - lastTime > 500))) {
log.fine("Woke up to update selected positions: " + selectionUpdateReason +
" haveToRepaintSelection: " + haveToRepaintSelection +
" haveToRepaintSelectionImmediately: " + haveToRepaintSelectionImmediately +
" haveToRecenterMap: " + haveToRecenterMap);
recenter = isRecenteringMap() && haveToRecenterMap;
haveToRecenterMap = false;
haveToRepaintSelectionImmediately = false;
haveToRepaintSelection = false;
copiedSelectedPositionIndices = new int[selectedPositionIndices.length];
System.arraycopy(selectedPositionIndices, 0, copiedSelectedPositionIndices, 0, copiedSelectedPositionIndices.length);
copiedPositions = new ArrayList<>(positions);
} else
continue;
}
List<NavigationPosition> render = positionReducer.reduceSelectedPositions(copiedPositions, copiedSelectedPositionIndices);
NavigationPosition centerPosition = render.size() > 0 ? new BoundingBox(render).getCenter() : null;
selectPositions(render, recenter ? centerPosition : null);
log.info("Selected positions updated for " + render.size() + " positions, recentering: " + recenter + " to: " + centerPosition);
lastTime = currentTimeMillis();
}
}
}, "MapViewSelectionUpdater");
selectionUpdater.start();
}
private ServerSocket createCallbackListenerServerSocket() {
try {
ServerSocket serverSocket = new ServerSocket(0, 0, InetAddress.getByAddress(new byte[]{127, 0, 0, 1}));
serverSocket.setSoTimeout(1000);
int port = serverSocket.getLocalPort();
log.info("Map listens on port " + port + " for callbacks");
setCallbackListenerPort(port);
return serverSocket;
} catch (IOException e) {
log.severe("Cannot open callback listener socket: " + e);
return null;
}
}
protected void initializeCallbackListener() {
callbackListenerServerSocket = createCallbackListenerServerSocket();
if (callbackListenerServerSocket == null)
return;
callbackListener = new Thread(new Runnable() {
public void run() {
while (true) {
synchronized (notificationMutex) {
if (!running) {
return;
}
}
try {
final Socket socket = callbackListenerServerSocket.accept();
executor.execute(new Runnable() {
public void run() {
try {
processStream(socket);
} catch (IOException e) {
log.severe("Cannot process stream from callback listener socket: " + e);
}
}
});
} catch (SocketTimeoutException e) {
// intentionally left empty
} catch (IOException e) {
synchronized (notificationMutex) {
//noinspection ConstantConditions
if (running) {
log.severe("Cannot accept callback listener socket: " + e);
}
}
}
}
}
}, "MapViewCallbackListener");
callbackListener.start();
}
protected void initializeCallbackPoller() {
callbackPoller = new Thread(new Runnable() {
public void run() {
while (true) {
synchronized (notificationMutex) {
if (!running) {
return;
}
}
String callbacks = trim(getCallbacks());
if (callbacks != null) {
String[] lines = callbacks.split("--");
for (String line : lines) {
processCallback(line);
}
}
try {
sleep(250);
} catch (InterruptedException e) {
// intentionally left empty
}
}
}
}, "MapViewCallbackPoller");
callbackPoller.start();
}
protected void checkLocalhostResolution() {
try {
InetAddress localhost = InetAddress.getByName("localhost");
log.info("localhost is resolved to: " + localhost);
String localhostName = localhost.getHostAddress();
log.info("IP of localhost is: " + localhostName);
if (!localhostName.equals("127.0.0.1"))
throw new Exception("localhost does not resolve to 127.0.0.1");
InetAddress ip = InetAddress.getByAddress(new byte[]{127, 0, 0, 1});
log.info("127.0.0.1 is resolved to: " + ip);
String ipName = localhost.getHostName();
log.info("Name of 127.0.0.1 is: " + ipName);
if (!ipName.equals("localhost"))
throw new Exception("127.0.0.1 does not resolve to localhost");
} catch (Exception e) {
e.printStackTrace();
final String message = "Probably faulty network setup: " + e.getLocalizedMessage() + ".\nPlease check your network settings.";
log.severe(message);
invokeLater(new Runnable() {
public void run() {
showMessageDialog(getComponent(), message, "Error", ERROR_MESSAGE);
}
});
}
}
protected void checkCallback() {
final Boolean[] receivedCallback = new Boolean[1];
receivedCallback[0] = false;
final MapViewListener callbackWaiter = new AbstractMapViewListener() {
public void receivedCallback(int port) {
synchronized (receivedCallback) {
receivedCallback[0] = true;
receivedCallback.notifyAll();
}
}
};
executor.execute(new Runnable() {
public void run() {
addMapViewListener(callbackWaiter);
try {
executeScript("checkCallbackListenerPort();");
long start = currentTimeMillis();
while (true) {
synchronized (receivedCallback) {
if (receivedCallback[0]) {
long end = currentTimeMillis();
log.info("Received callback from browser after " + (end - start) + " milliseconds");
break;
}
}
if (start + 5000 < currentTimeMillis())
break;
try {
sleep(50);
} catch (InterruptedException e) {
// intentionally left empty
}
}
synchronized (receivedCallback) {
if (!receivedCallback[0]) {
setCallbackListenerPort(-1);
initializeCallbackPoller();
log.warning("Switched from callback to polling the browser");
}
}
} finally {
removeMapViewListener(callbackWaiter);
}
}
});
}
// disposal
public void dispose() {
long start = currentTimeMillis();
synchronized (notificationMutex) {
running = false;
notificationMutex.notifyAll();
}
if (selectionUpdater != null) {
try {
safeJoin(selectionUpdater);
} catch (InterruptedException e) {
// intentionally left empty
}
long end = currentTimeMillis();
log.info("PositionUpdater stopped after " + (end - start) + " ms");
}
if (positionListUpdater != null) {
try {
safeJoin(positionListUpdater);
} catch (InterruptedException e) {
// intentionally left empty
}
long end = currentTimeMillis();
log.info("RouteUpdater stopped after " + (end - start) + " ms");
}
if (callbackListenerServerSocket != null) {
try {
callbackListenerServerSocket.close();
} catch (IOException e) {
log.warning("Cannot close callback listener socket:" + e);
}
long end = currentTimeMillis();
log.info("CallbackListenerSocket stopped after " + (end - start) + " ms");
}
if (callbackListener != null) {
try {
safeJoin(callbackListener);
} catch (InterruptedException e) {
// intentionally left empty
}
long end = currentTimeMillis();
log.info("CallbackListener stopped after " + (end - start) + " ms");
}
if (callbackPoller != null && callbackPoller.isAlive()) {
try {
safeJoin(callbackPoller);
} catch (InterruptedException e) {
// intentionally left empty
}
long end = currentTimeMillis();
log.info("CallbackPoller stopped after " + (end - start) + " ms");
}
executor.shutdownNow();
insertWaypointsExecutor.shutdownNow();
long end = currentTimeMillis();
log.info("Executors stopped after " + (end - start) + " ms");
}
// getter and setter
protected boolean isVisible() {
return getComponent().getWidth() > 0;
}
private boolean hasPositions() {
synchronized (notificationMutex) {
return isInitialized() && positions != null;
}
}
private void setCallbackListenerPort(int callbackListenerPort) {
synchronized (notificationMutex) {
executeScript("setCallbackListenerPort(" + callbackListenerPort + ")");
}
}
public void setSelectedPositions(int[] selectedPositions, boolean replaceSelection) {
synchronized (notificationMutex) {
if (replaceSelection)
this.selectedPositionIndices = selectedPositions;
else {
int[] indices = new int[selectedPositionIndices.length + selectedPositions.length];
System.arraycopy(selectedPositionIndices, 0, indices, 0, selectedPositionIndices.length);
System.arraycopy(selectedPositions, 0, indices, selectedPositionIndices.length, selectedPositions.length);
this.selectedPositionIndices = indices;
}
haveToRecenterMap = selectedPositions.length > 0;
haveToRepaintSelection = true;
selectionUpdateReason = "selected " + selectedPositions.length + " positions; " +
"replacing selection: " + replaceSelection;
notificationMutex.notifyAll();
}
}
public void setRecenterAfterZooming(boolean recenterAfterZooming) {
this.recenterAfterZooming = recenterAfterZooming;
}
public void setShowCoordinates(boolean showCoordinates) {
this.showCoordinates = showCoordinates;
setShowCoordinates();
}
public void setShowWaypointDescription(boolean showWaypointDescription) {
this.showWaypointDescription = showWaypointDescription;
if (positionsModel.getRoute().getCharacteristics() == Waypoints)
update(false);
}
protected void setShowCoordinates() {
executeScript("setShowCoordinates(" + showCoordinates + ");");
}
protected void setDegreeFormat() {
executeScript("setDegreeFormat('" + unitSystemModel.getDegreeFormat() + "');");
}
public void showMapBorder(BoundingBox mapBoundingBox) {
throw new UnsupportedOperationException();
}
public NavigationPosition getCenter() {
if (isInitialized())
return getCurrentMapCenter();
else
return getLastMapCenter();
}
private int getZoom() {
return preferences.getInt(CENTER_ZOOM_PREFERENCE, 2);
}
private void setZoom(int zoom) {
preferences.putInt(CENTER_ZOOM_PREFERENCE, zoom);
}
private boolean isRecenteringMap() {
return preferences.getBoolean(RECENTER_MAP_PREFERENCE, true);
}
protected abstract NavigationPosition getNorthEastBounds();
protected abstract NavigationPosition getSouthWestBounds();
protected abstract NavigationPosition getCurrentMapCenter();
protected abstract Integer getCurrentZoom();
protected abstract String getCallbacks();
private NavigationPosition getLastMapCenter() {
double latitude = preferences.getDouble(CENTER_LATITUDE_PREFERENCE, 35.0);
double longitude = preferences.getDouble(CENTER_LONGITUDE_PREFERENCE, -25.0);
return new SimpleNavigationPosition(longitude, latitude);
}
protected NavigationPosition extractLatLng(String script) {
String result = executeScriptWithResult(script);
if (result == null)
return null;
StringTokenizer tokenizer = new StringTokenizer(result, ",");
if (tokenizer.countTokens() != 2)
return null;
String latitude = tokenizer.nextToken();
String longitude = tokenizer.nextToken();
return new SimpleNavigationPosition(parseDouble(longitude), parseDouble(latitude));
}
// draw on map
@SuppressWarnings({"unchecked"})
protected void update(boolean haveToReplaceRoute) {
if (!isInitialized() || !getComponent().isShowing())
return;
synchronized (notificationMutex) {
this.positions = positionsModel.getRoute() != null ? positionsModel.getRoute().getPositions() : null;
this.haveToUpdateRoute = true;
routeUpdateReason = "update route";
if (haveToReplaceRoute) {
this.haveToReplaceRoute = true;
routeUpdateReason = "replace route";
positionReducer.clear();
this.haveToRepaintSelection = true;
selectionUpdateReason = "replace route";
}
notificationMutex.notifyAll();
}
}
private void updateRouteButDontRecenter() {
// repaint route immediately, simulates update(true) without recentering
synchronized (notificationMutex) {
haveToRepaintRouteImmediately = true;
routeUpdateReason = "update route but don't recenter";
positionReducer.clear();
notificationMutex.notifyAll();
}
}
private void updateSelection() {
synchronized (notificationMutex) {
haveToRepaintSelection = true;
selectionUpdateReason = "update selection";
notificationMutex.notifyAll();
}
}
private void removeOverlays() {
executeScript("removeOverlays();");
}
private void removeDirections() {
executeScript("removeDirections();");
}
private void addDirectionsToMap(List<NavigationPosition> positions) {
executeScript("resetDirections();");
// avoid throwing javascript exceptions if there is nothing to direct
if (positions.size() < 2) {
addMarkersToMap(positions);
return;
}
removeOverlays();
String color = preferences.get("routeLineColor", "6CB1F3");
int width = preferences.getInt("routeLineWidth", 5);
int maximumRouteSegmentLength = positionReducer.getMaximumSegmentLength(Route);
int directionsCount = ceiling(positions.size(), maximumRouteSegmentLength, false);
for (int j = 0; j < directionsCount; j++) {
StringBuilder waypoints = new StringBuilder();
int start = max(0, j * maximumRouteSegmentLength - 1);
int end = min(positions.size(), (j + 1) * maximumRouteSegmentLength) - 1;
for (int i = start + 1; i < end; i++) {
NavigationPosition position = positions.get(i);
waypoints.append("{location: new google.maps.LatLng(").append(position.getLatitude()).append(",").
append(position.getLongitude()).append(")}");
if (i < end - 1)
waypoints.append(",");
}
NavigationPosition origin = positions.get(start);
NavigationPosition destination = positions.get(end);
StringBuilder buffer = new StringBuilder();
buffer.append("renderDirections({origin: new google.maps.LatLng(").append(origin.getLatitude()).
append(",").append(origin.getLongitude()).append("), ");
buffer.append("destination: new google.maps.LatLng(").append(destination.getLatitude()).
append(",").append(destination.getLongitude()).append("), ");
buffer.append("waypoints: [").append(waypoints).append("], ").
append("travelMode: google.maps.DirectionsTravelMode.").append(mapViewCallback.getTravelMode().getName().toUpperCase()).append(", ");
buffer.append("avoidFerries: ").append(mapViewCallback.isAvoidFerries()).append(", ");
buffer.append("avoidHighways: ").append(mapViewCallback.isAvoidHighways()).append(", ");
buffer.append("avoidTolls: ").append(mapViewCallback.isAvoidTolls()).append(", ");
buffer.append("region: \"").append(Locale.getDefault().getCountry().toLowerCase()).append("\"}, ");
int startIndex = positionsModel.getIndex(origin);
buffer.append(startIndex).append(", ");
boolean lastSegment = (j == directionsCount - 1);
buffer.append(lastSegment).append(",\"#").append(color).append("\",").append(width).append(");\n");
try {
sleep(preferences.getInt("routeSegmentTimeout", 250));
} catch (InterruptedException e) {
// intentionally left empty
}
executeScript(buffer.toString());
}
}
private void addPolylinesToMap(final List<NavigationPosition> positions) {
// display markers if there is no polyline to show
if (positions.size() < 2) {
addMarkersToMap(positions);
return;
}
String color = preferences.get("trackLineColor", "0033FF");
int width = preferences.getInt("trackLineWidth", 2);
int maximumPolylineSegmentLength = positionReducer.getMaximumSegmentLength(Track);
int polylinesCount = ceiling(positions.size(), maximumPolylineSegmentLength, true);
for (int j = 0; j < polylinesCount; j++) {
StringBuilder latlngs = new StringBuilder();
int maximum = min(positions.size(), (j + 1) * maximumPolylineSegmentLength + 1);
for (int i = j * maximumPolylineSegmentLength; i < maximum; i++) {
NavigationPosition position = positions.get(i);
latlngs.append("new google.maps.LatLng(").append(position.getLatitude()).append(",").
append(position.getLongitude()).append(")");
if (i < maximum - 1)
latlngs.append(",");
}
executeScript("addPolyline([" + latlngs + "],\"#" + color + "\"," + width + ");");
}
removeOverlays();
removeDirections();
}
private void addMarkersToMap(List<NavigationPosition> positions) {
int maximumMarkerSegmentLength = positionReducer.getMaximumSegmentLength(Waypoints);
int markersCount = ceiling(positions.size(), maximumMarkerSegmentLength, false);
for (int j = 0; j < markersCount; j++) {
StringBuilder buffer = new StringBuilder();
int maximum = min(positions.size(), (j + 1) * maximumMarkerSegmentLength);
for (int i = j * maximumMarkerSegmentLength; i < maximum; i++) {
NavigationPosition position = positions.get(i);
buffer.append("addMarker(").append(position.getLatitude()).append(",").
append(position.getLongitude()).append(",").
append("\"").append(escape(position.getDescription())).append("\",").
append(showWaypointDescription).append(");\n");
}
executeScript(buffer.toString());
}
removeOverlays();
removeDirections();
}
private void setCenterOfMap(List<NavigationPosition> positions, boolean recenter) {
StringBuilder buffer = new StringBuilder();
boolean fitBoundsToPositions = positions.size() > 0 && recenter;
if (fitBoundsToPositions) {
BoundingBox boundingBox = new BoundingBox(positions);
buffer.append("fitBounds(").append(boundingBox.getSouthWest().getLatitude()).append(",").
append(boundingBox.getSouthWest().getLongitude()).append(",").
append(boundingBox.getNorthEast().getLatitude()).append(",").
append(boundingBox.getNorthEast().getLongitude()).append(");\n");
ignoreNextZoomCallback = true;
}
if (haveToInitializeMapOnFirstStart) {
NavigationPosition center;
// if there are positions right at the start center on them else take the last known center and zoom
if (positions.size() > 0) {
center = new BoundingBox(positions).getCenter();
} else {
int zoom = getZoom();
buffer.append("setZoom(").append(zoom).append(");\n");
center = getLastMapCenter();
}
buffer.append("setCenter(").append(center.getLatitude()).append(",").append(center.getLongitude()).append(");\n");
}
executeScript(buffer.toString());
haveToInitializeMapOnFirstStart = false;
if (fitBoundsToPositions) {
// need to update zoom since fitBounds() changes the zoom level without firing a notification
Integer zoom = getCurrentZoom();
if (zoom != null)
setZoom(zoom);
}
}
private void selectPositions(List<NavigationPosition> selectedPositions, NavigationPosition center) {
lastSelectedPositions = new ArrayList<>(selectedPositions);
StringBuilder buffer = new StringBuilder();
for (int i = 0; i < selectedPositions.size(); i++) {
NavigationPosition selectedPosition = selectedPositions.get(i);
buffer.append("selectPosition(").append(selectedPosition.getLatitude()).append(",").
append(selectedPosition.getLongitude()).append(",").
append("\"").append(escape(selectedPosition.getDescription())).append("\",").
append(i).append(");\n");
}
if (center != null && center.hasCoordinates())
buffer.append("panTo(").append(center.getLatitude()).append(",").append(center.getLongitude()).append(");\n");
buffer.append("removeSelectedPositions();");
executeScript(buffer.toString());
}
private final Map<Integer, PositionPair> insertWaypointsQueue = new LinkedHashMap<Integer, PositionPair>();
private final ExecutorService insertWaypointsExecutor = newSingleThreadExecutor();
private void insertWaypoints(final String mode, int[] startPositions) {
final Map<Integer, PositionPair> addToQueue = new LinkedHashMap<Integer, PositionPair>();
Random random = new Random();
synchronized (notificationMutex) {
for (int i = 0; i < startPositions.length; i++) {
// skip the very last position without successor
if (i == positions.size() - 1 || i == startPositions.length - 1)
continue;
addToQueue.put(random.nextInt(), new PositionPair(positions.get(startPositions[i]), positions.get(startPositions[i] + 1)));
}
}
synchronized (insertWaypointsQueue) {
insertWaypointsQueue.putAll(addToQueue);
}
insertWaypointsExecutor.execute(new Runnable() {
public void run() {
for (Integer key : addToQueue.keySet()) {
PositionPair pair = addToQueue.get(key);
NavigationPosition origin = pair.getFirst();
NavigationPosition destination = pair.getSecond();
StringBuilder buffer = new StringBuilder();
buffer.append(mode).append("({");
buffer.append("origin: new google.maps.LatLng(").append(origin.getLatitude()).append(",").append(origin.getLongitude()).append("), ");
buffer.append("destination: new google.maps.LatLng(").append(destination.getLatitude()).append(",").append(destination.getLongitude()).append("), ");
buffer.append("travelMode: google.maps.DirectionsTravelMode.").append(mapViewCallback.getTravelMode().getName().toUpperCase()).append(", ");
buffer.append("avoidFerries: ").append(mapViewCallback.isAvoidFerries()).append(", ");
buffer.append("avoidHighways: ").append(mapViewCallback.isAvoidHighways()).append(", ");
buffer.append("avoidTolls: ").append(mapViewCallback.isAvoidTolls()).append(", ");
buffer.append("region: \"").append(Locale.getDefault().getCountry().toLowerCase()).append("\"}, ");
buffer.append(key).append(");\n");
executeScript(buffer.toString());
try {
sleep(preferences.getInt("insertWaypointsSegmentTimeout", 1000));
} catch (InterruptedException e) {
// intentionally left empty
}
}
}
});
}
// call Google Maps API functions
public void insertAllWaypoints(int[] startPositions) {
insertWaypoints("insertAllWaypoints", startPositions);
}
public void insertOnlyTurnpoints(int[] startPositions) {
insertWaypoints("insertOnlyTurnpoints", startPositions);
}
public void print(String title, boolean withDirections) {
executeScript("printMap(\"" + title + "\", " + withDirections + ");");
}
// script execution
private String escape(String string) {
if (string == null)
return "";
StringBuilder buffer = new StringBuilder(string);
for (int i = 0; i < buffer.length(); i++) {
char c = buffer.charAt(i);
if (!(isLetterOrDigit(c) || isWhitespace(c) || c == '\'' || c == ',')) {
buffer.deleteCharAt(i);
i--;
}
}
return buffer.toString();
}
protected void logJavaScript(String script, Object result) {
log.info("script '" + script + (result != null ? "'\nwith result '" + result : "") + "'");
}
protected abstract void executeScript(String script);
protected abstract String executeScriptWithResult(String script);
// browser callbacks
private void processStream(Socket socket) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()), 64 * 1024);
OutputStream outputStream = socket.getOutputStream();
List<String> lines = new ArrayList<String>();
boolean processingPost = false, processingBody = false;
try {
while (true) {
try {
String line = trim(reader.readLine());
// log.fine("read line " + line);
if (line == null) {
if (processingPost && !processingBody) {
processingBody = true;
continue;
} else
break;
}
if (line.startsWith("POST"))
processingPost = true;
lines.add(line);
} catch (IOException e) {
log.severe("Cannot read line from callback listener port:" + e);
break;
}
}
} finally {
reader.close();
outputStream.close();
}
StringBuilder buffer = new StringBuilder();
for (String line : lines) {
buffer.append(" ").append(line).append("\n");
}
log.fine("processing callback: \n" + buffer.toString());
if (!isAuthenticated(lines))
return;
processLines(lines);
}
private boolean isAuthenticated(List<String> lines) {
Map<String, String> map = asMap(lines);
String host = trim(map.get("Host"));
return host != null && host.equals("127.0.0.1:" + getCallbackPort());
}
int getCallbackPort() {
return callbackListenerServerSocket.getLocalPort();
}
private static final Pattern NAME_VALUE_PATTERN = Pattern.compile("^(.+?):(.+)$");
private Map<String, String> asMap(List<String> lines) {
Map<String, String> map = new HashMap<>();
for (String line : lines) {
Matcher matcher = NAME_VALUE_PATTERN.matcher(line);
if (matcher.matches())
map.put(matcher.group(1), matcher.group(2));
}
return map;
}
private static final Pattern CALLBACK_REQUEST_PATTERN = Pattern.compile("^(GET|OPTIONS|POST) /(\\d+)/(.*) HTTP.+$");
private int lastCallbackNumber = -1;
void processLines(List<String> lines) {
boolean hasValidCallbackNumber = false;
for (String line : lines) {
// log.fine("processing line " + line);
Matcher matcher = CALLBACK_REQUEST_PATTERN.matcher(line);
if (matcher.matches()) {
int callbackNumber = parseInt(matcher.group(2));
if (lastCallbackNumber >= callbackNumber) {
log.info("ignoring callback number: " + callbackNumber + " last callback number is: " + lastCallbackNumber);
break;
}
lastCallbackNumber = callbackNumber;
hasValidCallbackNumber = true;
String callback = matcher.group(3);
if (processCallback(callback)) {
log.fine("processed " + matcher.group(1) + " callback " + callback + " with number: " + callbackNumber);
break;
}
}
// process body of POST requests
if (hasValidCallbackNumber && processCallback(line)) {
log.fine("processed POST callback " + line + " with number: " + lastCallbackNumber);
break;
}
}
}
private static final Pattern DIRECTIONS_LOAD_PATTERN = Pattern.compile("^directions-load/(\\d*)/(\\d*)$");
private static final Pattern ADD_POSITION_PATTERN = Pattern.compile("^add-position/(.*)/(.*)$");
private static final Pattern INSERT_POSITION_PATTERN = Pattern.compile("^insert-position/(.*)/(.*)/(.*)$");
private static final Pattern MOVE_POSITION_PATTERN = Pattern.compile("^move-position/(.*)/(.*)/(.*)$");
private static final Pattern REMOVE_POSITION_PATTERN = Pattern.compile("^remove-position/(.*)/(.*)/(.*)$");
private static final Pattern SELECT_POSITION_PATTERN = Pattern.compile("^select-position/(.*)/(.*)/(.*)/(.*)$");
private static final Pattern SELECT_POSITIONS_PATTERN = Pattern.compile("^select-positions/(.*)/(.*)/(.*)/(.*)/(.*)");
private static final Pattern MAP_TYPE_CHANGED_PATTERN = Pattern.compile("^map-type-changed/(.*)$");
private static final Pattern ZOOM_CHANGED_PATTERN = Pattern.compile("^zoom-changed/(.*)$");
private static final Pattern CENTER_CHANGED_PATTERN = Pattern.compile("^center-changed/(.*)/(.*)$");
private static final Pattern CALLBACK_PORT_PATTERN = Pattern.compile("^callback-port/(\\d+)$");
private static final Pattern OVER_QUERY_LIMIT_PATTERN = Pattern.compile("^over-query-limit$");
private static final Pattern ZERO_RESULTS_PATTERN = Pattern.compile("^zero-results$");
private static final Pattern INSERT_WAYPOINTS_PATTERN = Pattern.compile("^(Insert-All-Waypoints|Insert-Only-Turnpoints): (-?\\d+)/(.*)$");
boolean processCallback(String callback) {
Matcher directionsLoadMatcher = DIRECTIONS_LOAD_PATTERN.matcher(callback);
if (directionsLoadMatcher.matches()) {
int meters = parseInt(directionsLoadMatcher.group(1));
int seconds = parseInt(directionsLoadMatcher.group(2));
fireCalculatedDistance(meters, seconds);
return true;
}
Matcher insertPositionMatcher = INSERT_POSITION_PATTERN.matcher(callback);
if (insertPositionMatcher.matches()) {
final int row = parseInt(insertPositionMatcher.group(1)) + 1;
final Double latitude = parseDouble(insertPositionMatcher.group(2));
final Double longitude = parseDouble(insertPositionMatcher.group(3));
invokeLater(new Runnable() {
public void run() {
insertPosition(row, longitude, latitude);
}
});
return true;
}
Matcher addPositionMatcher = ADD_POSITION_PATTERN.matcher(callback);
if (addPositionMatcher.matches()) {
final int row = getAddRow();
final Double latitude = parseDouble(addPositionMatcher.group(1));
final Double longitude = parseDouble(addPositionMatcher.group(2));
invokeLater(new Runnable() {
public void run() {
insertPosition(row, longitude, latitude);
}
});
return true;
}
Matcher movePositionMatcher = MOVE_POSITION_PATTERN.matcher(callback);
if (movePositionMatcher.matches()) {
final int row = getMoveRow(parseInt(movePositionMatcher.group(1)));
final Double latitude = parseDouble(movePositionMatcher.group(2));
final Double longitude = parseDouble(movePositionMatcher.group(3));
invokeLater(new Runnable() {
public void run() {
movePosition(row, longitude, latitude);
}
});
return true;
}
Matcher removePositionMatcher = REMOVE_POSITION_PATTERN.matcher(callback);
if (removePositionMatcher.matches()) {
final Double latitude = parseDouble(removePositionMatcher.group(1));
final Double longitude = parseDouble(removePositionMatcher.group(2));
final Double threshold = parseDouble(removePositionMatcher.group(3));
invokeLater(new Runnable() {
public void run() {
removePosition(longitude, latitude, threshold);
}
});
return true;
}
Matcher selectPositionMatcher = SELECT_POSITION_PATTERN.matcher(callback);
if (selectPositionMatcher.matches()) {
final Double latitude = parseDouble(selectPositionMatcher.group(1));
final Double longitude = parseDouble(selectPositionMatcher.group(2));
final Double threshold = parseDouble(selectPositionMatcher.group(3));
final Boolean replaceSelection = parseBoolean(selectPositionMatcher.group(4));
invokeLater(new Runnable() {
public void run() {
selectPosition(longitude, latitude, threshold, replaceSelection);
}
});
return true;
}
Matcher selectPositionsMatcher = SELECT_POSITIONS_PATTERN.matcher(callback);
if (selectPositionsMatcher.matches()) {
Double latitudeNorthEast = parseDouble(selectPositionsMatcher.group(1));
Double longitudeNorthEast = parseDouble(selectPositionsMatcher.group(2));
Double latitudeSouthWest = parseDouble(selectPositionsMatcher.group(3));
Double longitudeSouthWest = parseDouble(selectPositionsMatcher.group(4));
final BoundingBox boundingBox = new BoundingBox(longitudeNorthEast, latitudeNorthEast,
longitudeSouthWest, latitudeSouthWest);
final Boolean replaceSelection = parseBoolean(selectPositionsMatcher.group(5));
invokeLater(new Runnable() {
public void run() {
selectPositions(boundingBox, replaceSelection);
}
});
return true;
}
Matcher mapTypeChangedMatcher = MAP_TYPE_CHANGED_PATTERN.matcher(callback);
if (mapTypeChangedMatcher.matches()) {
String mapType = decodeUri(mapTypeChangedMatcher.group(1));
preferences.put(MAP_TYPE_PREFERENCE, mapType);
return true;
}
Matcher zoomChangedMatcher = ZOOM_CHANGED_PATTERN.matcher(callback);
if (zoomChangedMatcher.matches()) {
Integer zoom = parseInt(zoomChangedMatcher.group(1));
zoomChanged(zoom);
return true;
}
Matcher centerChangedMatcher = CENTER_CHANGED_PATTERN.matcher(callback);
if (centerChangedMatcher.matches()) {
Double latitude = parseDouble(centerChangedMatcher.group(1));
Double longitude = parseDouble(centerChangedMatcher.group(2));
centerChanged(longitude, latitude);
return true;
}
Matcher callbackPortMatcher = CALLBACK_PORT_PATTERN.matcher(callback);
if (callbackPortMatcher.matches()) {
int port = parseInt(callbackPortMatcher.group(1));
fireReceivedCallback(port);
return true;
}
Matcher overQueryLimitMatcher = OVER_QUERY_LIMIT_PATTERN.matcher(callback);
if (overQueryLimitMatcher.matches()) {
overQueryLimitCount++;
log.warning("Google Directions API is over query limit, count: " + overQueryLimitCount);
return true;
}
Matcher zeroResultsMatcher = ZERO_RESULTS_PATTERN.matcher(callback);
if (zeroResultsMatcher.matches()) {
zeroResultsCount++;
log.warning("Google Directions API returns zero results, count: " + zeroResultsCount);
return true;
}
Matcher insertWaypointsMatcher = INSERT_WAYPOINTS_PATTERN.matcher(callback);
if (insertWaypointsMatcher.matches()) {
Integer key = parseInt(insertWaypointsMatcher.group(2));
List<String> coordinates = parseCoordinates(insertWaypointsMatcher.group(3));
PositionPair pair;
synchronized (insertWaypointsQueue) {
pair = insertWaypointsQueue.remove(key);
}
if (coordinates.size() < 5 || pair == null)
return true;
final NavigationPosition before = pair.getFirst();
NavigationPosition after = pair.getSecond();
final BaseRoute route = parseRoute(coordinates, before, after);
synchronized (notificationMutex) {
int row = positions.indexOf(before) + 1;
insertPositions(row, route);
}
invokeLater(new Runnable() {
public void run() {
int row;
synchronized (notificationMutex) {
row = positions.indexOf(before) + 1;
}
complementPositions(row, route);
}
});
log.info("processed insert " + callback);
return false;
}
return false;
}
private void centerChanged(Double longitude, Double latitude) {
preferences.putDouble(CENTER_LATITUDE_PREFERENCE, latitude);
preferences.putDouble(CENTER_LONGITUDE_PREFERENCE, longitude);
if (positionReducer.hasFilteredVisibleArea()) {
NavigationPosition mapNorthEast = getNorthEastBounds();
NavigationPosition mapSouthWest = getSouthWestBounds();
if (!positionReducer.isWithinVisibleArea(mapNorthEast, mapSouthWest)) {
synchronized (notificationMutex) {
haveToRepaintRouteImmediately = true;
routeUpdateReason = "repaint not visible positions";
positionReducer.clear();
notificationMutex.notifyAll();
}
}
}
}
private void zoomChanged(Integer zoom) {
setZoom(zoom);
synchronized (notificationMutex) {
// since setCenter() leads to a callback and thus paints the track twice
if (ignoreNextZoomCallback)
ignoreNextZoomCallback = false;
// directions are automatically scaled by the Google Maps API when zooming
else if (positionsModel.getRoute().getCharacteristics() != Route ||
positionReducer.hasFilteredVisibleArea() || recenterAfterZooming) {
haveToRepaintRouteImmediately = true;
// if enabled, recenter map to selected positions after zooming
if (recenterAfterZooming)
haveToRecenterMap = true;
haveToRepaintSelectionImmediately = true;
selectionUpdateReason = "zoomed from " + lastZoom + " to " + zoom;
lastZoom = zoom;
notificationMutex.notifyAll();
}
}
}
private boolean isDuplicate(NavigationPosition position, NavigationPosition insert) {
if (position == null)
return false;
Double distance = position.calculateDistance(insert);
return distance != null && distance < 10.0;
}
private String trimSpaces(String string) {
if ("-".equals(string))
return null;
try {
return trim(new String(string.getBytes(), UTF8_ENCODING));
} catch (UnsupportedEncodingException e) {
return null;
}
}
private List<String> parseCoordinates(String coordinates) {
List<String> result = new ArrayList<>();
StringTokenizer tokenizer = new StringTokenizer(coordinates, "/");
while (tokenizer.hasMoreTokens()) {
String latitude = trim(tokenizer.nextToken());
if (tokenizer.hasMoreTokens()) {
String longitude = trim(tokenizer.nextToken());
if (tokenizer.hasMoreTokens()) {
String meters = trim(tokenizer.nextToken());
if (tokenizer.hasMoreTokens()) {
String seconds = trim(tokenizer.nextToken());
if (tokenizer.hasMoreTokens()) {
String instructions = trimSpaces(tokenizer.nextToken());
result.add(latitude);
result.add(longitude);
result.add(meters);
result.add(seconds);
result.add(instructions);
}
}
}
}
}
return result;
}
private Double parseSeconds(String string) {
Double result = parseDouble(string);
return !isEmpty(result) ? result : null;
}
@SuppressWarnings("unchecked")
private BaseRoute parseRoute(List<String> coordinates, NavigationPosition before, NavigationPosition after) {
BaseRoute route = new NavigatingPoiWarnerFormat().createRoute(Waypoints, null, new ArrayList<NavigationPosition>());
// count backwards as inserting at position 0
CompactCalendar time = after.getTime();
int positionInsertionCount = coordinates.size() / 5;
for (int i = coordinates.size() - 1; i > 0; i -= 5) {
String instructions = trim(coordinates.get(i));
Double seconds = parseSeconds(coordinates.get(i - 1));
// Double meters = parseDouble(coordinates.get(i - 2));
Double longitude = parseDouble(coordinates.get(i - 3));
Double latitude = parseDouble(coordinates.get(i - 4));
if (seconds != null && time != null) {
Calendar calendar = time.getCalendar();
calendar.add(SECOND, -seconds.intValue());
time = fromCalendar(calendar);
}
int positionNumber = positionsModel.getRowCount() + (positionInsertionCount - route.getPositionCount()) - 1;
String description = instructions != null ? instructions : mapViewCallback.createDescription(positionNumber, null);
BaseNavigationPosition position = route.createPosition(longitude, latitude, null, null, seconds != null ? time : null, description);
if (!isDuplicate(before, position) && !isDuplicate(after, position)) {
route.add(0, position);
}
}
return route;
}
@SuppressWarnings("unchecked")
private void insertPositions(int row, BaseRoute route) {
try {
positionsModel.add(row, route);
} catch (IOException e) {
log.severe("Cannot insert route: " + e);
}
}
private void complementPositions(int row, BaseRoute route) {
int[] rows = asRange(row, row + route.getPositions().size());
// do not complement description since this is limited to 2500 calls/day
mapViewCallback.complementData(rows, false, true, true);
}
private void insertPosition(int row, Double longitude, Double latitude) {
positionsModel.add(row, longitude, latitude, null, null, null, mapViewCallback.createDescription(positionsModel.getRowCount() + 1, null));
int[] rows = new int[]{row};
positionsSelectionModel.setSelectedPositions(rows, true);
mapViewCallback.complementData(rows, true, true, true);
}
private int getAddRow() {
NavigationPosition position = lastSelectedPositions.size() > 0 ? lastSelectedPositions.get(lastSelectedPositions.size() - 1) : null;
// quite crude logic to be as robust as possible on failures
if (position == null && positionsModel.getRowCount() > 0)
position = positionsModel.getPosition(positionsModel.getRowCount() - 1);
return position != null ? positionsModel.getIndex(position) + 1 : 0;
}
private int getMoveRow(int index) {
NavigationPosition position = lastSelectedPositions.get(index);
final int row;
synchronized (notificationMutex) {
row = positions.indexOf(position);
}
return row;
}
private void movePosition(int row, Double longitude, Double latitude) {
NavigationPosition reference = positionsModel.getPosition(row);
Double diffLongitude = reference != null ? longitude - reference.getLongitude() : 0.0;
Double diffLatitude = reference != null ? latitude - reference.getLatitude() : 0.0;
boolean moveCompleteSelection = preferences.getBoolean(MOVE_COMPLETE_SELECTION_PREFERENCE, true);
boolean cleanElevation = preferences.getBoolean(CLEAN_ELEVATION_ON_MOVE_PREFERENCE, false);
boolean complementElevation = preferences.getBoolean(COMPLEMENT_ELEVATION_ON_MOVE_PREFERENCE, true);
boolean cleanTime = preferences.getBoolean(CLEAN_TIME_ON_MOVE_PREFERENCE, false);
boolean complementTime = preferences.getBoolean(COMPLEMENT_TIME_ON_MOVE_PREFERENCE, true);
int minimum = row;
for (int index : selectedPositionIndices) {
if (index < minimum)
minimum = index;
NavigationPosition position = positionsModel.getPosition(index);
if (position == null)
continue;
if (index != row) {
if (!moveCompleteSelection)
continue;
positionsModel.edit(index, new PositionColumnValues(asList(LONGITUDE_COLUMN_INDEX, LATITUDE_COLUMN_INDEX),
Arrays.<Object>asList(position.getLongitude() + diffLongitude, position.getLatitude() + diffLatitude)), false, true);
} else {
positionsModel.edit(index, new PositionColumnValues(asList(LONGITUDE_COLUMN_INDEX, LATITUDE_COLUMN_INDEX),
Arrays.<Object>asList(longitude, latitude)), false, true);
}
if (cleanTime)
positionsModel.edit(index, new PositionColumnValues(DATE_TIME_COLUMN_INDEX, null), false, false);
if (cleanElevation)
positionsModel.edit(index, new PositionColumnValues(ELEVATION_COLUMN_INDEX, null), false, false);
if (complementTime || complementElevation)
mapViewCallback.complementData(new int[]{index}, false, complementTime, complementElevation);
}
// updating all rows behind the modified is quite expensive, but necessary due to the distance
// calculation - if that didn't exist the single update of row would be sufficient
int size;
synchronized (notificationMutex) {
size = positions.size() - 1;
haveToRepaintRouteImmediately = true;
routeUpdateReason = "move position";
positionReducer.clear();
haveToRepaintSelectionImmediately = true;
selectionUpdateReason = "move position";
}
positionsModel.fireTableRowsUpdated(minimum, size, ALL_COLUMNS);
}
private void selectPosition(Double longitude, Double latitude, Double threshold, boolean replaceSelection) {
int row = positionsModel.getClosestPosition(longitude, latitude, threshold);
if (row != -1)
positionsSelectionModel.setSelectedPositions(new int[]{row}, replaceSelection);
}
private void selectPositions(BoundingBox boundingBox, boolean replaceSelection) {
int[] rows = positionsModel.getContainedPositions(boundingBox);
if (rows.length > 0) {
positionsSelectionModel.setSelectedPositions(rows, replaceSelection);
}
}
private void removePosition(Double longitude, Double latitude, Double threshold) {
int row = positionsModel.getClosestPosition(longitude, latitude, threshold);
if (row != -1) {
positionsModel.remove(new int[]{row});
executor.execute(new Runnable() {
public void run() {
synchronized (notificationMutex) {
haveToRepaintRouteImmediately = true;
routeUpdateReason = "remove position";
notificationMutex.notifyAll();
}
}
});
}
}
// listeners
private final List<MapViewListener> mapViewListeners = new CopyOnWriteArrayList<>();
public void addMapViewListener(MapViewListener listener) {
mapViewListeners.add(listener);
}
public void removeMapViewListener(MapViewListener listener) {
mapViewListeners.remove(listener);
}
private void fireCalculatedDistance(int meters, int seconds) {
for (MapViewListener listener : mapViewListeners) {
listener.calculatedDistance(meters, seconds);
}
}
private void fireReceivedCallback(int port) {
for (MapViewListener listener : mapViewListeners) {
listener.receivedCallback(port);
}
}
}