/*
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.mapsforge;
import org.mapsforge.core.graphics.Bitmap;
import org.mapsforge.core.graphics.Paint;
import org.mapsforge.core.model.Dimension;
import org.mapsforge.core.model.LatLong;
import org.mapsforge.core.model.MapPosition;
import org.mapsforge.map.controller.FrameBufferController;
import org.mapsforge.map.layer.Layer;
import org.mapsforge.map.layer.LayerManager;
import org.mapsforge.map.layer.Layers;
import org.mapsforge.map.layer.TileLayer;
import org.mapsforge.map.layer.cache.FileSystemTileCache;
import org.mapsforge.map.layer.cache.InMemoryTileCache;
import org.mapsforge.map.layer.cache.TileCache;
import org.mapsforge.map.layer.cache.TwoLevelTileCache;
import org.mapsforge.map.layer.download.TileDownloadLayer;
import org.mapsforge.map.layer.download.tilesource.TileSource;
import org.mapsforge.map.layer.overlay.Marker;
import org.mapsforge.map.layer.renderer.MapWorkerPool;
import org.mapsforge.map.layer.renderer.TileRendererLayer;
import org.mapsforge.map.model.MapViewPosition;
import org.mapsforge.map.model.common.Observer;
import org.mapsforge.map.reader.MapFile;
import org.mapsforge.map.reader.ReadBuffer;
import org.mapsforge.map.scalebar.DefaultMapScaleBar;
import org.mapsforge.map.scalebar.ImperialUnitAdapter;
import org.mapsforge.map.scalebar.MetricUnitAdapter;
import org.mapsforge.map.scalebar.NauticalUnitAdapter;
import org.mapsforge.map.util.MapViewProjection;
import slash.navigation.base.BaseRoute;
import slash.navigation.base.RouteCharacteristics;
import slash.navigation.common.BoundingBox;
import slash.navigation.common.DistanceAndTime;
import slash.navigation.common.NavigationPosition;
import slash.navigation.common.UnitSystem;
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.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.gui.actions.ActionManager;
import slash.navigation.gui.actions.FrameAction;
import slash.navigation.maps.LocalMap;
import slash.navigation.maps.MapManager;
import slash.navigation.mapview.MapView;
import slash.navigation.mapview.MapViewCallback;
import slash.navigation.mapview.MapViewListener;
import slash.navigation.mapview.mapsforge.helpers.ColorHelper;
import slash.navigation.mapview.mapsforge.helpers.MapViewCoordinateDisplayer;
import slash.navigation.mapview.mapsforge.helpers.MapViewMoverAndZoomer;
import slash.navigation.mapview.mapsforge.helpers.MapViewPopupMenu;
import slash.navigation.mapview.mapsforge.helpers.MapViewResizer;
import slash.navigation.mapview.mapsforge.lines.Line;
import slash.navigation.mapview.mapsforge.lines.Polyline;
import slash.navigation.mapview.mapsforge.overlays.DraggableMarker;
import slash.navigation.mapview.mapsforge.renderer.RouteRenderer;
import slash.navigation.mapview.mapsforge.updater.EventMapUpdater;
import slash.navigation.mapview.mapsforge.updater.PairWithLayer;
import slash.navigation.mapview.mapsforge.updater.PositionWithLayer;
import slash.navigation.mapview.mapsforge.updater.SelectionOperation;
import slash.navigation.mapview.mapsforge.updater.SelectionUpdater;
import slash.navigation.mapview.mapsforge.updater.TrackOperation;
import slash.navigation.mapview.mapsforge.updater.TrackUpdater;
import slash.navigation.mapview.mapsforge.updater.WaypointOperation;
import slash.navigation.mapview.mapsforge.updater.WaypointUpdater;
import javax.swing.*;
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 java.awt.*;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Logger;
import java.util.prefs.Preferences;
import static java.awt.event.InputEvent.CTRL_DOWN_MASK;
import static java.awt.event.KeyEvent.VK_DOWN;
import static java.awt.event.KeyEvent.VK_LEFT;
import static java.awt.event.KeyEvent.VK_MINUS;
import static java.awt.event.KeyEvent.VK_PLUS;
import static java.awt.event.KeyEvent.VK_RIGHT;
import static java.awt.event.KeyEvent.VK_UP;
import static java.lang.Integer.MAX_VALUE;
import static java.lang.Math.max;
import static java.lang.System.currentTimeMillis;
import static java.util.Arrays.asList;
import static javax.swing.JComponent.WHEN_IN_FOCUSED_WINDOW;
import static javax.swing.KeyStroke.getKeyStroke;
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.mapsforge.core.graphics.Color.BLUE;
import static org.mapsforge.core.util.LatLongUtils.zoomForBounds;
import static org.mapsforge.core.util.MercatorProjection.calculateGroundResolution;
import static org.mapsforge.core.util.MercatorProjection.getMapSize;
import static org.mapsforge.map.scalebar.DefaultMapScaleBar.ScaleBarMode.SINGLE;
import static slash.common.helpers.ThreadHelper.safeJoin;
import static slash.common.io.Directories.getTemporaryDirectory;
import static slash.common.io.Transfer.encodeUri;
import static slash.common.io.Transfer.isEmpty;
import static slash.navigation.base.RouteCharacteristics.Route;
import static slash.navigation.base.RouteCharacteristics.Waypoints;
import static slash.navigation.converter.gui.models.PositionColumns.DESCRIPTION_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.gui.helpers.JMenuHelper.createItem;
import static slash.navigation.gui.helpers.JTableHelper.isFirstToLastRow;
import static slash.navigation.maps.helpers.MapTransfer.asBoundingBox;
import static slash.navigation.maps.helpers.MapTransfer.asLatLong;
import static slash.navigation.maps.helpers.MapTransfer.asNavigationPosition;
import static slash.navigation.maps.helpers.MapTransfer.toBoundingBox;
import static slash.navigation.mapview.MapViewConstants.TRACK_LINE_WIDTH_PREFERENCE;
import static slash.navigation.mapview.mapsforge.AwtGraphicMapView.GRAPHIC_FACTORY;
import static slash.navigation.mapview.mapsforge.models.LocalNames.MAP;
/**
* Implementation for a component that displays the positions of a position list on a map
* using the rewrite branch of the mapsforge project.
*
* @author Christian Pesch
*/
public class MapsforgeMapView implements MapView {
private static final Preferences preferences = Preferences.userNodeForPackage(MapsforgeMapView.class);
private static final Logger log = Logger.getLogger(MapsforgeMapView.class.getName());
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 READ_BUFFER_SIZE_PREFERENCE = "readBufferSize";
private static final String FIRST_LEVEL_TILE_CACHE_SIZE_PREFERENCE = "firstLevelTileCacheSize";
private static final String SECOND_LEVEL_TILE_CACHE_SIZE_PREFERENCE = "secondLevelTileCacheSize";
private static final int SCROLL_DIFF = 100;
private PositionsModel positionsModel;
private PositionsSelectionModel positionsSelectionModel;
private CharacteristicsModel characteristicsModel;
private BooleanModel showAllPositionsAfterLoading;
private BooleanModel recenterAfterZooming;
private BooleanModel showCoordinates;
private ColorModel routeColorModel;
private ColorModel trackColorModel;
private UnitSystemModel unitSystemModel;
private MapViewCallbackOffline mapViewCallback;
private PositionsModelListener positionsModelListener = new PositionsModelListener();
private CharacteristicsModelListener characteristicsModelListener = new CharacteristicsModelListener();
private MapViewCallbackListener mapViewCallbackListener = new MapViewCallbackListener();
private ShowCoordinatesListener showCoordinatesListener = new ShowCoordinatesListener();
private RepaintPositionListListener repaintPositionListListener = new RepaintPositionListListener();
private UnitSystemListener unitSystemListener = new UnitSystemListener();
private DisplayedMapListener displayedMapListener = new DisplayedMapListener();
private AppliedThemeListener appliedThemeListener = new AppliedThemeListener();
private MapSelector mapSelector;
private AwtGraphicMapView mapView;
private MapViewMoverAndZoomer mapViewMoverAndZoomer;
private MapViewCoordinateDisplayer mapViewCoordinateDisplayer = new MapViewCoordinateDisplayer();
private static Bitmap markerIcon, waypointIcon;
private File backgroundMap;
private TileRendererLayer backgroundLayer;
private SelectionUpdater selectionUpdater;
private EventMapUpdater eventMapUpdater, routeUpdater, trackUpdater, waypointUpdater;
private RouteRenderer routeRenderer;
private Thread routeReplacer;
private final Object notificationMutex = new Object();
private final Object eventMapUpdaterLock = new Object();
private boolean running = true, haveToReplaceRoute = false;
// initialization
public void initialize(PositionsModel positionsModel,
PositionsSelectionModel positionsSelectionModel,
CharacteristicsModel characteristicsModel,
MapViewCallback mapViewCallback,
BooleanModel showAllPositionsAfterLoading,
BooleanModel recenterAfterZooming,
BooleanModel showCoordinates,
BooleanModel showWaypointDescription, /* ignored */
FixMapModeModel fixMapModeModel, /* ignored */
ColorModel aRouteColorModel,
final ColorModel aTrackColorModel,
UnitSystemModel unitSystemModel, /* ignored */
GoogleMapsServerModel googleMapsServerModel /* ignored */) {
this.mapViewCallback = (MapViewCallbackOffline) mapViewCallback;
this.positionsModel = positionsModel;
this.positionsSelectionModel = positionsSelectionModel;
this.characteristicsModel = characteristicsModel;
this.showAllPositionsAfterLoading = showAllPositionsAfterLoading;
this.recenterAfterZooming = recenterAfterZooming;
this.showCoordinates = showCoordinates;
this.routeColorModel = aRouteColorModel;
this.trackColorModel = aTrackColorModel;
this.unitSystemModel = unitSystemModel;
this.selectionUpdater = new SelectionUpdater(positionsModel, new SelectionOperation() {
public void add(List<PositionWithLayer> positionWithLayers) {
LatLong center = null;
for (final PositionWithLayer positionWithLayer : positionWithLayers) {
if(!positionWithLayer.hasCoordinates())
continue;
LatLong latLong = asLatLong(positionWithLayer.getPosition());
Marker marker = new DraggableMarker(latLong, markerIcon, 8, -16) {
public void onDrop(LatLong latLong) {
int index = MapsforgeMapView.this.positionsModel.getIndex(positionWithLayer.getPosition());
MapsforgeMapView.this.positionsModel.edit(index, new PositionColumnValues(asList(LONGITUDE_COLUMN_INDEX, LATITUDE_COLUMN_INDEX),
Arrays.<Object>asList(latLong.longitude, latLong.latitude)), true, true);
// ensure this marker is on top of the moved waypoint marker
removeLayer(this);
addLayer(this);
}
};
positionWithLayer.setLayer(marker);
addLayer(marker);
center = latLong;
}
if (center != null)
setCenter(center, false);
}
public void remove(List<PositionWithLayer> positionWithLayers) {
for (PositionWithLayer positionWithLayer : positionWithLayers)
removeLayer(positionWithLayer);
}
});
this.routeUpdater = new TrackUpdater(positionsModel, new TrackOperation() {
private List<PairWithLayer> pairs = new ArrayList<>();
public void add(List<PairWithLayer> pairWithLayers) {
internalAdd(pairWithLayers);
}
public void update(List<PairWithLayer> pairWithLayers) {
internalRemove(pairWithLayers);
internalAdd(pairWithLayers);
updateSelectionAfterUpdate(pairWithLayers);
}
public void remove(List<PairWithLayer> pairWithLayers) {
internalRemove(pairWithLayers);
fireDistanceAndTime();
updateSelectionAfterRemove(pairWithLayers);
}
private void internalAdd(List<PairWithLayer> pairWithLayers) {
pairs.addAll(pairWithLayers);
routeRenderer.renderRoute(pairWithLayers, new Runnable() {
public void run() {
fireDistanceAndTime();
}
});
}
private void internalRemove(List<PairWithLayer> pairWithLayers) {
pairs.removeAll(pairWithLayers);
for (PairWithLayer pairWithLayer : pairWithLayers) {
removeLayer(pairWithLayer);
pairWithLayer.setDistanceAndTime(null);
}
}
private void fireDistanceAndTime() {
Map<Integer, DistanceAndTime> result = new HashMap<>(pairs.size());
double aggregatedDistance = 0.0;
long aggregatedTime = 0L;
for (int i = 0; i < pairs.size(); i++) {
PairWithLayer pairWithLayer = pairs.get(i);
DistanceAndTime distanceAndTime = pairWithLayer.getDistanceAndTime();
if(distanceAndTime != null) {
Double distance = distanceAndTime.getDistance();
if (!isEmpty(distance))
aggregatedDistance += distance;
Long time = distanceAndTime.getTime();
if (!isEmpty(time))
aggregatedTime += time;
}
result.put(i + 1, new DistanceAndTime(aggregatedDistance, aggregatedTime));
}
fireCalculatedDistances(result);
}
});
this.trackUpdater = new TrackUpdater(positionsModel, new TrackOperation() {
public void add(List<PairWithLayer> pairWithLayers) {
internalAdd(pairWithLayers);
}
public void update(List<PairWithLayer> pairWithLayers) {
internalRemove(pairWithLayers);
internalAdd(pairWithLayers);
updateSelectionAfterUpdate(pairWithLayers);
}
public void remove(List<PairWithLayer> pairWithLayers) {
internalRemove(pairWithLayers);
updateSelectionAfterRemove(pairWithLayers);
}
private void internalAdd(List<PairWithLayer> pairWithLayers) {
Paint paint = GRAPHIC_FACTORY.createPaint();
paint.setColor(ColorHelper.asRGBA(trackColorModel));
paint.setStrokeWidth(preferences.getInt(TRACK_LINE_WIDTH_PREFERENCE, 2));
int tileSize = getTileSize();
for (PairWithLayer pair : pairWithLayers) {
if(!pair.hasCoordinates())
continue;
Line line = new Line(asLatLong(pair.getFirst()), asLatLong(pair.getSecond()), paint, tileSize);
pair.setLayer(line);
addLayer(line);
}
}
private void internalRemove(List<PairWithLayer> pairWithLayers) {
for (PairWithLayer pairWithLayer : pairWithLayers)
removeLayer(pairWithLayer);
}
});
this.waypointUpdater = new WaypointUpdater(positionsModel, new WaypointOperation() {
public void add(List<PositionWithLayer> positionWithLayers) {
for (PositionWithLayer positionWithLayer : positionWithLayers) {
internalAdd(positionWithLayer);
}
}
public void update(List<PositionWithLayer> positionWithLayers) {
List<NavigationPosition> updated = new ArrayList<>();
for (PositionWithLayer positionWithLayer : positionWithLayers) {
internalRemove(positionWithLayer);
internalAdd(positionWithLayer);
updated.add(positionWithLayer.getPosition());
}
selectionUpdater.updatedPositions(new ArrayList<>(updated));
}
public void remove(List<PositionWithLayer> positionWithLayers) {
List<NavigationPosition> removed = new ArrayList<>();
for (PositionWithLayer positionWithLayer : positionWithLayers) {
internalRemove(positionWithLayer);
removed.add(positionWithLayer.getPosition());
}
selectionUpdater.removedPositions(removed);
}
private void internalAdd(PositionWithLayer positionWithLayer) {
if(!positionWithLayer.hasCoordinates())
return;
Marker marker = new Marker(asLatLong(positionWithLayer.getPosition()), waypointIcon, 1, 0);
positionWithLayer.setLayer(marker);
addLayer(marker);
}
private void internalRemove(PositionWithLayer positionWithLayer) {
removeLayer(positionWithLayer);
}
});
this.eventMapUpdater = getEventMapUpdaterFor(Waypoints);
positionsModel.addTableModelListener(positionsModelListener);
characteristicsModel.addListDataListener(characteristicsModelListener);
mapViewCallback.addRoutingServiceChangeListener(mapViewCallbackListener);
showCoordinates.addChangeListener(showCoordinatesListener);
routeColorModel.addChangeListener(repaintPositionListListener);
trackColorModel.addChangeListener(repaintPositionListListener);
unitSystemModel.addChangeListener(unitSystemListener);
initializeActions();
initializeMapView();
routeRenderer = new RouteRenderer(this, this.mapViewCallback, routeColorModel, GRAPHIC_FACTORY);
routeReplacer = new Thread(new Runnable() {
public void run() {
while (true) {
synchronized (notificationMutex) {
try {
notificationMutex.wait(50);
} catch (InterruptedException e) {
// ignore this
}
if (!running)
return;
if (!haveToReplaceRoute)
continue;
haveToReplaceRoute = false;
}
synchronized (eventMapUpdaterLock) {
// remove all from previous event map updater
eventMapUpdater.handleRemove(0, MAX_VALUE);
// select current event map updater and let him add all
eventMapUpdater = getEventMapUpdaterFor(lastCharacteristics);
eventMapUpdater.handleAdd(0, MapsforgeMapView.this.positionsModel.getRowCount() - 1);
}
}
}
}, "RouteReplacer");
routeReplacer.start();
}
private void initializeActions() {
ActionManager actionManager = Application.getInstance().getContext().getActionManager();
actionManager.register("select-position", new SelectPositionAction());
actionManager.register("extend-selection", new ExtendSelectionAction());
actionManager.register("add-position", new AddPositionAction());
actionManager.register("delete-position-from-map", new DeletePositionAction());
actionManager.registerLocal("delete", MAP, "delete-position-from-map");
actionManager.register("center-here", new CenterAction());
actionManager.register("zoom-in", new ZoomAction(+1));
actionManager.register("zoom-out", new ZoomAction(-1));
}
private MapManager getMapManager() {
return mapViewCallback.getMapManager();
}
private LayerManager getLayerManager() {
return mapView.getLayerManager();
}
private void initializeMapView() {
mapView = createMapView();
handleUnitSystem();
try {
markerIcon = GRAPHIC_FACTORY.createResourceBitmap(MapsforgeMapView.class.getResourceAsStream("marker.png"), -1);
waypointIcon = GRAPHIC_FACTORY.createResourceBitmap(MapsforgeMapView.class.getResourceAsStream("waypoint.png"), -1);
} catch (IOException e) {
log.severe("Cannot create marker and waypoint icon: " + e);
}
mapSelector = new MapSelector(getMapManager(), mapView);
mapViewMoverAndZoomer = new MapViewMoverAndZoomer(mapView, getLayerManager());
mapViewCoordinateDisplayer.initialize(mapView, mapViewCallback);
new MapViewPopupMenu(mapView, createPopupMenu());
final ActionManager actionManager = Application.getInstance().getContext().getActionManager();
mapSelector.getMapViewPanel().registerKeyboardAction(new FrameAction() {
public void run() {
actionManager.run("zoom-in");
}
}, getKeyStroke(VK_PLUS, CTRL_DOWN_MASK), WHEN_IN_FOCUSED_WINDOW);
mapSelector.getMapViewPanel().registerKeyboardAction(new FrameAction() {
public void run() {
actionManager.run("zoom-out");
}
}, getKeyStroke(VK_MINUS, CTRL_DOWN_MASK), WHEN_IN_FOCUSED_WINDOW);
mapSelector.getMapViewPanel().registerKeyboardAction(new FrameAction() {
public void run() {
mapViewMoverAndZoomer.animateCenter(SCROLL_DIFF, 0);
}
}, getKeyStroke(VK_LEFT, CTRL_DOWN_MASK), WHEN_IN_FOCUSED_WINDOW);
mapSelector.getMapViewPanel().registerKeyboardAction(new FrameAction() {
public void run() {
mapViewMoverAndZoomer.animateCenter(-SCROLL_DIFF, 0);
}
}, getKeyStroke(VK_RIGHT, CTRL_DOWN_MASK), WHEN_IN_FOCUSED_WINDOW);
mapSelector.getMapViewPanel().registerKeyboardAction(new FrameAction() {
public void run() {
mapViewMoverAndZoomer.animateCenter(0, SCROLL_DIFF);
}
}, getKeyStroke(VK_UP, CTRL_DOWN_MASK), WHEN_IN_FOCUSED_WINDOW);
mapSelector.getMapViewPanel().registerKeyboardAction(new FrameAction() {
public void run() {
mapViewMoverAndZoomer.animateCenter(0, -SCROLL_DIFF);
}
}, getKeyStroke(VK_DOWN, CTRL_DOWN_MASK), WHEN_IN_FOCUSED_WINDOW);
final MapViewPosition mapViewPosition = mapView.getModel().mapViewPosition;
mapViewPosition.addObserver(new Observer() {
public void onChange() {
mapSelector.zoomChanged(mapViewPosition.getZoomLevel());
}
});
double longitude = preferences.getDouble(CENTER_LONGITUDE_PREFERENCE, -25.0);
double latitude = preferences.getDouble(CENTER_LATITUDE_PREFERENCE, 35.0);
byte zoom = (byte) preferences.getInt(CENTER_ZOOM_PREFERENCE, 2);
mapViewPosition.setMapPosition(new MapPosition(new LatLong(latitude, longitude), zoom));
mapView.getModel().mapViewDimension.addObserver(new Observer() {
private boolean initialized = false;
public void onChange() {
if (!initialized) {
handleMapAndThemeUpdate(true, true);
initialized = true;
}
}
});
getMapManager().getDisplayedMapModel().addChangeListener(displayedMapListener);
getMapManager().getAppliedThemeModel().addChangeListener(appliedThemeListener);
}
public void setBackgroundMap(File backgroundMap) {
this.backgroundMap = backgroundMap;
updateMapAndThemesAfterDirectoryScanning();
}
public void updateMapAndThemesAfterDirectoryScanning() {
if (mapView != null)
handleMapAndThemeUpdate(false, false);
}
private AwtGraphicMapView createMapView() {
// Multithreaded map rendering
MapWorkerPool.NUMBER_OF_THREADS = Runtime.getRuntime().availableProcessors();
// Maximum read buffer size
ReadBuffer.MAXIMUM_BUFFER_SIZE = preferences.getInt(READ_BUFFER_SIZE_PREFERENCE,2500000);
// No square frame buffer since the device orientation hardly changes
FrameBufferController.SQUARE_FRAME_BUFFER = false;
AwtGraphicMapView mapView = new AwtGraphicMapView();
new MapViewResizer(mapView, mapView.getModel().mapViewDimension);
mapView.getMapScaleBar().setVisible(true);
((DefaultMapScaleBar) mapView.getMapScaleBar()).setScaleBarMode(SINGLE);
return mapView;
}
private void handleUnitSystem() {
UnitSystem unitSystem = unitSystemModel.getUnitSystem();
switch(unitSystem) {
case Metric:
mapView.getMapScaleBar().setDistanceUnitAdapter(MetricUnitAdapter.INSTANCE);
break;
case Statute:
mapView.getMapScaleBar().setDistanceUnitAdapter(ImperialUnitAdapter.INSTANCE);
break;
case Nautic:
mapView.getMapScaleBar().setDistanceUnitAdapter(NauticalUnitAdapter.INSTANCE);
break;
default:
throw new IllegalArgumentException("Unknown UnitSystem " + unitSystem);
}
}
private JPopupMenu createPopupMenu() {
JPopupMenu menu = new JPopupMenu();
menu.add(createItem("select-position"));
menu.add(createItem("add-position")); // TODO should be "new-position"
menu.add(createItem("delete-position-from-map"));
menu.addSeparator();
menu.add(createItem("center-here"));
menu.add(createItem("zoom-in"));
menu.add(createItem("zoom-out"));
return menu;
}
private TileRendererLayer createTileRendererLayer(File mapFile, String cacheId) {
TileRendererLayer tileRendererLayer = new TileRendererLayer(createTileCache(cacheId), new MapFile(mapFile), mapView.getModel().mapViewPosition, true, true, true, GRAPHIC_FACTORY);
tileRendererLayer.setXmlRenderTheme(getMapManager().getAppliedThemeModel().getItem().getXmlRenderTheme());
return tileRendererLayer;
}
private TileDownloadLayer createTileDownloadLayer(TileSource tileSource, String cacheId) {
return new TileDownloadLayer(createTileCache(cacheId), mapView.getModel().mapViewPosition, tileSource, GRAPHIC_FACTORY);
}
private TileCache createTileCache(String cacheId) {
TileCache firstLevelTileCache = new InMemoryTileCache(preferences.getInt(FIRST_LEVEL_TILE_CACHE_SIZE_PREFERENCE,256));
File cacheDirectory = new File(getTemporaryDirectory(), encodeUri(cacheId));
TileCache secondLevelTileCache = new FileSystemTileCache(preferences.getInt(SECOND_LEVEL_TILE_CACHE_SIZE_PREFERENCE,2048), cacheDirectory, GRAPHIC_FACTORY);
return new TwoLevelTileCache(firstLevelTileCache, secondLevelTileCache);
}
private void updateSelectionAfterUpdate(List<PairWithLayer> pairWithLayers) {
Set<NavigationPosition> updated = new HashSet<>();
for (PairWithLayer pair : pairWithLayers) {
updated.add(pair.getFirst());
updated.add(pair.getSecond());
}
selectionUpdater.updatedPositions(new ArrayList<>(updated));
}
private void updateSelectionAfterRemove(List<PairWithLayer> pairWithLayers) {
Set<NavigationPosition> removed = new HashSet<>();
for (PairWithLayer pair : pairWithLayers) {
removed.add(pair.getFirst());
removed.add(pair.getSecond());
}
selectionUpdater.removedPositions(new ArrayList<>(removed));
}
private java.util.Map<LocalMap, Layer> mapsToLayers = new HashMap<>();
private void handleMapAndThemeUpdate(boolean centerAndZoom, boolean alwaysRecenter) {
Layers layers = getLayerManager().getLayers();
// add new map with a theme
LocalMap map = getMapManager().getDisplayedMapModel().getItem();
Layer layer;
try {
layer = map.isVector() ? createTileRendererLayer(map.getFile(), map.getUrl()) : createTileDownloadLayer(map.getTileSource(), map.getUrl());
} catch (Exception e) {
mapViewCallback.showMapException(map != null ? map.getDescription() : "<no map>", e);
return;
}
// remove old map
for (Map.Entry<LocalMap, Layer> entry : mapsToLayers.entrySet()) {
Layer remove = entry.getValue();
layers.remove(remove);
remove.onDestroy();
if(remove instanceof TileLayer)
((TileLayer)remove).getTileCache().destroy();
}
mapsToLayers.clear();
// add map as the first to be behind all additional layers
layers.add(0, layer);
mapsToLayers.put(map, layer);
// initialize tile renderer layer for background map
if (backgroundMap != null) {
backgroundLayer = createTileRendererLayer(backgroundMap, backgroundMap.getName());
backgroundMap = null;
}
if(backgroundLayer != null) {
layers.remove(backgroundLayer);
if (map.isVector())
layers.add(0, backgroundLayer);
}
// then start download layer threads
if (layer instanceof TileDownloadLayer)
((TileDownloadLayer) layer).start();
// center and zoom: if map is initialized, doesn't contain route or there is no route
BoundingBox mapBoundingBox = getMapBoundingBox();
BoundingBox routeBoundingBox = getRouteBoundingBox();
if (centerAndZoom &&
((mapBoundingBox != null && routeBoundingBox != null && !mapBoundingBox.contains(routeBoundingBox)) ||
routeBoundingBox == null)) {
centerAndZoom(mapBoundingBox, routeBoundingBox, alwaysRecenter);
}
limitZoomLevel();
log.info("Using map " + mapsToLayers.keySet() + " and theme " + getMapManager().getAppliedThemeModel().getItem() + " with zoom " + getZoom());
}
private void replaceRoute() {
synchronized (notificationMutex) {
haveToReplaceRoute = true;
notificationMutex.notifyAll();
}
}
private BaseRoute lastRoute = null;
private RouteCharacteristics lastCharacteristics = Waypoints; // corresponds to default eventMapUpdater
private void updateRouteButDontRecenter() {
// avoid duplicate work
RouteCharacteristics characteristics = MapsforgeMapView.this.characteristicsModel.getSelectedCharacteristics();
BaseRoute route = positionsModel.getRoute();
if (lastCharacteristics.equals(characteristics) && lastRoute != null && lastRoute.equals(positionsModel.getRoute()))
return;
lastCharacteristics = characteristics;
lastRoute = route;
replaceRoute();
}
private EventMapUpdater getEventMapUpdaterFor(RouteCharacteristics characteristics) {
switch (characteristics) {
case Route:
return routeUpdater;
case Track:
return trackUpdater;
case Waypoints:
return waypointUpdater;
default:
throw new IllegalArgumentException("RouteCharacteristics " + characteristics + " is not supported");
}
}
public boolean isInitialized() {
return true;
}
public boolean isDownload() {
return true;
}
public Throwable getInitializationCause() {
return null;
}
public void dispose() {
getMapManager().getDisplayedMapModel().removeChangeListener(displayedMapListener);
getMapManager().getAppliedThemeModel().removeChangeListener(appliedThemeListener);
positionsModel.removeTableModelListener(positionsModelListener);
characteristicsModel.removeListDataListener(characteristicsModelListener);
mapViewCallback.removeRoutingServiceChangeListener(mapViewCallbackListener);
routeColorModel.removeChangeListener(repaintPositionListListener);
trackColorModel.removeChangeListener(repaintPositionListListener);
unitSystemModel.removeChangeListener(unitSystemListener);
long start = currentTimeMillis();
synchronized (notificationMutex) {
running = false;
notificationMutex.notifyAll();
}
if (routeReplacer != null) {
try {
safeJoin(routeReplacer);
} catch (InterruptedException e) {
// intentionally left empty
}
long end = currentTimeMillis();
log.info("RouteReplacer stopped after " + (end - start) + " ms");
}
routeRenderer.dispose();
NavigationPosition center = getCenter();
preferences.putDouble(CENTER_LONGITUDE_PREFERENCE, center.getLongitude());
preferences.putDouble(CENTER_LATITUDE_PREFERENCE, center.getLatitude());
int zoom = getZoom();
preferences.putInt(CENTER_ZOOM_PREFERENCE, zoom);
mapView.destroyAll();
}
public Component getComponent() {
return mapSelector.getComponent();
}
public void resize() {
// intentionally left empty
}
@SuppressWarnings("unchecked")
public void showAllPositions() {
List<NavigationPosition> positions = positionsModel.getRoute().getPositions();
if (positions.size() > 0) {
BoundingBox both = new BoundingBox(positions);
zoomToBounds(both);
setCenter(both.getCenter(), true);
}
}
private Polyline mapBorder, routeBorder;
public void showMapBorder(BoundingBox mapBoundingBox) {
if (mapBorder != null) {
removeLayer(mapBorder);
mapBorder = null;
}
if (routeBorder != null) {
removeLayer(routeBorder);
routeBorder = null;
}
if (mapBoundingBox != null) {
mapBorder = drawBorder(mapBoundingBox);
BoundingBox routeBoundingBox = getRouteBoundingBox();
if (routeBoundingBox != null)
routeBorder = drawBorder(routeBoundingBox);
centerAndZoom(mapBoundingBox, routeBoundingBox, true);
}
}
private Polyline drawBorder(BoundingBox boundingBox) {
Paint paint = GRAPHIC_FACTORY.createPaint();
paint.setColor(BLUE);
paint.setStrokeWidth(3);
paint.setDashPathEffect(new float[]{3, 12});
Polyline polyline = new Polyline(asLatLong(boundingBox), paint, getTileSize());
addLayer(polyline);
return polyline;
}
public void addLayer(Layer layer) {
mapView.addLayer(layer);
}
private void removeLayer(Layer layer) {
mapView.removeLayer(layer);
}
private void removeLayer(PositionWithLayer positionWithLayer) {
Layer layer = positionWithLayer.getLayer();
if (layer != null)
removeLayer(layer);
else
log.warning("Could not find layer for position " + positionWithLayer);
positionWithLayer.setLayer(null);
}
public void removeLayer(PairWithLayer pairWithLayer) {
Layer layer = pairWithLayer.getLayer();
if (layer != null)
removeLayer(layer);
else
log.warning("Could not find layer for pair " + pairWithLayer);
pairWithLayer.setLayer(null);
}
private BoundingBox getMapBoundingBox() {
Collection<Layer> values = mapsToLayers.values();
if (!values.isEmpty()) {
Layer layer = values.iterator().next();
if (layer instanceof TileRendererLayer) {
TileRendererLayer tileRendererLayer = (TileRendererLayer) layer;
return toBoundingBox(tileRendererLayer.getMapDataStore().boundingBox());
}
}
return null;
}
public int getTileSize() {
return mapView.getModel().displayModel.getTileSize();
}
@SuppressWarnings("unchecked")
private BoundingBox getRouteBoundingBox() {
BaseRoute route = positionsModel.getRoute();
return route != null && route.getPositions().size() > 0 ? new BoundingBox(route.getPositions()) : null;
}
private void centerAndZoom(BoundingBox mapBoundingBox, BoundingBox routeBoundingBox, boolean alwaysRecenter) {
List<NavigationPosition> positions = new ArrayList<>();
// if there is a route and we center and zoom, then use the route bounding box
if (routeBoundingBox != null) {
positions.add(routeBoundingBox.getNorthEast());
positions.add(routeBoundingBox.getSouthWest());
}
// if the map is limited
if (mapBoundingBox != null) {
// if there is a route
if (routeBoundingBox != null) {
positions.add(routeBoundingBox.getNorthEast());
positions.add(routeBoundingBox.getSouthWest());
// if the map is limited and doesn't cover the route
if (!mapBoundingBox.contains(routeBoundingBox)) {
positions.add(mapBoundingBox.getNorthEast());
positions.add(mapBoundingBox.getSouthWest());
}
// if there just a map
} else {
positions.add(mapBoundingBox.getNorthEast());
positions.add(mapBoundingBox.getSouthWest());
}
}
if (positions.size() > 0) {
BoundingBox both = new BoundingBox(positions);
zoomToBounds(both);
setCenter(both.getCenter(), alwaysRecenter);
}
}
private void limitZoomLevel() {
// limit minimum zoom to prevent zooming out too much and losing the map
byte zoomLevelMin = 2;
LocalMap map = mapsToLayers.keySet().iterator().next();
if (map.isVector() && mapView.getModel().mapViewDimension.getDimension() != null)
zoomLevelMin = (byte) max(0, zoomForBounds(mapView.getModel().mapViewDimension.getDimension(),
asBoundingBox(map.getBoundingBox()), getTileSize()) - 3);
mapView.setZoomLevelMin(zoomLevelMin);
// limit maximum to prevent zooming in to grey area
byte zoomLevelMax = (byte) (map.isVector() ? 22 : 18);
mapView.setZoomLevelMax(zoomLevelMax);
}
private boolean isVisible(LatLong latLong, int border) {
MapViewProjection projection = new MapViewProjection(mapView);
LatLong upperLeft = projection.fromPixels(border, border);
Dimension dimension = mapView.getDimension();
LatLong lowerRight = projection.fromPixels(dimension.width - border, dimension.height - border);
return upperLeft != null && lowerRight != null && new org.mapsforge.core.model.BoundingBox(lowerRight.latitude, upperLeft.longitude, upperLeft.latitude, lowerRight.longitude).contains(latLong);
}
public NavigationPosition getCenter() {
return asNavigationPosition(mapView.getModel().mapViewPosition.getCenter());
}
private void setCenter(LatLong center, boolean alwaysRecenter) {
if (alwaysRecenter || recenterAfterZooming.getBoolean() || !isVisible(center, 20))
mapView.getModel().mapViewPosition.animateTo(center);
}
private void setCenter(NavigationPosition center, boolean alwaysRecenter) {
setCenter(asLatLong(center), alwaysRecenter);
}
private int getZoom() {
return mapView.getModel().mapViewPosition.getZoomLevel();
}
private void setZoom(int zoom) {
mapView.setZoomLevel((byte) zoom);
}
private void zoomToBounds(org.mapsforge.core.model.BoundingBox boundingBox) {
Dimension dimension = mapView.getModel().mapViewDimension.getDimension();
if (dimension == null)
return;
byte zoom = zoomForBounds(dimension, boundingBox, getTileSize());
setZoom(zoom);
}
private void zoomToBounds(BoundingBox boundingBox) {
zoomToBounds(asBoundingBox(boundingBox));
}
public boolean isSupportsPrinting() {
return false;
}
public boolean isSupportsPrintingWithDirections() {
return false;
}
public void print(String title, boolean withDirections) {
throw new UnsupportedOperationException("Printing not supported");
}
public void setSelectedPositions(int[] selectedPositions, boolean replaceSelection) {
if (selectionUpdater == null)
return;
selectionUpdater.setSelectedPositions(selectedPositions, replaceSelection);
}
public void setSelectedPositions(List<NavigationPosition> selectedPositions) {
throw new UnsupportedOperationException("photo panel not available in Offline Edition");
}
private LatLong getMousePosition() {
Point point = mapViewMoverAndZoomer.getLastMousePoint();
return point != null ? new MapViewProjection(mapView).fromPixels(point.getX(), point.getY()) :
mapView.getModel().mapViewPosition.getCenter();
}
private double getThresholdForPixel(LatLong latLong, int pixel) {
long mapSize = getMapSize(mapView.getModel().mapViewPosition.getZoomLevel(), getTileSize());
double metersPerPixel = calculateGroundResolution(latLong.latitude, mapSize);
return metersPerPixel * pixel;
}
private void selectPosition(Double longitude, Double latitude, Double threshold, boolean replaceSelection) {
int row = positionsModel.getClosestPosition(longitude, latitude, threshold);
if (row != -1 && !mapViewMoverAndZoomer.isMousePressedOnMarker())
positionsSelectionModel.setSelectedPositions(new int[]{row}, replaceSelection);
}
private class SelectPositionAction extends FrameAction {
public void run() {
LatLong latLong = getMousePosition();
if (latLong != null) {
Double threshold = getThresholdForPixel(latLong, 15);
selectPosition(latLong.longitude, latLong.latitude, threshold, true);
}
}
}
private class ExtendSelectionAction extends FrameAction {
public void run() {
LatLong latLong = getMousePosition();
if (latLong != null) {
Double threshold = getThresholdForPixel(latLong, 15);
selectPosition(latLong.longitude, latLong.latitude, threshold, false);
}
}
}
private class AddPositionAction extends FrameAction {
private int getAddRow() {
List<PositionWithLayer> lastSelectedPositions = selectionUpdater.getPositionWithLayers();
NavigationPosition position = lastSelectedPositions.size() > 0 ? lastSelectedPositions.get(lastSelectedPositions.size() - 1).getPosition() : 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 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);
}
public void run() {
LatLong latLong = getMousePosition();
if (latLong != null) {
int row = getAddRow();
insertPosition(row, latLong.longitude, latLong.latitude);
}
}
}
private class DeletePositionAction extends FrameAction {
private void removePosition(Double longitude, Double latitude, Double threshold) {
int row = positionsModel.getClosestPosition(longitude, latitude, threshold);
if (row != -1) {
positionsModel.remove(new int[]{row});
}
}
public void run() {
LatLong latLong = getMousePosition();
if (latLong != null) {
Double threshold = getThresholdForPixel(latLong, 15);
removePosition(latLong.longitude, latLong.latitude, threshold);
}
}
}
private class CenterAction extends FrameAction {
public void run() {
mapViewMoverAndZoomer.centerToMousePosition();
}
}
private class ZoomAction extends FrameAction {
private byte zoomLevelDiff;
private ZoomAction(int zoomLevelDiff) {
this.zoomLevelDiff = (byte) zoomLevelDiff;
}
public void run() {
mapViewMoverAndZoomer.zoomToMousePosition(zoomLevelDiff);
}
}
// 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 class MapViewCallbackListener implements ChangeListener {
public void stateChanged(ChangeEvent e) {
if (positionsModel.getRoute().getCharacteristics().equals(Route))
replaceRoute();
}
}
private class PositionsModelListener implements TableModelListener {
public void tableChanged(TableModelEvent e) {
switch (e.getType()) {
case INSERT:
case DELETE:
handleUpdate(e.getType(), e.getFirstRow(), e.getLastRow());
break;
case UPDATE:
if (positionsModel.isContinousRange())
return;
if (!(e.getColumn() == DESCRIPTION_COLUMN_INDEX ||
e.getColumn() == LONGITUDE_COLUMN_INDEX ||
e.getColumn() == LATITUDE_COLUMN_INDEX ||
e.getColumn() == ALL_COLUMNS))
return;
boolean allRowsChanged = isFirstToLastRow(e);
if (!allRowsChanged)
handleUpdate(e.getType(), e.getFirstRow(), e.getLastRow());
if (allRowsChanged && showAllPositionsAfterLoading.getBoolean())
centerAndZoom(getMapBoundingBox(), getRouteBoundingBox(), true);
break;
default:
throw new IllegalArgumentException("Event type " + e.getType() + " is not supported");
}
}
private void handleUpdate(final int eventType, final int firstRow, final int lastRow) {
new Thread(new Runnable() {
public void run() {
synchronized (eventMapUpdaterLock) {
switch(eventType) {
case INSERT:
eventMapUpdater.handleAdd(firstRow, lastRow);
break;
case UPDATE:
eventMapUpdater.handleUpdate(firstRow, lastRow);
break;
case DELETE:
eventMapUpdater.handleRemove(firstRow, lastRow);
break;
default:
throw new IllegalArgumentException("Event type " + eventType + " is not supported");
}
}
}
}, "UpdateDecoupler").start();
}
}
private class CharacteristicsModelListener implements ListDataListener {
public void intervalAdded(ListDataEvent e) {
}
public void intervalRemoved(ListDataEvent e) {
}
public void contentsChanged(ListDataEvent e) {
updateRouteButDontRecenter();
}
}
private class ShowCoordinatesListener implements ChangeListener {
public void stateChanged(ChangeEvent e) {
mapViewCoordinateDisplayer.setShowCoordinates(showCoordinates.getBoolean());
}
}
private class RepaintPositionListListener implements ChangeListener {
public void stateChanged(ChangeEvent e) {
replaceRoute();
}
}
private class UnitSystemListener implements ChangeListener {
public void stateChanged(ChangeEvent e) {
handleUnitSystem();
}
}
private class DisplayedMapListener implements ChangeListener {
public void stateChanged(ChangeEvent e) {
handleMapAndThemeUpdate(true, !isVisible(mapView.getModel().mapViewPosition.getCenter(), 20));
}
}
private class AppliedThemeListener implements ChangeListener {
public void stateChanged(ChangeEvent e) {
handleMapAndThemeUpdate(false, false);
}
}
}