From 99edceb43dc6af61aca52c1cc2ae0462b8959383 Mon Sep 17 00:00:00 2001 From: Tobias Zwick Date: Thu, 30 Sep 2021 21:40:43 +0200 Subject: [PATCH 1/7] implement tracking - draw line where one already walked - compass-mode is now navigation mode, i.e. movement direction instead of compass rotation - in navigation mode, center map on what's before you, not on you --- .../assets/map_theme/jawg/streetcomplete.yaml | 22 +++ .../location/FineLocationManager.kt | 20 +- .../westnordost/streetcomplete/map/Bearing.kt | 51 ++++++ .../map/LocationAwareMapFragment.kt | 173 +++++++++++++----- .../streetcomplete/map/MainFragment.kt | 26 +-- .../streetcomplete/map/MapFragment.kt | 10 +- .../map/components/TracksMapComponent.kt | 75 ++++++++ .../map/tangram/KtMapController.kt | 1 - .../map/tangram/TangramExtensions.kt | 15 ++ 9 files changed, 319 insertions(+), 74 deletions(-) create mode 100644 app/src/main/java/de/westnordost/streetcomplete/map/Bearing.kt create mode 100644 app/src/main/java/de/westnordost/streetcomplete/map/components/TracksMapComponent.kt diff --git a/app/src/main/assets/map_theme/jawg/streetcomplete.yaml b/app/src/main/assets/map_theme/jawg/streetcomplete.yaml index 88e7f75faa..ebf1a75cc5 100644 --- a/app/src/main/assets/map_theme/jawg/streetcomplete.yaml +++ b/app/src/main/assets/map_theme/jawg/streetcomplete.yaml @@ -1,5 +1,6 @@ global: geometry_color: '#44D14000' # accent color + alpha + location_dot_color: '#44536dfe' textures: pins: @@ -40,6 +41,9 @@ styles: geometry-points: base: points blend: overlay + track-lines: + base: lines + blend: overlay layers: streetcomplete_selected_pins: @@ -114,3 +118,21 @@ layers: size: 32px collide: false order: 1000 + streetcomplete_track: + data: { source: streetcomplete_track } + draw: + track-lines: + color: global.location_dot_color + width: [[14, 6px],[18, 12px]] + collide: false + join: round + order: 1000 + streetcomplete_track2: + data: { source: streetcomplete_track2 } + draw: + track-lines: + color: global.location_dot_color + width: [[14, 6px],[18, 12px]] + collide: false + join: round + order: 1000 diff --git a/app/src/main/java/de/westnordost/streetcomplete/location/FineLocationManager.kt b/app/src/main/java/de/westnordost/streetcomplete/location/FineLocationManager.kt index 0eb1e114ce..5921ec3206 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/location/FineLocationManager.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/location/FineLocationManager.kt @@ -57,7 +57,7 @@ class FineLocationManager(private val mgr: LocationManager, private var location // taken from https://developer.android.com/guide/topics/location/strategies.html#kotlin -private const val TWO_MINUTES: Long = 1000 * 60 * 2 +private const val TWO_MINUTES = 1000L * 60 * 2 /** Determines whether one Location reading is better than the current Location fix * @param location The new Location that you want to evaluate @@ -74,19 +74,19 @@ private fun isBetterLocation(location: Location, currentBestLocation: Location?) } // Check whether the new location fix is newer or older - val timeDelta: Long = location.time - currentBestLocation.time - val isSignificantlyNewer: Boolean = timeDelta > TWO_MINUTES - val isSignificantlyOlder:Boolean = timeDelta < -TWO_MINUTES - val isNewer: Boolean = timeDelta > 0L + val timeDelta = location.time - currentBestLocation.time + val isSignificantlyNewer = timeDelta > TWO_MINUTES + val isSignificantlyOlder = timeDelta < -TWO_MINUTES + val isNewer = timeDelta > 0L // Check whether the new location fix is more or less accurate - val accuracyDelta: Float = location.accuracy - currentBestLocation.accuracy - val isLessAccurate: Boolean = accuracyDelta > 0f - val isMoreAccurate: Boolean = accuracyDelta < 0f - val isSignificantlyLessAccurate: Boolean = accuracyDelta > 200f + val accuracyDelta = location.accuracy - currentBestLocation.accuracy + val isLessAccurate = accuracyDelta > 0f + val isMoreAccurate = accuracyDelta < 0f + val isSignificantlyLessAccurate = accuracyDelta > 200f // Check if the old and new location are from the same provider - val isFromSameProvider: Boolean = location.provider == currentBestLocation.provider + val isFromSameProvider = location.provider == currentBestLocation.provider // Determine location quality using a combination of timeliness and accuracy return when { diff --git a/app/src/main/java/de/westnordost/streetcomplete/map/Bearing.kt b/app/src/main/java/de/westnordost/streetcomplete/map/Bearing.kt new file mode 100644 index 0000000000..d112ffab6e --- /dev/null +++ b/app/src/main/java/de/westnordost/streetcomplete/map/Bearing.kt @@ -0,0 +1,51 @@ +package de.westnordost.streetcomplete.map + +import android.location.Location +import kotlin.math.PI +import kotlin.math.atan2 +import kotlin.math.cos +import kotlin.math.sin + +/** Utility functions to estimate current bearing from a track. This is necessary because + * Location.bearingAccuracy doesn't exist on Android versions below Android API 26, otherwise + * a solution based on this would be less code. E.g. take bearing if accuracy < X */ + +/** Return estimated bearing of a given track. The final result is a smoothing of the last few + * bearings in the track. The bearing of the last point is taken into account the most etc. */ +fun getTrackBearing(track: List): Double? { + val angles = getRelevantBearings(track) + if (angles.isEmpty()) return null + var x = 0.0 + var y = 0.0 + var f = 1.0 + for (angle in angles) { + x += cos(angle * PI / 180.0) * f + y += sin(angle * PI / 180.0) * f + f *= BEARING_SMOOTHING + } + return atan2(y, x) * 180.0 / PI +} + +/** Return the last X bearings from the given track, i.e. those that are not too old and not too + * close to each other */ +private fun getRelevantBearings(track: List): List { + var current = track.lastOrNull() ?: return emptyList() + val last = current + val result = ArrayList() + for(i in track.lastIndex downTo 0) { + val pos = track[i] + // ignore if too close to last position + if (pos.distanceTo(current) < MIN_TRACK_DISTANCE_FOR_BEARING) continue + // break when too old + if (last.time - pos.time > MAX_TRACK_AGE_FOR_BEARING) break + + result.add(pos.bearingTo(current)) + + current = pos + } + return result +} + +private const val MIN_TRACK_DISTANCE_FOR_BEARING = 10f // 10 meters +private const val MAX_TRACK_AGE_FOR_BEARING = 20L * 1000 // 20 seconds +private const val BEARING_SMOOTHING = 0.5 // (0..1) the lower the value, the less smoothing diff --git a/app/src/main/java/de/westnordost/streetcomplete/map/LocationAwareMapFragment.kt b/app/src/main/java/de/westnordost/streetcomplete/map/LocationAwareMapFragment.kt index 727f8cf95b..d7bee4e7c1 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/map/LocationAwareMapFragment.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/map/LocationAwareMapFragment.kt @@ -6,14 +6,20 @@ import android.content.Context import android.hardware.SensorManager import android.location.Location import android.location.LocationManager +import android.os.Bundle import android.view.WindowManager -import android.view.animation.DecelerateInterpolator import androidx.core.content.edit import androidx.core.content.getSystemService +import androidx.lifecycle.lifecycleScope import de.westnordost.streetcomplete.data.osm.mapdata.LatLon import de.westnordost.streetcomplete.location.FineLocationManager import de.westnordost.streetcomplete.location.toLatLon import de.westnordost.streetcomplete.map.components.CurrentLocationMapComponent +import de.westnordost.streetcomplete.map.components.TracksMapComponent +import de.westnordost.streetcomplete.map.tangram.screenBottomToCenterDistance +import de.westnordost.streetcomplete.util.translate +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import kotlin.math.PI /** Manages a map that shows the device's GPS location and orientation as markers on the map with @@ -24,10 +30,14 @@ open class LocationAwareMapFragment : MapFragment() { private lateinit var locationManager: FineLocationManager private var locationMapComponent: CurrentLocationMapComponent? = null + private var tracksMapComponent: TracksMapComponent? = null + /** The GPS position at which the user is displayed at */ var displayedLocation: Location? = null private set - private var compassRotation: Double? = null + + /** The GPS trackpoints the user has walked */ + private var tracks: MutableList> /** Whether the view should automatically center on the GPS location */ var isFollowingPosition = true @@ -36,33 +46,30 @@ open class LocationAwareMapFragment : MapFragment() { centerCurrentPositionIfFollowing() } - /** When the view follows the GPS position, whether the view already zoomed to the location once*/ - private var zoomedYet = false - - /** Whether the view should automatically rotate with the compass (like during navigation) */ - // Since the with-compass rotation happens with no animation, it's better to start the tilt - // animation abruptly and slide out, rather than sliding in and out (the default interpolator) - private val interpolator = DecelerateInterpolator() - var isCompassMode: Boolean = false + /** Whether the view should automatically rotate with bearing (like during navigation) */ + var isNavigationMode: Boolean = false set(value) { - if (field != value) { - field = value - controller?.updateCameraPosition(300, interpolator) { - tilt = if (value) PI.toFloat() / 5f else 0f - } - } + field = value + centerCurrentPositionIfFollowing() } - private var viewDirection: Float? = null + + /** When the view follows the GPS position, whether the view already zoomed to the location once*/ + private var zoomedYet = false interface Listener { /** Called after the map fragment updated its displayed location */ - fun onLocationDidChange() + fun onDisplayedLocationDidChange() } private val listener: Listener? get() = parentFragment as? Listener ?: activity as? Listener /* ------------------------------------ Lifecycle ------------------------------------------- */ + init { + tracks = ArrayList() + tracks.add(ArrayList()) + } + override fun onAttach(context: Context) { super.onAttach(context) compass = Compass(context.getSystemService()!!, @@ -76,6 +83,15 @@ open class LocationAwareMapFragment : MapFragment() { ) } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + displayedLocation = savedInstanceState?.getParcelable(DISPLAYED_LOCATION) + val nullTerminatedTracks = savedInstanceState?.getParcelableArrayList(TRACKS) as ArrayList? + if (nullTerminatedTracks != null) { + tracks = nullTerminatedTracks.unflattenNullTerminated() + } + } + override fun onStop() { super.onStop() saveMapState() @@ -93,6 +109,9 @@ open class LocationAwareMapFragment : MapFragment() { locationMapComponent = CurrentLocationMapComponent(ctx, ctrl) locationMapComponent?.location = displayedLocation + tracksMapComponent = TracksMapComponent(ctrl) + tracksMapComponent?.setTracks(tracks) + centerCurrentPositionIfFollowing() } @@ -106,14 +125,23 @@ open class LocationAwareMapFragment : MapFragment() { @SuppressLint("MissingPermission") fun startPositionTracking() { locationMapComponent?.isVisible = true - locationManager.requestUpdates(2000, 5f) + locationManager.requestUpdates(2000, 1f) } fun stopPositionTracking() { locationMapComponent?.isVisible = false locationManager.removeUpdates() - isCompassMode = false + } + + fun clearPositionTracking() { + stopPositionTracking() displayedLocation = null + isNavigationMode = false + + tracks = ArrayList() + tracks.add(ArrayList()) + + tracksMapComponent?.clear() } protected open fun shouldCenterCurrentPosition(): Boolean { @@ -121,10 +149,28 @@ open class LocationAwareMapFragment : MapFragment() { } fun centerCurrentPosition() { - val controller = controller ?: return - val targetPosition = displayedLocation?.toLatLon() ?: return - controller.updateCameraPosition(600) { - position = targetPosition + val displayedPosition = displayedLocation?.toLatLon() ?: return + var centerPosition = displayedPosition + + updateCameraPosition(1000) { + if (isNavigationMode) { + val bearing = getTrackBearing(tracks.last()) + if (bearing != null) { + rotation = -(bearing * PI / 180.0).toFloat() + // move center position down a bit, so there is more space in front of than + // behind user + val distance = controller?.screenBottomToCenterDistance() + if (distance != null) { + centerPosition = centerPosition.translate(distance * 0.5, bearing) + } + } + tilt = PI.toFloat() / 6f + } else { + tilt = 0f + } + + position = centerPosition + if (!zoomedYet) { zoomedYet = true zoom = 19f @@ -137,33 +183,39 @@ open class LocationAwareMapFragment : MapFragment() { } private fun onLocationChanged(location: Location) { - this.displayedLocation = location + displayedLocation = location locationMapComponent?.location = location + addTrackLocation(location) compass.setLocation(location) centerCurrentPositionIfFollowing() - listener?.onLocationDidChange() + listener?.onDisplayedLocationDidChange() } - /* --------------------------------- Rotation tracking -------------------------------------- */ + private fun addTrackLocation(location: Location) { + // ignore if too imprecise + if (location.accuracy > MIN_TRACK_ACCURACY) return + val lastLocation = tracks.last().lastOrNull() - private fun onCompassRotationChanged(rot: Float, tilt: Float) { - compassRotation = rot * 180 / PI - locationMapComponent?.rotation = compassRotation - - if (isCompassMode) { - viewDirection = - if (viewDirection == null) -rot - else smoothenAngle(-rot, viewDirection ?: 0f, 0.05f) + // create new track if last position too old + if (lastLocation != null) { + if ((displayedLocation?.time ?: 0) - lastLocation.time > MAX_TIME_BETWEEN_LOCATIONS) { + tracks.add(ArrayList()) + tracksMapComponent?.startNewTrack() + } + } - controller?.updateCameraPosition { rotation = viewDirection } + tracks.last().add(location) + // delay update by 1 second because the animation to the new location takes 1 second + lifecycleScope.launch { + delay(1000) + tracksMapComponent?.addToCurrentTrack(location) } } - private fun smoothenAngle( newValue: Float, oldValue: Float, factor: Float): Float { - var delta = newValue - oldValue - while (delta > +PI) delta -= 2 * PI.toFloat() - while (delta < -PI) delta += 2 * PI.toFloat() - return oldValue + factor * delta + /* --------------------------------- Rotation tracking -------------------------------------- */ + + private fun onCompassRotationChanged(rot: Float, tilt: Float) { + locationMapComponent?.rotation = rot * 180 / PI } /* -------------------------------- Save and Restore State ---------------------------------- */ @@ -171,18 +223,49 @@ open class LocationAwareMapFragment : MapFragment() { private fun restoreMapState() { val prefs = activity?.getPreferences(Activity.MODE_PRIVATE) ?: return isFollowingPosition = prefs.getBoolean(PREF_FOLLOWING, true) - isCompassMode = prefs.getBoolean(PREF_COMPASS_MODE, false) + isNavigationMode = prefs.getBoolean(PREF_NAVIGATION_MODE, false) } private fun saveMapState() { activity?.getPreferences(Activity.MODE_PRIVATE)?.edit { putBoolean(PREF_FOLLOWING, isFollowingPosition) - putBoolean(PREF_COMPASS_MODE, isCompassMode) + putBoolean(PREF_NAVIGATION_MODE, isNavigationMode) } } + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putParcelable(DISPLAYED_LOCATION, displayedLocation) + outState.putParcelableArrayList(TRACKS, tracks.flattenToNullTerminated()) + } + companion object { - const val PREF_FOLLOWING = "map_following" - const val PREF_COMPASS_MODE = "map_compass_mode" + private const val PREF_FOLLOWING = "map_following" + private const val PREF_NAVIGATION_MODE = "map_navigation_mode" + + private const val DISPLAYED_LOCATION = "displayed_location" + private const val TRACKS = "tracks" + + private const val MIN_TRACK_ACCURACY = 20f + private const val MAX_TIME_BETWEEN_LOCATIONS = 60L * 1000 // 1 minute + } +} + +private fun List>.flattenToNullTerminated(): ArrayList = + ArrayList(flatMap { it + null }) + +private fun List.unflattenNullTerminated(): ArrayList> { + val result = ArrayList>() + var current = ArrayList() + for (it in this) { + if (it != null) { + current.add(it) + } + else { + result.add(current) + current = ArrayList() + } } + result.add(current) + return result } diff --git a/app/src/main/java/de/westnordost/streetcomplete/map/MainFragment.kt b/app/src/main/java/de/westnordost/streetcomplete/map/MainFragment.kt index 0c6e4b36b5..22da85924c 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/map/MainFragment.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/map/MainFragment.kt @@ -97,7 +97,7 @@ class MainFragment : Fragment(R.layout.fragment_main), private val binding by viewBinding(FragmentMainBinding::bind) private var wasFollowingPosition = true - private var wasCompassMode = false + private var wasNavigationMode = false private var locationWhenOpenedQuest: Location? = null @@ -218,7 +218,7 @@ class MainFragment : Fragment(R.layout.fragment_main), override fun onStop() { super.onStop() wasFollowingPosition = mapFragment?.isFollowingPosition ?: true - wasCompassMode = mapFragment?.isCompassMode ?: false + wasNavigationMode = mapFragment?.isNavigationMode ?: false visibleQuestsSource.removeListener(this) requireContext().unregisterReceiver(locationAvailabilityReceiver) LocalBroadcastManager.getInstance(requireContext()).unregisterReceiver(locationRequestFinishedReceiver) @@ -282,7 +282,7 @@ class MainFragment : Fragment(R.layout.fragment_main), /* ---------------------------- LocationAwareMapFragment.Listener --------------------------- */ - override fun onLocationDidChange() { + override fun onDisplayedLocationDidChange() { updateLocationPointerPin() } @@ -563,7 +563,7 @@ class MainFragment : Fragment(R.layout.fragment_main), binding.gpsTrackingButton.state = if (requireContext().hasLocationPermission) LocationState.ALLOWED else LocationState.DENIED binding.locationPointerPin.visibility = View.GONE - mapFragment!!.stopPositionTracking() + mapFragment!!.clearPositionTracking() locationManager.removeUpdates() } @@ -613,11 +613,11 @@ class MainFragment : Fragment(R.layout.fragment_main), val isFlat = mapFragment.cameraPosition?.tilt?.let { it <= margin } ?: false if (mapFragment.isFollowingPosition && mapFragment.displayedLocation != null) { - setIsCompassMode(!mapFragment.isCompassMode) + setIsNavigationMode(!mapFragment.isNavigationMode) } else { if (isNorthUp) { mapFragment.updateCameraPosition(300) { - tilt = if (isFlat) PI.toFloat() / 5f else 0f + tilt = if (isFlat) PI.toFloat() / 6f else 0f } } else { mapFragment.updateCameraPosition(300) { @@ -628,9 +628,9 @@ class MainFragment : Fragment(R.layout.fragment_main), } } - private fun setIsCompassMode(compassMode: Boolean) { + private fun setIsNavigationMode(compassMode: Boolean) { val mapFragment = mapFragment ?: return - mapFragment.isCompassMode = compassMode + mapFragment.isNavigationMode = compassMode } private fun onClickTrackingButton() { @@ -650,7 +650,7 @@ class MainFragment : Fragment(R.layout.fragment_main), binding.gpsTrackingButton.isActivated = follow val isPositionKnown = mapFragment.displayedLocation != null binding.gpsTrackingButton.visibility = if (isPositionKnown && follow) View.INVISIBLE else View.VISIBLE - if (!follow) setIsCompassMode(false) + if (!follow) setIsNavigationMode(false) } /* -------------------------------------- Context Menu -------------------------------------- */ @@ -839,9 +839,9 @@ class MainFragment : Fragment(R.layout.fragment_main), val mapFragment = mapFragment ?: return wasFollowingPosition = mapFragment.isFollowingPosition - wasCompassMode = mapFragment.isCompassMode + wasNavigationMode = mapFragment.isNavigationMode mapFragment.isFollowingPosition = false - mapFragment.isCompassMode = false + mapFragment.isNavigationMode = false } private fun resetFreezeMap() { @@ -856,7 +856,7 @@ class MainFragment : Fragment(R.layout.fragment_main), val mapFragment = mapFragment ?: return mapFragment.isFollowingPosition = wasFollowingPosition - mapFragment.isCompassMode = wasCompassMode + mapFragment.isNavigationMode = wasNavigationMode mapFragment.endFocusQuest() mapFragment.show3DBuildings = true mapFragment.pinMode = QuestsMapFragment.PinMode.QUESTS @@ -931,7 +931,7 @@ class MainFragment : Fragment(R.layout.fragment_main), fun setCameraPosition(position: LatLon, zoom: Float) { mapFragment?.isFollowingPosition = false - mapFragment?.isCompassMode = false + mapFragment?.isNavigationMode = false mapFragment?.setInitialCameraPosition(CameraPosition(position, 0f, 0f, zoom)) setIsFollowingPosition(false) } diff --git a/app/src/main/java/de/westnordost/streetcomplete/map/MapFragment.kt b/app/src/main/java/de/westnordost/streetcomplete/map/MapFragment.kt index bb8601afd2..9910500efd 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/map/MapFragment.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/map/MapFragment.kt @@ -374,11 +374,11 @@ open class MapFragment : Fragment(), fun getDisplayedArea(): BoundingBox? = controller?.screenAreaToBoundingBox(RectF()) companion object { - const val PREF_ROTATION = "map_rotation" - const val PREF_TILT = "map_tilt" - const val PREF_ZOOM = "map_zoom" - const val PREF_LAT = "map_lat" - const val PREF_LON = "map_lon" + private const val PREF_ROTATION = "map_rotation" + private const val PREF_TILT = "map_tilt" + private const val PREF_ZOOM = "map_zoom" + private const val PREF_LAT = "map_lat" + private const val PREF_LON = "map_lon" } } diff --git a/app/src/main/java/de/westnordost/streetcomplete/map/components/TracksMapComponent.kt b/app/src/main/java/de/westnordost/streetcomplete/map/components/TracksMapComponent.kt new file mode 100644 index 0000000000..29c794ab27 --- /dev/null +++ b/app/src/main/java/de/westnordost/streetcomplete/map/components/TracksMapComponent.kt @@ -0,0 +1,75 @@ +package de.westnordost.streetcomplete.map.components + +import android.location.Location +import com.mapzen.tangram.LngLat +import com.mapzen.tangram.geometry.Polyline +import de.westnordost.streetcomplete.map.tangram.KtMapController +import de.westnordost.streetcomplete.map.tangram.toLngLat +import kotlin.math.max + +/** Takes care of showing the path(s) walked on the map */ +class TracksMapComponent(ctrl: KtMapController) { + + /* There are two layers simply as a performance optimization: If there are thousands of + trackpoints, we don't want to update (=copy) the thousands of points each time a new + trackpoint is added. Instead, we only update a list of 100 trackpoints each time a new + trackpoint is added and every 100th time, we update the other layer. + + So, the list of points updated ~per second doesn't grow too long. + */ + private val activeLayer = ctrl.addDataLayer(LAYER1) + private val oldLayer = ctrl.addDataLayer(LAYER2) + + private var index = 0 + private var tracks: MutableList> + + init { + tracks = ArrayList() + tracks.add(ArrayList()) + } + + /** Add a point to the current track */ + fun addToCurrentTrack(pos: Location) { + val track = tracks.last() + track.add(pos.toLngLat()) + + // every 100th trackpoint, move the index to the back + if (track.size - index > 100) { + putAllTracksInOldLayer() + } else { + activeLayer.setFeatures(listOf(track.subList(index, track.size).toPolyline())) + } + } + + /** Start a new track. I.e. the points in that track will be drawn as an own polyline */ + fun startNewTrack() { + tracks.add(ArrayList()) + putAllTracksInOldLayer() + } + + /** Set all the tracks (when re-initializing) */ + fun setTracks(tracks: List>) { + this.tracks = tracks.map { track -> track.map { it.toLngLat() }.toMutableList() }.toMutableList() + putAllTracksInOldLayer() + } + + private fun putAllTracksInOldLayer() { + index = max(0, tracks.last().lastIndex) + activeLayer.clear() + oldLayer.setFeatures(tracks.map { it.toPolyline() }) + } + + fun clear() { + tracks = ArrayList() + startNewTrack() + } + + companion object { + // see streetcomplete.yaml for the definitions of the layer + private const val LAYER1 = "streetcomplete_track" + private const val LAYER2 = "streetcomplete_track2" + + } +} + +private fun List.toPolyline() = Polyline(this, mapOf("type" to "line")) diff --git a/app/src/main/java/de/westnordost/streetcomplete/map/tangram/KtMapController.kt b/app/src/main/java/de/westnordost/streetcomplete/map/tangram/KtMapController.kt index b653a1ddf7..f6af646211 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/map/tangram/KtMapController.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/map/tangram/KtMapController.kt @@ -19,7 +19,6 @@ import de.westnordost.streetcomplete.data.osm.mapdata.BoundingBox import de.westnordost.streetcomplete.data.osm.mapdata.LatLon import de.westnordost.streetcomplete.util.* import kotlinx.coroutines.* -import java.util.Locale import java.util.concurrent.ConcurrentLinkedQueue import kotlin.coroutines.Continuation import kotlin.coroutines.resume diff --git a/app/src/main/java/de/westnordost/streetcomplete/map/tangram/TangramExtensions.kt b/app/src/main/java/de/westnordost/streetcomplete/map/tangram/TangramExtensions.kt index 9530641823..118e11aaae 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/map/tangram/TangramExtensions.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/map/tangram/TangramExtensions.kt @@ -2,6 +2,7 @@ package de.westnordost.streetcomplete.map.tangram import android.graphics.PointF import android.graphics.RectF +import android.location.Location import com.mapzen.tangram.LngLat import com.mapzen.tangram.geometry.Geometry import com.mapzen.tangram.geometry.Point @@ -12,6 +13,7 @@ import de.westnordost.streetcomplete.data.osm.geometry.ElementPointGeometry import de.westnordost.streetcomplete.data.osm.geometry.ElementPolygonsGeometry import de.westnordost.streetcomplete.data.osm.geometry.ElementPolylinesGeometry import de.westnordost.streetcomplete.data.osm.mapdata.LatLon +import de.westnordost.streetcomplete.util.distanceTo fun ElementGeometry.toTangramGeometry(): List = when(this) { is ElementPolylinesGeometry -> { @@ -38,6 +40,8 @@ fun LngLat.toLatLon(): LatLon = LatLon(latitude, longitude) fun LatLon.toLngLat(): LngLat = LngLat(longitude, latitude) +fun Location.toLngLat(): LngLat = LngLat(longitude, latitude) + fun KtMapController.screenAreaContains(g: ElementGeometry, offset: RectF): Boolean { val p = PointF() val mapView = glViewHolder!!.view @@ -53,3 +57,14 @@ fun KtMapController.screenAreaContains(g: ElementGeometry, offset: RectF): Boole && p.y <= mapView.height - offset.bottom } } + +fun KtMapController.screenBottomToCenterDistance(): Double? { + val view = glViewHolder?.view ?: return null + val w = view.width + val h = view.height + if (w == 0 || h == 0) return null + + val center = screenPositionToLatLon(PointF(w/2f, h/2f)) ?: return null + val bottom = screenPositionToLatLon(PointF(w/2f, h*1f)) ?: return null + return center.distanceTo(bottom) +} From c1d76739f4807aaacdbdf16424455a41e7e2987b Mon Sep 17 00:00:00 2001 From: Tobias Zwick Date: Fri, 1 Oct 2021 01:38:48 +0200 Subject: [PATCH 2/7] change what buttons do what also, only display the compass button if not north... it's free screen real estate https://www.youtube.com/watch?v=yNxPVj0hejg --- .../streetcomplete/location/LocationState.kt | 2 +- .../location/LocationStateButton.kt | 74 +++++++++------- .../map/LocationAwareMapFragment.kt | 30 ++++--- .../streetcomplete/map/MainFragment.kt | 88 ++++++++----------- .../components/CurrentLocationMapComponent.kt | 6 +- .../quests/cycleway/AddCyclewayForm.kt | 2 +- .../quests/lanes/AddLanesForm.kt | 2 +- .../quests/oneway/AddOnewayForm.kt | 2 +- .../oneway_suspects/AddSuspectedOnewayForm.kt | 2 +- .../quests/sidewalk/AddSidewalkForm.kt | 2 +- .../steps_incline/AddStepsInclineForm.kt | 2 +- .../res/drawable/ic_compass_needle_24dp.xml | 10 --- .../res/drawable/ic_compass_needle_48dp.xml | 12 +++ .../drawable/ic_location_navigation_24dp.xml | 10 +++ .../res/drawable/ic_location_state_24dp.xml | 1 + app/src/main/res/layout/fragment_main.xml | 35 ++------ .../main/res/layout/view_little_compass.xml | 31 ++----- app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/strings.xml | 1 - res/graphics/compass_needle.svg | 7 ++ 20 files changed, 152 insertions(+), 168 deletions(-) delete mode 100644 app/src/main/res/drawable/ic_compass_needle_24dp.xml create mode 100644 app/src/main/res/drawable/ic_compass_needle_48dp.xml create mode 100644 app/src/main/res/drawable/ic_location_navigation_24dp.xml create mode 100644 res/graphics/compass_needle.svg diff --git a/app/src/main/java/de/westnordost/streetcomplete/location/LocationState.kt b/app/src/main/java/de/westnordost/streetcomplete/location/LocationState.kt index b11fac84dd..9122188bc9 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/location/LocationState.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/location/LocationState.kt @@ -9,4 +9,4 @@ enum class LocationState { // receiving location updates val isEnabled: Boolean get() = ordinal >= ENABLED.ordinal -} \ No newline at end of file +} diff --git a/app/src/main/java/de/westnordost/streetcomplete/location/LocationStateButton.kt b/app/src/main/java/de/westnordost/streetcomplete/location/LocationStateButton.kt index 1cb8be8a8c..698cae9bb1 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/location/LocationStateButton.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/location/LocationStateButton.kt @@ -11,7 +11,7 @@ import android.view.View import androidx.annotation.Keep import androidx.appcompat.widget.AppCompatImageButton import de.westnordost.streetcomplete.R -import java.util.Arrays +import de.westnordost.streetcomplete.location.LocationState.* /** * An image button which shows the current location state @@ -23,10 +23,10 @@ class LocationStateButton @JvmOverloads constructor( ) : AppCompatImageButton(context, attrs, defStyle) { var state: LocationState - get() = _state ?: LocationState.DENIED + get() = _state ?: DENIED set(value) { _state = value } - // this is necessary because state is accessed before it is initialized (in contructor of super) + // this is necessary because state is accessed before it is initialized (in constructor of super) private var _state: LocationState? = null set(value) { if (field != value) { @@ -34,26 +34,33 @@ class LocationStateButton @JvmOverloads constructor( refreshDrawableState() } } + private val tint: ColorStateList? + var isNavigation: Boolean = false + set(value) { + if (field != value) { + field = value + refreshDrawableState() + } + } + + init { val a = context.obtainStyledAttributes(attrs, R.styleable.LocationStateButton) state = determineStateFrom(a) tint = a.getColorStateList(R.styleable.LocationStateButton_tint) + isNavigation = a.getBoolean(R.styleable.LocationStateButton_is_navigation, false) + a.recycle() } - private fun determineStateFrom(a: TypedArray): LocationState { - if (a.getBoolean(R.styleable.LocationStateButton_state_updating,false)) - return LocationState.UPDATING - if (a.getBoolean(R.styleable.LocationStateButton_state_searching,false)) - return LocationState.SEARCHING - if (a.getBoolean(R.styleable.LocationStateButton_state_enabled,false)) - return LocationState.ENABLED - if (a.getBoolean(R.styleable.LocationStateButton_state_allowed,false)) - return LocationState.ALLOWED - else - return LocationState.DENIED + private fun determineStateFrom(a: TypedArray): LocationState = when { + a.getBoolean(R.styleable.LocationStateButton_state_updating,false) -> UPDATING + a.getBoolean(R.styleable.LocationStateButton_state_searching,false) -> SEARCHING + a.getBoolean(R.styleable.LocationStateButton_state_enabled,false) -> ENABLED + a.getBoolean(R.styleable.LocationStateButton_state_allowed,false) -> ALLOWED + else -> DENIED } override fun drawableStateChanged() { @@ -69,19 +76,22 @@ class LocationStateButton @JvmOverloads constructor( } override fun onCreateDrawableState(extraSpace: Int): IntArray { - val additionalLength = STATES.size + 1 - val drawableState = super.onCreateDrawableState(extraSpace + additionalLength) - val arrPos = state.ordinal - val additionalArray = Arrays.copyOf(Arrays.copyOf(STATES, arrPos), additionalLength) - View.mergeDrawableStates(drawableState, additionalArray) + val attributes = ArrayList() + attributes += state.styleableAttributes + if (isNavigation) attributes += R.attr.is_navigation + + val drawableState = super.onCreateDrawableState(extraSpace + attributes.size) + + View.mergeDrawableStates(drawableState, attributes.toIntArray()) return drawableState } - public override fun onSaveInstanceState(): Parcelable? { + public override fun onSaveInstanceState(): Parcelable { val superState = super.onSaveInstanceState() val ss = SavedState(superState) ss.state = state ss.activated = isActivated + ss.navigation = isNavigation return ss } @@ -90,23 +100,27 @@ class LocationStateButton @JvmOverloads constructor( super.onRestoreInstanceState(ss.superState) state = ss.state isActivated = ss.activated + isNavigation = ss.navigation requestLayout() } internal class SavedState : BaseSavedState { - var state: LocationState = LocationState.DENIED + var state: LocationState = DENIED var activated = false + var navigation = false constructor(superState: Parcelable?) : super(superState) constructor(parcel: Parcel) : super(parcel) { state = LocationState.valueOf(parcel.readString()!!) activated = parcel.readInt() == 1 + navigation = parcel.readInt() == 1 } override fun writeToParcel(out: Parcel, flags: Int) { super.writeToParcel(out, flags) out.writeString(state.name) out.writeInt(if (activated) 1 else 0) + out.writeInt(if (navigation) 1 else 0) } companion object { @@ -117,14 +131,12 @@ class LocationStateButton @JvmOverloads constructor( } } } - - companion object { - // must be defined in the same order as the LocationState enum (but minus the first) - private val STATES = intArrayOf( - R.attr.state_allowed, - R.attr.state_enabled, - R.attr.state_searching, - R.attr.state_updating - ) - } } + +private val LocationState.styleableAttributes: List get() = + listOf( + R.attr.state_allowed, + R.attr.state_enabled, + R.attr.state_searching, + R.attr.state_updating + ).subList(0, ordinal) diff --git a/app/src/main/java/de/westnordost/streetcomplete/map/LocationAwareMapFragment.kt b/app/src/main/java/de/westnordost/streetcomplete/map/LocationAwareMapFragment.kt index d7bee4e7c1..316019ac62 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/map/LocationAwareMapFragment.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/map/LocationAwareMapFragment.kt @@ -43,15 +43,21 @@ open class LocationAwareMapFragment : MapFragment() { var isFollowingPosition = true set(value) { field = value - centerCurrentPositionIfFollowing() + if (field != value && !value) { + _isNavigationMode = false + } } /** Whether the view should automatically rotate with bearing (like during navigation) */ - var isNavigationMode: Boolean = false + private var _isNavigationMode: Boolean = false + var isNavigationMode: Boolean set(value) { - field = value - centerCurrentPositionIfFollowing() + if (_isNavigationMode != value && !value) { + updateCameraPosition(300) { tilt = 0f } + } + _isNavigationMode = value } + get() = _isNavigationMode /** When the view follows the GPS position, whether the view already zoomed to the location once*/ private var zoomedYet = false @@ -152,21 +158,19 @@ open class LocationAwareMapFragment : MapFragment() { val displayedPosition = displayedLocation?.toLatLon() ?: return var centerPosition = displayedPosition - updateCameraPosition(1000) { + updateCameraPosition(600) { if (isNavigationMode) { val bearing = getTrackBearing(tracks.last()) if (bearing != null) { rotation = -(bearing * PI / 180.0).toFloat() - // move center position down a bit, so there is more space in front of than - // behind user + /* move center position down a bit, so there is more space in front of than + behind user */ val distance = controller?.screenBottomToCenterDistance() if (distance != null) { - centerPosition = centerPosition.translate(distance * 0.5, bearing) + centerPosition = centerPosition.translate(distance * 0.4, bearing) } } tilt = PI.toFloat() / 6f - } else { - tilt = 0f } position = centerPosition @@ -178,7 +182,7 @@ open class LocationAwareMapFragment : MapFragment() { } } - protected fun centerCurrentPositionIfFollowing() { + fun centerCurrentPositionIfFollowing() { if (shouldCenterCurrentPosition()) centerCurrentPosition() } @@ -205,9 +209,9 @@ open class LocationAwareMapFragment : MapFragment() { } tracks.last().add(location) - // delay update by 1 second because the animation to the new location takes 1 second + // delay update by 600 ms because the animation to the new location takes that long lifecycleScope.launch { - delay(1000) + delay(600) tracksMapComponent?.addToCurrentTrack(location) } } diff --git a/app/src/main/java/de/westnordost/streetcomplete/map/MainFragment.kt b/app/src/main/java/de/westnordost/streetcomplete/map/MainFragment.kt index 22da85924c..7942262b66 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/map/MainFragment.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/map/MainFragment.kt @@ -25,6 +25,7 @@ import androidx.core.graphics.minus import androidx.core.graphics.toPointF import androidx.core.graphics.toRectF import androidx.core.view.isGone +import androidx.core.view.isInvisible import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager.POP_BACK_STACK_INCLUSIVE import androidx.fragment.app.commit @@ -60,10 +61,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject -import kotlin.math.PI -import kotlin.math.cos -import kotlin.math.sin -import kotlin.math.sqrt +import kotlin.math.* import kotlin.random.Random /** Contains the quests map and the controls for it. @@ -242,15 +240,16 @@ class MainFragment : Fragment(R.layout.fragment_main), override fun onMapInitialized() { val isFollowingPosition = mapFragment?.isFollowingPosition ?: false - val isPositionKnown = mapFragment?.displayedLocation != null binding.gpsTrackingButton.isActivated = isFollowingPosition - binding.gpsTrackingButton.visibility = if (isFollowingPosition && isPositionKnown) View.INVISIBLE else View.VISIBLE updateLocationPointerPin() } override fun onMapIsChanging(position: LatLon, rotation: Float, tilt: Float, zoom: Float) { - binding.compassNeedleView.rotation = (180 * rotation / Math.PI).toFloat() - binding.compassNeedleView.rotationX = (180 * tilt / Math.PI).toFloat() + binding.compassView.rotation = (180 * rotation / PI).toFloat() + binding.compassView.rotationX = (180 * tilt / PI).toFloat() + + val margin = 2 * PI / 180 + binding.compassView.isInvisible = abs(rotation) < margin && tilt < margin updateLocationPointerPin() @@ -550,7 +549,6 @@ class MainFragment : Fragment(R.layout.fragment_main), @SuppressLint("MissingPermission") private fun onLocationIsEnabled() { - binding.gpsTrackingButton.visibility = View.VISIBLE binding.gpsTrackingButton.state = LocationState.SEARCHING mapFragment!!.startPositionTracking() @@ -559,7 +557,6 @@ class MainFragment : Fragment(R.layout.fragment_main), } private fun onLocationIsDisabled() { - binding.gpsTrackingButton.visibility = View.VISIBLE binding.gpsTrackingButton.state = if (requireContext().hasLocationPermission) LocationState.ALLOWED else LocationState.DENIED binding.locationPointerPin.visibility = View.GONE @@ -569,7 +566,6 @@ class MainFragment : Fragment(R.layout.fragment_main), private fun onLocationRequestFinished(state: LocationState) { if (activity == null) return - binding.gpsTrackingButton.visibility = View.VISIBLE binding.gpsTrackingButton.state = state if (state.isEnabled) { updateLocationAvailability() @@ -577,8 +573,6 @@ class MainFragment : Fragment(R.layout.fragment_main), } private fun onLocationChanged(location: Location) { - val isFollowingPosition = mapFragment?.isFollowingPosition ?: false - binding.gpsTrackingButton.visibility = if (isFollowingPosition) View.INVISIBLE else View.VISIBLE binding.gpsTrackingButton.state = LocationState.UPDATING updateLocationPointerPin() } @@ -600,57 +594,53 @@ class MainFragment : Fragment(R.layout.fragment_main), } private fun onClickCompassButton() { + /* Clicking the compass button will always rotate the map back to north and remove tilt */ val mapFragment = mapFragment ?: return - // Allow a small margin of error around north/flat. This both matches - // UX expectations ("it looks straight..") and works around a bug where - // the rotation/tilt are not set to perfectly 0 during animation - val margin = 0.025f // About 4% - // 2PI radians = full circle of rotation = also north - val isNorthUp = mapFragment.cameraPosition?.rotation?.let { - it <= margin || 2f*PI.toFloat()-it <= margin - } ?: false - // Camera cannot rotate upside down => full circle check not needed - val isFlat = mapFragment.cameraPosition?.tilt?.let { it <= margin } ?: false - - if (mapFragment.isFollowingPosition && mapFragment.displayedLocation != null) { - setIsNavigationMode(!mapFragment.isNavigationMode) + val camera = mapFragment.cameraPosition ?: return + + // if the user wants to rotate back north, it means he also doesn't want to use nav mode anymore + if (mapFragment.isNavigationMode) { + mapFragment.updateCameraPosition(300) { rotation = 0f } + setIsNavigationMode(false) } else { - if (isNorthUp) { - mapFragment.updateCameraPosition(300) { - tilt = if (isFlat) PI.toFloat() / 6f else 0f - } - } else { - mapFragment.updateCameraPosition(300) { - rotation = 0f - tilt = 0f - } + mapFragment.updateCameraPosition(300) { + rotation = 0f + tilt = 0f } } } - private fun setIsNavigationMode(compassMode: Boolean) { + private fun onClickTrackingButton() { val mapFragment = mapFragment ?: return - mapFragment.isNavigationMode = compassMode + + when { + !binding.gpsTrackingButton.state.isEnabled -> { + val tag = LocationRequestFragment::class.java.simpleName + val locationRequestFragment = activity?.supportFragmentManager?.findFragmentByTag(tag) as LocationRequestFragment? + locationRequestFragment?.startRequest() + } + !mapFragment.isFollowingPosition -> { + setIsFollowingPosition(true) + } + else -> { + setIsNavigationMode(!mapFragment.isNavigationMode) + } + } } - private fun onClickTrackingButton() { + private fun setIsNavigationMode(navigation: Boolean) { val mapFragment = mapFragment ?: return - if (binding.gpsTrackingButton.state.isEnabled) { - setIsFollowingPosition(!mapFragment.isFollowingPosition) - } else { - val tag = LocationRequestFragment::class.java.simpleName - val locationRequestFragment = activity?.supportFragmentManager?.findFragmentByTag(tag) as LocationRequestFragment? - locationRequestFragment?.startRequest() - } + mapFragment.isNavigationMode = navigation + binding.gpsTrackingButton.isNavigation = navigation + // always re-center position because navigation mode shifts the center position + mapFragment.centerCurrentPositionIfFollowing() } private fun setIsFollowingPosition(follow: Boolean) { val mapFragment = mapFragment ?: return mapFragment.isFollowingPosition = follow binding.gpsTrackingButton.isActivated = follow - val isPositionKnown = mapFragment.displayedLocation != null - binding.gpsTrackingButton.visibility = if (isPositionKnown && follow) View.INVISIBLE else View.VISIBLE - if (!follow) setIsNavigationMode(false) + if (follow) mapFragment.centerCurrentPositionIfFollowing() } /* -------------------------------------- Context Menu -------------------------------------- */ @@ -742,7 +732,7 @@ class MainFragment : Fragment(R.layout.fragment_main), } private fun onClickLocationPointer() { - mapFragment?.centerCurrentPosition() + setIsFollowingPosition(true) } //endregion diff --git a/app/src/main/java/de/westnordost/streetcomplete/map/components/CurrentLocationMapComponent.kt b/app/src/main/java/de/westnordost/streetcomplete/map/components/CurrentLocationMapComponent.kt index 4e3de78436..7f719f31c8 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/map/components/CurrentLocationMapComponent.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/map/components/CurrentLocationMapComponent.kt @@ -118,11 +118,11 @@ class CurrentLocationMapComponent(ctx: Context, private val ctrl: KtMapControlle val pos = location?.toLatLon() ?: return accuracyMarker.isVisible = true - accuracyMarker.setPointEased(pos, 1000, MapController.EaseType.CUBIC) + accuracyMarker.setPointEased(pos, 600, MapController.EaseType.CUBIC) locationMarker.isVisible = true - locationMarker.setPointEased(pos, 1000, MapController.EaseType.CUBIC) + locationMarker.setPointEased(pos, 600, MapController.EaseType.CUBIC) directionMarker.isVisible = rotation != null - directionMarker.setPointEased(pos, 1000, MapController.EaseType.CUBIC) + directionMarker.setPointEased(pos, 600, MapController.EaseType.CUBIC) updateAccuracy() } diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/cycleway/AddCyclewayForm.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/cycleway/AddCyclewayForm.kt index e8b098a344..6c9d533c23 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/quests/cycleway/AddCyclewayForm.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/quests/cycleway/AddCyclewayForm.kt @@ -91,7 +91,7 @@ class AddCyclewayForm : AbstractQuestFormAnswerFragment() { streetSideRotater = StreetSideRotater( binding.puzzleView, - binding.littleCompass.compassNeedleView, + binding.littleCompass.root, elementGeometry as ElementPolylinesGeometry ) diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/lanes/AddLanesForm.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/lanes/AddLanesForm.kt index dfae56e991..67f7572e74 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/quests/lanes/AddLanesForm.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/quests/lanes/AddLanesForm.kt @@ -201,7 +201,7 @@ class AddLanesForm : AbstractQuestFormAnswerFragment() { streetSideRotater = StreetSideRotater( streetLanesPuzzleBinding.puzzleViewRotateContainer, - streetLanesPuzzleBinding.littleCompass.compassNeedleView, + streetLanesPuzzleBinding.littleCompass.root, elementGeometry as ElementPolylinesGeometry ) streetSideRotater?.onMapOrientation(lastRotation, lastTilt) diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/oneway/AddOnewayForm.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/oneway/AddOnewayForm.kt index 0dd186c79e..c4c1184744 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/quests/oneway/AddOnewayForm.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/quests/oneway/AddOnewayForm.kt @@ -59,7 +59,7 @@ class AddOnewayForm : AbstractQuestFormAnswerFragment() { streetSideRotater = StreetSideRotater( binding.puzzleView, - binding.littleCompass.compassNeedleView, + binding.littleCompass.root, elementGeometry as ElementPolylinesGeometry ) } diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/oneway_suspects/AddSuspectedOnewayForm.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/oneway_suspects/AddSuspectedOnewayForm.kt index 08a72dce38..e916916bfa 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/quests/oneway_suspects/AddSuspectedOnewayForm.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/quests/oneway_suspects/AddSuspectedOnewayForm.kt @@ -56,7 +56,7 @@ class AddSuspectedOnewayForm : AbstractQuestAnswerFragment() { streetSideRotater = StreetSideRotater( binding.puzzleView, - binding.littleCompass.compassNeedleView, + binding.littleCompass.root, elementGeometry as ElementPolylinesGeometry ) diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/steps_incline/AddStepsInclineForm.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/steps_incline/AddStepsInclineForm.kt index 27eaeea6b8..aac569e8e3 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/quests/steps_incline/AddStepsInclineForm.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/quests/steps_incline/AddStepsInclineForm.kt @@ -58,7 +58,7 @@ class AddStepsInclineForm : AbstractQuestFormAnswerFragment() { streetSideRotater = StreetSideRotater( binding.puzzleView, - binding.littleCompass.compassNeedleView, + binding.littleCompass.root, elementGeometry as ElementPolylinesGeometry ) } diff --git a/app/src/main/res/drawable/ic_compass_needle_24dp.xml b/app/src/main/res/drawable/ic_compass_needle_24dp.xml deleted file mode 100644 index 564905163f..0000000000 --- a/app/src/main/res/drawable/ic_compass_needle_24dp.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_compass_needle_48dp.xml b/app/src/main/res/drawable/ic_compass_needle_48dp.xml new file mode 100644 index 0000000000..89e6d4c9a7 --- /dev/null +++ b/app/src/main/res/drawable/ic_compass_needle_48dp.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_location_navigation_24dp.xml b/app/src/main/res/drawable/ic_location_navigation_24dp.xml new file mode 100644 index 0000000000..33fdf0afe4 --- /dev/null +++ b/app/src/main/res/drawable/ic_location_navigation_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_location_state_24dp.xml b/app/src/main/res/drawable/ic_location_state_24dp.xml index e1ec794b87..33c24d725a 100644 --- a/app/src/main/res/drawable/ic_location_state_24dp.xml +++ b/app/src/main/res/drawable/ic_location_state_24dp.xml @@ -1,6 +1,7 @@ + diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml index 25fa15d0ae..9b67ced98a 100644 --- a/app/src/main/res/layout/fragment_main.xml +++ b/app/src/main/res/layout/fragment_main.xml @@ -15,6 +15,7 @@ android:id="@+id/mapControls" android:layout_width="match_parent" android:layout_height="match_parent" + android:animateLayoutChanges="true" tools:ignore="RtlHardcoded"> + tools:layout="@layout/fragment_notification_button" /> - - - - - - - + android:elevation="2dp" + android:padding="14dp" + android:src="@drawable/ic_compass_needle_48dp"/> - - - - - - + android:layout_alignParentTop="true"/> diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 4aaa4091ad..55821fd9c9 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -13,6 +13,7 @@ + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 15e8cc62be..91dec72926 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -373,7 +373,6 @@ Otherwise, you can download another keyboard in the app store. Popular keyboards Crossing controlled by traffic lights Marked crossing Crossing without road markings - N Undo Undo last edit Show edit history diff --git a/res/graphics/compass_needle.svg b/res/graphics/compass_needle.svg new file mode 100644 index 0000000000..9e973256a2 --- /dev/null +++ b/res/graphics/compass_needle.svg @@ -0,0 +1,7 @@ + + + + + + + From e8850b4bed3cb79a4a961464967cd97752f8c745 Mon Sep 17 00:00:00 2001 From: Tobias Zwick Date: Fri, 1 Oct 2021 14:21:32 +0200 Subject: [PATCH 3/7] draw previous track with less alpha --- .../assets/map_theme/jawg/streetcomplete.yaml | 57 +++++++++++++------ .../map/components/TracksMapComponent.kt | 13 +++-- 2 files changed, 48 insertions(+), 22 deletions(-) diff --git a/app/src/main/assets/map_theme/jawg/streetcomplete.yaml b/app/src/main/assets/map_theme/jawg/streetcomplete.yaml index ebf1a75cc5..f820bb6e10 100644 --- a/app/src/main/assets/map_theme/jawg/streetcomplete.yaml +++ b/app/src/main/assets/map_theme/jawg/streetcomplete.yaml @@ -1,6 +1,7 @@ global: geometry_color: '#44D14000' # accent color + alpha - location_dot_color: '#44536dfe' + track_color: '#66536dfe' + old_track_color: '#33536dfe' textures: pins: @@ -118,21 +119,45 @@ layers: size: 32px collide: false order: 1000 + # streetcomplete_track and streetcomplete_track2 layers are exactly the same except the source. + # It is not possible in tangram to define a layer for several sources. streetcomplete_track: data: { source: streetcomplete_track } - draw: - track-lines: - color: global.location_dot_color - width: [[14, 6px],[18, 12px]] - collide: false - join: round - order: 1000 + current: + filter: { old: [false] } + draw: + track-lines: + color: global.track_color + width: [[14, 6px],[18, 12px]] + collide: false + join: round + order: 1000 + old: + filter: { old: [true] } + draw: + track-lines: + color: global.old_track_color + width: [[14, 6px],[18, 12px]] + collide: false + join: round + order: 1000 streetcomplete_track2: - data: { source: streetcomplete_track2 } - draw: - track-lines: - color: global.location_dot_color - width: [[14, 6px],[18, 12px]] - collide: false - join: round - order: 1000 + data: { source: streetcomplete_track2 } + current: + filter: { old: [false] } + draw: + track-lines: + color: global.track_color + width: [[14, 6px],[18, 12px]] + collide: false + join: round + order: 1000 + old: + filter: { old: [true] } + draw: + track-lines: + color: global.old_track_color + width: [[14, 6px],[18, 12px]] + collide: false + join: round + order: 1000 diff --git a/app/src/main/java/de/westnordost/streetcomplete/map/components/TracksMapComponent.kt b/app/src/main/java/de/westnordost/streetcomplete/map/components/TracksMapComponent.kt index 29c794ab27..3e80bad950 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/map/components/TracksMapComponent.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/map/components/TracksMapComponent.kt @@ -17,8 +17,8 @@ class TracksMapComponent(ctrl: KtMapController) { So, the list of points updated ~per second doesn't grow too long. */ - private val activeLayer = ctrl.addDataLayer(LAYER1) - private val oldLayer = ctrl.addDataLayer(LAYER2) + private val layer1 = ctrl.addDataLayer(LAYER1) + private val layer2 = ctrl.addDataLayer(LAYER2) private var index = 0 private var tracks: MutableList> @@ -37,7 +37,7 @@ class TracksMapComponent(ctrl: KtMapController) { if (track.size - index > 100) { putAllTracksInOldLayer() } else { - activeLayer.setFeatures(listOf(track.subList(index, track.size).toPolyline())) + layer1.setFeatures(listOf(track.subList(index, track.size).toPolyline(false))) } } @@ -55,8 +55,8 @@ class TracksMapComponent(ctrl: KtMapController) { private fun putAllTracksInOldLayer() { index = max(0, tracks.last().lastIndex) - activeLayer.clear() - oldLayer.setFeatures(tracks.map { it.toPolyline() }) + layer1.clear() + layer2.setFeatures(tracks.map { it.toPolyline(true) }) } fun clear() { @@ -72,4 +72,5 @@ class TracksMapComponent(ctrl: KtMapController) { } } -private fun List.toPolyline() = Polyline(this, mapOf("type" to "line")) +private fun List.toPolyline(old: Boolean) = + Polyline(this, mapOf("type" to "line", "old" to old.toString())) From b647575583ebc83c2fa11642afa069a9557e18c6 Mon Sep 17 00:00:00 2001 From: Tobias Zwick Date: Fri, 1 Oct 2021 14:22:55 +0200 Subject: [PATCH 4/7] don't care how old the last trackpoint is --- .../java/de/westnordost/streetcomplete/map/Bearing.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/de/westnordost/streetcomplete/map/Bearing.kt b/app/src/main/java/de/westnordost/streetcomplete/map/Bearing.kt index d112ffab6e..8be177cba6 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/map/Bearing.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/map/Bearing.kt @@ -30,22 +30,22 @@ fun getTrackBearing(track: List): Double? { * close to each other */ private fun getRelevantBearings(track: List): List { var current = track.lastOrNull() ?: return emptyList() - val last = current val result = ArrayList() for(i in track.lastIndex downTo 0) { val pos = track[i] // ignore if too close to last position if (pos.distanceTo(current) < MIN_TRACK_DISTANCE_FOR_BEARING) continue - // break when too old - if (last.time - pos.time > MAX_TRACK_AGE_FOR_BEARING) break result.add(pos.bearingTo(current)) current = pos + + // break when found enough + if (result.size > MAX_TRACK_POINTS_FOR_BEARING) break } return result } -private const val MIN_TRACK_DISTANCE_FOR_BEARING = 10f // 10 meters -private const val MAX_TRACK_AGE_FOR_BEARING = 20L * 1000 // 20 seconds +private const val MIN_TRACK_DISTANCE_FOR_BEARING = 15f // 15 meters +private const val MAX_TRACK_POINTS_FOR_BEARING = 6 private const val BEARING_SMOOTHING = 0.5 // (0..1) the lower the value, the less smoothing From 83cd6962117fe81ae570241ba254df58a38e3453 Mon Sep 17 00:00:00 2001 From: Tobias Zwick Date: Fri, 1 Oct 2021 22:55:28 +0200 Subject: [PATCH 5/7] easier calculation --- .../westnordost/streetcomplete/map/Bearing.kt | 44 ++----------------- .../map/LocationAwareMapFragment.kt | 2 +- 2 files changed, 5 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/de/westnordost/streetcomplete/map/Bearing.kt b/app/src/main/java/de/westnordost/streetcomplete/map/Bearing.kt index 8be177cba6..9235d67950 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/map/Bearing.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/map/Bearing.kt @@ -1,51 +1,15 @@ package de.westnordost.streetcomplete.map import android.location.Location -import kotlin.math.PI -import kotlin.math.atan2 -import kotlin.math.cos -import kotlin.math.sin /** Utility functions to estimate current bearing from a track. This is necessary because * Location.bearingAccuracy doesn't exist on Android versions below Android API 26, otherwise * a solution based on this would be less code. E.g. take bearing if accuracy < X */ -/** Return estimated bearing of a given track. The final result is a smoothing of the last few - * bearings in the track. The bearing of the last point is taken into account the most etc. */ -fun getTrackBearing(track: List): Double? { - val angles = getRelevantBearings(track) - if (angles.isEmpty()) return null - var x = 0.0 - var y = 0.0 - var f = 1.0 - for (angle in angles) { - x += cos(angle * PI / 180.0) * f - y += sin(angle * PI / 180.0) * f - f *= BEARING_SMOOTHING - } - return atan2(y, x) * 180.0 / PI -} - -/** Return the last X bearings from the given track, i.e. those that are not too old and not too - * close to each other */ -private fun getRelevantBearings(track: List): List { - var current = track.lastOrNull() ?: return emptyList() - val result = ArrayList() - for(i in track.lastIndex downTo 0) { - val pos = track[i] - // ignore if too close to last position - if (pos.distanceTo(current) < MIN_TRACK_DISTANCE_FOR_BEARING) continue - - result.add(pos.bearingTo(current)) - - current = pos - - // break when found enough - if (result.size > MAX_TRACK_POINTS_FOR_BEARING) break - } - return result +fun getTrackBearing(track: List): Float? { + val last = track.lastOrNull() ?: return null + val point = track.findLast { it.distanceTo(last) > MIN_TRACK_DISTANCE_FOR_BEARING } ?: return null + return point.bearingTo(last) } private const val MIN_TRACK_DISTANCE_FOR_BEARING = 15f // 15 meters -private const val MAX_TRACK_POINTS_FOR_BEARING = 6 -private const val BEARING_SMOOTHING = 0.5 // (0..1) the lower the value, the less smoothing diff --git a/app/src/main/java/de/westnordost/streetcomplete/map/LocationAwareMapFragment.kt b/app/src/main/java/de/westnordost/streetcomplete/map/LocationAwareMapFragment.kt index 316019ac62..b7aec71d5f 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/map/LocationAwareMapFragment.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/map/LocationAwareMapFragment.kt @@ -167,7 +167,7 @@ open class LocationAwareMapFragment : MapFragment() { behind user */ val distance = controller?.screenBottomToCenterDistance() if (distance != null) { - centerPosition = centerPosition.translate(distance * 0.4, bearing) + centerPosition = centerPosition.translate(distance * 0.4, bearing.toDouble()) } } tilt = PI.toFloat() / 6f From 11bf88d361c2ff8ad3bc99766c654da0b0bb8105 Mon Sep 17 00:00:00 2001 From: Tobias Zwick Date: Fri, 1 Oct 2021 23:08:08 +0200 Subject: [PATCH 6/7] use old pref key because just the name changed --- .../westnordost/streetcomplete/map/LocationAwareMapFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/de/westnordost/streetcomplete/map/LocationAwareMapFragment.kt b/app/src/main/java/de/westnordost/streetcomplete/map/LocationAwareMapFragment.kt index b7aec71d5f..d5a31284aa 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/map/LocationAwareMapFragment.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/map/LocationAwareMapFragment.kt @@ -245,7 +245,7 @@ open class LocationAwareMapFragment : MapFragment() { companion object { private const val PREF_FOLLOWING = "map_following" - private const val PREF_NAVIGATION_MODE = "map_navigation_mode" + private const val PREF_NAVIGATION_MODE = "map_compass_mode" private const val DISPLAYED_LOCATION = "displayed_location" private const val TRACKS = "tracks" From 6064c1c0cddb093819329dc9a0173196cc0b5bc8 Mon Sep 17 00:00:00 2001 From: Tobias Zwick Date: Fri, 1 Oct 2021 23:13:01 +0200 Subject: [PATCH 7/7] less alpha for tracks --- app/src/main/assets/map_theme/jawg/streetcomplete.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/assets/map_theme/jawg/streetcomplete.yaml b/app/src/main/assets/map_theme/jawg/streetcomplete.yaml index f820bb6e10..a7bfe68835 100644 --- a/app/src/main/assets/map_theme/jawg/streetcomplete.yaml +++ b/app/src/main/assets/map_theme/jawg/streetcomplete.yaml @@ -1,7 +1,7 @@ global: geometry_color: '#44D14000' # accent color + alpha - track_color: '#66536dfe' - old_track_color: '#33536dfe' + track_color: '#44536dfe' + old_track_color: '#22536dfe' textures: pins: