diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java index fddea79..8173f7c 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java @@ -1,837 +1,837 @@ /* BusTO - Fragments components Copyright (C) 2020 Andrea Ugo Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.fragments; import android.Manifest; import android.animation.ObjectAnimator; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.drawable.Drawable; import android.location.Location; 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.Toast; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.content.res.ResourcesCompat; import androidx.lifecycle.ViewModelProvider; import androidx.preference.PreferenceManager; import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate; import it.reyboz.bustorino.backend.mato.MQTTMatoClient; import it.reyboz.bustorino.backend.utils; import it.reyboz.bustorino.data.gtfs.MatoPattern; import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops; import it.reyboz.bustorino.map.*; import it.reyboz.bustorino.viewmodels.LivePositionsViewModel; import it.reyboz.bustorino.viewmodels.StopsMapViewModel; import org.osmdroid.api.IGeoPoint; import org.osmdroid.api.IMapController; import org.osmdroid.config.Configuration; import org.osmdroid.events.DelayedMapListener; import org.osmdroid.events.MapListener; import org.osmdroid.events.ScrollEvent; import org.osmdroid.events.ZoomEvent; import org.osmdroid.tileprovider.tilesource.TileSourceFactory; import org.osmdroid.util.BoundingBox; import org.osmdroid.util.GeoPoint; import org.osmdroid.views.MapView; import org.osmdroid.views.overlay.FolderOverlay; import org.osmdroid.views.overlay.Marker; import org.osmdroid.views.overlay.infowindow.InfoWindow; import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider; import java.util.*; import kotlin.Pair; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.middleware.GeneralActivity; import it.reyboz.bustorino.util.Permissions; import static it.reyboz.bustorino.fragments.SettingsFragment.LIVE_POSITIONS_PREF_MQTT_VALUE; public class MapFragment extends ScreenBaseFragment { //private static final String TAG = "Busto-MapActivity"; private static final String MAP_CURRENT_ZOOM_KEY = "map-current-zoom"; private static final String MAP_CENTER_LAT_KEY = "map-center-lat"; private static final String MAP_CENTER_LON_KEY = "map-center-lon"; private static final String FOLLOWING_LOCAT_KEY ="following"; public static final String BUNDLE_LATIT = "lat"; public static final String BUNDLE_LONGIT = "lon"; public static final String BUNDLE_NAME = "name"; public static final String BUNDLE_ID = "ID"; public static final String BUNDLE_ROUTES_STOPPING = "routesStopping"; public static final String FRAGMENT_TAG="BusTOMapFragment"; private static final double DEFAULT_CENTER_LAT = 45.0708; private static final double DEFAULT_CENTER_LON = 7.6858; private static final double POSITION_FOUND_ZOOM = 18.3; public static final double NO_POSITION_ZOOM = 17.1; private static final String DEBUG_TAG=FRAGMENT_TAG; protected FragmentListenerMain listenerMain; private HashSet shownStops = null; private MapView map = null; public Context ctx; private LocationOverlay mLocationOverlay = null; private FolderOverlay stopsFolderOverlay = null; private Bundle savedMapState = null; protected ImageButton btCenterMap; protected ImageButton btFollowMe; protected CoordinatorLayout coordLayout; private boolean hasMapStartFinished = false; private boolean followingLocation = false; //the ViewModel from which we get the stop to display in the map private StopsMapViewModel stopsViewModel; //private GtfsPositionsViewModel gtfsPosViewModel; //= new ViewModelProvider(this).get(MapViewModel.class); private LivePositionsViewModel livePositionsViewModel; private Boolean useMQTTViewModel = true; private final HashMap busPositionMarkersByTrip = new HashMap<>(); private FolderOverlay busPositionsOverlay = null; private final HashMap tripMarkersAnimators = new HashMap<>(); protected final CustomInfoWindow.TouchResponder responder = new CustomInfoWindow.TouchResponder() { @Override public void onActionUp(@NonNull String stopID, @Nullable String stopName) { if (listenerMain!= null){ Log.d(DEBUG_TAG, "Asked to show arrivals for stop ID: "+stopID); listenerMain.requestArrivalsForStopID(stopID); } } }; protected final LocationOverlay.OverlayCallbacks locationCallbacks = new LocationOverlay.OverlayCallbacks() { @Override public void onDisableFollowMyLocation() { updateGUIForLocationFollowing(false); followingLocation=false; } @Override public void onEnableFollowMyLocation() { updateGUIForLocationFollowing(true); followingLocation=true; } }; private final ActivityResultLauncher positionRequestLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), result -> { if (result == null){ Log.w(DEBUG_TAG, "Got asked permission but request is null, doing nothing?"); } else if(Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_COARSE_LOCATION)) && Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_FINE_LOCATION))){ map.getOverlays().remove(mLocationOverlay); startLocationOverlay(true, map); if(getContext()==null || getContext().getSystemService(Context.LOCATION_SERVICE)==null) return; LocationManager locationManager = (LocationManager) getContext().getSystemService(Context.LOCATION_SERVICE); @SuppressLint("MissingPermission") Location userLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); if (userLocation != null) { map.getController().setZoom(POSITION_FOUND_ZOOM); GeoPoint startPoint = new GeoPoint(userLocation); setLocationFollowing(true); map.getController().setCenter(startPoint); } } else Log.w(DEBUG_TAG,"No location permission"); }); public MapFragment() { } public static MapFragment getInstance(){ return new MapFragment(); } public static MapFragment getInstance(@NonNull Stop stop){ MapFragment fragment= new MapFragment(); Bundle args = new Bundle(); args.putDouble(BUNDLE_LATIT, stop.getLatitude()); args.putDouble(BUNDLE_LONGIT, stop.getLongitude()); args.putString(BUNDLE_NAME, stop.getStopDisplayName()); args.putString(BUNDLE_ID, stop.ID); args.putString(BUNDLE_ROUTES_STOPPING, stop.routesThatStopHereToString()); fragment.setArguments(args); return fragment; } //public static MapFragment getInstance(@NonNull Stop stop){ // return getInstance(stop.getLatitude(), stop.getLongitude(), stop.getStopDisplayName(), stop.ID); //} @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { //use the same layout as the activity View root = inflater.inflate(R.layout.fragment_map, container, false); if (getContext() == null){ throw new IllegalStateException(); } ctx = getContext().getApplicationContext(); Configuration.getInstance().load(ctx, PreferenceManager.getDefaultSharedPreferences(ctx)); map = root.findViewById(R.id.map); map.setTileSource(TileSourceFactory.MAPNIK); //map.setTilesScaledToDpi(true); map.setFlingEnabled(true); // add ability to zoom with 2 fingers map.setMultiTouchControls(true); - btCenterMap = root.findViewById(R.id.icon_center_map); - btFollowMe = root.findViewById(R.id.icon_follow); + btCenterMap = root.findViewById(R.id.centerMapImageButton); + btFollowMe = root.findViewById(R.id.followUserImageButton); coordLayout = root.findViewById(R.id.coord_layout); //setup FolderOverlay stopsFolderOverlay = new FolderOverlay(); //setup Bus Markers Overlay busPositionsOverlay = new FolderOverlay(); //reset shown bus updates busPositionMarkersByTrip.clear(); tripMarkersAnimators.clear(); //set map not done hasMapStartFinished = false; String keySourcePositions=getString(R.string.pref_positions_source); useMQTTViewModel = ( PreferenceManager.getDefaultSharedPreferences(requireContext()) .getString(keySourcePositions,LIVE_POSITIONS_PREF_MQTT_VALUE).contentEquals(LIVE_POSITIONS_PREF_MQTT_VALUE)); //Start map from bundle if (savedInstanceState !=null) startMap(getArguments(), savedInstanceState); else startMap(getArguments(), savedMapState); //set listeners map.addMapListener(new DelayedMapListener(new MapListener() { @Override public boolean onScroll(ScrollEvent paramScrollEvent) { requestStopsToShow(); //Log.d(DEBUG_TAG, "Scrolling"); //if (moveTriggeredByCode) moveTriggeredByCode =false; //else setLocationFollowing(false); return true; } @Override public boolean onZoom(ZoomEvent event) { requestStopsToShow(); return true; } })); btCenterMap.setOnClickListener(v -> { //Log.i(TAG, "centerMap clicked "); if(Permissions.bothLocationPermissionsGranted(getContext())) { final GeoPoint myPosition = mLocationOverlay.getMyLocation(); map.getController().animateTo(myPosition); } else Toast.makeText(getContext(), R.string.enable_position_message_map, Toast.LENGTH_SHORT) .show(); }); btFollowMe.setOnClickListener(v -> { //Log.i(TAG, "btFollowMe clicked "); if(Permissions.bothLocationPermissionsGranted(getContext())) setLocationFollowing(!followingLocation); else Toast.makeText(getContext(), R.string.enable_position_message_map, Toast.LENGTH_SHORT) .show(); }); return root; } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); ViewModelProvider provider = new ViewModelProvider(this); //gtfsPosViewModel = provider.get(GtfsPositionsViewModel.class); livePositionsViewModel = provider.get(LivePositionsViewModel.class); stopsViewModel = provider.get(StopsMapViewModel.class); if (context instanceof FragmentListenerMain) { listenerMain = (FragmentListenerMain) context; } else { throw new RuntimeException(context.toString() + " must implement FragmentListenerMain"); } } @Override public void onDetach() { super.onDetach(); listenerMain = null; //stop animations // setupOnAttached = true; Log.w(DEBUG_TAG, "Fragment detached"); } @Override public void onPause() { super.onPause(); Log.w(DEBUG_TAG, "On pause called mapfrag"); saveMapState(); for (ObjectAnimator animator : tripMarkersAnimators.values()) { if(animator!=null && animator.isRunning()){ animator.cancel(); } } tripMarkersAnimators.clear(); if(useMQTTViewModel) livePositionsViewModel.stopMatoUpdates(); } /** * Save the map state inside the fragment * (calls saveMapState(bundle)) */ private void saveMapState(){ savedMapState = new Bundle(); saveMapState(savedMapState); } /** * Save the state of the map to restore it to a later time * @param bundle the bundle in which to save the data */ private void saveMapState(Bundle bundle){ Log.d(DEBUG_TAG, "Saving state, location following: "+followingLocation); bundle.putBoolean(FOLLOWING_LOCAT_KEY, followingLocation); if (map == null){ //The map is null, it can happen? Log.e(DEBUG_TAG, "Cannot save map center, map is null"); return; } final IGeoPoint loc = map.getMapCenter(); bundle.putDouble(MAP_CENTER_LAT_KEY, loc.getLatitude()); bundle.putDouble(MAP_CENTER_LON_KEY, loc.getLongitude()); bundle.putDouble(MAP_CURRENT_ZOOM_KEY, map.getZoomLevelDouble()); } @Override public void onResume() { super.onResume(); //TODO: cleanup duplicate code (maybe merging the positions classes?) if(listenerMain!=null) listenerMain.readyGUIfor(FragmentKind.MAP); /// choose which to use String keySourcePositions=getString(R.string.pref_positions_source); useMQTTViewModel = PreferenceManager.getDefaultSharedPreferences(requireContext()) .getString(keySourcePositions,LIVE_POSITIONS_PREF_MQTT_VALUE).contentEquals( LIVE_POSITIONS_PREF_MQTT_VALUE); if(livePositionsViewModel !=null) { //gtfsPosViewModel.requestUpdates(); if(useMQTTViewModel) livePositionsViewModel.requestMatoPosUpdates(MQTTMatoClient.LINES_ALL); else livePositionsViewModel.requestGTFSUpdates(); //mapViewModel.testCascade(); livePositionsViewModel.isLastWorkResultGood().observe(this, d -> Log.d(DEBUG_TAG, "Last trip download result is "+d)); livePositionsViewModel.getTripsGtfsIDsToQuery().observe(this, dat -> { Log.i(DEBUG_TAG, "Have these trips IDs missing from the DB, to be queried: "+dat); livePositionsViewModel.downloadTripsFromMato(dat); /*MatoTripsDownloadWorker.Companion.requestMatoTripsDownload(dat, requireContext().getApplicationContext(), "BusTO-MatoTripDownload"); */ }); } /*else if(gtfsPosViewModel!=null){ gtfsPosViewModel.requestUpdates(); gtfsPosViewModel.getTripsGtfsIDsToQuery().observe(this, dat -> { Log.i(DEBUG_TAG, "Have these trips IDs missing from the DB, to be queried: "+dat); //gtfsPosViewModel.downloadTripsFromMato(dat); MatoTripsDownloadWorker.Companion.downloadTripsFromMato(dat,getContext().getApplicationContext(), "BusTO-MatoTripDownload"); }); } */ else Log.e(DEBUG_TAG, "livePositionsViewModel is null at onResume"); //rerequest stop stopsViewModel.requestStopsInBoundingBox(map.getBoundingBox()); } private void startRequestsPositions(){ if (livePositionsViewModel != null) { //should always be the case livePositionsViewModel.getUpdatesWithTripAndPatterns().observe(getViewLifecycleOwner(), data -> { Log.d(DEBUG_TAG, "Have " + data.size() + " trip updates, has Map start finished: " + hasMapStartFinished); if (hasMapStartFinished) updateBusPositionsInMap(data); if(!isDetached() && !useMQTTViewModel) livePositionsViewModel.requestDelayedGTFSUpdates(3000); }); } else { Log.e(DEBUG_TAG, "PositionsViewModel is null"); } } @Override public void onSaveInstanceState(@NonNull Bundle outState) { saveMapState(outState); super.onSaveInstanceState(outState); } //own methods /** * Switch following the location on and off * @param value true if we want to follow location */ public void setLocationFollowing(Boolean value){ followingLocation = value; if(mLocationOverlay==null || getContext() == null || map ==null) //nothing else to do return; if (value){ mLocationOverlay.enableFollowLocation(); } else { mLocationOverlay.disableFollowLocation(); } } /** * Do all the stuff you need to do on the gui, when parameter is changed to value * @param following value */ protected void updateGUIForLocationFollowing(boolean following){ if (following) btFollowMe.setImageResource(R.drawable.ic_follow_me_on); else btFollowMe.setImageResource(R.drawable.ic_follow_me); } /** * Build the location overlay. Enable only when * a) we know we have the permission * b) the location map is set */ private void startLocationOverlay(boolean enableLocation, MapView map){ if(getActivity()== null) throw new IllegalStateException("Cannot enable LocationOverlay now"); // Location Overlay // from OpenBikeSharing (THANK GOD) Log.d(DEBUG_TAG, "Starting position overlay"); GpsMyLocationProvider imlp = new GpsMyLocationProvider(getActivity().getBaseContext()); imlp.setLocationUpdateMinDistance(5); imlp.setLocationUpdateMinTime(2000); final LocationOverlay overlay = new LocationOverlay(imlp,map, locationCallbacks); if (enableLocation) overlay.enableMyLocation(); overlay.setOptionsMenuEnabled(true); //map.getOverlays().add(this.mLocationOverlay); this.mLocationOverlay = overlay; map.getOverlays().add(mLocationOverlay); } public void startMap(Bundle incoming, Bundle savedInstanceState) { //Check that we're attached GeneralActivity activity = getActivity() instanceof GeneralActivity ? (GeneralActivity) getActivity() : null; if(getContext()==null|| activity==null){ //we are not attached Log.e(DEBUG_TAG, "Calling startMap when not attached"); return; }else{ Log.d(DEBUG_TAG, "Starting map from scratch"); } //clear previous overlays map.getOverlays().clear(); //parse incoming bundle GeoPoint marker = null; String name = null; String ID = null; String routesStopping = ""; if (incoming != null) { double lat = incoming.getDouble(BUNDLE_LATIT); double lon = incoming.getDouble(BUNDLE_LONGIT); marker = new GeoPoint(lat, lon); name = incoming.getString(BUNDLE_NAME); ID = incoming.getString(BUNDLE_ID); routesStopping = incoming.getString(BUNDLE_ROUTES_STOPPING, ""); } //ask for location permission if(!Permissions.bothLocationPermissionsGranted(activity)){ 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); } shownStops = new HashSet<>(); // move the map on the marker position or on a default view point: Turin, Piazza Castello // and set the start zoom IMapController mapController = map.getController(); GeoPoint startPoint = null; startLocationOverlay(Permissions.bothLocationPermissionsGranted(activity), map); // set the center point if (marker != null) { //startPoint = marker; mapController.setZoom(POSITION_FOUND_ZOOM); setLocationFollowing(false); // put the center a little bit off (animate later) startPoint = new GeoPoint(marker); startPoint.setLatitude(marker.getLatitude()+ utils.angleRawDifferenceFromMeters(20)); startPoint.setLongitude(marker.getLongitude()-utils.angleRawDifferenceFromMeters(20)); //don't need to do all the rest since we want to show a point } else if (savedInstanceState != null && savedInstanceState.containsKey(MAP_CURRENT_ZOOM_KEY)) { mapController.setZoom(savedInstanceState.getDouble(MAP_CURRENT_ZOOM_KEY)); mapController.setCenter(new GeoPoint(savedInstanceState.getDouble(MAP_CENTER_LAT_KEY), savedInstanceState.getDouble(MAP_CENTER_LON_KEY))); Log.d(DEBUG_TAG, "Location following from savedInstanceState: "+savedInstanceState.getBoolean(FOLLOWING_LOCAT_KEY)); setLocationFollowing(savedInstanceState.getBoolean(FOLLOWING_LOCAT_KEY)); } else { Log.d(DEBUG_TAG, "No position found from intent or saved state"); boolean found = false; LocationManager locationManager = (LocationManager) getContext().getSystemService(Context.LOCATION_SERVICE); //check for permission if (locationManager != null && Permissions.bothLocationPermissionsGranted(activity)) { @SuppressLint("MissingPermission") Location userLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); if (userLocation != null) { double distan = utils.measuredistanceBetween(userLocation.getLatitude(), userLocation.getLongitude(), DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON); if (distan < 100_000.0) { mapController.setZoom(POSITION_FOUND_ZOOM); startPoint = new GeoPoint(userLocation); found = true; setLocationFollowing(true); } } } if(!found){ startPoint = new GeoPoint(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON); mapController.setZoom(NO_POSITION_ZOOM); setLocationFollowing(false); } } // set the minimum zoom level map.setMinZoomLevel(15.0); //add contingency check (shouldn't happen..., but) if (startPoint != null) { mapController.setCenter(startPoint); } //add stops overlay //map.getOverlays().add(mLocationOverlay); map.getOverlays().add(this.stopsFolderOverlay); Log.d(DEBUG_TAG, "Requesting stops load"); // This is not necessary, by setting the center we already move // the map and we trigger a stop request //requestStopsToShow(); if (marker != null) { // make a marker with the info window open for the searched marker //TODO: make Stop Bundle-able Marker stopMarker = makeMarker(marker, ID , name, routesStopping,true); map.getController().animateTo(marker); } //add the overlays with the bus stops if(busPositionsOverlay == null){ //Log.i(DEBUG_TAG, "Null bus positions overlay,redo"); busPositionsOverlay = new FolderOverlay(); } startRequestsPositions(); if(stopsViewModel !=null){ stopsViewModel.getStopsInBoundingBox().observe(getViewLifecycleOwner(), this::showStopsMarkers ); } else Log.d(DEBUG_TAG, "Cannot observe new stops in map, stopsViewModel is null"); map.getOverlays().add(this.busPositionsOverlay); //set map as started hasMapStartFinished = true; } /** * Start a request to load the stops that are in the current view * from the database */ private void requestStopsToShow(){ // get the top, bottom, left and right screen's coordinate BoundingBox bb = map.getBoundingBox(); Log.d(DEBUG_TAG, "Requesting stops in bounding box, stopViewModel is null "+(stopsViewModel==null)); if(stopsViewModel!=null){ stopsViewModel.requestStopsInBoundingBox(bb); } /*double latFrom = bb.getLatSouth(); double latTo = bb.getLatNorth(); double lngFrom = bb.getLonWest(); double lngTo = bb.getLonEast(); if (stopFetcher!= null && stopFetcher.getStatus()!= AsyncTask.Status.FINISHED) stopFetcher.cancel(true); stopFetcher = new AsyncStopFetcher(this); stopFetcher.execute( new AsyncStopFetcher.BoundingBoxLimit(lngFrom,lngTo,latFrom, latTo)); */ } private void updateBusMarker(final Marker marker, final LivePositionUpdate posUpdate, @Nullable boolean justCreated){ GeoPoint position; final String updateID = posUpdate.getTripID(); if(!justCreated){ position = marker.getPosition(); if(posUpdate.getLatitude()!=position.getLatitude() || posUpdate.getLongitude()!=position.getLongitude()){ GeoPoint newpos = new GeoPoint(posUpdate.getLatitude(), posUpdate.getLongitude()); ObjectAnimator valueAnimator = MarkerUtils.makeMarkerAnimator( map, marker, newpos, MarkerUtils.LINEAR_ANIMATION, 1200); valueAnimator.setAutoCancel(true); tripMarkersAnimators.put(updateID,valueAnimator); valueAnimator.start(); } //marker.setPosition(new GeoPoint(posUpdate.getLatitude(), posUpdate.getLongitude())); } else { position = new GeoPoint(posUpdate.getLatitude(), posUpdate.getLongitude()); marker.setPosition(position); } if(posUpdate.getBearing()!=null) marker.setRotation(posUpdate.getBearing()*(-1.f)); } private void updateBusPositionsInMap(HashMap> tripsPatterns){ Log.d(DEBUG_TAG, "Updating positions of the buses"); //if(busPositionsOverlay == null) busPositionsOverlay = new FolderOverlay(); final ArrayList noPatternsTrips = new ArrayList<>(); for(String tripID: tripsPatterns.keySet()) { final Pair pair = tripsPatterns.get(tripID); if (pair == null) continue; final LivePositionUpdate update = pair.getFirst(); final TripAndPatternWithStops tripWithPatternStops = pair.getSecond(); //check if Marker is already created if (busPositionMarkersByTrip.containsKey(tripID)){ //need to change the position of the marker final Marker marker = busPositionMarkersByTrip.get(tripID); assert marker!=null; updateBusMarker(marker, update, false); if(marker.getInfoWindow()!=null && marker.getInfoWindow() instanceof BusInfoWindow){ BusInfoWindow window = (BusInfoWindow) marker.getInfoWindow(); if(tripWithPatternStops != null) { //Log.d(DEBUG_TAG, "Update pattern for trip: "+tripID); window.setPatternAndDraw(tripWithPatternStops.getPattern()); } } } else{ //marker is not there, need to make it if(map==null) Log.e(DEBUG_TAG, "Creating marker with null map, things will explode"); final Marker marker = new Marker(map); /*final Drawable mDrawable = DrawableUtils.Companion.getScaledDrawableResources( getResources(), R.drawable.point_heading_icon, R.dimen.map_icons_size, R.dimen.map_icons_size); */ //String route = GtfsUtils.getLineNameFromGtfsID(update.getRouteID()); final Drawable mdraw = ResourcesCompat.getDrawable(getResources(),R.drawable.map_bus_position_icon, null); /*final Drawable mdraw = DrawableUtils.Companion.writeOnDrawable(getResources(), R.drawable.point_heading_icon, R.color.white, route,12); */ assert mdraw != null; //mdraw.setBounds(0,0,28,28); marker.setIcon(mdraw); if(tripWithPatternStops == null){ noPatternsTrips.add(tripID); } MatoPattern markerPattern = null; if(tripWithPatternStops != null && tripWithPatternStops.getPattern()!=null) markerPattern = tripWithPatternStops.getPattern(); marker.setInfoWindow(new BusInfoWindow(map, update, markerPattern , false, (pattern) -> { })); marker.setInfoWindowAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER); marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER); updateBusMarker(marker, update, true); // the overlay is null when it's not attached yet?5 // cannot recreate it because it becomes null very soon // if(busPositionsOverlay == null) busPositionsOverlay = new FolderOverlay(); //save the marker if(busPositionsOverlay!=null) { busPositionsOverlay.add(marker); busPositionMarkersByTrip.put(tripID, marker); } } } if(noPatternsTrips.size()>0){ Log.i(DEBUG_TAG, "These trips have no matching pattern: "+noPatternsTrips); } } /** * Add stops as Markers on the map * @param stops the list of stops that must be included */ protected void showStopsMarkers(List stops){ if (getContext() == null || stops == null){ //we are not attached return; } boolean good = true; for (Stop stop : stops) { if (shownStops.contains(stop.ID)){ continue; } if(stop.getLongitude()==null || stop.getLatitude()==null) continue; shownStops.add(stop.ID); if(!map.isShown()){ if(good) Log.d(DEBUG_TAG, "Need to show stop but map is not shown, probably detached already"); good = false; continue; } else if(map.getRepository() == null){ Log.e(DEBUG_TAG, "Map view repository is null"); } GeoPoint marker = new GeoPoint(stop.getLatitude(), stop.getLongitude()); Marker stopMarker = makeMarker(marker, stop, false); stopsFolderOverlay.add(stopMarker); if (!map.getOverlays().contains(stopsFolderOverlay)) { Log.w(DEBUG_TAG, "Map doesn't have folder overlay"); } good=true; } //Log.d(DEBUG_TAG,"We have " +stopsFolderOverlay.getItems().size()+" stops in the folderOverlay"); //force redraw of markers map.invalidate(); } public Marker makeMarker(GeoPoint geoPoint, Stop stop, boolean isStartMarker){ return makeMarker(geoPoint,stop.ID, stop.getStopDefaultName(), stop.routesThatStopHereToString(), isStartMarker); } public Marker makeMarker(GeoPoint geoPoint, String stopID, String stopName, String routesStopping, boolean isStartMarker) { // add a marker final Marker marker = new Marker(map); // set custom info window as info window CustomInfoWindow popup = new CustomInfoWindow(map, stopID, stopName, routesStopping, responder, R.layout.linedetail_stop_infowindow, R.color.red_darker); marker.setInfoWindow(popup); // make the marker clickable marker.setOnMarkerClickListener((thisMarker, mapView) -> { if (thisMarker.isInfoWindowOpen()) { // on second click Log.w(DEBUG_TAG, "Pressed on the click marker"); } else { // on first click // hide all opened info window InfoWindow.closeAllInfoWindowsOn(map); // show this particular info window thisMarker.showInfoWindow(); // move the map to its position map.getController().animateTo(thisMarker.getPosition()); } return true; }); // set its position marker.setPosition(geoPoint); marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER); // add to it an icon //marker.setIcon(getResources().getDrawable(R.drawable.bus_marker)); marker.setIcon(ResourcesCompat.getDrawable(getResources(), R.drawable.bus_stop, ctx.getTheme())); // add to it a title marker.setTitle(stopName); // set the description as the ID marker.setSnippet(stopID); // show popup info window of the searched marker if (isStartMarker) { marker.showInfoWindow(); //map.getController().animateTo(marker.getPosition()); } return marker; } @Nullable @Override public View getBaseViewForSnackBar() { return coordLayout; } } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/MapFragmentKt.kt b/app/src/main/java/it/reyboz/bustorino/fragments/MapFragmentKt.kt index 2a319a4..cdf88f1 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/MapFragmentKt.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/MapFragmentKt.kt @@ -1,768 +1,767 @@ /* BusTO - Fragments components Copyright (C) 2020 Andrea Ugo Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.fragments import android.Manifest import android.animation.ObjectAnimator import android.annotation.SuppressLint import android.content.Context 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.Toast import androidx.activity.result.ActivityResultCallback import androidx.activity.result.contract.ActivityResultContracts import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.content.res.ResourcesCompat import androidx.fragment.app.viewModels -import androidx.lifecycle.ViewModelProvider import androidx.preference.PreferenceManager import it.reyboz.bustorino.R import it.reyboz.bustorino.backend.Stop import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate import it.reyboz.bustorino.backend.mato.MQTTMatoClient import it.reyboz.bustorino.backend.utils import it.reyboz.bustorino.data.gtfs.MatoPattern import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops import it.reyboz.bustorino.map.BusInfoWindow import it.reyboz.bustorino.map.CustomInfoWindow import it.reyboz.bustorino.map.CustomInfoWindow.TouchResponder import it.reyboz.bustorino.map.LocationOverlay import it.reyboz.bustorino.map.LocationOverlay.OverlayCallbacks import it.reyboz.bustorino.map.MarkerUtils import it.reyboz.bustorino.middleware.GeneralActivity import it.reyboz.bustorino.util.Permissions import it.reyboz.bustorino.viewmodels.LivePositionsViewModel import it.reyboz.bustorino.viewmodels.StopsMapViewModel import org.osmdroid.config.Configuration import org.osmdroid.events.DelayedMapListener import org.osmdroid.events.MapListener import org.osmdroid.events.ScrollEvent import org.osmdroid.events.ZoomEvent import org.osmdroid.tileprovider.tilesource.TileSourceFactory import org.osmdroid.util.GeoPoint import org.osmdroid.views.MapView import org.osmdroid.views.overlay.FolderOverlay import org.osmdroid.views.overlay.Marker import org.osmdroid.views.overlay.infowindow.InfoWindow import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider open class MapFragmentKt : ScreenBaseFragment() { protected var listenerMain: FragmentListenerMain? = null private var shownStops: HashSet? = null private lateinit var map: MapView var ctx: Context? = null private lateinit var mLocationOverlay: LocationOverlay private lateinit var stopsFolderOverlay: FolderOverlay private var savedMapState: Bundle? = null protected lateinit var btCenterMap: ImageButton protected lateinit var btFollowMe: ImageButton protected var coordLayout: CoordinatorLayout? = null private var hasMapStartFinished = false private var followingLocation = false //the ViewModel from which we get the stop to display in the map private val stopsViewModel: StopsMapViewModel by viewModels() //private GtfsPositionsViewModel gtfsPosViewModel; //= new ViewModelProvider(this).get(MapViewModel.class); private val livePositionsViewModel: LivePositionsViewModel by viewModels() private var useMQTTViewModel = true private val busPositionMarkersByTrip = HashMap() private var busPositionsOverlay: FolderOverlay? = null private val tripMarkersAnimators = HashMap() protected val responder = TouchResponder { stopID, stopName -> if (listenerMain != null) { Log.d(DEBUG_TAG, "Asked to show arrivals for stop ID: $stopID") listenerMain!!.requestArrivalsForStopID(stopID) } } protected val locationCallbacks: OverlayCallbacks = object : OverlayCallbacks { override fun onDisableFollowMyLocation() { updateGUIForLocationFollowing(false) followingLocation = false } override fun onEnableFollowMyLocation() { updateGUIForLocationFollowing(true) followingLocation = true } } 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 map.overlays.remove(mLocationOverlay) startLocationOverlay(true, map) 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) { map!!.controller.setZoom(POSITION_FOUND_ZOOM) val startPoint = GeoPoint(userLocation) setLocationFollowing(true) map!!.controller.setCenter(startPoint) } } else Log.w(DEBUG_TAG, "No location permission") }) //public static MapFragment getInstance(@NonNull Stop stop){ // return getInstance(stop.getLatitude(), stop.getLongitude(), stop.getStopDisplayName(), stop.ID); //} override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { //use the same layout as the activity val root = inflater.inflate(R.layout.fragment_map, container, false) val context = requireContext() ctx = context.applicationContext Configuration.getInstance().load(ctx, PreferenceManager.getDefaultSharedPreferences(context)) map = root.findViewById(R.id.map) map.setTileSource(TileSourceFactory.MAPNIK) //map.setTilesScaledToDpi(true); map.setFlingEnabled(true) // add ability to zoom with 2 fingers map.setMultiTouchControls(true) - btCenterMap = root.findViewById(R.id.icon_center_map) - btFollowMe = root.findViewById(R.id.icon_follow) + btCenterMap = root.findViewById(R.id.centerMapImageButton) + btFollowMe = root.findViewById(R.id.followUserImageButton) coordLayout = root.findViewById(R.id.coord_layout) //setup FolderOverlay stopsFolderOverlay = FolderOverlay() //setup Bus Markers Overlay busPositionsOverlay = FolderOverlay() //reset shown bus updates busPositionMarkersByTrip.clear() tripMarkersAnimators.clear() //set map not done hasMapStartFinished = false val keySourcePositions = getString(R.string.pref_positions_source) useMQTTViewModel = PreferenceManager.getDefaultSharedPreferences(requireContext()) .getString(keySourcePositions, SettingsFragment.LIVE_POSITIONS_PREF_MQTT_VALUE) .contentEquals(SettingsFragment.LIVE_POSITIONS_PREF_MQTT_VALUE) //Start map from bundle if (savedInstanceState != null) startMap(arguments, savedInstanceState) else startMap( arguments, savedMapState ) //set listeners map.addMapListener(DelayedMapListener(object : MapListener { override fun onScroll(paramScrollEvent: ScrollEvent): Boolean { requestStopsToShow() //Log.d(DEBUG_TAG, "Scrolling"); //if (moveTriggeredByCode) moveTriggeredByCode =false; //else setLocationFollowing(false); return true } override fun onZoom(event: ZoomEvent): Boolean { requestStopsToShow() return true } })) btCenterMap.setOnClickListener(View.OnClickListener { v: View? -> //Log.i(TAG, "centerMap clicked "); if (Permissions.bothLocationPermissionsGranted(context)) { val myPosition = mLocationOverlay!!.myLocation map.getController().animateTo(myPosition) } else Toast.makeText(context, R.string.enable_position_message_map, Toast.LENGTH_SHORT) .show() }) btFollowMe.setOnClickListener(View.OnClickListener { v: View? -> //Log.i(TAG, "btFollowMe clicked "); if (Permissions.bothLocationPermissionsGranted(context)) setLocationFollowing(!followingLocation) else Toast.makeText( context, R.string.enable_position_message_map, Toast.LENGTH_SHORT ) .show() }) return root } override fun onAttach(context: Context) { super.onAttach(context) listenerMain = if (context is FragmentListenerMain) { context } else { throw RuntimeException( context.toString() + " must implement FragmentListenerMain" ) } } override fun onDetach() { super.onDetach() listenerMain = null Log.w(DEBUG_TAG, "Fragment detached") } override fun onPause() { super.onPause() Log.w(DEBUG_TAG, "On pause called mapfrag") saveMapState() for (animator in tripMarkersAnimators.values) { if (animator != null && animator.isRunning) { animator.cancel() } } tripMarkersAnimators.clear() if (useMQTTViewModel) livePositionsViewModel!!.stopMatoUpdates() } /** * Save the map state inside the fragment * (calls saveMapState(bundle)) */ private fun saveMapState() { savedMapState = Bundle() saveMapState(savedMapState!!) } /** * Save the state of the map to restore it to a later time * @param bundle the bundle in which to save the data */ private fun saveMapState(bundle: Bundle) { Log.d(DEBUG_TAG, "Saving state, location following: $followingLocation") bundle.putBoolean(FOLLOWING_LOCAT_KEY, followingLocation) if (map == null) { //The map is null, it can happen? Log.e(DEBUG_TAG, "Cannot save map center, map is null") return } val loc = map!!.mapCenter bundle.putDouble(MAP_CENTER_LAT_KEY, loc.latitude) bundle.putDouble(MAP_CENTER_LON_KEY, loc.longitude) bundle.putDouble(MAP_CURRENT_ZOOM_KEY, map!!.zoomLevelDouble) } override fun onResume() { super.onResume() //TODO: cleanup duplicate code (maybe merging the positions classes?) if (listenerMain != null) listenerMain!!.readyGUIfor(FragmentKind.MAP) /// choose which to use val keySourcePositions = getString(R.string.pref_positions_source) useMQTTViewModel = PreferenceManager.getDefaultSharedPreferences(requireContext()) .getString(keySourcePositions, SettingsFragment.LIVE_POSITIONS_PREF_MQTT_VALUE) .contentEquals( SettingsFragment.LIVE_POSITIONS_PREF_MQTT_VALUE ) if (livePositionsViewModel != null) { //gtfsPosViewModel.requestUpdates(); if (useMQTTViewModel) livePositionsViewModel!!.requestMatoPosUpdates(MQTTMatoClient.LINES_ALL) else livePositionsViewModel!!.requestGTFSUpdates() //mapViewModel.testCascade(); livePositionsViewModel!!.isLastWorkResultGood.observe(this) { d: Boolean -> Log.d( DEBUG_TAG, "Last trip download result is $d" ) } livePositionsViewModel!!.tripsGtfsIDsToQuery.observe(this) { dat: List -> Log.i(DEBUG_TAG, "Have these trips IDs missing from the DB, to be queried: $dat") livePositionsViewModel!!.downloadTripsFromMato(dat) } } /*else if(gtfsPosViewModel!=null){ gtfsPosViewModel.requestUpdates(); gtfsPosViewModel.getTripsGtfsIDsToQuery().observe(this, dat -> { Log.i(DEBUG_TAG, "Have these trips IDs missing from the DB, to be queried: "+dat); //gtfsPosViewModel.downloadTripsFromMato(dat); MatoTripsDownloadWorker.Companion.downloadTripsFromMato(dat,getContext().getApplicationContext(), "BusTO-MatoTripDownload"); }); } */ else Log.e(DEBUG_TAG, "livePositionsViewModel is null at onResume") //rerequest stop stopsViewModel!!.requestStopsInBoundingBox(map!!.boundingBox) } private fun startRequestsPositions() { if (livePositionsViewModel != null) { //should always be the case livePositionsViewModel!!.updatesWithTripAndPatterns.observe(viewLifecycleOwner) { data: HashMap> -> Log.d( DEBUG_TAG, "Have " + data.size + " trip updates, has Map start finished: " + hasMapStartFinished ) if (hasMapStartFinished) updateBusPositionsInMap(data) if (!isDetached && !useMQTTViewModel) livePositionsViewModel!!.requestDelayedGTFSUpdates( 3000 ) } } else { Log.e(DEBUG_TAG, "PositionsViewModel is null") } } override fun onSaveInstanceState(outState: Bundle) { saveMapState(outState) super.onSaveInstanceState(outState) } //own methods /** * Switch following the location on and off * @param value true if we want to follow location */ fun setLocationFollowing(value: Boolean) { followingLocation = value if (mLocationOverlay == null || context == null || map == null) //nothing else to do return if (value) { mLocationOverlay!!.enableFollowLocation() } else { mLocationOverlay!!.disableFollowLocation() } } /** * Do all the stuff you need to do on the gui, when parameter is changed to value * @param following value */ protected fun updateGUIForLocationFollowing(following: Boolean) { if (following) btFollowMe!!.setImageResource(R.drawable.ic_follow_me_on) else btFollowMe!!.setImageResource( R.drawable.ic_follow_me ) } /** * Build the location overlay. Enable only when * a) we know we have the permission * b) the location map is set */ private fun startLocationOverlay(enableLocation: Boolean, map: MapView?) { checkNotNull(activity) { "Cannot enable LocationOverlay now" } // Location Overlay // from OpenBikeSharing (THANK GOD) Log.d(DEBUG_TAG, "Starting position overlay") val imlp = GpsMyLocationProvider(requireActivity().baseContext) imlp.locationUpdateMinDistance = 5f imlp.locationUpdateMinTime = 2000 val overlay = LocationOverlay(imlp, map, locationCallbacks) if (enableLocation) overlay.enableMyLocation() overlay.isOptionsMenuEnabled = true //map.getOverlays().add(this.mLocationOverlay); mLocationOverlay = overlay map!!.overlays.add(mLocationOverlay) } fun startMap(incoming: Bundle?, savedInstanceState: Bundle?) { //Check that we're attached val activity = if (activity is GeneralActivity) activity as GeneralActivity? else null if (context == null || activity == null) { //we are not attached Log.e(DEBUG_TAG, "Calling startMap when not attached") return } else { Log.d(DEBUG_TAG, "Starting map from scratch") } //clear previous overlays map!!.overlays.clear() //parse incoming bundle var marker: GeoPoint? = null var name: String? = null var ID: String? = null var routesStopping: String? = "" if (incoming != null) { val lat = incoming.getDouble(BUNDLE_LATIT) val lon = incoming.getDouble(BUNDLE_LONGIT) marker = GeoPoint(lat, lon) name = incoming.getString(BUNDLE_NAME) ID = incoming.getString(BUNDLE_ID) routesStopping = incoming.getString(BUNDLE_ROUTES_STOPPING, "") } //ask for location permission if (!Permissions.bothLocationPermissionsGranted(activity)) { 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) } shownStops = HashSet() // move the map on the marker position or on a default view point: Turin, Piazza Castello // and set the start zoom val mapController = map!!.controller var startPoint: GeoPoint? = null startLocationOverlay( Permissions.bothLocationPermissionsGranted(activity), map ) // set the center point if (marker != null) { //startPoint = marker; mapController.setZoom(POSITION_FOUND_ZOOM) setLocationFollowing(false) // put the center a little bit off (animate later) startPoint = GeoPoint(marker) startPoint.latitude = marker.latitude + utils.angleRawDifferenceFromMeters(20.0) startPoint.longitude = marker.longitude - utils.angleRawDifferenceFromMeters(20.0) //don't need to do all the rest since we want to show a point } else if (savedInstanceState != null && savedInstanceState.containsKey(MAP_CURRENT_ZOOM_KEY)) { mapController.setZoom(savedInstanceState.getDouble(MAP_CURRENT_ZOOM_KEY)) mapController.setCenter( GeoPoint( savedInstanceState.getDouble(MAP_CENTER_LAT_KEY), savedInstanceState.getDouble(MAP_CENTER_LON_KEY) ) ) Log.d( DEBUG_TAG, "Location following from savedInstanceState: " + savedInstanceState.getBoolean( FOLLOWING_LOCAT_KEY ) ) setLocationFollowing(savedInstanceState.getBoolean(FOLLOWING_LOCAT_KEY)) } else { Log.d(DEBUG_TAG, "No position found from intent or saved state") var found = false val locationManager = requireContext().getSystemService(Context.LOCATION_SERVICE) as LocationManager //check for permission if (Permissions.bothLocationPermissionsGranted(activity)) { @SuppressLint("MissingPermission") val userLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) if (userLocation != null) { val distan = utils.measuredistanceBetween( userLocation.latitude, userLocation.longitude, DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON ) if (distan < 100000.0) { mapController.setZoom(POSITION_FOUND_ZOOM) startPoint = GeoPoint(userLocation) found = true setLocationFollowing(true) } } } if (!found) { startPoint = GeoPoint(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON) mapController.setZoom(NO_POSITION_ZOOM) setLocationFollowing(false) } } // set the minimum zoom level map!!.minZoomLevel = 15.0 //add contingency check (shouldn't happen..., but) if (startPoint != null) { mapController.setCenter(startPoint) } //add stops overlay //map.getOverlays().add(mLocationOverlay); map!!.overlays.add(stopsFolderOverlay) Log.d(DEBUG_TAG, "Requesting stops load") // This is not necessary, by setting the center we already move // the map and we trigger a stop request //requestStopsToShow(); if (marker != null) { // make a marker with the info window open for the searched marker //TODO: make Stop Bundle-able val stopMarker = makeMarker(marker, ID, name, routesStopping, true) map!!.controller.animateTo(marker) } //add the overlays with the bus stops if (busPositionsOverlay == null) { //Log.i(DEBUG_TAG, "Null bus positions overlay,redo"); busPositionsOverlay = FolderOverlay() } startRequestsPositions() if (stopsViewModel != null) { stopsViewModel!!.stopsInBoundingBox.observe(viewLifecycleOwner) { stops: List? -> showStopsMarkers( stops ) } } else Log.d(DEBUG_TAG, "Cannot observe new stops in map, stopsViewModel is null") map!!.overlays.add(busPositionsOverlay) //set map as started hasMapStartFinished = true } /** * Start a request to load the stops that are in the current view * from the database */ private fun requestStopsToShow() { // get the top, bottom, left and right screen's coordinate val bb = map!!.boundingBox Log.d( DEBUG_TAG, "Requesting stops in bounding box, stopViewModel is null " + (stopsViewModel == null) ) if (stopsViewModel != null) { stopsViewModel!!.requestStopsInBoundingBox(bb) } } private fun updateBusMarker( marker: Marker?, posUpdate: LivePositionUpdate, justCreated: Boolean ) { val position: GeoPoint val updateID = posUpdate.tripID if (!justCreated) { position = marker!!.position if (posUpdate.latitude != position.latitude || posUpdate.longitude != position.longitude) { val newpos = GeoPoint(posUpdate.latitude, posUpdate.longitude) val valueAnimator = MarkerUtils.makeMarkerAnimator( map, marker, newpos, MarkerUtils.LINEAR_ANIMATION, 1200 ) valueAnimator.setAutoCancel(true) tripMarkersAnimators[updateID] = valueAnimator valueAnimator.start() } //marker.setPosition(new GeoPoint(posUpdate.getLatitude(), posUpdate.getLongitude())); } else { position = GeoPoint(posUpdate.latitude, posUpdate.longitude) marker!!.position = position } if (posUpdate.bearing != null) marker.rotation = posUpdate.bearing * -1f } private fun updateBusPositionsInMap(tripsPatterns: HashMap>) { Log.d(DEBUG_TAG, "Updating positions of the buses") //if(busPositionsOverlay == null) busPositionsOverlay = new FolderOverlay(); val noPatternsTrips = ArrayList() for (tripID in tripsPatterns.keys) { val (update, tripWithPatternStops) = tripsPatterns[tripID] ?: continue //check if Marker is already created if (busPositionMarkersByTrip.containsKey(tripID)) { //need to change the position of the marker val marker = busPositionMarkersByTrip[tripID]!! updateBusMarker(marker, update, false) if (marker.infoWindow != null && marker.infoWindow is BusInfoWindow) { val window = marker.infoWindow as BusInfoWindow if (tripWithPatternStops != null) { //Log.d(DEBUG_TAG, "Update pattern for trip: "+tripID); window.setPatternAndDraw(tripWithPatternStops.pattern) } } } else { //marker is not there, need to make it if (map == null) Log.e( DEBUG_TAG, "Creating marker with null map, things will explode" ) val marker = Marker(map) /*final Drawable mDrawable = DrawableUtils.Companion.getScaledDrawableResources( getResources(), R.drawable.point_heading_icon, R.dimen.map_icons_size, R.dimen.map_icons_size); */ //String route = GtfsUtils.getLineNameFromGtfsID(update.getRouteID()); val mdraw = ResourcesCompat.getDrawable(resources, R.drawable.map_bus_position_icon, null)!! //mdraw.setBounds(0,0,28,28); marker.icon = mdraw if (tripWithPatternStops == null) { noPatternsTrips.add(tripID) } var markerPattern: MatoPattern? = null if (tripWithPatternStops != null && tripWithPatternStops.pattern != null) markerPattern = tripWithPatternStops.pattern marker.infoWindow = BusInfoWindow(map!!, update, markerPattern, false) { pattern: MatoPattern? -> } marker.setInfoWindowAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER) marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER) updateBusMarker(marker, update, true) // the overlay is null when it's not attached yet?5 // cannot recreate it because it becomes null very soon // if(busPositionsOverlay == null) busPositionsOverlay = new FolderOverlay(); //save the marker if (busPositionsOverlay != null) { busPositionsOverlay!!.add(marker) busPositionMarkersByTrip[tripID] = marker } } } if (noPatternsTrips.size > 0) { Log.i(DEBUG_TAG, "These trips have no matching pattern: $noPatternsTrips") } } /** * Add stops as Markers on the map * @param stops the list of stops that must be included */ protected fun showStopsMarkers(stops: List?) { if (context == null || stops == null) { //we are not attached return } var good = true for (stop in stops) { if (shownStops!!.contains(stop.ID)) { continue } if (stop.longitude == null || stop.latitude == null) continue shownStops!!.add(stop.ID) if (!map!!.isShown) { if (good) Log.d( DEBUG_TAG, "Need to show stop but map is not shown, probably detached already" ) good = false continue } else if (map!!.repository == null) { Log.e(DEBUG_TAG, "Map view repository is null") } val marker = GeoPoint(stop.latitude!!, stop.longitude!!) val stopMarker = makeMarker(marker, stop, false) stopsFolderOverlay!!.add(stopMarker) if (!map!!.overlays.contains(stopsFolderOverlay)) { Log.w(DEBUG_TAG, "Map doesn't have folder overlay") } good = true } //Log.d(DEBUG_TAG,"We have " +stopsFolderOverlay.getItems().size()+" stops in the folderOverlay"); //force redraw of markers map!!.invalidate() } fun makeMarker(geoPoint: GeoPoint?, stop: Stop, isStartMarker: Boolean): Marker { return makeMarker( geoPoint, stop.ID, stop.stopDefaultName, stop.routesThatStopHereToString(), isStartMarker ) } fun makeMarker( geoPoint: GeoPoint?, stopID: String?, stopName: String?, routesStopping: String?, isStartMarker: Boolean ): Marker { // add a marker val marker = Marker(map) // set custom info window as info window val popup = CustomInfoWindow( map, stopID, stopName, routesStopping, responder, R.layout.linedetail_stop_infowindow, R.color.red_darker ) marker.infoWindow = popup // make the marker clickable marker.setOnMarkerClickListener { thisMarker: Marker, mapView: MapView? -> if (thisMarker.isInfoWindowOpen) { // on second click Log.w(DEBUG_TAG, "Pressed on the click marker") } else { // on first click // hide all opened info window InfoWindow.closeAllInfoWindowsOn(map) // show this particular info window thisMarker.showInfoWindow() // move the map to its position map!!.controller.animateTo(thisMarker.position) } true } // set its position marker.position = geoPoint marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER) // add to it an icon //marker.setIcon(getResources().getDrawable(R.drawable.bus_marker)); marker.icon = ResourcesCompat.getDrawable(resources, R.drawable.bus_stop, ctx!!.theme) // add to it a title marker.title = stopName // set the description as the ID marker.snippet = stopID // show popup info window of the searched marker if (isStartMarker) { marker.showInfoWindow() //map.getController().animateTo(marker.getPosition()); } return marker } override fun getBaseViewForSnackBar(): View? { return coordLayout } companion object { //private static final String TAG = "Busto-MapActivity"; private const val MAP_CURRENT_ZOOM_KEY = "map-current-zoom" private const val MAP_CENTER_LAT_KEY = "map-center-lat" private const val MAP_CENTER_LON_KEY = "map-center-lon" private const val FOLLOWING_LOCAT_KEY = "following" const val BUNDLE_LATIT = "lat" const val BUNDLE_LONGIT = "lon" const val BUNDLE_NAME = "name" const val BUNDLE_ID = "ID" const val BUNDLE_ROUTES_STOPPING = "routesStopping" const val FRAGMENT_TAG = "BusTOMapFragment" private const val DEFAULT_CENTER_LAT = 45.0708 private const val DEFAULT_CENTER_LON = 7.6858 private const val POSITION_FOUND_ZOOM = 18.3 const val NO_POSITION_ZOOM = 17.1 private const val DEBUG_TAG = FRAGMENT_TAG @JvmStatic fun getInstance(): MapFragmentKt { return MapFragmentKt() } @JvmStatic fun getInstance(stop: Stop): MapFragmentKt { val fragment = MapFragmentKt() val args = Bundle() args.putDouble(MapFragment.BUNDLE_LATIT, stop.latitude!!) args.putDouble(MapFragment.BUNDLE_LONGIT, stop.longitude!!) args.putString(MapFragment.BUNDLE_NAME, stop.stopDisplayName) args.putString(MapFragment.BUNDLE_ID, stop.ID) args.putString(MapFragment.BUNDLE_ROUTES_STOPPING, stop.routesThatStopHereToString()) fragment.arguments = args return fragment } } } 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 30d82d6..85bdda4 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt @@ -1,671 +1,732 @@ 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 lateinit var centerUserButton: ImageButton + private lateinit var followUserButton: ImageButton + private var followingUserLocation = false + private var ignoreCameraMovementForFollowing = true + private var enablingPositionFromClick = false 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) + setMapLocationEnabled(true, true, false) } } 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) + setMapLocationEnabled(true, true, enablingPositionFromClick) } 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) + followUserButton = rootView.findViewById(R.id.followUserImageButton) + centerUserButton = rootView.findViewById(R.id.centerMapImageButton) bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet) bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN arrivalsCard.setOnClickListener { if(context!=null){ Toast.makeText(context,"ARRIVALS", Toast.LENGTH_SHORT).show() } } + centerUserButton.setOnClickListener { + if(context!=null && locationComponent.isLocationComponentEnabled) { + val location = locationComponent.lastKnownLocation + + location?.let { + mapView.getMapAsync { map -> + map.animateCamera(CameraUpdateFactory.newCameraPosition( + CameraPosition.Builder().target(LatLng(location.latitude, location.longitude)).build()), 500) + } + } + } + } + followUserButton.setOnClickListener { + if(context!=null && locationComponent.isLocationComponentEnabled){ + if(followingUserLocation) + locationComponent.cameraMode = CameraMode.NONE + else locationComponent.cameraMode = CameraMode.TRACKING + + setFollowingUser(!followingUserLocation) + } + } 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() 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 + //if we are moving away from the position, disable it + /* + */ } + + } + + } + mapReady.addOnCameraMoveStartedListener { + if (ignoreCameraMovementForFollowing){ + ignoreCameraMovementForFollowing = false + } + else if (followingUserLocation){ + setFollowingUser(false) } } 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)) } } 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 { + setLocationIconEnabled(true) + Toast.makeText(requireContext(), "Determining the location", Toast.LENGTH_SHORT).show() 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) + setMapLocationEnabled(true, true, false) } } 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) + + lastLocation?.let { + if (it.accuracy < 200) + locationComponent.forceLocationUpdate(it) + } } 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) } + /** + * Handles logic of enabling the user location on the map + */ @SuppressLint("MissingPermission") - private fun setMapLocationEnabled(enabled: Boolean, assumePermissions: Boolean) { + private fun setMapLocationEnabled(enabled: Boolean, assumePermissions: Boolean, fromClick: Boolean) { if (enabled) { val permissionOk = assumePermissions || haveLocationPermissions() if (permissionOk) { Log.d(DEBUG_TAG, "Permission OK, starting location component, assumed: $assumePermissions") locationComponent.isLocationComponentEnabled = true + locationComponent.cameraMode = CameraMode.TRACKING //CameraMode.TRACKING + setFollowingUser(true) showUserPositionButton.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.location_circlew_red)) - + if (fromClick) Toast.makeText(context, R.string.location_enabled, Toast.LENGTH_SHORT).show() } 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") + enablingPositionFromClick = fromClick showUserPositionRequestLauncher.launch(Permissions.LOCATION_PERMISSIONS) } } else{ locationComponent.isLocationComponentEnabled = false + setFollowingUser(false) showUserPositionButton.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.location_circlew_grey)) - } + if (fromClick) Toast.makeText(requireContext(), R.string.location_disabled, Toast.LENGTH_SHORT).show() } + + } + private fun setLocationIconEnabled(enabled: Boolean){ + if (enabled) + showUserPositionButton.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.location_circlew_red)) + else + showUserPositionButton.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.location_circlew_grey)) + + } + + /** + * Helper method for GUI + */ + private fun updateFollowingIcon(enabled: Boolean){ + if(enabled) + followUserButton.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_follow_me_on)) + else + followUserButton.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_follow_me)) } + private fun setFollowingUser(following: Boolean){ + updateFollowingIcon(following) + followingUserLocation = following + if(following) + ignoreCameraMovementForFollowing = true + } + + private fun switchUserLocationStatus(view: View?){ - if(locationComponent.isLocationComponentEnabled) setMapLocationEnabled(false, false) + if(locationComponent.isLocationComponentEnabled) setMapLocationEnabled(false, false, true) else{ Log.d(DEBUG_TAG, "Request enable location") - setMapLocationEnabled(true, false) + setMapLocationEnabled(true, false, true) } } 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.xml b/app/src/main/res/layout/fragment_map.xml index 6c12770..e1b14d9 100644 --- a/app/src/main/res/layout/fragment_map.xml +++ b/app/src/main/res/layout/fragment_map.xml @@ -1,45 +1,45 @@ \ 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 c51dc7e..d35d69e 100644 --- a/app/src/main/res/layout/fragment_map_libre.xml +++ b/app/src/main/res/layout/fragment_map_libre.xml @@ -1,177 +1,195 @@ - + -