diff --git a/OsmAnd-java/src/main/java/net/osmand/util/MapUtils.java b/OsmAnd-java/src/main/java/net/osmand/util/MapUtils.java index 8869be9eb22..c134df6322d 100644 --- a/OsmAnd-java/src/main/java/net/osmand/util/MapUtils.java +++ b/OsmAnd-java/src/main/java/net/osmand/util/MapUtils.java @@ -758,11 +758,15 @@ public static boolean areLatLonEqualPrecise(Location l, double lat, double lon) } public static LatLon rhumbDestinationPoint(LatLon latLon, double distance, double bearing) { + return rhumbDestinationPoint(latLon.getLatitude(), latLon.getLongitude(), distance, bearing); + } + + public static LatLon rhumbDestinationPoint(double lat, double lon, double distance, double bearing) { double radius = EARTH_RADIUS_A; double d = distance / radius; // angular distance in radians - double phi1 = Math.toRadians(latLon.getLatitude()); - double lambda1 = Math.toRadians(latLon.getLongitude()); + double phi1 = Math.toRadians(lat); + double lambda1 = Math.toRadians(lon); double theta = Math.toRadians(bearing); double deltaPhi = d * Math.cos(theta); diff --git a/OsmAnd/src/net/osmand/plus/AppInitializer.java b/OsmAnd/src/net/osmand/plus/AppInitializer.java index bbe07ab5735..557244540c7 100644 --- a/OsmAnd/src/net/osmand/plus/AppInitializer.java +++ b/OsmAnd/src/net/osmand/plus/AppInitializer.java @@ -37,6 +37,7 @@ import net.osmand.plus.backup.BackupHelper; import net.osmand.plus.backup.NetworkSettingsHelper; import net.osmand.plus.base.MapViewTrackingUtilities; +import net.osmand.plus.base.MyLocationAnimation; import net.osmand.plus.base.dialog.DialogManager; import net.osmand.plus.download.LocalIndexHelper; import net.osmand.plus.download.LocalIndexInfo; @@ -350,6 +351,7 @@ public void onCreateApplication() { app.searchUICore = startupInit(new QuickSearchHelper(app), QuickSearchHelper.class); app.mapViewTrackingUtilities = startupInit(new MapViewTrackingUtilities(app), MapViewTrackingUtilities.class); app.osmandMap = startupInit(new OsmandMap(app), OsmandMap.class); + app.myLocationAnimation = startupInit(new MyLocationAnimation(app), MyLocationAnimation.class); app.travelHelper = startupInit(new TravelObfHelper(app), TravelHelper.class); app.travelRendererHelper = startupInit(new TravelRendererHelper(app), TravelRendererHelper.class); diff --git a/OsmAnd/src/net/osmand/plus/OsmAndLocationSimulation.java b/OsmAnd/src/net/osmand/plus/OsmAndLocationSimulation.java index 0318f59fea8..5ad1f695cec 100644 --- a/OsmAnd/src/net/osmand/plus/OsmAndLocationSimulation.java +++ b/OsmAnd/src/net/osmand/plus/OsmAndLocationSimulation.java @@ -284,7 +284,7 @@ public void run() { meters = (float) result.get(1); } float speed = meters / intervalTime * coeff; - if (intervalTime != 0 && speedSimulation) { + if (intervalTime != 0) { current.setSpeed(speed); } if ((!current.hasAccuracy() || Double.isNaN(current.getAccuracy()) || diff --git a/OsmAnd/src/net/osmand/plus/OsmandApplication.java b/OsmAnd/src/net/osmand/plus/OsmandApplication.java index aff12e93843..a0c20cee810 100644 --- a/OsmAnd/src/net/osmand/plus/OsmandApplication.java +++ b/OsmAnd/src/net/osmand/plus/OsmandApplication.java @@ -52,6 +52,7 @@ import net.osmand.plus.backup.BackupHelper; import net.osmand.plus.backup.NetworkSettingsHelper; import net.osmand.plus.base.MapViewTrackingUtilities; +import net.osmand.plus.base.MyLocationAnimation; import net.osmand.plus.base.dialog.DialogManager; import net.osmand.plus.download.DownloadIndexesThread; import net.osmand.plus.download.DownloadService; @@ -191,6 +192,7 @@ public class OsmandApplication extends MultiDexApplication { InAppPurchaseHelper inAppPurchaseHelper; MapViewTrackingUtilities mapViewTrackingUtilities; OsmandMap osmandMap; + MyLocationAnimation myLocationAnimation; LockHelper lockHelper; KeyEventHelper keyEventHelper; FileSettingsHelper fileSettingsHelper; @@ -1091,6 +1093,10 @@ public OsmandMap getOsmandMap() { return osmandMap; } + public MyLocationAnimation getMyLocationAnimation() { + return myLocationAnimation; + } + public boolean useOpenGlRenderer() { return NativeCoreContext.isInit() && settings.USE_OPENGL_RENDER.get(); } diff --git a/OsmAnd/src/net/osmand/plus/base/MapViewTrackingUtilities.java b/OsmAnd/src/net/osmand/plus/base/MapViewTrackingUtilities.java index f71c097b586..7e3d52fbbd7 100644 --- a/OsmAnd/src/net/osmand/plus/base/MapViewTrackingUtilities.java +++ b/OsmAnd/src/net/osmand/plus/base/MapViewTrackingUtilities.java @@ -220,65 +220,16 @@ public void updateLocation(Location location) { if (mapView != null) { RotatedTileBox tb = mapView.getCurrentRotatedTileBox().copy(); if (isMapLinkedToLocation() && location != null) { - Pair zoom = null; - Float rotation = null; - boolean pendingRotation = false; - if (settings.AUTO_ZOOM_MAP.get()) { - zoom = autozoom(tb, location); - } int currentMapRotation = settings.ROTATE_MAP.get(); boolean smallSpeedForCompass = isSmallSpeedForCompass(location); - boolean smallSpeedForAnimation = isSmallSpeedForAnimation(location); - showViewAngle = (!location.hasBearing() || smallSpeedForCompass) && (tb != null && - NativeUtilities.containsLatLon(mapView.getMapRenderer(), tb, location.getLatitude(), location.getLongitude())); - if (currentMapRotation == OsmandSettings.ROTATE_MAP_BEARING) { - // special case when bearing equals to zero (we don't change anything) - if (location.hasBearing() && location.getBearing() != 0f) { - rotation = -location.getBearing(); - } - if (rotation == null && prevLocation != null && tb != null) { - double distDp = (tb.getPixDensity() * MapUtils.getDistance(prevLocation, location)) / tb.getDensity(); - if (distDp > SKIP_ANIMATION_DP_THRESHOLD) { - movingTime = 0; - } - } - } else if (currentMapRotation == OsmandSettings.ROTATE_MAP_COMPASS) { + if (currentMapRotation == OsmandSettings.ROTATE_MAP_COMPASS) { showViewAngle = routePlanningMode; // disable compass rotation in that mode - pendingRotation = true; - } else if (currentMapRotation == OsmandSettings.ROTATE_MAP_NONE) { - rotation = 0.0f; - pendingRotation = true; - } else if (currentMapRotation == OsmandSettings.ROTATE_MAP_MANUAL) { - pendingRotation = true; - } - registerUnregisterSensor(location, smallSpeedForCompass); - if (settings.ANIMATE_MY_LOCATION.get() && !smallSpeedForAnimation && !movingToMyLocation) { - mapView.getAnimatedDraggingThread().startMoving( - location.getLatitude(), location.getLongitude(), zoom, - pendingRotation, rotation, movingTime, false, - () -> movingToMyLocation = false); } else { - if (mapView.hasMapRenderer()) { - movingTime = movingToMyLocation - ? (long) Math.min(movingTime * 0.7, MOVE_ANIMATION_TIME) : MOVE_ANIMATION_TIME; - if (mapView.getSettings().DO_NOT_USE_ANIMATIONS.get()) { - movingTime = 0; - } - mapView.getAnimatedDraggingThread().startMoving( - location.getLatitude(), location.getLongitude(), zoom, - pendingRotation, rotation, movingTime, false, - () -> movingToMyLocation = false); - } else { - if (zoom != null && zoom.first != null && zoom.second != null) { - mapView.getAnimatedDraggingThread().startZooming(zoom.first, zoom.second, null, false); - } - if (rotation != null) { - mapView.setRotate(rotation, false); - } - mapView.setLatLon(location.getLatitude(), location.getLongitude()); - } + showViewAngle = (!location.hasBearing() || smallSpeedForCompass) + && NativeUtilities.containsLatLon(mapView.getMapRenderer(), tb, location.getLatitude(), location.getLongitude()); } + registerUnregisterSensor(location, smallSpeedForCompass); } else if (location != null) { showViewAngle = (!location.hasBearing() || isSmallSpeedForCompass(location)) && (tb != null && NativeUtilities.containsLatLon(mapView.getMapRenderer(), tb, location.getLatitude(), location.getLongitude())); @@ -383,7 +334,7 @@ public Pair autozoom(RotatedTileBox tb, Location location) { // decrease a bit zdelta += 1; } - double targetZoom = Math.min(tb.getZoom() + tb.getZoomFloatPart() + zdelta, settings.AUTO_ZOOM_MAP_SCALE.get().maxZoom); + double targetZoom = Math.min(tb.getZoom() + tb.getZoomFloatPart() + zdelta, settings.AUTO_ZOOM_MAP_SCALE.get().maxZoomFromSpeed); boolean isUserZoomed = lastTimeManualZooming > lastTimeAutoZooming; int threshold = settings.AUTO_FOLLOW_ROUTE.get(); if ((now - lastTimeAutoZooming > AUTO_ZOOM_DEFAULT_CHANGE_ZOOM && !isUserZoomed) @@ -403,6 +354,75 @@ public Pair autozoom(RotatedTileBox tb, Location location) { return null; } +// @Nullable +// private Pair calculateAutoZoom(@NonNull MapRendererView mapRenderer, @NonNull RotatedTileBox tileBox, @NonNull Location location) { +// if (!shouldAutoZoom(location)) { +// return null; +// } +// +// LatLon target = tileBox.getLatLonFromPixel(tileBox.getCenterPixelX(), tileBox.getPixHeight() / 3f); +// +// NextDirectionInfo nextDirectionInfo = new NextDirectionInfo(); +// app.getRoutingHelper().getNextRouteDirectionInfo(nextDirectionInfo, true); +// if (nextDirectionInfo.distanceTo > 0 && nextDirectionInfo.directionInfo != null) { +// Location turnLocation = app.getRoutingHelper().getLocationFromRouteDirection(nextDirectionInfo.directionInfo); +// +// if (turnLocation != null) { +// PointI point31 = NativeUtilities.getPoint31FromLatLon(turnLocation.getLatitude(), turnLocation.getLongitude()); +// if (mapRenderer.isPositionVisible(point31)) { +// double distanceToTurnLocation = MapUtils.getDistance(turnLocation.getLatitude(), turnLocation.getLongitude(), location.getLatitude(), location.getLongitude()); +// double distanceToTarget = MapUtils.getDistance(target, location.getLatitude(), location.getLongitude()); +// if (distanceToTurnLocation <= distanceToTarget) { +// +// } +// } +// } +// } +// +// AutoZoomMap autoZoomScale = settings.AUTO_ZOOM_MAP_SCALE.get(); +// +// int currentIntZoom = tileBox.getZoom(); +// float currentZoomFloatPart = (float) tileBox.getZoomFloatPart(); +// +// RotatedTileBox intZoomTileBox = tileBox.copy(); +// intZoomTileBox.setZoomAndAnimation(currentIntZoom, 0, 0); +// +// +// float currentMetersToTarget = (float) MapUtils.getDistance(focusLocation, location.getLatitude(), location.getLongitude()); +// float requiredMetersToTarget = location.getSpeed() * 40 / autoZoomScale.coefficient; +// float zoomDelta = (float) (Math.log(currentMetersToTarget / requiredMetersToTarget) / Math.log(2)) - currentZoomFloatPart; +// +// int minZoom = mapView.getMinZoom(); +// int maxZoom = Math.min(mapView.getMaxZoom(), Math.round(autoZoomScale.maxZoom)); // todo float zoom +// Zoom zoom = new Zoom(currentIntZoom, currentZoomFloatPart, minZoom, maxZoom); +// zoom.calculateAnimatedZoom(currentIntZoom, zoomDelta); +// +// if (Math.abs(zoomDelta) < 0.5f) { +// return null; +// } +// +// Log.v("M_MapViewTrackingUtilities", "--------------"); +// Log.v("M_MapViewTrackingUtilities", "Current dist: " + currentMetersToTarget); +// Log.v("M_MapViewTrackingUtilities", "Expected dist: " + requiredMetersToTarget); +// Log.v("M_MapViewTrackingUtilities", "Current zoom: " + currentIntZoom + currentZoomFloatPart); +// Log.v("M_MapViewTrackingUtilities", "Expected zoom: " + zoom.getZoom()); +// Log.v("M_MapViewTrackingUtilities", "Zoom delta: " + (zoom.getZoom() - currentIntZoom - currentZoomFloatPart)); +// return new Pair<>(zoom.getBaseZoom(), (double) (zoom.getZoomFloatPart() + zoom.getZoomAnimation())); +// } + + private boolean shouldAutoZoom(@NonNull Location location) { + if (!location.hasSpeed() || location.getSpeed() < 7 / 3.6f) { + return false; + } + + boolean isUserZoomed = lastTimeManualZooming > lastTimeAutoZooming; + long now = System.currentTimeMillis(); + int autoZoomAfterUserThreshold = Math.max(settings.AUTO_FOLLOW_ROUTE.get(), AUTO_ZOOM_DEFAULT_CHANGE_ZOOM); + boolean zoomAfterUserZoom = isUserZoomed && now - lastTimeManualZooming > autoZoomAfterUserThreshold; + boolean zoomAfterAutoZoom = !isUserZoomed && now - lastTimeAutoZooming > AUTO_ZOOM_DEFAULT_CHANGE_ZOOM; + return zoomAfterUserZoom || zoomAfterAutoZoom; + } + public void backToLocationImpl() { backToLocationImpl(15, true); } @@ -492,6 +512,10 @@ public void setMapLinkedToLocation(boolean isMapLinkedToLocation) { } } + public void setIsMovingToMyLocation(boolean movingToMyLocation) { + this.movingToMyLocation = movingToMyLocation; + } + public boolean isMovingToMyLocation() { return movingToMyLocation; } diff --git a/OsmAnd/src/net/osmand/plus/base/MyLocationAnimation.java b/OsmAnd/src/net/osmand/plus/base/MyLocationAnimation.java new file mode 100644 index 00000000000..d3279b62756 --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/base/MyLocationAnimation.java @@ -0,0 +1,618 @@ +package net.osmand.plus.base; + +import android.os.Handler; + +import net.osmand.CallbackWithObject; +import net.osmand.Location; +import net.osmand.core.android.MapRendererView; +import net.osmand.core.jni.MapRendererState; +import net.osmand.core.jni.PointD; +import net.osmand.core.jni.PointI; +import net.osmand.data.LatLon; +import net.osmand.data.RotatedTileBox; +import net.osmand.plus.OsmAndLocationProvider.OsmAndLocationListener; +import net.osmand.plus.OsmandApplication; +import net.osmand.plus.routing.RouteCalculationResult.NextDirectionInfo; +import net.osmand.plus.settings.backend.OsmandSettings; +import net.osmand.plus.settings.enums.AutoZoomMap; +import net.osmand.plus.utils.NativeUtilities; +import net.osmand.plus.views.AnimateDraggingMapThread; +import net.osmand.plus.views.AnimateDraggingMapThread.FinishAnimationCallback; +import net.osmand.plus.views.OsmandMapTileView; +import net.osmand.plus.views.Zoom; +import net.osmand.plus.views.Zoom.ComplexZoom; +import net.osmand.util.MapUtils; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.util.Pair; + +import static net.osmand.plus.views.AnimateDraggingMapThread.SKIP_ANIMATION_DP_THRESHOLD; + +public class MyLocationAnimation implements OsmAndLocationListener { + + private static final float ELEVATION_DEGREES_PER_MS = 12 / 1000f; // 12 degrees per second + private static final float MAX_ZOOM_PER_MS = 1 / 1000f; + + private static final long MAX_SECOND_TO_START_2D_TILT = 15; + private static final long MOVE_ANIMATION_TIME = 500; + private static final long MIN_SECONDS_TO_FOCUS_LOCATION_AFTER_2D_TILT = 3; + private static final long MIN_SECONDS_BETWEEN_3D_AND_2D_TILT = 3; + // Delay after passing focus locations to animate auto zoom, auto tilt and auto rotation + private static final long MILLIS_TO_RESUME_ANIMATIONS = 5000; + + public static final int AUTO_ZOOM_DEFAULT_CHANGE_ZOOM = 4500; + + + + private final OsmandApplication app; + private final OsmandSettings settings; + private final OsmandMapTileView mapView; + private final MapViewTrackingUtilities mapViewTrackingUtilities; + private final AnimateDraggingMapThread animateDraggingThread; + private final LocationChangeObserver locationChangeObserver; + + @Nullable + private Location myLocation; + private long finishMovingTime; + @Nullable + private Location originalFocusLocation; + @Nullable + private Location focusLocationToAnimate; + @Nullable + private PointI focusLocationToAnimate31; + + private long timeToAllow2dTilt = 0; + + public MyLocationAnimation(@NonNull OsmandApplication app) { + this.app = app; + this.settings = app.getSettings(); + this.mapView = app.getOsmandMap().getMapView(); + this.mapViewTrackingUtilities = app.getMapViewTrackingUtilities(); + this.animateDraggingThread = mapView.getAnimatedDraggingThread(); + this.locationChangeObserver = new LocationChangeObserver( + this::getOriginalFocusLocation, + newFocusLocation -> { + checkFocusLocationIsNew(newFocusLocation); + return true; + }); + + // todo start properly + locationChangeObserver.startObserving(); + + app.getLocationProvider().addLocationListener(this); + } + + @Override + public void updateLocation(@Nullable Location location) { + Location previousMyLocation = myLocation; + myLocation = location; + + if (location != null && mapViewTrackingUtilities.isMapLinkedToLocation()) { + long animationDuration = getAnimationDuration(location, previousMyLocation); + boolean animateMyLocation = settings.ANIMATE_MY_LOCATION.get() + && !MapViewTrackingUtilities.isSmallSpeedForAnimation(location) + && !mapViewTrackingUtilities.isMovingToMyLocation(); + + if (animateMyLocation) { + // Temp fix to avoid late tilting to 2d after starting moving + if (timeToAllow2dTilt == 0) { + timeToAllow2dTilt = System.currentTimeMillis() + 5000; + } + animateMyLocation(location, previousMyLocation); + } else { + timeToAllow2dTilt = 0; + setMyLocation(location, animationDuration); + } + + mapView.refreshMap(); + } + } + + private long getAnimationDuration(@NonNull Location myLocation, @Nullable Location previousMyLocation) { + if (previousMyLocation == null) { + return 0; + } + + boolean rotateByBearing = settings.ROTATE_MAP.get() == OsmandSettings.ROTATE_MAP_BEARING; + boolean checkDistance = rotateByBearing && (!myLocation.hasBearing() || myLocation.getBearing() == 0.0f); + if (checkDistance) { + RotatedTileBox tileBox = mapView.getRotatedTileBox(); + double distInPixels = tileBox.getPixDensity() * MapUtils.getDistance(previousMyLocation, myLocation); + double distInDp = distInPixels / tileBox.getDensity(); + if (distInDp > SKIP_ANIMATION_DP_THRESHOLD) { + return 0; + } + } + + return myLocation.getTime() - previousMyLocation.getTime(); + } + + private void setMyLocation(@NonNull Location myLocation, long animationDuration) { + boolean pendingRotation = isPendingRotation(); + Float rotation = getRotationToAnimateFromRotationMode(myLocation); + ComplexZoom zoom = calculateZoomFromSpeed(myLocation); + + if (mapView.hasMapRenderer()) { + if (settings.DO_NOT_USE_ANIMATIONS.get()) { + animationDuration = 0; + } else { + animationDuration = mapViewTrackingUtilities.isMovingToMyLocation() + ? (long) Math.min(animationDuration * 0.7, MOVE_ANIMATION_TIME) + : MOVE_ANIMATION_TIME; + } + + Pair pair = zoom == null ? null : new Pair<>(zoom.base, (double) zoom.floatPart); + animateDraggingThread.startMoving( + myLocation.getLatitude(), myLocation.getLongitude(), pair, + false, rotation, animationDuration, false, () -> mapViewTrackingUtilities.setIsMovingToMyLocation(false)); + } else { + if (zoom != null) { + animateDraggingThread.startZooming(zoom.base, zoom.floatPart, null, false); + } + if (rotation != null) { + mapView.setRotate(rotation, false); + } + mapView.setLatLon(myLocation.getLatitude(), myLocation.getLongitude()); + } + } + + + boolean animatingTilt = false; + private float userElevationAngle = 0.0f; + private long timeToUnfocusLocation = 0; + + private void animateMyLocation(@NonNull Location myLocation, @Nullable Location previousMyLocation) { + MapRendererView mapRenderer = mapView.getMapRenderer(); + boolean hasMapRenderer = mapRenderer != null; + + long animationDuration = getAnimationDuration(myLocation, previousMyLocation); + + if (!hasMapRenderer) { + ComplexZoom zoom = null; + if (shouldAutoZoomBySpeed(null)) { + zoom = calculateZoomFromSpeed(myLocation); + } + Float rotation = getRotationToAnimateFromRotationMode(myLocation); + animateDraggingThread.startMoving(myLocation.getLatitude(), myLocation.getLongitude(), + null, isPendingRotation(), rotation, animationDuration, false, () -> mapViewTrackingUtilities.setIsMovingToMyLocation(false)); + return; + } + +// animateDraggingThread.stopAnimatingSync(); + + focusLocationToAnimate = null; + focusLocationToAnimate31 = null; + if (settings.AUTO_TILT_MAP.get() || settings.AUTO_ZOOM_MAP_TO_FOCUS.get()) { + Location originalFocusLocation = getOriginalFocusLocation(); + checkFocusLocationIsNew(originalFocusLocation); + if (originalFocusLocation != null) { + focusLocationToAnimate = getFocusLocationForAnimation(myLocation, originalFocusLocation); + focusLocationToAnimate31 = NativeUtilities.getPoint31FromLatLon(focusLocationToAnimate.getLatitude(), focusLocationToAnimate.getLongitude()); + } + } + PointI focusPixel = getFocusPixel(); + + StateToFocus stateToFocus = focusLocationToAnimate31 != null && settings.AUTO_ZOOM_MAP_TO_FOCUS.get() + ? calculateZoomAndRotationToFocus(mapRenderer, myLocation, focusLocationToAnimate31, focusPixel) + : null; + + long delay = 0; + boolean passedFocusLocation = false; + boolean unfocusedLocation = false; + if (timeToUnfocusLocation != 0) { + passedFocusLocation = true; + long currentTimeMillis = System.currentTimeMillis(); + if (timeToUnfocusLocation < currentTimeMillis + animationDuration) { + delay = Math.max(0, timeToUnfocusLocation - currentTimeMillis); + unfocusedLocation = true; + timeToUnfocusLocation = 0; + } + } + + double secondsToFocusLocation = Double.NaN; + if (focusLocationToAnimate != null && myLocation.hasSpeed() && myLocation.getSpeed() > 0.0f) { + double distance = MapUtils.getDistance(myLocation, focusLocationToAnimate); + secondsToFocusLocation = distance / myLocation.getSpeed(); + } + + animateDraggingThread.startMoving(myLocation.getLatitude(), myLocation.getLongitude(), null, + false, null, animationDuration, false, () -> { + mapViewTrackingUtilities.setIsMovingToMyLocation(false); + }); + finishMovingTime = System.currentTimeMillis() + animationDuration; + + if (shouldTiltTo2D(stateToFocus, secondsToFocusLocation)) { + userElevationAngle = mapView.getElevationAngle(); + animateTilt(90, focusLocationToAnimate31, focusPixel); + } else if (unfocusedLocation && shouldRestore3DTilt(secondsToFocusLocation)) { + float targetElevationAngle = userElevationAngle; + app.runMessageInUiThread(123456, delay, () -> animateTilt(targetElevationAngle, focusLocationToAnimate31, focusPixel)); + userElevationAngle = 0.0f; + } else if (!passedFocusLocation || unfocusedLocation) { + if (delay == 0) { + zoomIfNeeded(stateToFocus); + } else { + app.runMessageInUiThread(1223344, delay, () -> zoomIfNeeded(stateToFocus)); + } + } + + if (isRotationToFocusEnabled() && stateToFocus != null && stateToFocus.rotation != null) { + animateDraggingThread.startRotate(stateToFocus.rotation, finishMovingTime - System.currentTimeMillis()); + } else { + Float rotation = getRotationToAnimateFromRotationMode(myLocation); + if (rotation != null) { + animateDraggingThread.startRotate(rotation, 1000); + } + } + } + + private boolean shouldAutoZoomBySpeed(@Nullable StateToFocus state) { + if (!settings.AUTO_ZOOM_MAP.get()) { + return false; + } + + if (state != null) { + int minZoomBaseToFocus = settings.AUTO_ZOOM_MAP_SCALE.get().minZoomBaseToFocus; + return state.zoom.base < minZoomBaseToFocus; + } else { + return true; + } + } + + private boolean shouldZoomToFocus(@NonNull StateToFocus state) { + if (!settings.AUTO_ZOOM_MAP_TO_FOCUS.get()) { + return false; + } + + AutoZoomMap autoZoomScale = settings.AUTO_ZOOM_MAP_SCALE.get(); + return state.zoom.base >= autoZoomScale.minZoomBaseToFocus + && state.zoom.base <= autoZoomScale.maxZoomBaseToFocus; + } + + private boolean shouldTiltTo2D(@Nullable StateToFocus stateToFocus, + double secondsToFocusLocation) { + if (!settings.AUTO_TILT_MAP.get() + || animatingTilt + || System.currentTimeMillis() < timeToAllow2dTilt + || timeToUnfocusLocation != 0) { + return false; + } + + float elevationAngle = mapView.getElevationAngle(); + if (elevationAngle >= 85f) { + return false; + } + + if (!Double.isNaN(secondsToFocusLocation)) { + float elevationAngleDelta = 90 - elevationAngle; + float secondToTilt = (elevationAngleDelta / ELEVATION_DEGREES_PER_MS) / 1000; + + // Do not tilt if there is not enough time before focus location + if (secondToTilt + MIN_SECONDS_TO_FOCUS_LOCATION_AFTER_2D_TILT > secondsToFocusLocation) { + return false; + } + + // Tilt if focus location is close enough + if (secondsToFocusLocation <= MAX_SECOND_TO_START_2D_TILT) { + return true; + } + } + + + if (stateToFocus != null) { + int maxZoomBaseToFocus = settings.AUTO_ZOOM_MAP_SCALE.get().maxZoomBaseToFocus; + if (stateToFocus.zoom.base > maxZoomBaseToFocus) { + return true; + } + } + + return false; + } + + private boolean shouldRestore3DTilt(double secondsToFocusLocation) { + float secondsToTiltTo2dAnd3D = (userElevationAngle / ELEVATION_DEGREES_PER_MS) / 1000 * 2; + float secondsToFitTiltTo2dAnd3d = secondsToTiltTo2dAnd3D + + MIN_SECONDS_BETWEEN_3D_AND_2D_TILT + + MAX_SECOND_TO_START_2D_TILT; + return settings.AUTO_TILT_MAP.get() + && !animatingTilt + && userElevationAngle != 0.0f + && secondsToFitTiltTo2dAnd3d < secondsToFocusLocation; + } + + private float getTiltDuration(float targetElevationAngle) { + float elevationAngle = mapView.getElevationAngle(); + return Math.abs(targetElevationAngle - elevationAngle) / ELEVATION_DEGREES_PER_MS; + } + + private void zoomIfNeeded(@Nullable StateToFocus stateToFocus) { + long animationDuration = finishMovingTime - System.currentTimeMillis(); + if (animationDuration == 0) { + return; + } + + if (shouldAutoZoomBySpeed(stateToFocus)) { + autoZoomBySpeed(animationDuration); + } else if (stateToFocus != null && shouldZoomToFocus(stateToFocus)) { + animateStateToFocus(stateToFocus, animationDuration); + } + } + + private void autoZoomBySpeed(long animationDuration) { + + } + + private void animateTilt(float elevationAngle, @Nullable PointI focusLocation31, @NonNull PointI focusPixel) { + FinishAnimationCallback finishAnimationCallback = (boolean cancelled) -> { + animatingTilt = false; + if (cancelled) { + // todo + } else { + long duration = finishMovingTime - System.currentTimeMillis(); + if (duration < 0) { + return; + } + + MapRendererView mapRenderer = mapView.getMapRenderer(); + if (mapRenderer == null) { + return; + } + + StateToFocus newStateToFocus = null; + if (this.myLocation != null && this.focusLocationToAnimate31 != null) { + newStateToFocus = calculateZoomAndRotationToFocus(mapRenderer, this.myLocation, this.focusLocationToAnimate31, getFocusPixel()); + } + + zoomIfNeeded(newStateToFocus); + // todo rotate to focus, if enabled + } + }; + if (focusLocation31 != null) { + animateDraggingThread.startTilting(elevationAngle, ELEVATION_DEGREES_PER_MS, null); + animateDraggingThread.startFittingLocationToPixel(focusLocation31, focusPixel, isRotationToFocusEnabled(), getTiltDuration(elevationAngle), finishAnimationCallback); + } else { + animateDraggingThread.startTilting(elevationAngle, ELEVATION_DEGREES_PER_MS, finishAnimationCallback); + } + } + + private void animateStateToFocus(@NonNull StateToFocus stateToFocus, float animationDuration) { + animateDraggingThread.startZooming(stateToFocus.zoom.base, stateToFocus.zoom.floatPart, animationDuration, null, false, false); + if (stateToFocus.rotation != null) { + animateDraggingThread.startRotate(stateToFocus.rotation, animationDuration); + } + } + + @Nullable + private Location getOriginalFocusLocation() { + NextDirectionInfo nextDirectionInfo = app.getRoutingHelper().getNextRouteDirectionInfo(new NextDirectionInfo(), true); + if (nextDirectionInfo == null || nextDirectionInfo.distanceTo <= 0 || nextDirectionInfo.directionInfo == null) { + return null; + } + return app.getRoutingHelper().getLocationFromRouteDirection(nextDirectionInfo.directionInfo); + } + + @NonNull + private Location getFocusLocationForAnimation(@NonNull Location myLocation, @NonNull Location originalFocusLocation) { + if (isRotationToFocusEnabled() && settings.ROTATE_MAP.get() == OsmandSettings.ROTATE_MAP_BEARING) { + return originalFocusLocation; + } + + int rotationMode = settings.ROTATE_MAP.get(); + float rotation = 0.0f; + if (rotationMode == OsmandSettings.ROTATE_MAP_COMPASS) { + Float heading = app.getLocationProvider().getHeading(); + rotation = heading == null ? mapView.getRotate() : heading; + } else if (rotationMode == OsmandSettings.ROTATE_MAP_BEARING) { + rotation = myLocation.hasBearing() ? -myLocation.getBearing() : mapView.getRotate(); + } else if (rotationMode == OsmandSettings.ROTATE_MAP_MANUAL) { + rotation = mapView.getRotate(); + } + + double distance = MapUtils.getDistance(myLocation, originalFocusLocation); + LatLon latLon = MapUtils.rhumbDestinationPoint(myLocation.getLatitude(), myLocation.getLongitude(), distance, rotation); + return new Location("", latLon.getLatitude(), latLon.getLongitude()); + } + + private long lastTimeManualZooming; + private long lastTimeAutoZooming; + + @Nullable + private ComplexZoom calculateZoomFromSpeed(@NonNull Location myLocation) { + if (!myLocation.hasSpeed()) { + return null; + } + + RotatedTileBox tb = mapView.getCurrentRotatedTileBox(); + + long now = System.currentTimeMillis(); + float zdelta = defineZoomFromSpeed(tb, myLocation.getSpeed()); + if (Math.abs(zdelta) >= 0.5/*?Math.sqrt(0.5)*/) { + // prevent ui hysteresis (check time interval for autozoom) + if (zdelta >= 2) { + // decrease a bit + zdelta -= 1; + } else if (zdelta <= -2) { + // decrease a bit + zdelta += 1; + } + double targetZoom = Math.min(tb.getZoom() + tb.getZoomFloatPart() + zdelta, settings.AUTO_ZOOM_MAP_SCALE.get().maxZoomFromSpeed); + boolean isUserZoomed = lastTimeManualZooming > lastTimeAutoZooming; + int threshold = settings.AUTO_FOLLOW_ROUTE.get(); + if ((now - lastTimeAutoZooming > AUTO_ZOOM_DEFAULT_CHANGE_ZOOM && !isUserZoomed) + || (now - lastTimeManualZooming > Math.max(threshold, AUTO_ZOOM_DEFAULT_CHANGE_ZOOM) && isUserZoomed)) { + lastTimeAutoZooming = now; +// double settingsZoomScale = Math.log(mapView.getSettingsMapDensity()) / Math.log(2.0f); +// double zoomScale = Math.log(tb.getMapDensity()) / Math.log(2.0f); +// double complexZoom = tb.getZoom() + zoomScale + zdelta; + // round to 0.33 + targetZoom = Math.round(targetZoom * 3) / 3f; + int newIntegerZoom = (int) Math.round(targetZoom); + float zPart = (float) (targetZoom - newIntegerZoom); + return newIntegerZoom > 0 ? new ComplexZoom(newIntegerZoom, zPart) : null; + } + } + + return null; + } + + private float defineZoomFromSpeed(RotatedTileBox tb, float speed) { + if (speed < 7f / 3.6) { + return 0; + } + double visibleDist = tb.getDistance(tb.getCenterPixelX(), 0, tb.getCenterPixelX(), tb.getCenterPixelY()); + float time = 75f; // > 83 km/h show 75 seconds + if (speed < 83f / 3.6) { + time = 60f; + } + time /= settings.AUTO_ZOOM_MAP_SCALE.get().coefficient; + double distToSee = speed * time; + // check if 17, 18 is correct? + return (float) (Math.log(visibleDist / distToSee) / Math.log(2.0f)); + } + + public void setUserZoomedTime(long time) { + lastTimeManualZooming = time; + } + + private boolean isPendingRotation() { + int rotationMode = settings.ROTATE_MAP.get(); + return rotationMode == OsmandSettings.ROTATE_MAP_NONE + || rotationMode == OsmandSettings.ROTATE_MAP_COMPASS + || rotationMode == OsmandSettings.ROTATE_MAP_MANUAL; + } + + @Nullable + private Float getRotationToAnimateFromRotationMode(@NonNull Location myLocation) { + int rotationMode = settings.ROTATE_MAP.get(); + if (rotationMode == OsmandSettings.ROTATE_MAP_NONE) { + return 0.0f; + } else if (rotationMode == OsmandSettings.ROTATE_MAP_BEARING) { + if (myLocation.hasBearing() && myLocation.getBearing() != 0.0f) { + return -myLocation.getBearing(); + } + } + + return null; + } + + private boolean isRotationToFocusEnabled() { + return settings.AUTO_ROTATE_MAP_TO_FOCUS.get() + && settings.ROTATE_MAP.get() == OsmandSettings.ROTATE_MAP_BEARING; + } + + private void checkFocusLocationIsNew(@Nullable Location focusLocation) { + boolean currentFocusLocationIsPassed = !MapUtils.areLatLonEqual(originalFocusLocation, focusLocation); + if (currentFocusLocationIsPassed) { + originalFocusLocation = focusLocation; + timeToUnfocusLocation = System.currentTimeMillis() + MILLIS_TO_RESUME_ANIMATIONS; + } + } + + @Nullable + private StateToFocus calculateZoomAndRotationToFocus(@NonNull MapRendererView mapRenderer, + @NonNull Location myLocation, + @NonNull PointI focusLocation31, + @NonNull PointI focusPixel) { + PointI myLocation31 = NativeUtilities.getPoint31FromLatLon(myLocation.getLatitude(), myLocation.getLongitude()); + MapRendererState state = mapRenderer.getState(); + if (!mapRenderer.setMapTargetForState(state, state.getFixedPixel(), myLocation31)) { + return null; + } + + // TODO heights + float myLocationHeight = 0.0f; + float focusLocationHeight = 0.0f; + PointD deltaZoomAndRotation = new PointD(); + boolean canFocus = mapRenderer.getZoomAndRotationAfterPinch(state, + myLocation31, myLocationHeight, state.getFixedPixel(), + focusLocation31, focusLocationHeight, focusPixel, + deltaZoomAndRotation); + + if (!canFocus) { + return null; + } + + Zoom zoom = new Zoom( + mapView.getZoom(), + mapView.getZoomFloatPart() + mapView.getZoomAnimation(), + mapView.getMinZoom(), + mapView.getMaxZoom()); + float deltaZoom = (float) deltaZoomAndRotation.getX(); + zoom.applyZoomDelta(deltaZoom); + ComplexZoom zoomToFocus = new ComplexZoom(zoom.getBaseZoom(), zoom.getZoomFloatPart()); + + Float rotationToFocus = null; + if (isRotationToFocusEnabled()) { + float deltaRotation = (float) -deltaZoomAndRotation.getY(); + rotationToFocus = MapUtils.unifyRotationTo360(mapView.getRotate() + deltaRotation); + } + + return new StateToFocus(zoomToFocus, rotationToFocus); + } + + @NonNull + private PointI getFocusPixel() { + RotatedTileBox tileBox = mapView.getRotatedTileBox(); + return new PointI(tileBox.getPixWidth() / 2, tileBox.getPixHeight() / 3); + } + + private static class LocationChangeObserver { + + interface LocationGetter { + + @Nullable + Location getLocation(); + } + + private static final int UPDATE_INTERVAL_MILLIS = 250; + + private final Handler handler; + private final Runnable handlerCallback; + + private boolean observing = false; + + public LocationChangeObserver(@NonNull LocationGetter locationGetter, @NonNull CallbackWithObject onLocationChanged) { + this.handler = new Handler(); + this.handlerCallback = new Runnable() { + + private Location previousLocation = null; + + @Override + public void run() { + Location newLocation = locationGetter.getLocation(); + if (!MapUtils.areLatLonEqual(previousLocation, newLocation)) { + onLocationChanged.processResult(newLocation); + } + previousLocation = newLocation; + handler.postDelayed(this, UPDATE_INTERVAL_MILLIS); + } + }; + } + + public void startObserving() { + if (!observing) { + observing = handler.post(handlerCallback); + } + } + + public void stopObserving() { + if (observing) { + handler.removeCallbacks(handlerCallback); + } + } + } + + private static class StateToFocus { + + @NonNull + public final ComplexZoom zoom; + @Nullable + public final Float rotation; + + public StateToFocus(@NonNull ComplexZoom zoom, @Nullable Float rotation) { + this.zoom = zoom; + this.rotation = rotation; + } + } +} \ No newline at end of file diff --git a/OsmAnd/src/net/osmand/plus/plugins/development/widget/ZoomLevelWidget.java b/OsmAnd/src/net/osmand/plus/plugins/development/widget/ZoomLevelWidget.java index 62cf5249a69..d63fd01bcb0 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/development/widget/ZoomLevelWidget.java +++ b/OsmAnd/src/net/osmand/plus/plugins/development/widget/ZoomLevelWidget.java @@ -45,7 +45,7 @@ public void updateInfo(@Nullable DrawSettings drawSettings) { float visualZoom = newZoomFloatPart >= 0.0f ? 1.0f + newZoomFloatPart : 1.0f + 0.5f * newZoomFloatPart; - float offsetFromLogicalZoom = getZoomDeltaFromMapScale(visualZoom * newMapDensity); + float offsetFromLogicalZoom = getZoomDeltaFromMapScale(visualZoom); float preFormattedOffset = Math.round(Math.abs(offsetFromLogicalZoom) * 100) / 100.0f; String formattedOffset = OsmAndFormatter .formatValue(preFormattedOffset, "", true, 2, app) diff --git a/OsmAnd/src/net/osmand/plus/settings/backend/OsmandSettings.java b/OsmAnd/src/net/osmand/plus/settings/backend/OsmandSettings.java index 5a6831bab95..7e596c06b3f 100644 --- a/OsmAnd/src/net/osmand/plus/settings/backend/OsmandSettings.java +++ b/OsmAnd/src/net/osmand/plus/settings/backend/OsmandSettings.java @@ -1396,6 +1396,10 @@ public boolean usingEnglishNames() { AUTO_ZOOM_MAP.setModeDefaultValue(ApplicationMode.PEDESTRIAN, false); } + public final CommonPreference AUTO_TILT_MAP = new BooleanPreference(this, "auto_tilt_map", true).makeProfile().cache(); + public final CommonPreference AUTO_ZOOM_MAP_TO_FOCUS = new BooleanPreference(this, "auto_zoom_map_to_focus", true).makeProfile().cache(); + public final CommonPreference AUTO_ROTATE_MAP_TO_FOCUS = new BooleanPreference(this, "auto_rotate_map_to_focus", false).makeProfile().cache(); + public final CommonPreference AUTO_ZOOM_MAP_SCALE = new EnumStringPreference(this, "auto_zoom_map_scale", AutoZoomMap.FAR, AutoZoomMap.values()).makeProfile().cache(); diff --git a/OsmAnd/src/net/osmand/plus/settings/enums/AutoZoomMap.java b/OsmAnd/src/net/osmand/plus/settings/enums/AutoZoomMap.java index ef3135a58e8..4d955d093eb 100644 --- a/OsmAnd/src/net/osmand/plus/settings/enums/AutoZoomMap.java +++ b/OsmAnd/src/net/osmand/plus/settings/enums/AutoZoomMap.java @@ -3,17 +3,21 @@ import net.osmand.plus.R; public enum AutoZoomMap { - FARTHEST(R.string.auto_zoom_farthest, 1f, 15.5f), - FAR(R.string.auto_zoom_far, 1.4f, 17f), - CLOSE(R.string.auto_zoom_close, 2f, 19f); + FARTHEST(R.string.auto_zoom_farthest, 1f, 15.5f, 14, 18), + FAR(R.string.auto_zoom_far, 1.4f, 17f, 15, 19), + CLOSE(R.string.auto_zoom_close, 2f, 19f, 17, 21); public final float coefficient; public final int name; - public final float maxZoom; + public final float maxZoomFromSpeed; + public final int minZoomBaseToFocus; + public final int maxZoomBaseToFocus; - AutoZoomMap(int name, float coefficient, float maxZoom) { + AutoZoomMap(int name, float coefficient, float maxZoomFromSpeed, int minZoomBaseToFocus, int maxZoomBaseToFocus) { this.name = name; this.coefficient = coefficient; - this.maxZoom = maxZoom; + this.maxZoomFromSpeed = maxZoomFromSpeed; + this.minZoomBaseToFocus = minZoomBaseToFocus; + this.maxZoomBaseToFocus = maxZoomBaseToFocus; } } \ No newline at end of file diff --git a/OsmAnd/src/net/osmand/plus/views/AnimateDraggingMapThread.java b/OsmAnd/src/net/osmand/plus/views/AnimateDraggingMapThread.java index 81b2139a59c..d00e2b60e35 100644 --- a/OsmAnd/src/net/osmand/plus/views/AnimateDraggingMapThread.java +++ b/OsmAnd/src/net/osmand/plus/views/AnimateDraggingMapThread.java @@ -83,6 +83,11 @@ public class AnimateDraggingMapThread { private SWIGTYPE_p_void userInteractionAnimationKey; private SWIGTYPE_p_void locationServicesAnimationKey; + public interface FinishAnimationCallback { + + void run(boolean cancelled); + } + public AnimateDraggingMapThread(@NonNull OsmandMapTileView tileView) { this.app = tileView.getApplication(); this.tileView = tileView; @@ -236,8 +241,8 @@ public void startMoving(double finalLat, double finalLon, Pair zoom = finalZoom.first; zoomFP = finalZoom.second; } else { - zoom = startZoom; - zoomFP = startZoomFP; + zoom = 0; + zoomFP = 0; } if (finalRotation != null) { rotation = finalRotation; @@ -262,7 +267,9 @@ public void startMoving(double finalLat, double finalLon, Pair || !NativeUtilities.containsLatLon(mapRenderer, rb, finalLat, finalLon); if (skipAnimation) { tileView.setLatLonAnimate(finalLat, finalLon, notifyListener); - tileView.setFractionalZoom(zoom, zoomFP, notifyListener); + if (zoom != 0) { + tileView.setFractionalZoom(zoom, zoomFP, notifyListener); + } tileView.rotateToAnimate(rotation); if (finishAnimationCallback != null) { finishAnimationCallback.run(); @@ -323,9 +330,6 @@ public void startMoving(double finalLat, double finalLon, Pair animator.animateZoomTo(zoom + (float) zoomFP, NAV_ANIMATION_TIME / 1000f, TimingFunction.EaseOutQuadratic, locationServicesAnimationKey); } - if (!animateZoom) { - tileView.setFractionalZoom(zoom, zoomFP, notifyListener); - } if (!animateRotation && finalRotation != null) { tileView.rotateToAnimate(rotation); } @@ -542,7 +546,6 @@ private void animatingMapAnimator() { PointI initFlatTarget31 = mapRenderer.getState().getTarget31(); float initZoom = mapRenderer.getZoom(); - int zoomThreshold = ((int) (targetFloatZoom * 2)); float initAzimuth = mapRenderer.getAzimuth(); float initElevationAngle = mapRenderer.getElevationAngle(); @@ -574,7 +577,7 @@ private void animatingMapAnimator() { || initFlatTarget31.getY() != flatTarget31.getY(); } if (!animateZoom) { - animateZoom = initZoom != zoom && targetIntZoom > 0; + animateZoom = initZoom != zoom; } if (!animateAzimuth) { animateAzimuth = initAzimuth != azimuth; @@ -588,9 +591,10 @@ private void animatingMapAnimator() { MapUtils.get31LongitudeX(target31.getX())); } if (!stopped && animateZoom) { - int baseZoom = (int) Math.round(zoom - 0.5 * zoomThreshold); - double zaAnimate = zoom - baseZoom; - tb.setZoomAndAnimation(baseZoom, zaAnimate, tb.getZoomFloatPart()); + // todo -0.6; + 0.6 + int baseZoom = Math.round(zoom); + double zAnimate = zoom - baseZoom - tb.getZoomFloatPart(); + tb.setZoomAndAnimation(baseZoom, zAnimate, tb.getZoomFloatPart()); } if (!stopped && animateAzimuth) { tb.setRotate(-azimuth); @@ -603,7 +607,7 @@ private void animatingMapAnimator() { break; } } - if (animateZoom && mapRenderer != null) { + if (mapRenderer != null && animateZoom && targetIntZoom > 0) { mapRenderer.setZoom(targetIntZoom + (float) targetFloatZoom); tb.setZoomAndAnimation(targetIntZoom, 0, targetFloatZoom); } @@ -716,11 +720,16 @@ public boolean isAnimatingMapTilt() { } public void startZooming(int zoomEnd, double zoomPart, @Nullable LatLon zoomingLatLon, boolean notifyListener) { + float animationTime = tileView.getSettings().DO_NOT_USE_ANIMATIONS.get() + ? 0 + : ZOOM_ANIMATION_TIME; + startZooming(zoomEnd, zoomPart, animationTime, zoomingLatLon, false, notifyListener); + } + + public void startZooming(int zoomEnd, double zoomPart, float animationTime, @Nullable LatLon zoomingLatLon, boolean forceTargetZoom, boolean notifyListener) { if (animationsDisabled) return; - boolean doNotUseAnimations = tileView.getSettings().DO_NOT_USE_ANIMATIONS.get(); - float animationTime = doNotUseAnimations ? 0 : ZOOM_ANIMATION_TIME; double targetLat = tileView.getLatitude(); double targetLon = tileView.getLongitude(); @@ -779,7 +788,11 @@ public void startZooming(int zoomEnd, double zoomPart, @Nullable LatLon zoomingL double finalLat = targetLat; double finalLon = targetLon; startThreadAnimating(() -> { - setTargetValues(zoomEnd, zoomPart, finalLat, finalLon); + if (forceTargetZoom) { + setTargetValues(zoomEnd, zoomPart, finalLat, finalLon); + } else { + setTargetValues(0, 0.0, finalLat, finalLon); + } if (mapRenderer != null) { if (targetChanged) { invalidateMapTarget(); @@ -879,16 +892,22 @@ public void startDragging(float velocityX, float velocityY, } public void startTilting(float elevationAngle) { + startTilting(elevationAngle, 5, null); + } + + public void startTilting(float elevationAngle, float anglesPerMs, @Nullable FinishAnimationCallback finishAnimationCallback) { if (animationsDisabled) return; stopAnimatingSync(); float initialElevationAngle = tileView.getElevationAngle(); - float elevationAngleDiff = elevationAngle - initialElevationAngle; + float elevationAngleDelta = elevationAngle - initialElevationAngle; boolean doNotUseAnimations = tileView.getSettings().DO_NOT_USE_ANIMATIONS.get(); - float animationTime = doNotUseAnimations ? 1 : Math.abs(elevationAngleDiff) * 5; + float animationTime = doNotUseAnimations + ? 1 + : Math.abs(elevationAngleDelta) / anglesPerMs; MapRendererView mapRenderer = getMapRenderer(); MapAnimator animator = getAnimator(); @@ -929,7 +948,7 @@ public void startTilting(float elevationAngle) { } interpolation = interpolator.getInterpolation(normalizedTime); - float newElevationAngle = initialElevationAngle + elevationAngleDiff * interpolation; + float newElevationAngle = initialElevationAngle + elevationAngleDelta * interpolation; tileView.setElevationAngle(newElevationAngle); tileView.setLatLonAnimate(tileView.getLatitude(), tileView.getLongitude(), false); @@ -941,8 +960,58 @@ public void startTilting(float elevationAngle) { resetInterpolation(); } animatingMapTilt = false; + + if (finishAnimationCallback != null) { + finishAnimationCallback.run(stopped); + } }); + } + + public void startFittingLocationToPixel(@NonNull PointI location31, @NonNull PointI pixel, boolean animateRotation, float animationTime, @Nullable FinishAnimationCallback finishAnimationCallback) { + if (animationsDisabled) { + return; + } + + MapRendererView mapRenderer = getMapRenderer(); + MapAnimator animator = getAnimator(); + if (mapRenderer == null || animator == null) { + return; + } + + animator.pause(); + + // Is it correct? + animator.cancelCurrentAnimation(userInteractionAnimationKey, AnimatedValue.Zoom); + animator.cancelCurrentAnimation(locationServicesAnimationKey, AnimatedValue.Zoom); + + if (animateRotation) { + // Is it correct? + animator.cancelCurrentAnimation(userInteractionAnimationKey, AnimatedValue.Azimuth); + animator.cancelCurrentAnimation(locationServicesAnimationKey, AnimatedValue.Azimuth); + } + + float duration = animationTime / 1000f; + animator.animateLocationFixationOnScreen(location31, pixel, animateRotation, duration, locationServicesAnimationKey); + + startThreadAnimating(() -> { + targetIntZoom = 0; + + animatingMapZoom = true; + if (animateRotation) { + animatingMapRotation = true; + } + + animatingMapAnimator(); + animatingMapZoom = false; + if (animateRotation) { + animatingMapRotation = false; + } + + if (finishAnimationCallback != null) { + finishAnimationCallback.run(stopped); + } + }); } private void sleepToRedraw(boolean stopIfInterrupted) { @@ -981,6 +1050,10 @@ private void setTargetValues(int zoom, double zoomPart, double lat, double lon) } public void startRotate(float rotate) { + startRotate(rotate, ROTATION_ANIMATION_TIME); + } + + public void startRotate(float rotate, float animationTime) { if (animationsDisabled) return; @@ -992,7 +1065,7 @@ public void startRotate(float rotate) { animator.cancelCurrentAnimation(locationServicesAnimationKey, AnimatedValue.Azimuth); animator.cancelCurrentAnimation(userInteractionAnimationKey, AnimatedValue.Azimuth); - animator.animateAzimuthTo(-rotate, ROTATION_ANIMATION_TIME / 1000f, TimingFunction.Linear, + animator.animateAzimuthTo(-rotate, animationTime / 1000f, TimingFunction.Linear, locationServicesAnimationKey); startThreadAnimating(() -> { diff --git a/OsmAnd/src/net/osmand/plus/views/OsmandMap.java b/OsmAnd/src/net/osmand/plus/views/OsmandMap.java index d1fabfda6c2..bf8bea3958a 100644 --- a/OsmAnd/src/net/osmand/plus/views/OsmandMap.java +++ b/OsmAnd/src/net/osmand/plus/views/OsmandMap.java @@ -125,6 +125,7 @@ public void refreshMap(boolean updateVectorRendering) { public void changeZoom(int stp, long time) { mapViewTrackingUtilities.setZoomTime(time); + app.getMyLocationAnimation().setUserZoomedTime(time); changeZoom(stp); } diff --git a/OsmAnd/src/net/osmand/plus/views/OsmandMapTileView.java b/OsmAnd/src/net/osmand/plus/views/OsmandMapTileView.java index 113a962cdaa..19a6b1a7ec4 100644 --- a/OsmAnd/src/net/osmand/plus/views/OsmandMapTileView.java +++ b/OsmAnd/src/net/osmand/plus/views/OsmandMapTileView.java @@ -1102,7 +1102,7 @@ public void drawOverMap(Canvas canvas, RotatedTileBox tileBox, DrawSettings draw } } WeatherPlugin plugin = PluginsHelper.getActivePlugin(WeatherPlugin.class); - if (showMapPosition || animatedDraggingThread.isAnimatingMapZoom() || (plugin != null && plugin.hasCustomForecast())) { + if (true) { drawMapPosition(canvas, c.x, c.y); } else if (multiTouchSupport != null && multiTouchSupport.isInZoomAndRotationMode()) { drawMapPosition(canvas, multiTouchSupport.getCenterPoint().x, multiTouchSupport.getCenterPoint().y); diff --git a/OsmAnd/src/net/osmand/plus/views/Zoom.java b/OsmAnd/src/net/osmand/plus/views/Zoom.java index 860016caaf8..27500437f15 100644 --- a/OsmAnd/src/net/osmand/plus/views/Zoom.java +++ b/OsmAnd/src/net/osmand/plus/views/Zoom.java @@ -8,14 +8,14 @@ public class Zoom { private float zoomFloatPart; private float zoomAnimation; - private final int minZoom; - private final int maxZoom; + private final int minZoomBase; + private final int maxZoomBase; - public Zoom(int baseZoom, float zoomFloatPart, int minZoom, int maxZoom) { + public Zoom(int baseZoom, float zoomFloatPart, int minZoomBase, int maxZoomBase) { this.baseZoom = baseZoom; this.zoomFloatPart = zoomFloatPart; - this.minZoom = minZoom; - this.maxZoom = maxZoom; + this.minZoomBase = minZoomBase; + this.maxZoomBase = maxZoomBase; } public int getBaseZoom() { @@ -31,11 +31,11 @@ public float getZoomAnimation() { } public boolean isZoomInAllowed() { - return baseZoom < maxZoom || baseZoom == maxZoom && zoomFloatPart < 0; + return baseZoom < maxZoomBase || baseZoom == maxZoomBase && zoomFloatPart < 0; } public boolean isZoomOutAllowed() { - return baseZoom > minZoom || baseZoom == minZoom && zoomFloatPart > 0; + return baseZoom > minZoomBase || baseZoom == minZoomBase && zoomFloatPart > 0; } public void zoomIn() { @@ -51,58 +51,82 @@ public void changeZoom(int step) { checkZoomBounds(); } - public void calculateAnimatedZoom(int currentBaseZoom, float deltaZoom) { - while (zoomFloatPart + deltaZoom >= 0.5 && baseZoom + 1 <= maxZoom) { - deltaZoom--; + public void applyZoomDelta(float delta) { + float normalizedDelta = normalizeZoomDelta(baseZoom, delta); + zoomFloatPart += normalizedDelta; + } + + public void calculateAnimatedZoom(int currentZoomBase, float zoomDelta) { + zoomAnimation = normalizeZoomDelta(currentZoomBase, zoomDelta); + } + + private float normalizeZoomDelta(int currentZoomBase, float zoomDelta) { + while (zoomFloatPart + zoomDelta >= 0.5 && baseZoom + 1 <= maxZoomBase) { + zoomDelta--; baseZoom++; } - while (zoomFloatPart + deltaZoom < -0.5 && baseZoom - 1 >= minZoom) { - deltaZoom++; + while (zoomFloatPart + zoomDelta < -0.5 && baseZoom - 1 >= minZoomBase) { + zoomDelta++; baseZoom--; } // Extend zoom float part from [-0.5 ... +0.5) to [-0.6 ... +0.6) // Example: previous zoom was 15 + 0.3f. With deltaZoom = 0.25f, // zoom will become 15 + 0.55f, not 16 - 0.45f - if (baseZoom + 1 == currentBaseZoom && zoomFloatPart + deltaZoom >= 0.4f) { + if (baseZoom + 1 == currentZoomBase && zoomFloatPart + zoomDelta >= 0.4f) { baseZoom++; - float invertedZoomFloatPart = (zoomFloatPart + deltaZoom) - 1.0f; - deltaZoom = invertedZoomFloatPart - zoomFloatPart; - } else if (baseZoom - 1 == currentBaseZoom && zoomFloatPart + deltaZoom < -0.4f) { + float invertedZoomFloatPart = (zoomFloatPart + zoomDelta) - 1.0f; + zoomDelta = invertedZoomFloatPart - zoomFloatPart; + } else if (baseZoom - 1 == currentZoomBase && zoomFloatPart + zoomDelta < -0.4f) { baseZoom--; - float invertedZoomFloatPart = 1.0f + (zoomFloatPart + deltaZoom); - deltaZoom = invertedZoomFloatPart - zoomFloatPart; + float invertedZoomFloatPart = 1.0f + (zoomFloatPart + zoomDelta); + zoomDelta = invertedZoomFloatPart - zoomFloatPart; } - boolean zoomInOverflow = baseZoom == maxZoom && zoomFloatPart + deltaZoom > 0; - boolean zoomOutOverflow = baseZoom == minZoom && zoomFloatPart + deltaZoom < 0; + boolean zoomInOverflow = baseZoom == maxZoomBase && zoomFloatPart + zoomDelta > 0; + boolean zoomOutOverflow = baseZoom == minZoomBase && zoomFloatPart + zoomDelta < 0; if (zoomInOverflow || zoomOutOverflow) { - deltaZoom = -zoomFloatPart; + zoomDelta = -zoomFloatPart; } - zoomAnimation = deltaZoom; + return zoomDelta; } private void checkZoomBounds() { - if (baseZoom == maxZoom) { + if (baseZoom == maxZoomBase) { zoomFloatPart = Math.min(0, zoomFloatPart); - } else if (baseZoom > maxZoom) { - baseZoom = maxZoom; + } else if (baseZoom > maxZoomBase) { + baseZoom = maxZoomBase; zoomFloatPart = 0; } - if (baseZoom == minZoom) { + if (baseZoom == minZoomBase) { zoomFloatPart = Math.max(0, zoomFloatPart); - } else if (baseZoom < minZoom) { - baseZoom = minZoom; + } else if (baseZoom < minZoomBase) { + baseZoom = minZoomBase; zoomFloatPart = 0; } } @NonNull - public static Zoom checkZoomBounds(int baseZoom, float zoomFloatPart, int maxZoom, int minZoom) { - Zoom zoom = new Zoom(baseZoom, zoomFloatPart, maxZoom, minZoom); + public static Zoom checkZoomBounds(int baseZoom, float zoomFloatPart, int minZoomBase, int maxZoomBase) { + Zoom zoom = new Zoom(baseZoom, zoomFloatPart, minZoomBase, maxZoomBase); zoom.checkZoomBounds(); return zoom; } + + public static class ComplexZoom { + + public final int base; + public final float floatPart; + + public ComplexZoom(int base, float floatPart) { + this.base = base; + this.floatPart = floatPart; + } + + boolean equals(int base, float floatPart) { + return this.base == base && this.floatPart == floatPart; + } + } } \ No newline at end of file diff --git a/OsmAnd/src/net/osmand/plus/views/controls/maphudbuttons/Map3DButton.java b/OsmAnd/src/net/osmand/plus/views/controls/maphudbuttons/Map3DButton.java index f0bbb2b4976..40aa44eece9 100644 --- a/OsmAnd/src/net/osmand/plus/views/controls/maphudbuttons/Map3DButton.java +++ b/OsmAnd/src/net/osmand/plus/views/controls/maphudbuttons/Map3DButton.java @@ -55,7 +55,7 @@ private void setElevationListener(OsmandMapTileView mapView) { private View.OnClickListener getOnCLickListener(OsmandMapTileView mapView) { return view -> { boolean defaultElevationAngle = isDefaultElevationAngle(); - float tiltAngle = defaultElevationAngle ? getElevationAngle(mapView.getZoom()) : DEFAULT_ELEVATION_ANGLE; + float tiltAngle = defaultElevationAngle ? 30 : DEFAULT_ELEVATION_ANGLE; if (!defaultElevationAngle) { settings.MAP_3D_MODE_ELEVATION_ANGLE.set(app.getOsmandMap().getMapView().getElevationAngle()); } diff --git a/OsmAnd/src/net/osmand/plus/views/layers/PointLocationLayer.java b/OsmAnd/src/net/osmand/plus/views/layers/PointLocationLayer.java index 669ea2218fd..5578af8bfaa 100644 --- a/OsmAnd/src/net/osmand/plus/views/layers/PointLocationLayer.java +++ b/OsmAnd/src/net/osmand/plus/views/layers/PointLocationLayer.java @@ -7,12 +7,15 @@ import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; +import android.graphics.Color; import android.graphics.Paint; import android.graphics.Paint.Style; +import android.graphics.Path; import android.graphics.PointF; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.drawable.LayerDrawable; +import android.util.Log; import androidx.annotation.ColorInt; import androidx.annotation.DrawableRes; @@ -529,6 +532,19 @@ public void onPrepareBufferImage(Canvas canvas, RotatedTileBox tileBox, DrawSett updateParams(view.getSettings().getApplicationMode(), nightMode, locationProvider.getLastKnownLocation() == null); } + private final Paint targetPaint = new Paint(); + private final Paint centerPaint = new Paint(); + private final Path path = new Path(); + { + targetPaint.setColor(Color.argb(100, 255, 0, 0)); + targetPaint.setStrokeWidth(getMapDensity() * 5); + targetPaint.setStyle(Style.STROKE); + + centerPaint.setColor(Color.argb(50, 0, 0, 0)); + centerPaint.setStrokeWidth(getMapDensity() * 5); + centerPaint.setStyle(Style.STROKE); + } + @Override public void onDraw(Canvas canvas, RotatedTileBox tileBox, DrawSettings settings) { Location lastKnownLocation = locationProvider.getLastStaleKnownLocation(); @@ -538,6 +554,29 @@ public void onDraw(Canvas canvas, RotatedTileBox tileBox, DrawSettings settings) if (!hasMapRenderer()) { drawMarkers(canvas, tileBox, lastKnownLocation); } + + canvas.rotate(-tileBox.getRotate(), tileBox.getCenterPixelX(), tileBox.getCenterPixelY()); + + float pointX = tileBox.getCenterPixelX(); + float pointY = tileBox.getPixHeight() / 3f; + float lineLength = getMapDensity() * 40; + + path.reset(); + path.moveTo(pointX - lineLength / 2, pointY); + path.lineTo(pointX + lineLength / 2, pointY); + path.moveTo(pointX, pointY - lineLength / 2); + path.lineTo(pointX, pointY + lineLength / 2); + + canvas.drawPath(path, targetPaint); + + path.reset(); + path.moveTo(0, tileBox.getPixHeight() / 2f); + path.lineTo(tileBox.getPixWidth(), tileBox.getPixHeight() / 2f); + path.moveTo(tileBox.getPixWidth() / 2f, 0); + path.lineTo(tileBox.getPixWidth() / 2f, tileBox.getPixHeight()); + canvas.drawPath(path, centerPaint); + + canvas.rotate(tileBox.getRotate(), tileBox.getCenterPixelX(), tileBox.getCenterPixelY()); } @Override