diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.kt b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.kt index 5b52c0ce5..26875927e 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.kt @@ -123,6 +123,7 @@ import org.osmdroid.views.CustomZoomButtonsController import org.osmdroid.views.MapView import org.osmdroid.views.overlay.MapEventsOverlay import org.osmdroid.views.overlay.Marker +import org.osmdroid.views.overlay.Overlay import org.osmdroid.views.overlay.ScaleBarOverlay import org.osmdroid.views.overlay.ScaleDiskOverlay import org.osmdroid.views.overlay.TilesOverlay @@ -267,6 +268,9 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), private var dataList: MutableList? = null private var bottomSheetAdapter: BottomSheetAdapter? = null + private var userLocationOverlay: Overlay? = null + private var userLocationErrorOverlay: Overlay? = null + private val galleryPickLauncherForResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> controller?.handleActivityResultWithCallback( @@ -724,7 +728,7 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), val targetP = GeoPoint(target.latitude, target.longitude) mapCenter = targetP binding?.map?.controller?.setCenter(targetP) - recenterMarkerToPosition(targetP) + updateUserLocationOverlays(targetP, true) if (!isCameFromExploreMap()) { moveCameraToPosition(targetP) } @@ -1863,6 +1867,8 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), lastKnownLocation = latLng NearbyController.currentLocation = lastKnownLocation presenter!!.updateMapAndList(locationChangeType) + + updateUserLocationOverlays(GeoPoint(latLng.latitude, latLng.longitude), true) } override fun onLocationChangedSignificantly(latLng: LatLng) { @@ -2644,43 +2650,14 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), */ override fun clearAllMarkers() { binding!!.map.overlayManager.clear() - binding!!.map.invalidate() - val geoPoint = mapCenter - if (geoPoint != null) { - val diskOverlay = - ScaleDiskOverlay( - this.context, - geoPoint, 2000, UnitOfMeasure.foot - ) - val circlePaint = Paint() - circlePaint.color = Color.rgb(128, 128, 128) - circlePaint.style = Paint.Style.STROKE - circlePaint.strokeWidth = 2f - diskOverlay.setCirclePaint2(circlePaint) - val diskPaint = Paint() - diskPaint.color = Color.argb(40, 128, 128, 128) - diskPaint.style = Paint.Style.FILL_AND_STROKE - diskOverlay.setCirclePaint1(diskPaint) - diskOverlay.setDisplaySizeMin(900) - diskOverlay.setDisplaySizeMax(1700) - binding!!.map.overlays.add(diskOverlay) - val startMarker = Marker( - binding!!.map - ) - startMarker.position = geoPoint - startMarker.setAnchor( - Marker.ANCHOR_CENTER, - Marker.ANCHOR_BOTTOM - ) - startMarker.icon = - getDrawable( - this.requireContext(), - fr.free.nrw.commons.R.drawable.current_location_marker - ) - startMarker.title = "Your Location" - startMarker.textLabelFontSize = 24 - binding!!.map.overlays.add(startMarker) + + var geoPoint = mapCenter + val lastLatLng = locationManager.getLastLocation() + if (lastLatLng != null) { + geoPoint = GeoPoint(lastLatLng.latitude, lastLatLng.longitude) } + updateUserLocationOverlays(geoPoint, false) + val scaleBarOverlay = ScaleBarOverlay(binding!!.map) scaleBarOverlay.setScaleBarOffset(15, 25) val barPaint = Paint() @@ -2690,6 +2667,7 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), binding!!.map.overlays.add(scaleBarOverlay) binding!!.map.overlays.add(mapEventsOverlay) binding!!.map.setMultiTouchControls(true) + binding!!.map.invalidate() } /** @@ -2700,45 +2678,149 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), private fun recenterMarkerToPosition(geoPoint: GeoPoint?) { geoPoint?.let { binding?.map?.controller?.setCenter(it) - val overlays = binding?.map?.overlays ?: return@let - // Remove markers and disks using index-based removal - var i = 0 - while (i < overlays.size) { - when (overlays[i]) { - is Marker, is ScaleDiskOverlay -> overlays.removeAt(i) - else -> i++ - } - } - - // Add disk overlay - ScaleDiskOverlay(context, it, 2000, UnitOfMeasure.foot).apply { - setCirclePaint2(Paint().apply { - color = Color.rgb(128, 128, 128) - style = Paint.Style.STROKE - strokeWidth = 2f - }) - setCirclePaint1(Paint().apply { - color = Color.argb(40, 128, 128, 128) - style = Paint.Style.FILL_AND_STROKE - }) - setDisplaySizeMin(900) - setDisplaySizeMax(1700) - overlays.add(this) - } - - // Add marker - Marker(binding?.map).apply { - position = it - setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM) - icon = getDrawable(context, R.drawable.current_location_marker) - title = "Your Location" - textLabelFontSize = 24 - overlays.add(this) - } + updateUserLocationOverlays(it, true); } } + /** + * Updates the user current location overlays (both the location and error overlays) by + * replacing any existing location overlays with new overlays at the given GeoPoint. If there + * are no existing location and error overlays, then new overlays are added. + * + * @param geoPoint The GeoPoint representing the user's current location. + * @param invalidate If true, the map overlays will be invalidated after the user + * location overlays are updated/added. If false, the overlays will not be invalidated. + */ + private fun updateUserLocationOverlays(geoPoint: GeoPoint?, invalidate: Boolean) { + geoPoint?.let{ + updateUserLocationOverlay(geoPoint) + updateUserLocationErrorOverlay(geoPoint) + } + + if (invalidate) { + binding!!.map.invalidate() + } + } + + /** + * Updates the user location error overlay by either replacing it with or adding a new one. + * + * If the user location error overlay is null, the new overlay is added. If the + * overlay is not null, it is replaced by the new overlay. + * + * @param geoPoint The GeoPoint representing the user's location + */ + private fun updateUserLocationErrorOverlay(geoPoint: GeoPoint) { + val overlays = binding?.map?.overlays ?: return + + // Multiply accuracy by 2 to get 95% confidence interval + val accuracy = getCurrentLocationAccuracy() * 2 + val overlay = createCurrentLocationErrorOverlay(this.context, geoPoint, + (accuracy).toInt(), UnitOfMeasure.meter) + + val index = overlays.indexOf(userLocationErrorOverlay) + + if (userLocationErrorOverlay == null || index == -1) { + overlays.add(overlay) + } else { + overlays[index] = overlay + } + + userLocationErrorOverlay = overlay + } + + /** + * Updates the user location overlay by either replacing it with or adding a new one. + * + * If the user location overlay is null, the new overlay is added. If the + * overlay is not null, it is replaced by the new overlay. + * + * @param geoPoint The GeoPoint representing the user's location + */ + private fun updateUserLocationOverlay(geoPoint: GeoPoint) { + val overlays = binding?.map?.overlays ?: return + + val overlay = createCurrentLocationOverlay(geoPoint) + + val index = overlays.indexOf(userLocationOverlay) + + if (userLocationOverlay == null || index == -1) { + overlays.add(overlay) + } else { + overlays[index] = overlay + } + + userLocationOverlay = overlay + } + + /** + * @return The accuracy of the current location with a confidence at the 68th percentile. + * Units are in meters. Returning 0 may indicate failure. + */ + private fun getCurrentLocationAccuracy(): Float { + var accuracy = 0f + val lastLocation = locationManager.getLastLocation() + if (lastLocation != null) { + accuracy = lastLocation.accuracy + } + + return accuracy + } + + /** + * Creates the current location overlay + * + * @param geoPoint The GeoPoint where the current location overlay will be placed. + * + * @return The current location overlay as a Marker + */ + private fun createCurrentLocationOverlay(geoPoint: GeoPoint): Marker { + val currentLocationOverlay = Marker( + binding!!.map + ) + currentLocationOverlay.position = geoPoint + currentLocationOverlay.icon = + getDrawable( + this.requireContext(), + fr.free.nrw.commons.R.drawable.current_location_marker + ) + currentLocationOverlay.title = "Your Location" + currentLocationOverlay.textLabelFontSize = 24 + currentLocationOverlay.setAnchor(0.5f, 0.5f) + + return currentLocationOverlay + } + + /** + * Creates the location error overlay to show the user how accurate the current location + * overlay is. The edge of the disk is the 95% confidence interval. + * + * @param context The Android context + * @param point The user's location as a GeoPoint + * @param value The radius of the disk + * @param unitOfMeasure The unit of measurement of the value/disk radius. + * + * @return The location error overlay as a ScaleDiskOverlay. + */ + private fun createCurrentLocationErrorOverlay(context: Context?, point: GeoPoint, value: Int, + unitOfMeasure: UnitOfMeasure): ScaleDiskOverlay { + val scaleDisk = ScaleDiskOverlay(context, point, value, unitOfMeasure) + + scaleDisk.setCirclePaint2(Paint().apply { + color = Color.rgb(128, 128, 128) + style = Paint.Style.STROKE + strokeWidth = 2f + }) + + scaleDisk.setCirclePaint1(Paint().apply { + color = Color.argb(40, 128, 128, 128) + style = Paint.Style.FILL_AND_STROKE + }) + + return scaleDisk + } + private fun moveCameraToPosition(geoPoint: GeoPoint) { binding!!.map.controller.animateTo(geoPoint) }