/* 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.mapview.browser; import slash.common.io.TokenResolver; import slash.common.type.CompactCalendar; import slash.navigation.base.BaseNavigationFormat; import slash.navigation.base.BaseNavigationPosition; import slash.navigation.base.BaseRoute; import slash.navigation.base.RouteCharacteristics; import slash.navigation.base.WaypointType; import slash.navigation.base.Wgs84Position; import slash.navigation.columbus.ColumbusGpsBinaryFormat; import slash.navigation.columbus.ColumbusGpsFormat; import slash.navigation.common.BoundingBox; import slash.navigation.common.DistanceAndTime; import slash.navigation.common.NavigationPosition; import slash.navigation.common.PositionPair; import slash.navigation.common.SimpleNavigationPosition; import slash.navigation.converter.gui.models.BooleanModel; import slash.navigation.converter.gui.models.CharacteristicsModel; import slash.navigation.converter.gui.models.ColorModel; import slash.navigation.converter.gui.models.FixMapMode; import slash.navigation.converter.gui.models.FixMapModeModel; import slash.navigation.converter.gui.models.GoogleMapsServerModel; import slash.navigation.converter.gui.models.PositionColumnValues; import slash.navigation.converter.gui.models.PositionsModel; import slash.navigation.converter.gui.models.PositionsSelectionModel; import slash.navigation.converter.gui.models.UnitSystemModel; import slash.navigation.gui.Application; import slash.navigation.mapview.AbstractMapViewListener; import slash.navigation.mapview.MapView; import slash.navigation.mapview.MapViewCallback; import slash.navigation.mapview.MapViewListener; import slash.navigation.mapview.tileserver.TileServerService; import slash.navigation.mapview.tileserver.binding.CopyrightType; import slash.navigation.mapview.tileserver.binding.TileServerType; import slash.navigation.nmn.NavigatingPoiWarnerFormat; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.ListDataEvent; import javax.swing.event.ListDataListener; import javax.swing.event.TableModelEvent; import javax.swing.event.TableModelListener; import javax.xml.bind.JAXBException; import java.awt.*; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketTimeoutException; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Random; import java.util.ResourceBundle; import java.util.StringTokenizer; 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.Collections.sort; 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.ALL_COLUMNS; import static javax.swing.event.TableModelEvent.DELETE; import static javax.swing.event.TableModelEvent.INSERT; import static javax.swing.event.TableModelEvent.UPDATE; import static org.apache.commons.lang.StringEscapeUtils.escapeHtml; import static slash.common.helpers.ExceptionHelper.getLocalizedMessage; import static slash.common.helpers.ThreadHelper.safeJoin; import static slash.common.io.Externalization.extractFile; import static slash.common.io.Transfer.UTF8_ENCODING; import static slash.common.io.Transfer.ceiling; import static slash.common.io.Transfer.decodeUri; import static slash.common.io.Transfer.isEmpty; import static slash.common.io.Transfer.parseDouble; import static slash.common.io.Transfer.parseInt; import static slash.common.io.Transfer.parseInteger; import static slash.common.io.Transfer.parseLong; import static slash.common.io.Transfer.toDouble; import static slash.common.io.Transfer.trim; import static slash.common.io.Transfer.trimLineFeeds; import static slash.common.type.CompactCalendar.fromCalendar; import static slash.common.type.HexadecimalNumber.encodeByte; import static slash.navigation.base.RouteCharacteristics.Route; import static slash.navigation.base.RouteCharacteristics.Track; import static slash.navigation.base.RouteCharacteristics.Waypoints; import static slash.navigation.base.WaypointType.End; import static slash.navigation.base.WaypointType.Start; import static slash.navigation.base.WaypointType.Waypoint; import static slash.navigation.converter.gui.models.CharacteristicsModel.IGNORE; import static slash.navigation.converter.gui.models.FixMapMode.Automatic; import static slash.navigation.converter.gui.models.FixMapMode.Yes; 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.googlemaps.GoogleMapsAPIKey.getAPIKey; import static slash.navigation.gui.events.Range.asRange; import static slash.navigation.gui.helpers.JTableHelper.isFirstToLastRow; import static slash.navigation.mapview.MapViewConstants.ROUTE_LINE_WIDTH_PREFERENCE; import static slash.navigation.mapview.MapViewConstants.TRACK_LINE_WIDTH_PREFERENCE; import static slash.navigation.mapview.browser.TransformUtil.delta; import static slash.navigation.mapview.browser.TransformUtil.isPositionInChina; /** * Base implementation for a browser-based map view. * * @author Christian Pesch */ public abstract class BrowserMapView implements MapView { protected static final Preferences preferences = Preferences.userNodeForPackage(BrowserMapView.class); protected static final Logger log = Logger.getLogger(MapView.class.getName()); private static final String RESOURCES_PACKAGE = "slash/navigation/mapview/browser/"; private static final String MAP_TYPE_PREFERENCE = "mapType"; protected static final String DEBUG_PREFERENCE = "debug"; protected static final String BROWSER_SCALE_FACTOR_PREFERENCE = "browserScaleFactor"; 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 PositionsModel positionsModel; private PositionsSelectionModel positionsSelectionModel; private CharacteristicsModel characteristicsModel; private List<NavigationPosition> lastSelectedPositions = new ArrayList<>(); private int[] selectedPositionIndices = new int[0]; private List<NavigationPosition> selectedPositions = new ArrayList<>(); 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 running = true, haveToInitializeMapOnFirstStart = true, haveToRepaintSelectionImmediately = false, haveToRepaintRouteImmediately = false, haveToRecenterMap = false, haveToUpdateRoute = false, haveToReplaceRoute = false, haveToRepaintSelection = false, ignoreNextZoomCallback = false; private BooleanModel showAllPositionsAfterLoading; private BooleanModel recenterAfterZooming; private BooleanModel showCoordinates; private BooleanModel showWaypointDescription; private FixMapModeModel fixMapModeModel; private ColorModel routeColorModel; private ColorModel trackColorModel; private UnitSystemModel unitSystemModel; private GoogleMapsServerModel googleMapsServerModel; private PositionsModelListener positionsModelListener = new PositionsModelListener(); private CharacteristicsModelListener characteristicsModelListener = new CharacteristicsModelListener(); private MapViewCallbackListener mapViewCallbackListener = new MapViewCallbackListener(); private ShowCoordinatesListener showCoordinatesListener = new ShowCoordinatesListener(); private ShowWaypointDescriptionListener showWaypointDescriptionListener = new ShowWaypointDescriptionListener(); private RepaintPositionListListener repaintPositionListListener = new RepaintPositionListListener(); private UnitSystemListener unitSystemListener = new UnitSystemListener(); private GoogleMapsServerListener googleMapsServerListener = new GoogleMapsServerListener(); private String routeUpdateReason = "?", selectionUpdateReason = "?"; protected 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, BooleanModel showAllPositionsAfterLoading, BooleanModel recenterAfterZooming, BooleanModel showCoordinates, BooleanModel showWaypointDescription, FixMapModeModel fixMapModeModel, ColorModel aRouteColorModel, ColorModel aTrackColorModel, UnitSystemModel unitSystemModel, GoogleMapsServerModel googleMapsServerModel) { this.positionsModel = positionsModel; this.positionsSelectionModel = positionsSelectionModel; this.characteristicsModel = characteristicsModel; this.mapViewCallback = mapViewCallback; this.mapViewCallback = mapViewCallback; this.showAllPositionsAfterLoading = showAllPositionsAfterLoading; this.recenterAfterZooming = recenterAfterZooming; this.showCoordinates = showCoordinates; this.showWaypointDescription = showWaypointDescription; this.fixMapModeModel = fixMapModeModel; this.routeColorModel = aRouteColorModel; this.trackColorModel = aTrackColorModel; this.unitSystemModel = unitSystemModel; this.googleMapsServerModel = googleMapsServerModel; initializeBrowser(); positionsModel.addTableModelListener(positionsModelListener); characteristicsModel.addListDataListener(characteristicsModelListener); mapViewCallback.addRoutingServiceChangeListener(mapViewCallbackListener); showCoordinates.addChangeListener(showCoordinatesListener); showWaypointDescription.addChangeListener(showWaypointDescriptionListener); fixMapModeModel.addChangeListener(repaintPositionListListener); routeColorModel.addChangeListener(repaintPositionListListener); trackColorModel.addChangeListener(repaintPositionListListener); unitSystemModel.addChangeListener(unitSystemListener); googleMapsServerModel.addChangeListener(googleMapsServerListener); positionReducer = new PositionReducer(new PositionReducer.Callback() { public int getZoom() { return BrowserMapView.this.getZoom(); } public NavigationPosition getNorthEastBounds() { return BrowserMapView.this.getNorthEastBounds(); } public NavigationPosition getSouthWestBounds() { return BrowserMapView.this.getSouthWestBounds(); } }); } protected abstract void initializeBrowser(); protected abstract void initializeWebPage(); protected double getBrowserScaleFactor() { return (double) preferences.getInt(BROWSER_SCALE_FACTOR_PREFERENCE, 100) / 100.0; } protected String getGoogleMapsServerApiUrl() { return googleMapsServerModel.getGoogleMapsServer().getApiUrl(); } protected String prepareWebPage() throws IOException { final String language = Locale.getDefault().getLanguage().toLowerCase(); final String country = Locale.getDefault().getCountry().toLowerCase(); final TileServerService tileServerService = loadAllTileServers(mapViewCallback.getTileServersDirectory()); File html = extractFile(RESOURCES_PACKAGE + "routeconverter.html", country, new TokenResolver() { public String resolveToken(String tokenName) { if (tokenName.equals("language")) return language; if (tokenName.equals("country")) return country; if (tokenName.equals("mapserverapiurl")) return getGoogleMapsServerApiUrl(); if (tokenName.equals("mapserverfileurl")) return googleMapsServerModel.getGoogleMapsServer().getFileUrl(); if (tokenName.equals("maptype")) return getMapType(); if (tokenName.equals("mapsapikey")) return getAPIKey("map"); if (tokenName.equals("tileservers1")) return registerTileServers(tileServerService, true); if (tokenName.equals("tileservers2")) return registerTileServers(tileServerService, false); if (tokenName.equals("menuItems")) return registerMenuItems(); return tokenName; } }); if (html == null) throw new IllegalArgumentException("Cannot extract routeconverter.html"); extractFile(RESOURCES_PACKAGE + "jquery.min.js"); extractFile(RESOURCES_PACKAGE + "contextmenu.js"); extractFile(RESOURCES_PACKAGE + "keydragzoom.js"); extractFile(RESOURCES_PACKAGE + "label.js"); extractFile(RESOURCES_PACKAGE + "latlngcontrol.js"); return html.toURI().toURL().toExternalForm(); } protected void tryToInitialize(int count, long start) { boolean initialized = getComponent() != null && isMapInitialized(); synchronized (INITIALIZED_LOCK) { this.initialized = initialized; } log.fine("Initialized map: " + initialized); if (isInitialized()) { runBrowserInteractionCallbacksAndTests(start); } else { long end = currentTimeMillis(); int timeout = count++ * 100; if (timeout > 3000) timeout = 3000; log.info("Failed to initialize map since " + (end - start) + " ms, sleeping for " + timeout + " ms"); try { sleep(timeout); } catch (InterruptedException e) { // intentionally left empty } tryToInitialize(count, start); } } protected void runBrowserInteractionCallbacksAndTests(long start) { long end = currentTimeMillis(); log.fine("Starting browser interaction, callbacks and tests after " + (end - start) + " ms"); initializeAfterLoading(); initializeBrowserInteraction(); initializeCallbackListener(); checkLocalhostResolution(); checkCallback(); setDegreeFormat(); setShowCoordinates(); end = currentTimeMillis(); log.fine("Browser interaction is running after " + (end - start) + " ms"); } protected abstract boolean isMapInitialized(); protected void initializeAfterLoading() { resize(); update(true, false); } private Throwable initializationCause = null; public Throwable getInitializationCause() { return initializationCause; } protected void setInitializationCause(Throwable initializationCause) { this.initializationCause = initializationCause; } private static final Object INITIALIZED_LOCK = new Object(); public boolean isInitialized() { synchronized (INITIALIZED_LOCK) { return initialized; } } public boolean isDownload() { return false; } 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() { @SuppressWarnings("unchecked") 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.info("Woke up to update route: " + routeUpdateReason + " haveToUpdateRoute:" + haveToUpdateRoute + " haveToReplaceRoute:" + haveToReplaceRoute + " haveToRepaintRouteImmediately:" + haveToRepaintRouteImmediately); copiedPositions = new ArrayList<>(positionsModel.getRoute().getPositions()); recenter = 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.getBoolean()); switch (characteristics) { case Route: addDirectionsToMap(render); break; case Track: addPolylinesToMap(render, copiedPositions); 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 + ", reason: " + routeUpdateReason + ", recentering: " + recenter); lastTime = currentTimeMillis(); } } }, "MapViewPositionListUpdater"); positionListUpdater.start(); selectionUpdater = new Thread(new Runnable() { @SuppressWarnings("unchecked") 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 = haveToRecenterMap; haveToRecenterMap = false; haveToRepaintSelectionImmediately = false; haveToRepaintSelection = false; copiedSelectedPositionIndices = new int[selectedPositionIndices.length]; System.arraycopy(selectedPositionIndices, 0, copiedSelectedPositionIndices, 0, copiedSelectedPositionIndices.length); copiedPositions = new ArrayList<>(positionsModel.getRoute().getPositions()); } else continue; } List<NavigationPosition> render = new ArrayList<>(positionReducer.reduceSelectedPositions(copiedPositions, copiedSelectedPositionIndices)); render.addAll(selectedPositions); NavigationPosition centerPosition = render.size() > 0 ? new BoundingBox(render).getCenter() : null; selectPositions(render, recenter ? centerPosition : null); log.info("Selected positions updated for " + render.size() + " positions , reason: " + selectionUpdateReason + ", 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) { e.printStackTrace(); 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) { final String message = "Probably faulty network setup: " + getLocalizedMessage(e) + ".\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); } } }); } // tile servers private static final List<String> GOOGLE_MAP_TYPES = asList("ROADMAP", "SATELLITE", "HYBRID", "TERRAIN"); private String getMapType() { return preferences.get(MAP_TYPE_PREFERENCE, "google.maps.MapTypeId.ROADMAP"); } private boolean isGoogleFixMap() { String mapType = getMapType(); return mapType != null && GOOGLE_MAP_TYPES.contains(mapType.toUpperCase()); } private static final String DOT_XML = ".xml"; private TileServerService loadAllTileServers(java.io.File directory) { TileServerService result = new TileServerService(); java.io.File[] files = directory.listFiles(new FilenameFilter() { public boolean accept(java.io.File dir, String name) { return name.endsWith(DOT_XML); } }); if (files != null) { for (File file : files) { try { try (InputStream inputStream = new FileInputStream(file)) { result.load(inputStream); } } catch (IOException | JAXBException e) { log.severe("Could not parse tile server definitions from " + file + ": " + getLocalizedMessage(e)); } } } return result; } private String registerTileServers(TileServerService tileServerService, boolean register) { StringBuilder buffer = new StringBuilder(); if (register) { for (String tileServerId : GOOGLE_MAP_TYPES) buffer.append("mapTypeIds.push(google.maps.MapTypeId.").append(tileServerId).append("); "). append("mapCopyrights[google.maps.MapTypeId.").append(tileServerId).append("] = \"Google\";\n"); } for (TileServerType tileServer : tileServerService.getTileServers()) { if (tileServer.getActive() != null && !tileServer.getActive()) continue; if (register) { CopyrightType copyrightType = tileServer.getCopyright(); buffer.append("mapTypeIds.push(\"").append(tileServer.getId()).append("\"); "). append("mapCopyrights[\"").append(tileServer.getId()).append("\"] = \""). append(copyrightType != null ? copyrightType.value() : "unknown").append("\";\n"); } else buffer.append("map.mapTypes.set(\"").append(tileServer.getId()).append("\", new google.maps.ImageMapType({\n"). append(" getTileUrl: function(coordinates, zoom) {\n"). append(" return ").append(trim(trimLineFeeds(tileServer.getValue()))).append(";\n"). append(" },\n"). append(" tileSize: DEFAULT_TILE_SIZE,\n"). append(" minZoom: ").append(tileServer.getMinZoom()).append(",\n"). append(" maxZoom: ").append(tileServer.getMaxZoom()).append(",\n"). append(" alt: \"").append(tileServer.getName()).append("\",\n"). append(" name: \"").append(tileServer.getId()).append("\"\n"). append("}));\n"); } return buffer.toString(); } private static final String[] MENU_ITEM_KEYS = new String[]{ "center-here-action", "delete-action", "new-position-action", "select-position-action", "zoom-in-action", "zoom-out-action" }; private String registerMenuItems() { StringBuilder buffer = new StringBuilder(); ResourceBundle bundle = Application.getInstance().getContext().getBundle(); for (String menuItemKey : MENU_ITEM_KEYS) buffer.append("menuItems[\"").append(menuItemKey).append("\"] = "). append("\"").append(escapeHtml(bundle.getString(menuItemKey))).append("\";\n"); return buffer.toString(); } private boolean isColumbusTrack() { BaseNavigationFormat format = positionsModel.getRoute().getFormat(); return format instanceof ColumbusGpsFormat || format instanceof ColumbusGpsBinaryFormat; } // resizing private boolean hasBeenResizedToInvisible = false; public void resize() { if (!isInitialized() || !getComponent().isShowing()) return; new Thread(new Runnable() { public void run() { synchronized (notificationMutex) { // if map is not visible remember to update and resize it again // once the map becomes visible again if (!isVisible()) { hasBeenResizedToInvisible = true; } else if (hasBeenResizedToInvisible) { hasBeenResizedToInvisible = false; update(true, false); } resizeMap(); } } }, "BrowserResizer").start(); } private int lastWidth = -1, lastHeight = -1; private void resizeMap() { synchronized (notificationMutex) { double browserScaleFactor = getBrowserScaleFactor(); int width = (int) max(getComponent().getWidth() / browserScaleFactor, 0.0); int height = (int) max(getComponent().getHeight() / browserScaleFactor, 0.0); if (width != lastWidth || height != lastHeight) { executeScript("resize(" + width + "," + height + ");"); } lastWidth = width; lastHeight = height; } } // disposal public void dispose() { if(positionsModel != null) { positionsModel.removeTableModelListener(positionsModelListener); characteristicsModel.removeListDataListener(characteristicsModelListener); mapViewCallback.removeRoutingServiceChangeListener(mapViewCallbackListener); showCoordinates.removeChangeListener(showCoordinatesListener); showWaypointDescription.removeChangeListener(showWaypointDescriptionListener); fixMapModeModel.removeChangeListener(repaintPositionListListener); routeColorModel.removeChangeListener(repaintPositionListListener); trackColorModel.removeChangeListener(repaintPositionListListener); unitSystemModel.removeChangeListener(unitSystemListener); googleMapsServerModel.removeChangeListener(googleMapsServerListener); } 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() && positionsModel.getRoute().getPositions() != 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; } this.selectedPositions = new ArrayList<>(); haveToRecenterMap = selectedPositions.length > 0; haveToRepaintSelection = true; selectionUpdateReason = "selected " + selectedPositions.length + " positions; " + "replacing selection: " + replaceSelection; notificationMutex.notifyAll(); } } public void setSelectedPositions(List<NavigationPosition> selectedPositions) { synchronized (notificationMutex) { this.selectedPositions = selectedPositions; this.selectedPositionIndices = new int[0]; haveToRecenterMap = selectedPositions.size() > 0; haveToRepaintSelection = true; selectionUpdateReason = "selected " + selectedPositions.size() + " positions without model"; notificationMutex.notifyAll(); } } protected void setShowCoordinates() { executeScript("setShowCoordinates(" + showCoordinates.getBoolean() + ");"); } protected void setDegreeFormat() { executeScript("setDegreeFormat('" + unitSystemModel.getDegreeFormat() + "');"); } @SuppressWarnings({"unchecked", "Convert2Diamond"}) public void showAllPositions() { setCenterOfMap(new ArrayList<NavigationPosition>(positionsModel.getRoute().getPositions()), true); } 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); } // bounds and center 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 parsePosition(String latLngString) { String result = executeScriptWithResult(latLngString); 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 parsePosition(latitude, longitude); } // WGS/GCJ conversion private boolean isFixMap(Double longitude, Double latitude) { FixMapMode fixMapMode = fixMapModeModel.getFixMapMode(); return fixMapMode.equals(Yes) || fixMapMode.equals(Automatic) && isGoogleFixMap() && isPositionInChina(longitude, latitude); } private NavigationPosition parsePosition(String latitudeString, String longitudeString) { Double longitude = parseDouble(longitudeString); Double latitude = parseDouble(latitudeString); if (longitude != null && latitude != null && isFixMap(longitude, latitude)) { double[] delta = delta(latitude, longitude); longitude -= delta[1]; latitude -= delta[0]; } return new SimpleNavigationPosition(longitude, latitude); } private String asCoordinates(NavigationPosition position) { Double longitude = position.getLongitude(); Double latitude = position.getLatitude(); if (longitude != null && latitude != null && isFixMap(longitude, latitude)) { double[] delta = delta(latitude, longitude); longitude += delta[1]; latitude += delta[0]; } return latitude + "," + longitude; } // draw on map protected void update(boolean haveToReplaceRoute, boolean clearPositionReducer) { if (!isInitialized() || !getComponent().isShowing()) return; synchronized (notificationMutex) { this.haveToUpdateRoute = true; routeUpdateReason = "update route"; if (haveToReplaceRoute) { this.haveToReplaceRoute = true; routeUpdateReason = "replace route"; this.haveToRepaintSelection = true; selectionUpdateReason = "replace route"; } if (clearPositionReducer) positionReducer.clear(); 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 removeDirections() { executeScript("removeOverlays();\nremoveDirections();"); } String asColor(Color color) { return encodeByte((byte) color.getRed()) + encodeByte((byte) color.getGreen()) + encodeByte((byte) color.getBlue()); } private static final float MINIMUM_OPACITY = 0.3f; float asOpacity(Color color) { return MINIMUM_OPACITY + color.getAlpha() / 256f * (1 - MINIMUM_OPACITY); } private void addDirectionsToMap(List<NavigationPosition> positions) { resetDirections(); // avoid throwing javascript exceptions if there is nothing to direct if (positions.size() < 2) { addMarkersToMap(positions); return; } generationId++; directionsPositions.addAll(positions); executeScript("removeOverlays();"); String color = asColor(routeColorModel.getColor()); float opacity = asOpacity(routeColorModel.getColor()); int width = preferences.getInt(ROUTE_LINE_WIDTH_PREFERENCE, 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(asCoordinates(position)).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(asCoordinates(origin)).append("),"); buffer.append("destination: new google.maps.LatLng(").append(asCoordinates(destination)).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("\"},"); buffer.append(generationId).append(","); buffer.append(start).append(","); int startIndex = positionsModel.getIndex(origin); buffer.append(startIndex).append(","); boolean lastSegment = (j == directionsCount - 1); buffer.append(lastSegment).append(",\"#").append(color).append("\",").append(opacity).append(",").append(width).append(");\n"); try { sleep(preferences.getInt("routeSegmentTimeout", 250)); } catch (InterruptedException e) { // intentionally left empty } executeScript(buffer.toString()); } try { sleep(preferences.getInt("routeCompleteTimeout", 1000)); } catch (InterruptedException e) { // intentionally left empty } } private void addPolylinesToMap(final List<NavigationPosition> reducedPositions, List<NavigationPosition> allPositions) { // display markers if there is no polyline to show if (reducedPositions.size() < 2) { addMarkersToMap(reducedPositions); return; } String color = asColor(trackColorModel.getColor()); float opacity = asOpacity(trackColorModel.getColor()); int width = preferences.getInt(TRACK_LINE_WIDTH_PREFERENCE, 2); int maximumPolylineSegmentLength = positionReducer.getMaximumSegmentLength(Track); int polylinesCount = ceiling(reducedPositions.size(), maximumPolylineSegmentLength, true); for (int j = 0; j < polylinesCount; j++) { StringBuilder latlngs = new StringBuilder(); int minimum = max(0, j * maximumPolylineSegmentLength - 1); int maximum = min(reducedPositions.size(), (j + 1) * maximumPolylineSegmentLength); for (int i = minimum; i < maximum; i++) { NavigationPosition position = reducedPositions.get(i); latlngs.append("new google.maps.LatLng(").append(asCoordinates(position)).append(")"); if (i < maximum - 1) latlngs.append(","); } executeScript("addPolyline([" + latlngs + "],\"#" + color + "\"," + opacity + "," + width + ");"); } addWaypointIconsToMap(allPositions); removeDirections(); } private void addWaypointIconsToMap(List<NavigationPosition> positions) { if (!isColumbusTrack()) return; List<NavigationPosition> reducedPositions = positionReducer.filterVisiblePositions(positions, getZoom()); StringBuilder icons = new StringBuilder(); for (int i = 0, c = reducedPositions.size(); i < c; i++) { NavigationPosition position = reducedPositions.get(i); Wgs84Position wgs84Position = Wgs84Position.class.cast(position); WaypointType waypointType = wgs84Position.getWaypointType(); if (i == c - 1) waypointType = End; if (i == 0) waypointType = Start; if (waypointType != null && waypointType != Waypoint) icons.append("addWaypointIcon(new google.maps.LatLng(").append(asCoordinates(position)).append("),\""). append(waypointType).append("\");\n"); } executeScript(icons.toString()); } 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(new google.maps.LatLng(").append(asCoordinates(position)).append("),"). append("\"").append(escape(position.getDescription())).append("\","). append(showWaypointDescription.getBoolean()).append(");\n"); } executeScript(buffer.toString()); } 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(new google.maps.LatLng(").append(asCoordinates(boundingBox.getSouthWest())).append("),"). append("new google.maps.LatLng(").append(asCoordinates(boundingBox.getNorthEast())).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(new google.maps.LatLng(").append(asCoordinates(center)).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(new google.maps.LatLng(").append(asCoordinates(selectedPosition)).append("),"). append("\"").append(escape(selectedPosition.getDescription())).append("\","). append(i).append(");\n"); } if (center != null && center.hasCoordinates()) buffer.append("panTo(new google.maps.LatLng(").append(asCoordinates(center)).append("));\n"); buffer.append("removeSelectedPositions();"); executeScript(buffer.toString()); } private final Map<Integer, PositionPair> insertWaypointsQueue = new LinkedHashMap<>(); private final ExecutorService insertWaypointsExecutor = newSingleThreadExecutor(); private void insertWaypoints(final String mode, int[] startPositions) { final Map<Integer, PositionPair> addToQueue = new LinkedHashMap<>(); Random random = new Random(); synchronized (notificationMutex) { @SuppressWarnings("unchecked") List<NavigationPosition> positions = positionsModel.getRoute().getPositions(); 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 (Map.Entry<Integer, PositionPair> entry : addToQueue.entrySet()) { NavigationPosition origin = entry.getValue().getFirst(); NavigationPosition destination = entry.getValue().getSecond(); executeScript(mode + "({" + "origin: new google.maps.LatLng(" + asCoordinates(origin) + ")," + "destination: new google.maps.LatLng(" + asCoordinates(destination) + ")," + "travelMode: google.maps.DirectionsTravelMode." + mapViewCallback.getTravelMode().getName().toUpperCase() + "," + "avoidFerries: " + mapViewCallback.isAvoidFerries() + "," + "avoidHighways: " + mapViewCallback.isAvoidHighways() + "," + "avoidTolls: " + mapViewCallback.isAvoidTolls() + "," + "region: \"" + Locale.getDefault().getCountry().toLowerCase() + "\"}," + entry.getKey() + ");\n"); try { sleep(preferences.getInt("insertWaypointsSegmentTimeout", 1000)); } catch (InterruptedException e) { // intentionally left empty } } } }); } private void insertWaypointsCallback(Integer key, List<String> parameters) { PositionPair pair; synchronized (insertWaypointsQueue) { pair = insertWaypointsQueue.remove(key); } if (parameters.size() < 5 || pair == null) return; final NavigationPosition before = pair.getFirst(); NavigationPosition after = pair.getSecond(); final BaseRoute route = parseRoute(parameters, before, after); @SuppressWarnings("unchecked") final List<NavigationPosition> positions = positionsModel.getRoute().getPositions(); 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); } }); } // call Google Maps API functions @SuppressWarnings("unused") public void insertAllWaypoints(int[] startPositions) { insertWaypoints("insertAllWaypoints", startPositions); } @SuppressWarnings("unused") public void insertOnlyTurnpoints(int[] startPositions) { insertWaypoints("insertOnlyTurnpoints", startPositions); } // 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("Executed '" + 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 { List<String> lines = new ArrayList<>(); boolean processingPost = false, processingBody = false; try (BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()), 64 * 1024)) { while (true) { try { String line = trim(reader.readLine()); 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; } } try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()))) { writer.write("HTTP/1.1 200 OK\n"); writer.write("Content-Type: text/plain\n"); } } StringBuilder buffer = new StringBuilder(); for (String line : lines) { buffer.append(" ").append(line).append("\n"); } log.fine("Processing callback @" + currentTimeMillis() + " from port " + socket.getPort() + ": \n" + buffer.toString()); if (!isAuthenticated(lines)) return; processLines(lines, socket.getPort()); } 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, int port) { boolean hasValidCallbackNumber = false; for (String line : lines) { 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 + " port is: " + port); break; } lastCallbackNumber = callbackNumber; hasValidCallbackNumber = true; String callback = matcher.group(3); if (processCallback(callback)) { log.info("Processed " + matcher.group(1) + " callback " + callback + " with number: " + callbackNumber + " from port: " + port); break; } } // process body of POST requests if (hasValidCallbackNumber && processCallback(line)) { log.info("Processed POST callback " + line + " with number: " + lastCallbackNumber + " from port: " + port); break; } } } private static final Pattern ADD_POSITION_PATTERN = Pattern.compile("^add-position/(.*)/(.*)$"); private static final Pattern ADD_POSITION_AT_PATTERN = Pattern.compile("^add-position-at/(.*)/(.*)/(.*)$"); private static final Pattern MOVE_POSITION_PATTERN = Pattern.compile("^move-position/(.*)/(.*)/(.*)$"); private static final Pattern DELETE_POSITION_PATTERN = Pattern.compile("^delete-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+)/(.*)$"); private static final Pattern DIRECTIONS_LOAD_PATTERN = Pattern.compile("^directions-load/(\\d+)/(\\d+)/(.*)$"); boolean processCallback(String callback) { Matcher insertPositionAtMatcher = ADD_POSITION_AT_PATTERN.matcher(callback); if (insertPositionAtMatcher.matches()) { final int row = parseInt(insertPositionAtMatcher.group(1)) + 1; final NavigationPosition position = parsePosition(insertPositionAtMatcher.group(2), insertPositionAtMatcher.group(3)); invokeLater(new Runnable() { public void run() { insertPosition(row, position.getLongitude(), position.getLatitude()); } }); return true; } Matcher insertPositionMatcher = ADD_POSITION_PATTERN.matcher(callback); if (insertPositionMatcher.matches()) { final int row = getAddRow(); final NavigationPosition position = parsePosition(insertPositionMatcher.group(1), insertPositionMatcher.group(2)); invokeLater(new Runnable() { public void run() { insertPosition(row, position.getLongitude(), position.getLatitude()); } }); return true; } Matcher movePositionMatcher = MOVE_POSITION_PATTERN.matcher(callback); if (movePositionMatcher.matches()) { final int row = getMoveRow(parseInt(movePositionMatcher.group(1))); final NavigationPosition position = parsePosition(movePositionMatcher.group(2), movePositionMatcher.group(3)); invokeLater(new Runnable() { public void run() { movePosition(row, position.getLongitude(), position.getLatitude()); } }); return true; } Matcher deletePositionMatcher = DELETE_POSITION_PATTERN.matcher(callback); if (deletePositionMatcher.matches()) { final NavigationPosition position = parsePosition(deletePositionMatcher.group(1), deletePositionMatcher.group(2)); final Double threshold = parseDouble(deletePositionMatcher.group(3)); invokeLater(new Runnable() { public void run() { deletePosition(position.getLongitude(), position.getLatitude(), threshold); } }); return true; } Matcher selectPositionMatcher = SELECT_POSITION_PATTERN.matcher(callback); if (selectPositionMatcher.matches()) { final NavigationPosition position = parsePosition(selectPositionMatcher.group(1), selectPositionMatcher.group(2)); final Double threshold = parseDouble(selectPositionMatcher.group(3)); final Boolean replaceSelection = parseBoolean(selectPositionMatcher.group(4)); invokeLater(new Runnable() { public void run() { selectPosition(position.getLongitude(), position.getLatitude(), threshold, replaceSelection); } }); return true; } Matcher selectPositionsMatcher = SELECT_POSITIONS_PATTERN.matcher(callback); if (selectPositionsMatcher.matches()) { NavigationPosition northEast = parsePosition(selectPositionsMatcher.group(1), selectPositionsMatcher.group(2)); NavigationPosition southWest = parsePosition(selectPositionsMatcher.group(3), selectPositionsMatcher.group(4)); final BoundingBox boundingBox = new BoundingBox(northEast, southWest); 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)); mapTypeChanged(mapType); return true; } Matcher zoomChangedMatcher = ZOOM_CHANGED_PATTERN.matcher(callback); if (zoomChangedMatcher.matches()) { Integer zoom = parseInteger(zoomChangedMatcher.group(1)); zoomChanged(zoom); return true; } Matcher centerChangedMatcher = CENTER_CHANGED_PATTERN.matcher(callback); if (centerChangedMatcher.matches()) { NavigationPosition center = parsePosition(centerChangedMatcher.group(1), centerChangedMatcher.group(2)); NavigationPosition northEast = parsePosition(centerChangedMatcher.group(3), centerChangedMatcher.group(4)); NavigationPosition southWest = parsePosition(centerChangedMatcher.group(5), centerChangedMatcher.group(6)); BoundingBox boundingBox = new BoundingBox(northEast, southWest); centerChanged(center, boundingBox); 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 directionsLoadMatcher = DIRECTIONS_LOAD_PATTERN.matcher(callback); if (directionsLoadMatcher.matches()) { Integer generation = parseInt(directionsLoadMatcher.group(1)); if (generation != generationId) { log.warning("Got directions load from generation id: " + generation + ", current: " + generationId); } else { Integer generationIndex = parseInt(directionsLoadMatcher.group(2)); List<DistanceAndTime> distanceAndTimes = parseDistanceAndTimeParameters(directionsLoadMatcher.group(3)); directionsLoadCallback(generationIndex, distanceAndTimes); } return true; } Matcher insertWaypointsMatcher = INSERT_WAYPOINTS_PATTERN.matcher(callback); if (insertWaypointsMatcher.matches()) { Integer key = parseInteger(insertWaypointsMatcher.group(2)); List<String> parameters = parsePositionParameters(insertWaypointsMatcher.group(3)); insertWaypointsCallback(key, parameters); return true; } return false; } private void centerChanged(NavigationPosition center, BoundingBox boundingBox) { preferences.putDouble(CENTER_LATITUDE_PREFERENCE, center.getLatitude()); preferences.putDouble(CENTER_LONGITUDE_PREFERENCE, center.getLongitude()); if (positionReducer.hasFilteredVisibleArea()) { if (!positionReducer.isWithinVisibleArea(boundingBox)) { 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; else if (// directions are automatically scaled by the Google Maps API when zooming !positionsModel.getRoute().getCharacteristics().equals(Route) && (recenterAfterZooming.getBoolean() || positionReducer.hasFilteredVisibleArea())) { haveToRepaintRouteImmediately = true; // if enabled, recenter map to selected positions after zooming if (recenterAfterZooming.getBoolean()) haveToRecenterMap = true; haveToRepaintSelectionImmediately = true; selectionUpdateReason = "zoomed from " + lastZoom + " to " + zoom; lastZoom = zoom; notificationMutex.notifyAll(); } } } private void mapTypeChanged(String mapType) { preferences.put(MAP_TYPE_PREFERENCE, mapType); if(fixMapModeModel.getFixMapMode().equals(Automatic)) { invokeLater(new Runnable() { public void run() { update(false, false); } }); } } private boolean isDuplicate(NavigationPosition position, NavigationPosition insert) { if (position == null) return false; Double distance = position.calculateDistance(insert); return toDouble(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> parsePositionParameters(String parameters) { List<String> result = new ArrayList<>(); StringTokenizer tokenizer = new StringTokenizer(parameters, "/"); 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 List<DistanceAndTime> parseDistanceAndTimeParameters(String parameters) { List<DistanceAndTime> result = new ArrayList<>(); StringTokenizer tokenizer = new StringTokenizer(parameters, "/"); while (tokenizer.hasMoreTokens()) { String distance = trim(tokenizer.nextToken()); if (tokenizer.hasMoreTokens()) { String time = trim(tokenizer.nextToken()); result.add(new DistanceAndTime(parseDouble(distance), parseLong(time))); } } return result; } private Double parseSeconds(String string) { Double result = parseDouble(string); return !isEmpty(result) ? result : null; } @SuppressWarnings("unchecked") private BaseRoute parseRoute(List<String> parameters, 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(); for (int i = parameters.size() - 1; i > 0; i -= 5) { String instructions = trim(parameters.get(i)); Double seconds = parseSeconds(parameters.get(i - 1)); // Double meters = parseDouble(parameters.get(i - 2)); NavigationPosition coordinates = parsePosition(parameters.get(i - 4), parameters.get(i - 3)); if (seconds != null && time != null) { Calendar calendar = time.getCalendar(); calendar.add(SECOND, -seconds.intValue()); time = fromCalendar(calendar); } BaseNavigationPosition position = route.createPosition(coordinates.getLongitude(), coordinates.getLatitude(), null, null, seconds != null ? time : null, instructions); 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() - 1); // do not complement description since this is limited to 2500 calls/day mapViewCallback.complementData(rows, false, true, true, false, false); } 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, true, false); } 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 = positionsModel.getRoute().getPositions().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, true, false); } // 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 = positionsModel.getRoute().getPositions().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 deletePosition(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 = "delete position"; notificationMutex.notifyAll(); } } }); } } private int generationId = 0; private List<NavigationPosition> directionsPositions = new ArrayList<>(); private Map<Integer, DistanceAndTime> indexToDistanceAndTime = new HashMap<>(); private void resetDirections() { directionsPositions.clear(); indexToDistanceAndTime.clear(); } private void directionsLoadCallback(final int generationIndex, final List<DistanceAndTime> distanceAndTimes) { executor.execute(new Runnable() { public void run() { for (int i = 0; i < distanceAndTimes.size(); i++) { // find successor of start position from directions for first DistanceAndTime NavigationPosition position = directionsPositions.get(generationIndex + i + 1); int index = positionsModel.getIndex(position); indexToDistanceAndTime.put(index, distanceAndTimes.get(i)); } Map<Integer, DistanceAndTime> result = new HashMap<>(indexToDistanceAndTime.size()); double aggregatedDistance = 0.0; long aggregatedTime = 0L; List<Integer> indices = new ArrayList<>(indexToDistanceAndTime.keySet()); sort(indices); for(Integer index : indices) { DistanceAndTime distanceAndTime = indexToDistanceAndTime.get(index); if(distanceAndTime != null) { Double distance = distanceAndTime.getDistance(); if (!isEmpty(distance)) aggregatedDistance += distance; Long time = distanceAndTime.getTime(); if (!isEmpty(time)) aggregatedTime += time; } result.put(index, new DistanceAndTime(aggregatedDistance, aggregatedTime)); } fireCalculatedDistances(result); } }); } // 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 fireCalculatedDistances(Map<Integer, DistanceAndTime> indexToDistanceAndTime) { for (MapViewListener listener : mapViewListeners) { listener.calculatedDistances(indexToDistanceAndTime); } } private void fireReceivedCallback(int port) { for (MapViewListener listener : mapViewListeners) { listener.receivedCallback(port); } } private class PositionsModelListener implements 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 usability 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; if (showAllPositionsAfterLoading.getBoolean()) update(allRowsChanged, true); else updateRouteButDontRecenter(); } // 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; } } } } } private class CharacteristicsModelListener implements 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(); } } private class MapViewCallbackListener implements ChangeListener { public void stateChanged(ChangeEvent e) { if (positionsModel.getRoute().getCharacteristics().equals(Route)) update(false, false); } } private class ShowCoordinatesListener implements ChangeListener { public void stateChanged(ChangeEvent e) { setShowCoordinates(); } } private class ShowWaypointDescriptionListener implements ChangeListener { public void stateChanged(ChangeEvent e) { if (positionsModel.getRoute().getCharacteristics().equals(Waypoints)) update(false, false); } } private class RepaintPositionListListener implements ChangeListener { public void stateChanged(ChangeEvent e) { update(true, false); } } private class GoogleMapsServerListener implements ChangeListener { public void stateChanged(ChangeEvent e) { initializeWebPage(); } } private class UnitSystemListener implements ChangeListener { public void stateChanged(ChangeEvent e) { setDegreeFormat(); } } }