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 @@
-
+
-
+
-
+
\ No newline at end of file