diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt index 20445aa..30d82d6 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt @@ -1,504 +1,671 @@ package it.reyboz.bustorino.fragments +import android.Manifest import android.annotation.SuppressLint import android.content.Context +import android.content.pm.PackageManager import android.graphics.Bitmap import android.graphics.Canvas import android.location.Location +import android.location.LocationListener +import android.location.LocationManager import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.ImageButton import android.widget.RelativeLayout import android.widget.TextView import android.widget.Toast +import androidx.activity.result.ActivityResultCallback +import androidx.activity.result.contract.ActivityResultContracts import androidx.cardview.widget.CardView +import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.gson.Gson import com.google.gson.JsonObject import it.reyboz.bustorino.R import it.reyboz.bustorino.backend.Stop +import it.reyboz.bustorino.fragments.MapFragmentKt.Companion import it.reyboz.bustorino.map.Styles +import it.reyboz.bustorino.util.Permissions import it.reyboz.bustorino.viewmodels.StopsMapViewModel import org.maplibre.android.MapLibre import org.maplibre.android.annotations.Icon import org.maplibre.android.annotations.IconFactory import org.maplibre.android.camera.CameraPosition import org.maplibre.android.camera.CameraUpdateFactory import org.maplibre.android.geometry.LatLng import org.maplibre.android.geometry.LatLngBounds import org.maplibre.android.location.LocationComponent import org.maplibre.android.location.LocationComponentActivationOptions import org.maplibre.android.location.LocationComponentOptions import org.maplibre.android.location.engine.LocationEngineRequest import org.maplibre.android.location.modes.CameraMode import org.maplibre.android.maps.MapLibreMap import org.maplibre.android.maps.MapView import org.maplibre.android.maps.OnMapReadyCallback import org.maplibre.android.maps.Style import org.maplibre.android.plugins.annotation.Symbol import org.maplibre.android.plugins.annotation.SymbolManager import org.maplibre.android.plugins.annotation.SymbolOptions import org.maplibre.android.style.layers.Property.ICON_ANCHOR_CENTER import org.maplibre.android.style.layers.PropertyFactory import org.maplibre.android.style.layers.SymbolLayer import org.maplibre.android.style.sources.GeoJsonSource import org.maplibre.geojson.Feature import org.maplibre.geojson.FeatureCollection import org.maplibre.geojson.Point +import org.osmdroid.util.GeoPoint // TODO: Rename parameter arguments, choose names that match // the fragment initialization parameters, e.g. ARG_ITEM_NUMBER private const val ARG_PARAM1 = "param1" private const val ARG_PARAM2 = "param2" /** * A simple [Fragment] subclass. * Use the [MapLibreFragment.newInstance] factory method to * create an instance of this fragment. */ class MapLibreFragment : Fragment(), OnMapReadyCallback { //private var param1: String? = null //private var param2: String? = null // Declare a variable for MapView private lateinit var mapView: MapView private lateinit var locationComponent: LocationComponent private var lastLocation: Location? = null private val stopsViewModel: StopsMapViewModel by viewModels() private val gson = Gson() private var stopsShowing = ArrayList(0) private var isBottomSheetShowing = false private lateinit var symbolManager: SymbolManager protected var map: MapLibreMap? = null // Sources for stops and buses private lateinit var stopsSource: GeoJsonSource private lateinit var busesSource: GeoJsonSource private var isStopsLayerStarted = false private var lastStopsSizeShown = 0 private var lastBBox = LatLngBounds.from(2.0, 2.0, 1.0,1.0) private lateinit var mapStyle: Style //bottom Sheet behavior private lateinit var bottomSheetBehavior: BottomSheetBehavior private var bottomLayout: RelativeLayout? = null private lateinit var stopTitleTextView: TextView private lateinit var stopNumberTextView: TextView private lateinit var linesPassingTextView: TextView private lateinit var arrivalsCard: CardView private lateinit var directionsCard: CardView private var stopActiveSymbol: Symbol? = null + // Location stuff + private lateinit var locationManager: LocationManager + private lateinit var showUserPositionButton: ImageButton + private val positionRequestLauncher = + registerForActivityResult, Map>( + ActivityResultContracts.RequestMultiplePermissions(), + ActivityResultCallback { result -> + if (result == null) { + Log.w(DEBUG_TAG, "Got asked permission but request is null, doing nothing?") + } else if (java.lang.Boolean.TRUE == result[Manifest.permission.ACCESS_COARSE_LOCATION] + && java.lang.Boolean.TRUE == result[Manifest.permission.ACCESS_FINE_LOCATION]) { + // We can use the position, restart location overlay + Log.d(DEBUG_TAG, "HAVE THE PERMISSIONS") + if (context == null || requireContext().getSystemService(Context.LOCATION_SERVICE) == null) + return@ActivityResultCallback ///@registerForActivityResult + val locationManager = + requireContext().getSystemService(Context.LOCATION_SERVICE) as LocationManager + @SuppressLint("MissingPermission") val userLocation = + locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) + if (userLocation != null) { + if(LatLng(userLocation.latitude, userLocation.longitude).distanceTo(DEFAULT_LATLNG) >= MAX_DIST_KM*1000){ + setMapLocationEnabled(true, true) + } + } else requestInitialUserLocation() + + } else{ + Toast.makeText(requireContext(),"User location disabled", Toast.LENGTH_SHORT).show() + Log.w(DEBUG_TAG, "No location permission") + } + }) + private val showUserPositionRequestLauncher = + registerForActivityResult, Map>( + ActivityResultContracts.RequestMultiplePermissions(), + ActivityResultCallback { result -> + if (result == null) { + Log.w(DEBUG_TAG, "Got asked permission but request is null, doing nothing?") + } else if (java.lang.Boolean.TRUE == result[Manifest.permission.ACCESS_COARSE_LOCATION] + && java.lang.Boolean.TRUE == result[Manifest.permission.ACCESS_FINE_LOCATION]) { + // We can use the position, restart location overlay + if (context == null || requireContext().getSystemService(Context.LOCATION_SERVICE) == null) + return@ActivityResultCallback ///@registerForActivityResult + setMapLocationEnabled(true, true) + } else Log.w(DEBUG_TAG, "No location permission") + }) + + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) /*arguments?.let { param1 = it.getString(ARG_PARAM1) param2 = it.getString(ARG_PARAM2) } */ MapLibre.getInstance(requireContext()) } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // Inflate the layout for this fragment val rootView = inflater.inflate(R.layout.fragment_map_libre, container, false) // Init layout view // Init the MapView mapView = rootView.findViewById(R.id.libreMapView) mapView.getMapAsync(this) //{ //map -> //map.setStyle("https://demotiles.maplibre.org/style.json") } //init bottom sheet val bottomSheet = rootView.findViewById(R.id.bottom_sheet) bottomLayout = bottomSheet stopTitleTextView = bottomSheet.findViewById(R.id.stopTitleTextView) stopNumberTextView = bottomSheet.findViewById(R.id.stopNumberTextView) linesPassingTextView = bottomSheet.findViewById(R.id.linesPassingTextView) arrivalsCard = bottomSheet.findViewById(R.id.arrivalsCardButton) directionsCard = bottomSheet.findViewById(R.id.directionsCardButton) + showUserPositionButton = rootView.findViewById(R.id.locationEnableIcon) + showUserPositionButton.setOnClickListener(this::switchUserLocationStatus) bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet) bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN arrivalsCard.setOnClickListener { if(context!=null){ Toast.makeText(context,"ARRIVALS", Toast.LENGTH_SHORT).show() } } + locationManager = requireActivity().getSystemService(Context.LOCATION_SERVICE) as LocationManager + if (haveLocationPermissions()) { + requestInitialUserLocation() + } else{ + if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)) { + //TODO: show dialog for permission rationale + Toast.makeText(activity, R.string.enable_position_message_map, Toast.LENGTH_SHORT) + .show() + } + positionRequestLauncher.launch(Permissions.LOCATION_PERMISSIONS) + } + + // Setup close button rootView.findViewById(R.id.btnClose).setOnClickListener { hideStopBottomSheet() } return rootView } /** * This method sets up the map */ override fun onMapReady(mapReady: MapLibreMap) { this.map = mapReady + //TODO: Check if we have the user last position and start the map there mapReady.cameraPosition = CameraPosition.Builder().target(LatLng(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON)).zoom( 15.0).build() val mjson = Styles.getJsonStyleFromAsset(requireContext(), "map_style_good_noshops.json")//ViewUtils.loadJsonFromAsset(requireContext(),"map_style_good.json") activity?.run { mapReady.setStyle(Style.Builder().fromJson(mjson!!)) { style -> mapStyle = style //setupLayers(style) symbolManager = SymbolManager(mapView,mapReady,style) symbolManager.iconAllowOverlap = true symbolManager.textAllowOverlap = true symbolManager.addClickListener{ _ -> if (stopActiveSymbol!=null){ hideStopBottomSheet() return@addClickListener true } else return@addClickListener false } // Start observing data observeViewModels() - initLocation(style, mapReady, requireContext()) + initMapLocation(style, mapReady, requireContext()) } mapReady.addOnCameraIdleListener { map?.let { val newBbox = it.projection.visibleRegion.latLngBounds if ((newBbox.center==lastBBox.center) && (newBbox.latitudeSpan==lastBBox.latitudeSpan) && (newBbox.longitudeSpan==lastBBox.latitudeSpan)){ //do nothing } else { stopsViewModel.loadStopsInLatLngBounds(newBbox) lastBBox = newBbox } } } mapReady.addOnMapClickListener { point -> val screenPoint = mapReady.projection.toScreenLocation(point) val features = mapReady.queryRenderedFeatures(screenPoint, STOPS_LAYER_ID) if (features.isNotEmpty()) { val feature = features[0] val id = feature.getStringProperty("id") val name = feature.getStringProperty("name") //Toast.makeText(requireContext(), "Clicked on $name ($id)", Toast.LENGTH_SHORT).show() val stop = stopsViewModel.getStopByID(id) stop?.let { if (isBottomSheetShowing){ hideStopBottomSheet() } showStopInBottomSheet(it) bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED isBottomSheetShowing = true //move camera if(it.latitude!=null && it.longitude!=null) //mapReady.cameraPosition = CameraPosition.Builder().target(LatLng(it.latitude!!, it.longitude!!)).build() mapReady.animateCamera(CameraUpdateFactory.newLatLng(LatLng(it.latitude!!,it.longitude!!)),750) } return@addOnMapClickListener true } false } //makeStyleMapBoxUrl(false)) } } - @SuppressLint("MissingPermission") - private fun initLocation(style: Style, map: MapLibreMap, context: Context){ - locationComponent = map.locationComponent - val locationComponentOptions = - LocationComponentOptions.builder(context) - .pulseEnabled(true) - .build() - val locationComponentActivationOptions = - buildLocationComponentActivationOptions(style, locationComponentOptions, context) - locationComponent.activateLocationComponent(locationComponentActivationOptions) - locationComponent.isLocationComponentEnabled = true - locationComponent.cameraMode = CameraMode.TRACKING //CameraMode.TRACKING - locationComponent.forceLocationUpdate(lastLocation) - } - private fun buildLocationComponentActivationOptions( - style: Style, - locationComponentOptions: LocationComponentOptions, - context: Context - ): LocationComponentActivationOptions { - return LocationComponentActivationOptions - .builder(context, style) - .locationComponentOptions(locationComponentOptions) - .useDefaultLocationEngine(true) - .locationEngineRequest( - LocationEngineRequest.Builder(750) - .setFastestInterval(750) - .setPriority(LocationEngineRequest.PRIORITY_HIGH_ACCURACY) - .build() - ) - .build() - } + private fun initStopsLayer(style: Style, features:FeatureCollection){ stopsSource = GeoJsonSource(STOPS_SOURCE_ID,features) style.addSource(stopsSource) // add icon style.addImage(STOP_IMAGE_ID, ResourcesCompat.getDrawable(resources,R.drawable.bus_stop_new, activity?.theme)!!) style.addImage(STOP_ACTIVE_IMG, ResourcesCompat.getDrawable(resources, R.drawable.bus_stop_new_highlight, activity?.theme)!!) // Stops layer val stopsLayer = SymbolLayer(STOPS_LAYER_ID, STOPS_SOURCE_ID) stopsLayer.withProperties( PropertyFactory.iconImage(STOP_IMAGE_ID), PropertyFactory.iconAllowOverlap(true), PropertyFactory.iconIgnorePlacement(true) ) style.addLayerBelow(stopsLayer, "label_country_1") isStopsLayerStarted = true } /** * Setup the Map Layers */ //private fun setupLayers(style: Style) { // Buses source // TODO when adding the buses //busesSource = GeoJsonSource(BUSES_SOURCE_ID) //style.addSource(busesSource) /* // TODO when adding the buses // Buses layer val busesLayer = SymbolLayer(BUSES_LAYER_ID, BUSES_SOURCE_ID).apply { withProperties( PropertyFactory.iconImage("bus"), PropertyFactory.iconSize(1.0f), PropertyFactory.iconAllowOverlap(true), PropertyFactory.iconRotate(Expression.get("bearing")) ) } style.addLayer(busesLayer) */ //} private fun showStopInBottomSheet(stop: Stop?){ if (stop==null) return bottomLayout?.let { //lay.findViewById(R.id.stopTitleTextView).text ="${stop.ID} - ${stop.stopDefaultName}" stopTitleTextView.text = stop.stopDefaultName stopNumberTextView.text = stop.ID val string_show = if (stop.numRoutesStopping==0) "" else if (stop.numRoutesStopping <= 1) requireContext().getString(R.string.line_fill, stop.routesThatStopHereToString()) else requireContext().getString(R.string.lines_fill, stop.routesThatStopHereToString()) linesPassingTextView.text = string_show } //add stop marker if (stop.latitude!=null && stop.longitude!=null) { /*val marker = map?.addMarker( MarkerOptions() .position(LatLng(stop.latitude!!, stop.longitude!!)) // example coords .icon( //IconFactory.getInstance(requireContext()).fromBitmap( getIconFromVectorDrawable(requireContext(), R.drawable.bus_stop_new_highlight) //R.drawable.bus_stop_new_highlight) //IconFactory.getInstance(requireContext()) //.fromResource(R.drawable.bus_stop_new_highlight) ) .title(stop.stopDefaultName) ) */ stopActiveSymbol = symbolManager.create( SymbolOptions() .withLatLng(LatLng(stop.latitude!!, stop.longitude!!)) .withIconImage(STOP_ACTIVE_IMG) .withIconAnchor(ICON_ANCHOR_CENTER) ) } } override fun onStart() { super.onStart() mapView.onStart() } override fun onResume() { super.onResume() mapView.onResume() } override fun onPause() { super.onPause() mapView.onPause() } override fun onStop() { super.onStop() mapView.onStop() } override fun onLowMemory() { super.onLowMemory() mapView.onLowMemory() } override fun onDestroy() { super.onDestroy() mapView.onDestroy() } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) mapView.onSaveInstanceState(outState) } private fun observeViewModels() { // Observe stops stopsViewModel.stopsToShow.observe(viewLifecycleOwner) { stops -> stopsShowing = ArrayList(stops) displayStops(stopsShowing) } } /** * Add the stops to the layers */ private fun displayStops(stops: List?) { if (stops.isNullOrEmpty()) return if (stops.size==lastStopsSizeShown){ Log.d(DEBUG_TAG, "Not updating, we have the same stop (can only increase!)") return } val features = ArrayList()//stops.mapNotNull { stop -> //stop.latitude?.let { lat -> // stop.longitude?.let { lon -> for (s in stops){ if (s.latitude!=null && s.longitude!=null) features.add( Feature.fromGeometry( Point.fromLngLat(s.longitude!!, s.latitude!!), JsonObject().apply { addProperty("id", s.ID) addProperty("name", s.stopDefaultName) addProperty("routes", s.routesThatStopHereToString()) // Add routes array to JSON object } ) ) } Log.d(DEBUG_TAG,"Have put ${features.size} stops to display") // if the layer is already started, substitute the stops inside, otherwise start it if (isStopsLayerStarted) { stopsSource.setGeoJson(FeatureCollection.fromFeatures(features)) lastStopsSizeShown = features.size } else map?.let { initStopsLayer(mapStyle, FeatureCollection.fromFeatures(features)) Log.d(DEBUG_TAG,"Started stops layer on map") lastStopsSizeShown = features.size } } // Hide the bottom sheet and remove extra symbol private fun hideStopBottomSheet(){ if (stopActiveSymbol!=null){ symbolManager.delete(stopActiveSymbol) stopActiveSymbol = null } bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN isBottomSheetShowing = false } + // ------ LOCATION STUFF ----- + /*private fun checkAndRequestInitialUserLocation() { + if (ContextCompat.checkSelfPermission( + requireContext(), Manifest.permission.ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED + ) { + requestInitialUserLocation() + } else { + requestPermissions(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), LOCATION_PERMISSION_REQUEST_CODE) + } + } + + */ + + @SuppressLint("MissingPermission") + private fun requestInitialUserLocation() { + val provider :String? = LocationManager.GPS_PROVIDER//getBestLocationProvider() + + provider?.let { + locationManager.requestSingleUpdate(it, object : LocationListener { + override fun onLocationChanged(location: Location) { + val userLatLng = LatLng(location.latitude, location.longitude) + val distanceToTarget = userLatLng.distanceTo(DEFAULT_LATLNG) + + if (distanceToTarget <= MAX_DIST_KM*1000.0) { + map?.let{ + //initMapLocation(mapStyle,map!!,requireContext()) + setMapLocationEnabled(true, true) + } + } else { + Toast.makeText(context, "You are too far, not showing the position", Toast.LENGTH_SHORT).show() + } + } + + override fun onProviderDisabled(provider: String) {} + override fun onProviderEnabled(provider: String) {} + + @Deprecated("Deprecated in Java") + override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {} + }, null) + } ?: run { + Toast.makeText(context, "No suitable location provider found.", Toast.LENGTH_SHORT).show() + } + } + + /** + * Initialize the map location, but do not enable the component + */ + @SuppressLint("MissingPermission") + private fun initMapLocation(style: Style, map: MapLibreMap, context: Context){ + locationComponent = map.locationComponent + val locationComponentOptions = + LocationComponentOptions.builder(context) + .pulseEnabled(true) + .build() + val locationComponentActivationOptions = + buildLocationComponentActivationOptions(style, locationComponentOptions, context) + locationComponent.activateLocationComponent(locationComponentActivationOptions) + locationComponent.isLocationComponentEnabled = false + locationComponent.cameraMode = CameraMode.TRACKING //CameraMode.TRACKING + locationComponent.forceLocationUpdate(lastLocation) + } + + private fun buildLocationComponentActivationOptions( + style: Style, + locationComponentOptions: LocationComponentOptions, + context: Context + ): LocationComponentActivationOptions { + return LocationComponentActivationOptions + .builder(context, style) + .locationComponentOptions(locationComponentOptions) + .useDefaultLocationEngine(true) + .locationEngineRequest( + LocationEngineRequest.Builder(750) + .setFastestInterval(750) + .setPriority(LocationEngineRequest.PRIORITY_HIGH_ACCURACY) + .build() + ) + .build() + } + private fun haveLocationPermissions(): Boolean{ + return !(ActivityCompat.checkSelfPermission( + requireContext(),Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && + ActivityCompat.checkSelfPermission(requireContext(),Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) + + } + + @SuppressLint("MissingPermission") + private fun setMapLocationEnabled(enabled: Boolean, assumePermissions: Boolean) { + if (enabled) { + val permissionOk = assumePermissions || haveLocationPermissions() + + if (permissionOk) { + Log.d(DEBUG_TAG, "Permission OK, starting location component, assumed: $assumePermissions") + locationComponent.isLocationComponentEnabled = true + showUserPositionButton.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.location_circlew_red)) + + } else { + if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)) { + //TODO: show dialog for permission rationale + Toast.makeText(activity, R.string.enable_position_message_map, Toast.LENGTH_SHORT).show() + } + Log.d(DEBUG_TAG, "Requesting permission to show user location") + showUserPositionRequestLauncher.launch(Permissions.LOCATION_PERMISSIONS) + } + } else{ + locationComponent.isLocationComponentEnabled = false + showUserPositionButton.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.location_circlew_grey)) + } + + } + + private fun switchUserLocationStatus(view: View?){ + if(locationComponent.isLocationComponentEnabled) setMapLocationEnabled(false, false) + else{ + Log.d(DEBUG_TAG, "Request enable location") + setMapLocationEnabled(true, false) + } + } + companion object { private const val STOPS_SOURCE_ID = "stops-source" private const val STOPS_LAYER_ID = "stops-layer" private const val STOPS_LAYER_SEL_ID ="stops-layer-selected" private const val BUSES_SOURCE_ID = "buses-source" private const val BUSES_LAYER_ID = "buses-layer" private const val STOP_IMAGE_ID ="bus-stop-icon" private const val DEFAULT_CENTER_LAT = 45.0708 private const val DEFAULT_CENTER_LON = 7.6858 + private val DEFAULT_LATLNG = LatLng(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON) private const val POSITION_FOUND_ZOOM = 16.5 private const val NO_POSITION_ZOOM = 17.1 + private const val MAX_DIST_KM = 90.0 private const val ACCESS_TOKEN="KxO8lF4U3kiO63m0c7lzqDCDrMUVg1OA2JVzRXxxmYSyjugr1xpe4W4Db5rFNvbQ" private const val MAPLIBRE_URL = "https://api.jawg.io/styles/" private const val DEBUG_TAG = "BusTO-MapLibreFrag" private const val STOP_ACTIVE_IMG = "Stop-active" + private const val LOCATION_PERMISSION_REQUEST_CODE = 981202 + /** * Use this factory method to create a new instance of * this fragment using the provided parameters. * * @param param1 Parameter 1. * @param param2 Parameter 2. * @return A new instance of fragment MapLibreFragment. */ // TODO: Rename and change types and number of parameters @JvmStatic fun newInstance(param1: String, param2: String) = MapLibreFragment().apply { arguments = Bundle().apply { putString(ARG_PARAM1, param1) putString(ARG_PARAM2, param2) } } private fun makeStyleUrl(style: String = "jawg-streets") = "${MAPLIBRE_URL+ style}.json?access-token=${ACCESS_TOKEN}" private fun makeStyleMapBoxUrl(dark: Boolean) = if(dark) "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json" else //"https://basemaps.cartocdn.com/gl/positron-gl-style/style.json" "https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json" const val OPENFREEMAP_LIBERY = "https://tiles.openfreemap.org/styles/liberty" const val OPENFREEMAP_BRIGHT = "https://tiles.openfreemap.org/styles/bright" fun getIconFromVectorDrawable(context: Context, drawableId: Int): Icon { val drawable = ContextCompat.getDrawable(context, drawableId) requireNotNull(drawable) { "Drawable not found." } drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight) val bitmap = Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, Bitmap.Config.ARGB_8888) val canvas = Canvas(bitmap) drawable.draw(canvas) return IconFactory.getInstance(context).fromBitmap(bitmap) } } } \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_map_libre.xml b/app/src/main/res/layout/fragment_map_libre.xml index a9c12f3..c51dc7e 100644 --- a/app/src/main/res/layout/fragment_map_libre.xml +++ b/app/src/main/res/layout/fragment_map_libre.xml @@ -1,141 +1,177 @@ + + + + + + +