diff --git a/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java b/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java
index 2bcfeb3..b32915d 100644
--- a/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java
+++ b/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java
@@ -1,93 +1,94 @@
/*
BusTO - Data components
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;
import android.os.Bundle;
import android.util.Log;
+import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.fragment.app.FragmentTransaction;
import it.reyboz.bustorino.backend.Stop;
import it.reyboz.bustorino.fragments.*;
import it.reyboz.bustorino.middleware.GeneralActivity;
public class ActivityExperiments extends GeneralActivity implements CommonFragmentListener {
final static String DEBUG_TAG = "ExperimentsActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_container_fragment);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(false);
actionBar.setIcon(R.drawable.ic_launcher);
}
if (savedInstanceState==null) {
getSupportFragmentManager().beginTransaction()
.setReorderingAllowed(true)
/* .add(R.id.fragment_container_view, LinesDetailFragment.class,
LinesDetailFragment.Companion.makeArgs("gtt:4U"))
*/
//.add(R.id.fragment_container_view, LinesGridShowingFragment.class, null)
//.add(R.id.fragment_container_view, IntroFragment.class, IntroFragment.makeArguments(0))
//.commit();
//.add(R.id.fragment_container_view, LinesDetailFragment.class,
// LinesDetailFragment.Companion.makeArgs("gtt:4U"))
.add(R.id.fragment_container_view, MapLibreFragment.class, null)
.commit();
}
}
@Override
public void showFloatingActionButton(boolean yes) {
Log.d(DEBUG_TAG, "Asked to show the action button");
}
@Override
public void readyGUIfor(FragmentKind fragmentType) {
Log.d(DEBUG_TAG, "Asked to prepare the GUI for fragmentType "+fragmentType);
}
@Override
public void requestArrivalsForStopID(String ID) {
}
@Override
public void showMapCenteredOnStop(Stop stop) {
}
@Override
- public void showLineOnMap(String routeGtfsId){
+ public void showLineOnMap(String routeGtfsId, @Nullable String stopIDFrom){
readyGUIfor(FragmentKind.LINES);
FragmentTransaction tr = getSupportFragmentManager().beginTransaction();
tr.replace(R.id.fragment_container_view, LinesDetailFragment.class,
- LinesDetailFragment.Companion.makeArgs(routeGtfsId));
+ LinesDetailFragment.Companion.makeArgs(routeGtfsId, stopIDFrom));
tr.addToBackStack("LineonMap-"+routeGtfsId);
tr.commit();
}
}
\ No newline at end of file
diff --git a/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java b/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java
index 75fe625..0125937 100644
--- a/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java
+++ b/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java
@@ -1,797 +1,797 @@
/*
BusTO - Arrival times for Turin public transport.
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;
import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.appcompat.widget.Toolbar;
import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.preference.PreferenceManager;
import androidx.work.WorkInfo;
import androidx.work.WorkManager;
import com.google.android.material.navigation.NavigationView;
import com.google.android.material.snackbar.Snackbar;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import it.reyboz.bustorino.backend.Stop;
import it.reyboz.bustorino.backend.utils;
import it.reyboz.bustorino.data.DBUpdateWorker;
import it.reyboz.bustorino.data.DatabaseUpdate;
import it.reyboz.bustorino.data.PreferencesHolder;
import it.reyboz.bustorino.data.gtfs.GtfsDatabase;
import it.reyboz.bustorino.fragments.*;
import it.reyboz.bustorino.middleware.GeneralActivity;
import static it.reyboz.bustorino.backend.utils.getBusStopIDFromUri;
import static it.reyboz.bustorino.backend.utils.openIceweasel;
public class ActivityPrincipal extends GeneralActivity implements FragmentListenerMain {
private DrawerLayout mDrawer;
private NavigationView mNavView;
private ActionBarDrawerToggle drawerToggle;
private final static String DEBUG_TAG="BusTO Act Principal";
private final static String TAG_FAVORITES="favorites_frag";
private Snackbar snackbar;
private boolean showingMainFragmentFromOther = false;
private boolean onCreateComplete = false;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(DEBUG_TAG, "onCreate, savedInstanceState is: "+savedInstanceState);
setContentView(R.layout.activity_principal);
boolean showingArrivalsFromIntent = false;
Toolbar mToolbar = findViewById(R.id.default_toolbar);
setSupportActionBar(mToolbar);
if (getSupportActionBar()!=null)
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
else Log.w(DEBUG_TAG, "NO ACTION BAR");
mToolbar.setOnMenuItemClickListener(new ToolbarItemClickListener(this));
mDrawer = findViewById(R.id.drawer_layout);
drawerToggle = setupDrawerToggle(mToolbar);
// Setup toggle to display hamburger icon with nice animation
drawerToggle.setDrawerIndicatorEnabled(true);
drawerToggle.syncState();
mDrawer.addDrawerListener(drawerToggle);
mDrawer.addDrawerListener(new DrawerLayout.DrawerListener() {
@Override
public void onDrawerSlide(@NonNull View drawerView, float slideOffset) {
}
@Override
public void onDrawerOpened(@NonNull View drawerView) {
hideKeyboard();
}
@Override
public void onDrawerClosed(@NonNull View drawerView) {
}
@Override
public void onDrawerStateChanged(int newState) {
}
});
mNavView = findViewById(R.id.nvView);
setupDrawerContent(mNavView);
/*View header = mNavView.getHeaderView(0);
*/
//mNavView.getMenu().findItem(R.id.versionFooter).
/// LEGACY CODE
//---------------------------- START INTENT CHECK QUEUE ------------------------------------
// Intercept calls from URL intent
boolean tryedFromIntent = false;
String busStopID = null;
Uri data = getIntent().getData();
if (data != null) {
busStopID = getBusStopIDFromUri(data);
Log.d(DEBUG_TAG, "Opening Intent: busStopID: "+busStopID);
tryedFromIntent = true;
}
// Intercept calls from other activities
if (!tryedFromIntent) {
Bundle b = getIntent().getExtras();
if (b != null) {
busStopID = b.getString("bus-stop-ID");
/*
* I'm not very sure if you are coming from an Intent.
* Some launchers work in strange ways.
*/
tryedFromIntent = busStopID != null;
}
}
//---------------------------- END INTENT CHECK QUEUE --------------------------------------
if (busStopID == null) {
// Show keyboard if can't start from intent
// JUST DON'T
// showKeyboard();
// You haven't obtained anything... from an intent?
if (tryedFromIntent) {
// This shows a luser warning
Toast.makeText(getApplicationContext(),
R.string.insert_bus_stop_number_error, Toast.LENGTH_SHORT).show();
}
} else {
// If you are here an intent has worked successfully
//setBusStopSearchByIDEditText(busStopID);
//Log.d(DEBUG_TAG, "Requesting arrivals for stop "+busStopID+" from intent");
requestArrivalsForStopID(busStopID); //this shows the fragment, too
showingArrivalsFromIntent = true;
}
//database check
GtfsDatabase gtfsDB = GtfsDatabase.Companion.getGtfsDatabase(this);
final int db_version = gtfsDB.getOpenHelper().getReadableDatabase().getVersion();
boolean dataUpdateRequested = false;
final SharedPreferences theShPr = getMainSharedPreferences();
final int old_version = PreferencesHolder.getGtfsDBVersion(theShPr);
Log.d(DEBUG_TAG, "GTFS Database: old version is "+old_version+ ", new version is "+db_version);
if (old_version < db_version){
//decide update conditions in the future
if(old_version < 2 && db_version >= 2) {
dataUpdateRequested = true;
DatabaseUpdate.requestDBUpdateWithWork(this, true, true);
}
PreferencesHolder.setGtfsDBVersion(theShPr, db_version);
}
//Try (hopefully) database update
if(!dataUpdateRequested)
DatabaseUpdate.requestDBUpdateWithWork(this, false, false);
/*
Watch for database update
*/
final WorkManager workManager = WorkManager.getInstance(this);
workManager.getWorkInfosForUniqueWorkLiveData(DBUpdateWorker.DEBUG_TAG)
.observe(this, workInfoList -> {
// If there are no matching work info, do nothing
if (workInfoList == null || workInfoList.isEmpty()) {
return;
}
Log.d(DEBUG_TAG, "WorkerInfo: "+workInfoList);
boolean showProgress = false;
for (WorkInfo workInfo : workInfoList) {
if (workInfo.getState() == WorkInfo.State.RUNNING) {
showProgress = true;
break;
}
}
if (showProgress) {
createDefaultSnackbar();
} else {
if(snackbar!=null) {
snackbar.dismiss();
snackbar = null;
}
}
});
// show the main fragment
Fragment f = getSupportFragmentManager().findFragmentById(R.id.mainActContentFrame);
Log.d(DEBUG_TAG, "OnCreate the fragment is "+f);
String vl = PreferenceManager.getDefaultSharedPreferences(this).getString(SettingsFragment.PREF_KEY_STARTUP_SCREEN, "");
//if (vl.length() == 0 || vl.equals("arrivals")) {
// showMainFragment();
Log.d(DEBUG_TAG, "The default screen to open is: "+vl);
if (showingArrivalsFromIntent){
//do nothing but exclude a case
}else if (savedInstanceState==null) {
//we are not restarting the activity from nothing
if (vl.equals("map")) {
requestMapFragment(false);
} else if (vl.equals("favorites")) {
checkAndShowFavoritesFragment(getSupportFragmentManager(), false);
} else if (vl.equals("lines")) {
showLinesFragment(getSupportFragmentManager(), false, null);
} else {
showMainFragment(false);
}
}
onCreateComplete = true;
//last but not least, set the good default values
manageDefaultValuesForSettings();
//check if first run activity (IntroActivity) has been started once or not
boolean hasIntroRun = theShPr.getBoolean(PreferencesHolder.PREF_INTRO_ACTIVITY_RUN,false);
if(!hasIntroRun){
startIntroductionActivity();
}
}
private ActionBarDrawerToggle setupDrawerToggle(Toolbar toolbar) {
// NOTE: Make sure you pass in a valid toolbar reference. ActionBarDrawToggle() does not require it
// and will not render the hamburger icon without it.
return new ActionBarDrawerToggle(this, mDrawer, toolbar, R.string.drawer_open, R.string.drawer_close);
}
/**
* Setup drawer actions
* @param navigationView the navigation view on which to set the callbacks
*/
private void setupDrawerContent(NavigationView navigationView) {
navigationView.setNavigationItemSelectedListener(
menuItem -> {
if (menuItem.getItemId() == R.id.drawer_action_settings) {
Log.d("MAINBusTO", "Pressed button preferences");
closeDrawerIfOpen();
startActivity(new Intent(ActivityPrincipal.this, ActivitySettings.class));
return true;
} else if(menuItem.getItemId() == R.id.nav_favorites_item){
closeDrawerIfOpen();
//get Fragment
checkAndShowFavoritesFragment(getSupportFragmentManager(), true);
return true;
} else if(menuItem.getItemId() == R.id.nav_arrivals){
closeDrawerIfOpen();
showMainFragment(true);
return true;
} else if(menuItem.getItemId() == R.id.nav_map_item){
closeDrawerIfOpen();
requestMapFragment(true);
return true;
} else if (menuItem.getItemId() == R.id.nav_lines_item) {
closeDrawerIfOpen();
showLinesFragment(getSupportFragmentManager(), true,null);
return true;
} else if(menuItem.getItemId() == R.id.drawer_action_info) {
closeDrawerIfOpen();
startActivity(new Intent(ActivityPrincipal.this, ActivityAbout.class));
return true;
}
//selectDrawerItem(menuItem);
Log.d(DEBUG_TAG, "pressed item "+menuItem);
return true;
});
}
private void closeDrawerIfOpen(){
if (mDrawer.isDrawerOpen(GravityCompat.START))
mDrawer.closeDrawer(GravityCompat.START);
}
// `onPostCreate` called when activity start-up is complete after `onStart()`
// NOTE 1: Make sure to override the method with only a single `Bundle` argument
// Note 2: Make sure you implement the correct `onPostCreate(Bundle savedInstanceState)` method.
// There are 2 signatures and only `onPostCreate(Bundle state)` shows the hamburger icon.
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
// Sync the toggle state after onRestoreInstanceState has occurred.
drawerToggle.syncState();
}
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// Pass any configuration change to the drawer toggles
drawerToggle.onConfigurationChanged(newConfig);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.principal_menu, menu);
MenuItem experimentsMenuItem = menu.findItem(R.id.action_experiments);
SharedPreferences shPr = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
boolean exper_On = shPr.getBoolean(getString(R.string.pref_key_experimental), false);
experimentsMenuItem.setVisible(exper_On);
return super.onCreateOptionsMenu(menu);
}
//requesting permissions
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode==STORAGE_PERMISSION_REQ){
final String storagePerm = Manifest.permission.WRITE_EXTERNAL_STORAGE;
if (permissionDoneRunnables.containsKey(storagePerm)) {
Runnable toRun = permissionDoneRunnables.get(storagePerm);
if (toRun != null)
toRun.run();
permissionDoneRunnables.remove(storagePerm);
}
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Log.d(DEBUG_TAG, "Permissions check: " + Arrays.toString(permissions));
if (permissionDoneRunnables.containsKey(storagePerm)) {
Runnable toRun = permissionDoneRunnables.get(storagePerm);
if (toRun != null)
toRun.run();
permissionDoneRunnables.remove(storagePerm);
}
} else {
//permission denied
showToastMessage(R.string.permission_storage_maps_msg, false);
}
}
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
int[] cases = {R.id.nav_arrivals, R.id.nav_favorites_item};
Log.d(DEBUG_TAG, "Item pressed");
if (item.getItemId() == android.R.id.home) {
mDrawer.openDrawer(GravityCompat.START);
return true;
}
if (drawerToggle.onOptionsItemSelected(item)) {
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onBackPressed() {
boolean foundFragment = false;
Fragment shownFrag = getSupportFragmentManager().findFragmentById(R.id.mainActContentFrame);
if (mDrawer.isDrawerOpen(GravityCompat.START))
mDrawer.closeDrawer(GravityCompat.START);
else if(shownFrag != null && shownFrag.isVisible() && shownFrag.getChildFragmentManager().getBackStackEntryCount() > 0){
//if we have been asked to show a stop from another fragment, we should go back even in the main
if(shownFrag instanceof MainScreenFragment){
//we have to stop the arrivals reload
((MainScreenFragment) shownFrag).cancelReloadArrivalsIfNeeded();
}
shownFrag.getChildFragmentManager().popBackStack();
if(showingMainFragmentFromOther && getSupportFragmentManager().getBackStackEntryCount() > 0){
getSupportFragmentManager().popBackStack();
Log.d(DEBUG_TAG, "Popping main back stack also");
}
}
else if (getSupportFragmentManager().getBackStackEntryCount() > 0) {
getSupportFragmentManager().popBackStack();
Log.d(DEBUG_TAG, "Popping main frame backstack for fragments");
}
else
super.onBackPressed();
}
/**
* Create and show the SnackBar with the message
*/
private void createDefaultSnackbar() {
View baseView = null;
final Fragment frag = getSupportFragmentManager().findFragmentById(R.id.mainActContentFrame);
if (frag instanceof ScreenBaseFragment){
baseView = ((ScreenBaseFragment) frag).getBaseViewForSnackBar();
}
if (baseView == null) baseView = findViewById(R.id.mainActContentFrame);
if (baseView == null) Log.e(DEBUG_TAG, "baseView null for default snackbar, probably exploding now");
snackbar = Snackbar.make(baseView, R.string.database_update_msg_inapp, Snackbar.LENGTH_INDEFINITE);
snackbar.show();
}
/**
* Show the fragment by adding it to the backstack
* @param fraMan the fragmentManager
* @param fragment the fragment
*/
private static void showMainFragment(FragmentManager fraMan, MainScreenFragment fragment, boolean addToBackStack){
FragmentTransaction ft = fraMan.beginTransaction()
.replace(R.id.mainActContentFrame, fragment, MainScreenFragment.FRAGMENT_TAG)
.setReorderingAllowed(false)
/*.setCustomAnimations(
R.anim.slide_in, // enter
R.anim.fade_out, // exit
R.anim.fade_in, // popEnter
R.anim.slide_out // popExit
)*/
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
if (addToBackStack) ft.addToBackStack(null);
ft.commit();
}
/**
* Show the fragment by adding it to the backstack
* @param fraMan the fragmentManager
* @param arguments args for the fragment
*/
private static void createShowMainFragment(FragmentManager fraMan,@Nullable Bundle arguments, boolean addToBackStack){
FragmentTransaction ft = fraMan.beginTransaction()
.replace(R.id.mainActContentFrame, MainScreenFragment.class, arguments, MainScreenFragment.FRAGMENT_TAG)
.setReorderingAllowed(false)
/*.setCustomAnimations(
R.anim.slide_in, // enter
R.anim.fade_out, // exit
R.anim.fade_in, // popEnter
R.anim.slide_out // popExit
)*/
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
if (addToBackStack) ft.addToBackStack(null);
ft.commit();
}
private void requestMapFragment(final boolean allowReturn){
// starting from Android 11, we don't need to have the STORAGE permission anymore for the map cache
/*if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R){
//nothing to do
Log.d(DEBUG_TAG, "Build codes allow the showing of the map");
createAndShowMapFragment(null, allowReturn);
return;
}
final String permission = Manifest.permission.WRITE_EXTERNAL_STORAGE;
int result = askForPermissionIfNeeded(permission, STORAGE_PERMISSION_REQ);
Log.d(DEBUG_TAG, "Permission for storage: "+result);
switch (result) {
case PERMISSION_OK:
createAndShowMapFragment(null, allowReturn);
break;
case PERMISSION_ASKING:
permissionDoneRunnables.put(permission,
() -> createAndShowMapFragment(null, allowReturn));
break;
case PERMISSION_NEG_CANNOT_ASK:
String storage_perm = getString(R.string.storage_permission);
String text = getString(R.string.too_many_permission_asks, storage_perm);
Toast.makeText(getApplicationContext(),text, Toast.LENGTH_LONG).show();
}
*/
//The permissions are handled in the MapLibreFragment instead
createAndShowMapFragment(null, allowReturn);
}
private static void checkAndShowFavoritesFragment(FragmentManager fragmentManager, boolean addToBackStack){
FragmentTransaction ft = fragmentManager.beginTransaction();
Fragment fragment = fragmentManager.findFragmentByTag(TAG_FAVORITES);
if(fragment!=null){
ft.replace(R.id.mainActContentFrame, fragment, TAG_FAVORITES);
}else{
//use new method
ft.replace(R.id.mainActContentFrame,FavoritesFragment.class,null,TAG_FAVORITES);
}
if (addToBackStack)
ft.addToBackStack("favorites_main");
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.setReorderingAllowed(false);
ft.commit();
}
private static void showLinesFragment(@NonNull FragmentManager fragmentManager, boolean addToBackStack, @Nullable Bundle fragArgs){
FragmentTransaction ft = fragmentManager.beginTransaction();
Fragment f = fragmentManager.findFragmentByTag(LinesGridShowingFragment.FRAGMENT_TAG);
if(f!=null){
ft.replace(R.id.mainActContentFrame, f, LinesGridShowingFragment.FRAGMENT_TAG);
}else{
//use new method
ft.replace(R.id.mainActContentFrame,LinesGridShowingFragment.class,fragArgs,
LinesGridShowingFragment.FRAGMENT_TAG);
}
if (addToBackStack)
ft.addToBackStack("linesGrid");
ft.setReorderingAllowed(true)
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.commit();
}
private void showMainFragment(boolean addToBackStack){
FragmentManager fraMan = getSupportFragmentManager();
Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG);
final MainScreenFragment mainScreenFragment;
if (fragment==null | !(fragment instanceof MainScreenFragment)){
createShowMainFragment(fraMan, null, addToBackStack);
}
else if(!fragment.isVisible()){
mainScreenFragment = (MainScreenFragment) fragment;
showMainFragment(fraMan, mainScreenFragment, addToBackStack);
Log.d(DEBUG_TAG, "Found the main fragment");
} else{
mainScreenFragment = (MainScreenFragment) fragment;
}
//return mainScreenFragment;
}
@Nullable
private MainScreenFragment getMainFragmentIfVisible(){
FragmentManager fraMan = getSupportFragmentManager();
Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG);
if (fragment!= null && fragment.isVisible()) return (MainScreenFragment) fragment;
else return null;
}
@Override
public void showFloatingActionButton(boolean yes) {
//TODO
}
/*
public void setDrawerSelectedItem(String fragmentTag){
switch (fragmentTag){
case MainScreenFragment.FRAGMENT_TAG:
mNavView.setCheckedItem(R.id.nav_arrivals);
break;
case MapFragment.FRAGMENT_TAG:
break;
case FavoritesFragment.FRAGMENT_TAG:
mNavView.setCheckedItem(R.id.nav_favorites_item);
break;
}
}*/
@Override
public void readyGUIfor(FragmentKind fragmentType) {
MainScreenFragment mainFragmentIfVisible = getMainFragmentIfVisible();
if (mainFragmentIfVisible!=null){
mainFragmentIfVisible.readyGUIfor(fragmentType);
}
int titleResId;
switch (fragmentType){
case MAP:
mNavView.setCheckedItem(R.id.nav_map_item);
titleResId = R.string.map;
break;
case FAVORITES:
mNavView.setCheckedItem(R.id.nav_favorites_item);
titleResId = R.string.nav_favorites_text;
break;
case ARRIVALS:
titleResId = R.string.nav_arrivals_text;
mNavView.setCheckedItem(R.id.nav_arrivals);
break;
case STOPS:
titleResId = R.string.stop_search_view_title;
mNavView.setCheckedItem(R.id.nav_arrivals);
break;
case MAIN_SCREEN_FRAGMENT:
case NEARBY_STOPS:
case NEARBY_ARRIVALS:
titleResId=R.string.app_name_full;
mNavView.setCheckedItem(R.id.nav_arrivals);
break;
case LINES:
titleResId=R.string.lines;
mNavView.setCheckedItem(R.id.nav_lines_item);
break;
default:
titleResId = 0;
}
if(getSupportActionBar()!=null && titleResId!=0)
getSupportActionBar().setTitle(titleResId);
}
@Override
public void requestArrivalsForStopID(String ID) {
//register if the request came from the main fragment or not
MainScreenFragment probableFragment = getMainFragmentIfVisible();
showingMainFragmentFromOther = (probableFragment==null);
if (showingMainFragmentFromOther){
FragmentManager fraMan = getSupportFragmentManager();
Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG);
Log.d(DEBUG_TAG, "Requested main fragment, not visible. Search by TAG returned: "+fragment);
if(fragment!=null){
//the fragment is there but not shown
probableFragment = (MainScreenFragment) fragment;
// set the flag
probableFragment.setSuppressArrivalsReload(true);
showMainFragment(fraMan, probableFragment, true);
probableFragment.requestArrivalsForStopID(ID);
} else {
// we have no fragment
final Bundle args = new Bundle();
args.putString(MainScreenFragment.PENDING_STOP_SEARCH, ID);
//if onCreate is complete, then we are not asking for the first showing fragment
boolean addtobackstack = onCreateComplete;
createShowMainFragment(fraMan, args ,addtobackstack);
}
} else {
//the MainScreeFragment is shown, nothing to do
probableFragment.requestArrivalsForStopID(ID);
}
mNavView.setCheckedItem(R.id.nav_arrivals);
}
@Override
- public void showLineOnMap(String routeGtfsId){
+ public void showLineOnMap(String routeGtfsId, @Nullable String stopIDFrom){
readyGUIfor(FragmentKind.LINES);
FragmentTransaction tr = getSupportFragmentManager().beginTransaction();
tr.replace(R.id.mainActContentFrame, LinesDetailFragment.class,
- LinesDetailFragment.Companion.makeArgs(routeGtfsId));
+ LinesDetailFragment.Companion.makeArgs(routeGtfsId, stopIDFrom));
tr.addToBackStack("LineonMap-"+routeGtfsId);
tr.commit();
}
@Override
public void toggleSpinner(boolean state) {
MainScreenFragment probableFragment = getMainFragmentIfVisible();
if (probableFragment!=null){
probableFragment.toggleSpinner(state);
}
}
@Override
public void enableRefreshLayout(boolean yes) {
MainScreenFragment probableFragment = getMainFragmentIfVisible();
if (probableFragment!=null){
probableFragment.enableRefreshLayout(yes);
}
}
@Override
public void showMapCenteredOnStop(Stop stop) {
createAndShowMapFragment(stop, true);
}
//Map Fragment stuff
void createAndShowMapFragment(@Nullable Stop stop, boolean addToBackStack){
final FragmentManager fm = getSupportFragmentManager();
final FragmentTransaction ft = fm.beginTransaction();
final MapLibreFragment fragment = MapLibreFragment.Companion.newInstance(stop);
ft.replace(R.id.mainActContentFrame, fragment, MapFragmentKt.FRAGMENT_TAG);
if (addToBackStack) ft.addToBackStack(null);
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
ft.commit();
}
void startIntroductionActivity(){
Intent intent = new Intent(ActivityPrincipal.this, ActivityIntro.class);
intent.putExtra(ActivityIntro.RESTART_MAIN, false);
startActivity(intent);
}
class ToolbarItemClickListener implements Toolbar.OnMenuItemClickListener{
private final Context activityContext;
public ToolbarItemClickListener(Context activityContext) {
this.activityContext = activityContext;
}
@Override
public boolean onMenuItemClick(MenuItem item) {
final int id = item.getItemId();
if(id == R.id.action_about){
startActivity(new Intent(ActivityPrincipal.this, ActivityAbout.class));
return true;
} else if (id == R.id.action_hack) {
openIceweasel(getString(R.string.hack_url), activityContext);
return true;
} else if (id == R.id.action_source){
openIceweasel("https://gitpull.it/source/libre-busto/", activityContext);
return true;
} else if (id == R.id.action_licence){
openIceweasel("https://www.gnu.org/licenses/gpl-3.0.html", activityContext);
return true;
} else if (id == R.id.action_experiments) {
startActivity(new Intent(ActivityPrincipal.this, ActivityExperiments.class));
return true;
} else if (id == R.id.action_tutorial) {
startIntroductionActivity();
return true;
}
return false;
}
}
/**
* Adjust setting to match the default ones
*/
private void manageDefaultValuesForSettings(){
SharedPreferences mainSharedPref = PreferenceManager.getDefaultSharedPreferences(this);
SharedPreferences.Editor editor = mainSharedPref.edit();
//Main fragment to show
String screen = mainSharedPref.getString(SettingsFragment.PREF_KEY_STARTUP_SCREEN, "");
boolean edit = false;
if (screen.isEmpty()){
editor.putString(SettingsFragment.PREF_KEY_STARTUP_SCREEN, "arrivals");
edit=true;
}
//Fetchers
final Set setSelected = mainSharedPref.getStringSet(SettingsFragment.KEY_ARRIVALS_FETCHERS_USE, new HashSet<>());
if (setSelected.isEmpty()){
String[] defaultVals = getResources().getStringArray(R.array.arrivals_sources_values_default);
editor.putStringSet(SettingsFragment.KEY_ARRIVALS_FETCHERS_USE, utils.convertArrayToSet(defaultVals));
edit=true;
}
//Live bus positions
final String keySourcePositions=getString(R.string.pref_positions_source);
final String positionsSource = mainSharedPref.getString(keySourcePositions, "");
if(positionsSource.isEmpty()){
String[] defaultVals = getResources().getStringArray(R.array.positions_source_values);
editor.putString(keySourcePositions, defaultVals[0]);
edit=true;
}
//Map style
final String mapStylePref = mainSharedPref.getString(SettingsFragment.LIBREMAP_STYLE_PREF_KEY, "");
if(mapStylePref.isEmpty()){
final String[] defaultVals = getResources().getStringArray(R.array.map_style_pref_values);
editor.putString(SettingsFragment.LIBREMAP_STYLE_PREF_KEY, defaultVals[0]);
edit=true;
}
if (edit){
editor.commit();
}
}
}
diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.java
index f7b88cf..f574581 100644
--- a/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.java
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.java
@@ -1,710 +1,710 @@
/*
BusTO - Fragments components
Copyright (C) 2018 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.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.widget.*;
import androidx.annotation.Nullable;
import androidx.annotation.NonNull;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.CursorLoader;
import androidx.loader.content.Loader;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import it.reyboz.bustorino.R;
import it.reyboz.bustorino.adapters.PalinaAdapter;
import it.reyboz.bustorino.adapters.RouteOnlyLineAdapter;
import it.reyboz.bustorino.backend.ArrivalsFetcher;
import it.reyboz.bustorino.backend.DBStatusManager;
import it.reyboz.bustorino.backend.Fetcher;
import it.reyboz.bustorino.backend.FiveTNormalizer;
import it.reyboz.bustorino.backend.Palina;
import it.reyboz.bustorino.backend.Passaggio;
import it.reyboz.bustorino.backend.Route;
import it.reyboz.bustorino.backend.Stop;
import it.reyboz.bustorino.backend.utils;
import it.reyboz.bustorino.data.AppDataProvider;
import it.reyboz.bustorino.data.NextGenDB;
import it.reyboz.bustorino.data.UserDB;
import it.reyboz.bustorino.middleware.AsyncStopFavoriteAction;
import it.reyboz.bustorino.util.LinesNameSorter;
import static it.reyboz.bustorino.fragments.ScreenBaseFragment.setOption;
public class ArrivalsFragment extends ResultBaseFragment implements LoaderManager.LoaderCallbacks {
private static final String OPTION_SHOW_LEGEND = "show_legend";
private final static String KEY_STOP_ID = "stopid";
private final static String KEY_STOP_NAME = "stopname";
private final static String DEBUG_TAG_ALL = "BUSTOArrivalsFragment";
private String DEBUG_TAG = DEBUG_TAG_ALL;
private final static int loaderFavId = 2;
private final static int loaderStopId = 1;
static final String STOP_TITLE = "messageExtra";
private final static String SOURCES_TEXT="sources_textview_message";
private @Nullable String stopID,stopName;
private DBStatusManager prefs;
private DBStatusManager.OnDBUpdateStatusChangeListener listener;
private boolean justCreated = false;
private Palina lastUpdatedPalina = null;
private boolean needUpdateOnAttach = false;
private boolean fetchersChangeRequestPending = false;
private boolean stopIsInFavorites = false;
//Views
protected ImageButton addToFavorites;
protected TextView timesSourceTextView;
protected TextView messageTextView;
protected RecyclerView arrivalsRecyclerView;
private PalinaAdapter mListAdapter = null;
private TextView howDoesItWorkTextView;
private Button hideHintButton;
//private NestedScrollView theScrollView;
protected RecyclerView noArrivalsRecyclerView;
private RouteOnlyLineAdapter noArrivalsAdapter;
private TextView noArrivalsTitleView;
private GridLayoutManager layoutManager;
//private View canaryEndView;
private List fetchers = null; //new ArrayList<>(Arrays.asList(utils.getDefaultArrivalsFetchers()));
private boolean reloadOnResume = true;
private final PalinaAdapter.PalinaClickListener palinaClickListener = new PalinaAdapter.PalinaClickListener() {
@Override
public void showRouteFullDirection(Route route) {
String routeName;
Log.d(DEBUG_TAG, "Make toast for line "+route.getName());
routeName = FiveTNormalizer.routeInternalToDisplay(route.getName());
if (routeName == null) {
routeName = route.getDisplayCode();
}
if(getContext()==null)
Log.e(DEBUG_TAG, "Touched on a route but Context is null");
else if (route.destinazione == null || route.destinazione.length() == 0) {
Toast.makeText(getContext(),
getString(R.string.route_towards_unknown, routeName), Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(getContext(),
getString(R.string.route_towards_destination, routeName, route.destinazione), Toast.LENGTH_SHORT).show();
}
}
@Override
public void requestShowingRoute(Route route) {
Log.d(DEBUG_TAG, "Need to show line for route:\ngtfsID "+route.getGtfsId()+ " name "+route.getName());
if(route.getGtfsId()!=null){
- mListener.showLineOnMap(route.getGtfsId());
+ mListener.showLineOnMap(route.getGtfsId(), stopID);
} else {
String gtfsID = FiveTNormalizer.getGtfsRouteID(route);
Log.d(DEBUG_TAG, "GtfsID for route is: " + gtfsID);
- mListener.showLineOnMap(gtfsID);
+ mListener.showLineOnMap(gtfsID, stopID);
}
}
};
public static ArrivalsFragment newInstance(String stopID){
return newInstance(stopID, null);
}
public static ArrivalsFragment newInstance(@NonNull String stopID, @Nullable String stopName){
ArrivalsFragment fragment = new ArrivalsFragment();
Bundle args = new Bundle();
args.putString(KEY_STOP_ID,stopID);
//parameter for ResultListFragmentrequestArrivalsForStopID
//args.putSerializable(LIST_TYPE,FragmentKind.ARRIVALS);
if (stopName != null){
args.putString(KEY_STOP_NAME,stopName);
}
fragment.setArguments(args);
return fragment;
}
public static String getFragmentTag(Palina p) {
return "palina_"+p.ID;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
stopID = getArguments().getString(KEY_STOP_ID);
DEBUG_TAG = DEBUG_TAG_ALL+" "+stopID;
//this might really be null
stopName = getArguments().getString(KEY_STOP_NAME);
final ArrivalsFragment arrivalsFragment = this;
listener = new DBStatusManager.OnDBUpdateStatusChangeListener() {
@Override
public void onDBStatusChanged(boolean updating) {
if(!updating){
getLoaderManager().restartLoader(loaderFavId,getArguments(),arrivalsFragment);
} else {
final LoaderManager lm = getLoaderManager();
lm.destroyLoader(loaderFavId);
lm.destroyLoader(loaderStopId);
}
}
@Override
public boolean defaultStatusValue() {
return true;
}
};
prefs = new DBStatusManager(getContext().getApplicationContext(),listener);
justCreated = true;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_arrivals, container, false);
messageTextView = root.findViewById(R.id.messageTextView);
addToFavorites = root.findViewById(R.id.addToFavorites);
// "How does it work part"
howDoesItWorkTextView = root.findViewById(R.id.howDoesItWorkTextView);
hideHintButton = root.findViewById(R.id.hideHintButton);
hideHintButton.setOnClickListener(this::onHideHint);
//theScrollView = root.findViewById(R.id.arrivalsScrollView);
// recyclerview holding the arrival times
arrivalsRecyclerView = root.findViewById(R.id.arrivalsRecyclerView);
final LinearLayoutManager manager = new LinearLayoutManager(getContext());
arrivalsRecyclerView.setLayoutManager(manager);
final DividerItemDecoration mDividerItemDecoration = new DividerItemDecoration(arrivalsRecyclerView.getContext(),
manager.getOrientation());
arrivalsRecyclerView.addItemDecoration(mDividerItemDecoration);
timesSourceTextView = root.findViewById(R.id.timesSourceTextView);
timesSourceTextView.setOnLongClickListener(view -> {
if(!fetchersChangeRequestPending){
rotateFetchers();
//Show we are changing provider
timesSourceTextView.setText(R.string.arrival_source_changing);
mListener.requestArrivalsForStopID(stopID);
fetchersChangeRequestPending = true;
return true;
}
return false;
});
timesSourceTextView.setOnClickListener(view -> {
Toast.makeText(getContext(), R.string.change_arrivals_source_message, Toast.LENGTH_SHORT)
.show();
});
//Button
addToFavorites.setClickable(true);
addToFavorites.setOnClickListener(v -> {
// add/remove the stop in the favorites
toggleLastStopToFavorites();
});
String displayName = getArguments().getString(STOP_TITLE);
if(displayName!=null)
setTextViewMessage(String.format(
getString(R.string.passages), displayName));
String probablemessage = getArguments().getString(MESSAGE_TEXT_VIEW);
if (probablemessage != null) {
//Log.d("BusTO fragment " + this.getTag(), "We have a possible message here in the savedInstaceState: " + probablemessage);
messageTextView.setText(probablemessage);
messageTextView.setVisibility(View.VISIBLE);
}
//no arrivals stuff
noArrivalsRecyclerView = root.findViewById(R.id.noArrivalsRecyclerView);
layoutManager = new GridLayoutManager(getContext(),60);
layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
return 12;
}
});
noArrivalsRecyclerView.setLayoutManager(layoutManager);
noArrivalsTitleView = root.findViewById(R.id.noArrivalsMessageTextView);
//canaryEndView = root.findViewById(R.id.canaryEndView);
/*String sourcesTextViewData = getArguments().getString(SOURCES_TEXT);
if (sourcesTextViewData!=null){
timesSourceTextView.setText(sourcesTextViewData);
}*/
//need to do this when we recreate the fragment but we haven't updated the arrival times
if (lastUpdatedPalina!=null)
showArrivalsSources(lastUpdatedPalina);
return root;
}
@Override
public void onResume() {
super.onResume();
LoaderManager loaderManager = getLoaderManager();
Log.d(DEBUG_TAG, "OnResume, justCreated "+justCreated+", lastUpdatedPalina is: "+lastUpdatedPalina);
/*if(needUpdateOnAttach){
updateFragmentData(null);
needUpdateOnAttach=false;
}*/
/*if(lastUpdatedPalina!=null){
updateFragmentData(null);
showArrivalsSources(lastUpdatedPalina);
}*/
mListener.readyGUIfor(FragmentKind.ARRIVALS);
if (mListAdapter!=null)
resetListAdapter(mListAdapter);
if(noArrivalsAdapter!=null){
noArrivalsRecyclerView.setAdapter(noArrivalsAdapter);
}
if(stopID!=null){
if(!justCreated){
fetchers = utils.getDefaultArrivalsFetchers(getContext());
adjustFetchersToSource();
if (reloadOnResume)
mListener.requestArrivalsForStopID(stopID);
}
else justCreated = false;
//start the loader
if(prefs.isDBUpdating(true)){
prefs.registerListener();
} else {
Log.d(DEBUG_TAG, "Restarting loader for stop");
loaderManager.restartLoader(loaderFavId, getArguments(), this);
}
updateMessage();
}
if (ScreenBaseFragment.getOption(requireContext(),OPTION_SHOW_LEGEND, true)) {
showHints();
}
}
@Override
public void onStart() {
super.onStart();
if (needUpdateOnAttach){
updateFragmentData(null);
needUpdateOnAttach = false;
}
}
@Override
public void onPause() {
if(listener!=null)
prefs.unregisterListener();
super.onPause();
LoaderManager loaderManager = getLoaderManager();
Log.d(DEBUG_TAG, "onPause, have running loaders: "+loaderManager.hasRunningLoaders());
loaderManager.destroyLoader(loaderFavId);
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
//get fetchers
fetchers = utils.getDefaultArrivalsFetchers(context);
}
@Nullable
public String getStopID() {
return stopID;
}
public boolean reloadsOnResume() {
return reloadOnResume;
}
public void setReloadOnResume(boolean reloadOnResume) {
this.reloadOnResume = reloadOnResume;
}
// HINT "HOW TO USE"
private void showHints() {
howDoesItWorkTextView.setVisibility(View.VISIBLE);
hideHintButton.setVisibility(View.VISIBLE);
//actionHelpMenuItem.setVisible(false);
}
private void hideHints() {
howDoesItWorkTextView.setVisibility(View.GONE);
hideHintButton.setVisibility(View.GONE);
//actionHelpMenuItem.setVisible(true);
}
public void onHideHint(View v) {
hideHints();
setOption(requireContext(),OPTION_SHOW_LEGEND, false);
}
/**
* Give the fetchers
* @return the list of the fetchers
*/
public ArrayList getCurrentFetchers(){
return new ArrayList<>(this.fetchers);
}
public ArrivalsFetcher[] getCurrentFetchersAsArray(){
ArrivalsFetcher[] arr = new ArrivalsFetcher[fetchers.size()];
fetchers.toArray(arr);
return arr;
}
private void rotateFetchers(){
Log.d(DEBUG_TAG, "Rotating fetchers, before: "+fetchers);
Collections.rotate(fetchers, -1);
Log.d(DEBUG_TAG, "Rotating fetchers, afterwards: "+fetchers);
}
/**
* Update the UI with the new data
* @param p the full Palina
*/
public void updateFragmentData(@Nullable Palina p){
if (p!=null)
lastUpdatedPalina = p;
if (!isAdded()){
//defer update at next show
if (p==null)
Log.w(DEBUG_TAG, "Asked to update the data, but we're not attached and the data is null");
else needUpdateOnAttach = true;
} else {
final PalinaAdapter adapter = new PalinaAdapter(getContext(), lastUpdatedPalina, palinaClickListener, true);
showArrivalsSources(lastUpdatedPalina);
resetListAdapter(adapter);
final ArrayList routesWithNoPassages = lastUpdatedPalina.getRoutesNamesWithNoPassages();
if(routesWithNoPassages.isEmpty()){
//hide the views if there are no empty routes
noArrivalsRecyclerView.setVisibility(View.GONE);
noArrivalsTitleView.setVisibility(View.GONE);
}else{
Collections.sort(routesWithNoPassages, new LinesNameSorter());
noArrivalsAdapter = new RouteOnlyLineAdapter(routesWithNoPassages, null);
if(noArrivalsRecyclerView!=null){
noArrivalsRecyclerView.setAdapter(noArrivalsAdapter);
noArrivalsRecyclerView.setVisibility(View.VISIBLE);
noArrivalsTitleView.setVisibility(View.VISIBLE);
}
}
//canaryEndView.setVisibility(View.VISIBLE);
//check if canaryEndView is visible
//boolean isCanaryVisibile = ViewUtils.Companion.isViewPartiallyVisibleInScroll(canaryEndView, theScrollView);
//Log.d(DEBUG_TAG, "Canary view fully visibile: "+isCanaryVisibile);
}
}
/**
* Set the message of the arrival times source
* @param p Palina with the arrival times
*/
protected void showArrivalsSources(Palina p){
final Passaggio.Source source = p.getPassaggiSourceIfAny();
if (source == null){
Log.e(DEBUG_TAG, "NULL SOURCE");
return;
}
String source_txt;
switch (source){
case GTTJSON:
source_txt = getString(R.string.gttjsonfetcher);
break;
case FiveTAPI:
source_txt = getString(R.string.fivetapifetcher);
break;
case FiveTScraper:
source_txt = getString(R.string.fivetscraper);
break;
case MatoAPI:
source_txt = getString(R.string.source_mato);
break;
case UNDETERMINED:
//Don't show the view
source_txt = getString(R.string.undetermined_source);
break;
default:
throw new IllegalStateException("Unexpected value: " + source);
}
//
final boolean updatedFetchers = adjustFetchersToSource(source);
if(!updatedFetchers)
Log.w(DEBUG_TAG, "Tried to update the source fetcher but it didn't work");
final String base_message = getString(R.string.times_source_fmt, source_txt);
timesSourceTextView.setText(base_message);
timesSourceTextView.setVisibility(View.VISIBLE);
if (p.getTotalNumberOfPassages() > 0) {
timesSourceTextView.setVisibility(View.VISIBLE);
} else {
timesSourceTextView.setVisibility(View.INVISIBLE);
}
fetchersChangeRequestPending = false;
}
protected boolean adjustFetchersToSource(Passaggio.Source source){
if (source == null) return false;
int count = 0;
if (source!= Passaggio.Source.UNDETERMINED)
while (source != fetchers.get(0).getSourceForFetcher() && count < 200){
//we need to update the fetcher that is requested
rotateFetchers();
count++;
}
return count < 200;
}
protected boolean adjustFetchersToSource(){
if (lastUpdatedPalina == null) return false;
final Passaggio.Source source = lastUpdatedPalina.getPassaggiSourceIfAny();
return adjustFetchersToSource(source);
}
/**
* Update the message in the fragment
*
* It may eventually change the "Add to Favorite" icon
*/
private void updateMessage(){
String message = null;
if (stopName != null && stopID != null && !stopName.isEmpty()) {
message = (stopID.concat(" - ").concat(stopName));
} else if(stopID!=null) {
message = stopID;
} else {
Log.e("ArrivalsFragm"+getTag(),"NO ID FOR THIS FRAGMENT - something went horribly wrong");
}
if(message!=null) {
setTextViewMessage(getString(R.string.passages,message));
}
// whatever is the case, update the star icon
//updateStarIconFromLastBusStop();
}
@NonNull
@Override
public Loader onCreateLoader(int id, Bundle args) {
if(args.getString(KEY_STOP_ID)==null) return null;
final String stopID = args.getString(KEY_STOP_ID);
final Uri.Builder builder = AppDataProvider.getUriBuilderToComplete();
CursorLoader cl;
switch (id){
case loaderFavId:
builder.appendPath("favorites").appendPath(stopID);
cl = new CursorLoader(getContext(),builder.build(),UserDB.getFavoritesColumnNamesAsArray,null,null,null);
break;
case loaderStopId:
builder.appendPath("stop").appendPath(stopID);
cl = new CursorLoader(getContext(),builder.build(),new String[]{NextGenDB.Contract.StopsTable.COL_NAME},
null,null,null);
break;
default:
return null;
}
cl.setUpdateThrottle(500);
return cl;
}
@Override
public void onLoadFinished(Loader loader, Cursor data) {
switch (loader.getId()){
case loaderFavId:
final int colUserName = data.getColumnIndex(UserDB.getFavoritesColumnNamesAsArray[1]);
if(data.getCount()>0){
// IT'S IN FAVORITES
data.moveToFirst();
final String probableName = data.getString(colUserName);
stopIsInFavorites = true;
if (probableName != null && !probableName.isEmpty())
stopName = probableName; //set the stop
//update the message in the textview
updateMessage();
} else {
stopIsInFavorites =false;
}
updateStarIcon();
if(stopName == null){
//stop is not inside the favorites and wasn't provided
Log.d("ArrivalsFragment"+getTag(),"Stop wasn't in the favorites and has no name, looking in the DB");
getLoaderManager().restartLoader(loaderStopId,getArguments(),this);
}
break;
case loaderStopId:
if(data.getCount()>0){
data.moveToFirst();
int index = data.getColumnIndex(
NextGenDB.Contract.StopsTable.COL_NAME
);
if (index == -1){
Log.e(DEBUG_TAG, "Index is -1, column not present. App may explode now...");
}
stopName = data.getString(index);
updateMessage();
} else {
Log.w("ArrivalsFragment"+getTag(),"Stop is not inside the database... CLOISTER BELL");
}
}
}
@Override
public void onLoaderReset(Loader loader) {
//NOTHING TO DO
}
protected void resetListAdapter(PalinaAdapter adapter) {
mListAdapter = adapter;
if (arrivalsRecyclerView != null) {
arrivalsRecyclerView.setAdapter(adapter);
arrivalsRecyclerView.setVisibility(View.VISIBLE);
}
}
/**
* Set the message textView
* @param message the whole message to write in the textView
*/
public void setTextViewMessage(String message) {
messageTextView.setText(message);
messageTextView.setVisibility(View.VISIBLE);
}
public void toggleLastStopToFavorites() {
Stop stop = lastUpdatedPalina;
if (stop != null) {
// toggle the status in background
new AsyncStopFavoriteAction(getContext().getApplicationContext(), AsyncStopFavoriteAction.Action.TOGGLE,
v->updateStarIconFromLastBusStop(v)).execute(stop);
} else {
// this case have no sense, but just immediately update the favorite icon
updateStarIconFromLastBusStop(true);
}
}
/**
* Update the star "Add to favorite" icon
*/
public void updateStarIconFromLastBusStop(Boolean toggleDone) {
if (stopIsInFavorites)
stopIsInFavorites = !toggleDone;
else stopIsInFavorites = toggleDone;
updateStarIcon();
// check if there is a last Stop
/*
if (stopID == null) {
addToFavorites.setVisibility(View.INVISIBLE);
} else {
// filled or outline?
if (isStopInFavorites(stopID)) {
addToFavorites.setImageResource(R.drawable.ic_star_filled);
} else {
addToFavorites.setImageResource(R.drawable.ic_star_outline);
}
addToFavorites.setVisibility(View.VISIBLE);
}
*/
}
/**
* Update the star icon according to `stopIsInFavorites`
*/
public void updateStarIcon() {
// no favorites no party!
// check if there is a last Stop
if (stopID == null) {
addToFavorites.setVisibility(View.INVISIBLE);
} else {
// filled or outline?
if (stopIsInFavorites) {
addToFavorites.setImageResource(R.drawable.ic_star_filled);
} else {
addToFavorites.setImageResource(R.drawable.ic_star_outline);
}
addToFavorites.setVisibility(View.VISIBLE);
}
}
@Override
public void onDestroyView() {
arrivalsRecyclerView = null;
if(getArguments()!=null) {
getArguments().putString(SOURCES_TEXT, timesSourceTextView.getText().toString());
getArguments().putString(MESSAGE_TEXT_VIEW, messageTextView.getText().toString());
}
super.onDestroyView();
}
public boolean isFragmentForTheSameStop(Palina p) {
if (getTag() != null)
return getTag().equals(getFragmentTag(p));
else return false;
}
}
diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/CommonFragmentListener.java b/app/src/main/java/it/reyboz/bustorino/fragments/CommonFragmentListener.java
index 03e6831..7c4f935 100644
--- a/app/src/main/java/it/reyboz/bustorino/fragments/CommonFragmentListener.java
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/CommonFragmentListener.java
@@ -1,45 +1,47 @@
package it.reyboz.bustorino.fragments;
import android.location.Location;
import android.view.View;
import it.reyboz.bustorino.backend.Stop;
+import javax.annotation.Nullable;
+
public interface CommonFragmentListener {
/**
* Tell the activity that we need to disable/enable its floatingActionButton
* @param yes or no
*/
void showFloatingActionButton(boolean yes);
/**
* Sends the message to the activity to adapt the GUI
* to the fragment that has been attached
* @param fragmentType the type of fragment attached
*/
void readyGUIfor(FragmentKind fragmentType);
/**
* Houston, we need another fragment!
*
* @param ID the Stop ID
*/
void requestArrivalsForStopID(String ID);
/**
* Method to call when we want to hide the keyboard
*/
void hideKeyboard();
/**
* We want to open the map on the specified stop
* @param stop needs to have location data (latitude, longitude)
*/
void showMapCenteredOnStop(Stop stop);
/**
* We want to show the line in detail for route
* @param routeGtfsId the route gtfsID (eg, "gtt:10U")
*/
- void showLineOnMap(String routeGtfsId);
+ void showLineOnMap(String routeGtfsId,@Nullable String fromStopID);
}
diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt
index 970c42a..11b6a1f 100644
--- a/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt
@@ -1,1491 +1,1534 @@
/*
BusTO - Fragments components
Copyright (C) 2023 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.animation.ValueAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.location.Location
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.LinearInterpolator
import android.widget.*
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.content.res.AppCompatResources
import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.fragment.app.viewModels
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.gson.JsonObject
import it.reyboz.bustorino.R
import it.reyboz.bustorino.adapters.NameCapitalize
import it.reyboz.bustorino.adapters.StopAdapterListener
import it.reyboz.bustorino.adapters.StopRecyclerAdapter
import it.reyboz.bustorino.backend.FiveTNormalizer
import it.reyboz.bustorino.backend.LivePositionTripPattern
import it.reyboz.bustorino.backend.Stop
import it.reyboz.bustorino.backend.gtfs.GtfsUtils
import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate
import it.reyboz.bustorino.backend.gtfs.PolylineParser
import it.reyboz.bustorino.backend.utils
import it.reyboz.bustorino.data.MatoTripsDownloadWorker
import it.reyboz.bustorino.data.PreferencesHolder
import it.reyboz.bustorino.data.gtfs.MatoPatternWithStops
import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops
import it.reyboz.bustorino.map.*
import it.reyboz.bustorino.middleware.LocationUtils
import it.reyboz.bustorino.util.Permissions
import it.reyboz.bustorino.viewmodels.LinesViewModel
import it.reyboz.bustorino.viewmodels.LivePositionsViewModel
import kotlinx.coroutines.Runnable
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.LocationComponentOptions
import org.maplibre.android.maps.MapLibreMap
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.expressions.Expression
import org.maplibre.android.style.layers.LineLayer
import org.maplibre.android.style.layers.Property
import org.maplibre.android.style.layers.Property.ICON_ANCHOR_CENTER
import org.maplibre.android.style.layers.Property.ICON_ROTATION_ALIGNMENT_MAP
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.LineString
import org.maplibre.geojson.Point
import java.util.concurrent.atomic.AtomicBoolean
class LinesDetailFragment() : GeneralMapLibreFragment() {
private var lineID = ""
private lateinit var patternsSpinner: Spinner
private var patternsAdapter: ArrayAdapter? = null
//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 lateinit var bottomrightImage: ImageView
private var isBottomSheetShowing = false
private var shouldMapLocationBeReactivated = true
private var toRunWhenMapReady : Runnable? = null
private var mapInitialized = AtomicBoolean(false)
//private var patternsSpinnerState: Parcelable? = null
private lateinit var currentPatterns: List
//private lateinit var map: MapView
private var patternShown: MatoPatternWithStops? = null
private val viewModel: LinesViewModel by viewModels()
private val mapViewModel: MapViewModel by viewModels()
private var firstInit = true
private var pausedFragment = false
private lateinit var switchButton: ImageButton
private var favoritesButton: ImageButton? = null
private var locationIcon: ImageButton? = null
private var isLineInFavorite = false
private var appContext: Context? = null
private var isLocationPermissionOK = false
private val lineSharedPrefMonitor = SharedPreferences.OnSharedPreferenceChangeListener { pref, keychanged ->
if(keychanged!=PreferencesHolder.PREF_FAVORITE_LINES || lineID.isEmpty()) return@OnSharedPreferenceChangeListener
val newFavorites = pref.getStringSet(PreferencesHolder.PREF_FAVORITE_LINES, HashSet())
newFavorites?.let {favorites->
isLineInFavorite = favorites.contains(lineID)
//if the button has been intialized, change the icon accordingly
favoritesButton?.let { button->
//avoid crashes if fragment not attached
if(context==null) return@let
if(isLineInFavorite) {
button.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_star_filled, null))
appContext?.let { Toast.makeText(it,R.string.favorites_line_add,Toast.LENGTH_SHORT).show()}
} else {
button.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_star_outline, null))
appContext?.let {Toast.makeText(it,R.string.favorites_line_remove,Toast.LENGTH_SHORT).show()}
}
}
}
}
private lateinit var stopsRecyclerView: RecyclerView
private lateinit var descripTextView: TextView
+ private var stopIDFromToShow: String? = null
//adapter for recyclerView
private val stopAdapterListener= object : StopAdapterListener {
override fun onTappedStop(stop: Stop?) {
if(viewModel.shouldShowMessage) {
Toast.makeText(context, R.string.long_press_stop_4_options, Toast.LENGTH_SHORT).show()
viewModel.shouldShowMessage=false
}
stop?.let {
fragmentListener.requestArrivalsForStopID(it.ID)
}
if(stop == null){
Log.e(DEBUG_TAG,"Passed wrong stop")
}
if(fragmentListener == null){
Log.e(DEBUG_TAG, "Fragment listener is null")
}
}
override fun onLongPressOnStop(stop: Stop?): Boolean {
TODO("Not yet implemented")
}
}
private val patternsSorter = Comparator{ p1: MatoPatternWithStops, p2: MatoPatternWithStops ->
if(p1.pattern.directionId != p2.pattern.directionId)
return@Comparator p1.pattern.directionId - p2.pattern.directionId
else
return@Comparator -1*(p1.stopsIndices.size - p2.stopsIndices.size)
}
//map data
//style and sources are in GeneralMapLibreFragment
private lateinit var locationComponent: LocationComponent
private lateinit var polylineSource: GeoJsonSource
private lateinit var polyArrowSource: GeoJsonSource
private var savedCameraPosition: CameraPosition? = null
private var vehShowing = ""
private var stopsLayerStarted = false
private var lastStopsSizeShown = 0
private var lastUpdateTime:Long = -2
//BUS POSITIONS
private val updatesByVehDict = HashMap(5)
private val animatorsByVeh = HashMap()
private var lastLocation : Location? = null
private var enablingPositionFromClick = false
private var polyline: LineString? = null
private val showUserPositionRequestLauncher =
registerForActivityResult(
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
setMapUserLocationEnabled(true, true, enablingPositionFromClick)
} else Log.w(DEBUG_TAG, "No location permission")
})
//private var stopPosList = ArrayList()
//fragment actions
private lateinit var fragmentListener: CommonFragmentListener
private var showOnTopOfLine = false
private var recyclerInitDone = false
private var useMQTTPositions = true
//position of live markers
private val tripMarkersAnimators = HashMap()
private val liveBusViewModel: LivePositionsViewModel by viewModels()
//extra items to use the LibreMap
private lateinit var symbolManager : SymbolManager
private var stopActiveSymbol: Symbol? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- lineID = requireArguments().getString(LINEID_KEY,"")
+ val args = requireArguments()
+ lineID = args.getString(LINEID_KEY,"")
+ stopIDFromToShow = args.getString(STOPID_FROM_KEY)
}
@SuppressLint("SetTextI18n")
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
//reset statuses
isBottomSheetShowing = false
//stopsLayerStarted = false
lastStopsSizeShown = 0
mapInitialized.set(false)
val rootView = inflater.inflate(R.layout.fragment_lines_detail, container, false)
//lineID = requireArguments().getString(LINEID_KEY, "")
arguments?.let {
lineID = it.getString(LINEID_KEY, "")
}
switchButton = rootView.findViewById(R.id.switchImageButton)
locationIcon = rootView.findViewById(R.id.locationEnableIcon)
favoritesButton = rootView.findViewById(R.id.favoritesButton)
stopsRecyclerView = rootView.findViewById(R.id.patternStopsRecyclerView)
descripTextView = rootView.findViewById(R.id.lineDescripTextView)
descripTextView.visibility = View.INVISIBLE
//map stuff
mapView = rootView.findViewById(R.id.lineMap)
mapView.getMapAsync(this)
//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)
bottomrightImage = bottomSheet.findViewById(R.id.rightmostImageView)
bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet)
// Setup close button
rootView.findViewById(R.id.btnClose).setOnClickListener {
hideStopBottomSheet()
}
val titleTextView = rootView.findViewById(R.id.titleTextView)
titleTextView.text = getString(R.string.line)+" "+FiveTNormalizer.fixShortNameForDisplay(
GtfsUtils.getLineNameFromGtfsID(lineID), true)
favoritesButton?.isClickable = true
favoritesButton?.setOnClickListener {
if(lineID.isNotEmpty())
PreferencesHolder.addOrRemoveLineToFavorites(requireContext(),lineID,!isLineInFavorite)
}
val preferences = PreferencesHolder.getMainSharedPreferences(requireContext())
val favorites = preferences.getStringSet(PreferencesHolder.PREF_FAVORITE_LINES, HashSet())
if(favorites!=null && favorites.contains(lineID)){
favoritesButton?.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_star_filled, null))
isLineInFavorite = true
}
appContext = requireContext().applicationContext
preferences.registerOnSharedPreferenceChangeListener(lineSharedPrefMonitor)
patternsSpinner = rootView.findViewById(R.id.patternsSpinner)
patternsAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, ArrayList())
patternsSpinner.adapter = patternsAdapter
initializeRecyclerView()
switchButton.setOnClickListener{
if(mapView.visibility == View.VISIBLE){
hideMapAndShowStopList()
} else{
hideStopListAndShowMap()
}
}
locationIcon?.let {view ->
if(!LocationUtils.isLocationEnabled(requireContext()) || !Permissions.anyLocationPermissionsGranted(requireContext()))
setLocationIconEnabled(false)
//set click Listener
view.setOnClickListener(this::onPositionIconButtonClick)
}
//set
//INITIALIZE VIEW MODELS
viewModel.setRouteIDQuery(lineID)
liveBusViewModel.setGtfsLineToFilterPos(lineID, null)
val keySourcePositions = getString(R.string.pref_positions_source)
useMQTTPositions = PreferenceManager.getDefaultSharedPreferences(requireContext())
.getString(keySourcePositions, "mqtt").contentEquals("mqtt")
viewModel.patternsWithStopsByRouteLiveData.observe(viewLifecycleOwner){
patterns -> savePatternsToShow(patterns)
}
/*
*/
viewModel.stopsForPatternLiveData.observe(viewLifecycleOwner) { stops ->
if(mapView.visibility ==View.VISIBLE)
patternShown?.let{
// We have the pattern and the stops here, time to display them
displayPatternWithStopsOnMap(it,stops, true)
} ?:{
Log.w(DEBUG_TAG, "The viewingPattern is null!")
}
else{
if(stopsRecyclerView.visibility==View.VISIBLE)
showStopsInRecyclerView(stops)
}
}
viewModel.gtfsRoute.observe(viewLifecycleOwner){route->
if(route == null){
//need to close the fragment
activity?.supportFragmentManager?.popBackStack()
return@observe
}
descripTextView.text = route.longName
descripTextView.visibility = View.VISIBLE
}
/*
*/
Log.d(DEBUG_TAG,"Data ${viewModel.stopsForPatternLiveData.value}")
//listeners
patternsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(p0: AdapterView<*>?, p1: View?, position: Int, p3: Long) {
val currentShownPattern = patternShown?.pattern
- val patternWithStops = currentPatterns.get(position)
- //viewModel.setPatternToDisplay(patternWithStops)
+ val patternWithStops = currentPatterns[position]
+
+ Log.d(DEBUG_TAG, "request stops for pattern ${patternWithStops.pattern.code}")
setPatternAndReqStops(patternWithStops)
- Log.d(DEBUG_TAG, "item Selected, cleaning bus markers")
if(mapView.visibility == View.VISIBLE) {
//Clear buses if we are changing direction
currentShownPattern?.let { patt ->
if(patt.directionId != patternWithStops.pattern.directionId){
stopAnimations()
updatesByVehDict.clear()
updatePositionsIcons(true)
liveBusViewModel.retriggerPositionUpdate()
}
}
}
liveBusViewModel.setGtfsLineToFilterPos(lineID, patternWithStops.pattern)
}
override fun onNothingSelected(p0: AdapterView<*>?) {
}
}
Log.d(DEBUG_TAG, "Views created!")
return rootView
}
// ------------- UI switch stuff ---------
private fun hideMapAndShowStopList(){
mapView.visibility = View.GONE
stopsRecyclerView.visibility = View.VISIBLE
locationIcon?.visibility = View.GONE
viewModel.setMapShowing(false)
if(useMQTTPositions) liveBusViewModel.stopMatoUpdates()
//map.overlayManager.remove(busPositionsOverlay)
switchButton.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.ic_map_white_30))
hideStopBottomSheet()
if(locationComponent.isLocationComponentEnabled){
locationComponent.isLocationComponentEnabled = false
shouldMapLocationBeReactivated = true
} else
shouldMapLocationBeReactivated = false
}
private fun hideStopListAndShowMap(){
stopsRecyclerView.visibility = View.GONE
mapView.visibility = View.VISIBLE
locationIcon?.visibility = View.VISIBLE
viewModel.setMapShowing(true)
//map.overlayManager.add(busPositionsOverlay)
//map.
if(useMQTTPositions)
liveBusViewModel.requestMatoPosUpdates(GtfsUtils.getLineNameFromGtfsID(lineID))
else
liveBusViewModel.requestGTFSUpdates()
switchButton.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.ic_list_30))
if(shouldMapLocationBeReactivated && Permissions.bothLocationPermissionsGranted(requireContext())){
locationComponent.isLocationComponentEnabled = true
}
}
private fun setLocationIconEnabled(setTrue: Boolean){
if(setTrue)
locationIcon?.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.location_circlew_red))
else
locationIcon?.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.location_circlew_grey))
}
/**
* Handles logic of enabling the user location on the map
*/
@SuppressLint("MissingPermission")
private fun setMapUserLocationEnabled(enabled: Boolean, assumePermissions: Boolean, fromClick: Boolean) {
if (enabled) {
val permissionOk = assumePermissions || Permissions.bothLocationPermissionsGranted(requireContext())
if (permissionOk) {
Log.d(DEBUG_TAG, "Permission OK, starting location component, assumed: $assumePermissions")
locationComponent.isLocationComponentEnabled = true
//locationComponent.cameraMode = CameraMode.TRACKING //CameraMode.TRACKING
setLocationIconEnabled(true)
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
setLocationIconEnabled(false)
if (fromClick) {
Toast.makeText(requireContext(), R.string.location_disabled, Toast.LENGTH_SHORT).show()
//TODO: Cancel the request for the enablement of the position if needed
}
}
}
/**
* Switch position icon from activ
*/
private fun onPositionIconButtonClick(view: View){
if(locationComponent.isLocationComponentEnabled) setMapUserLocationEnabled(false, false, true)
else{
setMapUserLocationEnabled(true, false, true)
}
}
// ------------- Map Code -------------------------
/**
* This method sets up the map and the layers
*/
override fun onMapReady(mapReady: MapLibreMap) {
this.map = mapReady
val context = requireContext()
val mjson = Styles.getJsonStyleFromAsset(context, PreferencesHolder.getMapLibreStyleFile(context)) //ViewUtils.loadJsonFromAsset(requireContext(),"map_style_good.json")
activity?.run {
val builder = Style.Builder().fromJson(mjson!!)
mapReady.setStyle(builder) { style ->
mapStyle = style
//setupLayers(style)
symbolManager = SymbolManager(mapView,mapReady,style)
symbolManager.iconAllowOverlap = true
symbolManager.textAllowOverlap = false
symbolManager.addClickListener{ _ ->
if (stopActiveSymbol!=null){
hideStopBottomSheet()
return@addClickListener true
} else
return@addClickListener false
}
// Start observing data
initMapUserLocation(style, mapReady, requireContext())
//if(!stopsLayerStarted)
initStopsPolyLineLayers(style, FeatureCollection.fromFeatures(ArrayList()), null, null)
if(patternShown!=null){
viewModel.stopsForPatternLiveData.value?.let {
Log.d(DEBUG_TAG, "Show stops from the cache")
displayPatternWithStopsOnMap(patternShown!!, it, true)
}
}
/*if(!stopsLayerStarted) {
Log.d(DEBUG_TAG, "Stop layer is not started yet")
initStopsPolyLineLayers(style, FeatureCollection.fromFeatures(ArrayList()), null)
}
*/
setupBusLayer(style)
mapViewModel.stopShowing?.let {
openStopInBottomSheet(it)
}
mapViewModel.stopShowing = null
toRunWhenMapReady?.run()
toRunWhenMapReady = null
mapInitialized.set(true)
}
mapReady.addOnMapClickListener { point ->
val screenPoint = mapReady.projection.toScreenLocation(point)
val features = mapReady.queryRenderedFeatures(screenPoint, STOPS_LAYER_ID)
val busNearby = mapReady.queryRenderedFeatures(screenPoint, BUSES_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 = viewModel.getStopByID(id)
stop?.let {
if (isBottomSheetShowing){
hideStopBottomSheet()
}
openStopInBottomSheet(it)
isBottomSheetShowing = true
//move camera
if(it.latitude!=null && it.longitude!=null)
mapReady.animateCamera(CameraUpdateFactory.newLatLng(LatLng(it.latitude!!,it.longitude!!)),750)
}
return@addOnMapClickListener true
} else if (busNearby.isNotEmpty()){
val feature = busNearby[0]
val vehid = feature.getStringProperty("veh")
val route = feature.getStringProperty("line")
if(isBottomSheetShowing && shownStopInBottomSheet!=null)
hideStopBottomSheet()
//if(context!=null){
// Toast.makeText(context, "Veh $vehid on route ${route.slice(0..route.length-2)}", Toast.LENGTH_SHORT).show()
//}
showVehicleTripInBottomSheet(vehid)
updatesByVehDict[vehid]?.let {
//if (it.posUpdate.latitude != null && it.longitude != null)
mapReady.animateCamera(
CameraUpdateFactory.newLatLng(LatLng(it.posUpdate.latitude, it.posUpdate.longitude)),
750
)
}
return@addOnMapClickListener true
}
false
}
// we start requesting the bus positions now
observeBusPositionUpdates()
}
/*savedMapStateOnPause?.let{
restoreMapStateFromBundle(it)
pendingLocationActivation = false
Log.d(DEBUG_TAG, "Restored map state from the saved bundle")
}
*/
val zoom = 12.0
val latlngTarget = LatLng(MapLibreFragment.DEFAULT_CENTER_LAT, MapLibreFragment.DEFAULT_CENTER_LON)
mapReady.cameraPosition = savedCameraPosition ?:CameraPosition.Builder().target(latlngTarget).zoom(zoom).build()
savedCameraPosition = null
if(shouldMapLocationBeReactivated) setMapUserLocationEnabled(true, false, false)
}
private fun observeBusPositionUpdates(){
//live bus positions
liveBusViewModel.filteredLocationUpdates.observe(viewLifecycleOwner){ updates ->
//Log.d(DEBUG_TAG, "Received ${updates.size} updates for the positions")
if(mapView.visibility == View.GONE || patternShown ==null){
//DO NOTHING
Log.w(DEBUG_TAG, "not doing anything because map is not visible")
return@observe
}
updateBusPositionsInMap(updates)
//if not using MQTT positions
if(!useMQTTPositions){
liveBusViewModel.requestDelayedGTFSUpdates(2000)
}
}
//download missing tripIDs
liveBusViewModel.tripsGtfsIDsToQuery.observe(viewLifecycleOwner){
//gtfsPosViewModel.downloadTripsFromMato(dat);
MatoTripsDownloadWorker.requestMatoTripsDownload(
it, requireContext().applicationContext,
"BusTO-MatoTripDownload"
)
}
}
/**
* Initialize the map location, but do not enable the component
*/
@SuppressLint("MissingPermission")
private fun initMapUserLocation(style: Style, map: MapLibreMap, context: Context){
locationComponent = map.locationComponent
val locationComponentOptions =
LocationComponentOptions.builder(context)
.pulseEnabled(false)
.build()
val locationComponentActivationOptions =
MapLibreUtils.buildLocationComponentActivationOptions(style, locationComponentOptions, context)
locationComponent.activateLocationComponent(locationComponentActivationOptions)
locationComponent.isLocationComponentEnabled = false
lastLocation?.let {
if (it.accuracy < 200)
locationComponent.forceLocationUpdate(it)
}
}
/**
* Update the bottom sheet with the stop information
*/
override fun openStopInBottomSheet(stop: Stop){
bottomLayout?.let {
//lay.findViewById(R.id.stopTitleTextView).text ="${stop.ID} - ${stop.stopDefaultName}"
val stopName = stop.stopUserName ?: stop.stopDefaultName
stopTitleTextView.text = stopName//stop.stopDefaultName
stopNumberTextView.text = stop.ID
stopTitleTextView.visibility = View.VISIBLE
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
//SET ON CLICK LISTENER
arrivalsCard.setOnClickListener{
fragmentListener?.requestArrivalsForStopID(stop.ID)
}
arrivalsCard.visibility = View.VISIBLE
directionsCard.setOnClickListener {
if(stop.latitude==null || stop.longitude==null){
//TODO: show message
Log.e(DEBUG_TAG, "Navigate to stop but longitude and/or latitude are null")
}else{
val uri = "geo:?q=${stop.latitude},${stop.longitude}(${stop.ID} - $stopName)"
val intent =Intent(Intent.ACTION_VIEW, Uri.parse(uri))
context?.run{
if(intent.resolveActivity(packageManager)!=null){
startActivity(intent)
}
}
}
}
bottomrightImage.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.navigation_right, activity?.theme))
}
//add stop marker
if (stop.latitude!=null && stop.longitude!=null) {
stopActiveSymbol = symbolManager.create(
SymbolOptions()
.withLatLng(LatLng(stop.latitude!!, stop.longitude!!))
.withIconImage(STOP_ACTIVE_IMG)
.withIconAnchor(ICON_ANCHOR_CENTER)
)
}
shownStopInBottomSheet = stop
bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
}
// 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
//remove initial stop
//if(initialStopToShow!=null){
// initialStopToShow = null
//}
shownStopInBottomSheet = null
vehShowing = ""
}
private fun showVehicleTripInBottomSheet(veh: String){
val data = updatesByVehDict[veh]
if(data==null) return
bottomLayout?.let {
val lineName = FiveTNormalizer.fixShortNameForDisplay(
GtfsUtils.getLineNameFromGtfsID(data.posUpdate.routeID), true)
stopNumberTextView.text = requireContext().getString(R.string.line_fill, lineName)
data.pattern?.let { pat ->
stopTitleTextView.text = pat.headsign
stopTitleTextView.visibility = View.VISIBLE
} ?:{
stopTitleTextView.visibility = View.GONE
}
linesPassingTextView.text = data.posUpdate.vehicle
}
arrivalsCard.visibility=View.GONE
bottomrightImage.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_magnifying_glass, activity?.theme))
directionsCard.setOnClickListener {
data.pattern?.let {
showPatternWithCode(it.code)
} //TODO
// ?: {
// context?.let { ctx -> Toast.makeText(ctx,"") }
//}
}
vehShowing = veh
bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
}
// ------- MAP LAYERS INITIALIZE ----
/**
* Initialize the map layers for the stops
*/
private fun initStopsPolyLineLayers(style: Style, stopFeatures:FeatureCollection, lineFeature: Feature?, arrowFeatures: FeatureCollection?){
Log.d(DEBUG_TAG, "INIT STOPS CALLED")
stopsSource = GeoJsonSource(STOPS_SOURCE_ID)
style.addSource(stopsSource)
//val context = requireContext()
val stopIcon = ResourcesCompat.getDrawable(resources,R.drawable.ball, activity?.theme)!!
val imgStop = ResourcesCompat.getDrawable(resources,R.drawable.bus_stop_new, activity?.theme)!!
val polyIconArrow = ResourcesCompat.getDrawable(resources, R.drawable.arrow_up_box_fill, activity?.theme)!!
//set the image tint
//DrawableCompat.setTint(imgBus,ContextCompat.getColor(context,R.color.line_drawn_poly))
// add icon
style.addImage(STOP_IMAGE_ID,stopIcon)
style.addImage(POLY_ARROW, polyIconArrow)
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)
)
polylineSource = GeoJsonSource(POLYLINE_SOURCE) //lineFeature?.let { GeoJsonSource(POLYLINE_SOURCE, it) } ?: GeoJsonSource(POLYLINE_SOURCE)
style.addSource(polylineSource)
val color=ContextCompat.getColor(requireContext(),R.color.line_drawn_poly)
//paint.style = Paint.Style.FILL_AND_STROKE
//paint.strokeJoin = Paint.Join.ROUND
//paint.strokeCap = Paint.Cap.ROUND
val lineLayer = LineLayer(POLYLINE_LAYER, POLYLINE_SOURCE).withProperties(
PropertyFactory.lineColor(color),
PropertyFactory.lineWidth(5.0f), //originally 13f
PropertyFactory.lineOpacity(1.0f),
PropertyFactory.lineJoin(Property.LINE_JOIN_ROUND),
PropertyFactory.lineCap(Property.LINE_CAP_ROUND)
)
polyArrowSource = GeoJsonSource(POLY_ARROWS_SOURCE, arrowFeatures)
style.addSource(polyArrowSource)
val arrowsLayer = SymbolLayer(POLY_ARROWS_LAYER, POLY_ARROWS_SOURCE).withProperties(
PropertyFactory.iconImage(POLY_ARROW),
PropertyFactory.iconRotate(Expression.get("bearing")),
PropertyFactory.iconRotationAlignment(ICON_ROTATION_ALIGNMENT_MAP)
)
val layers = style.layers
val lastLayers = layers.filter { l-> l.id.contains("city") }
//Log.d(DEBUG_TAG,"Layers:\n ${style.layers.map { l -> l.id }}")
Log.d(DEBUG_TAG, "City layers: ${lastLayers.map { l-> l.id }}")
if(lastLayers.isNotEmpty())
style.addLayerAbove(lineLayer,lastLayers[0].id)
else
style.addLayerBelow(lineLayer,"label_country_1")
style.addLayerAbove(stopsLayer, POLYLINE_LAYER)
style.addLayerAbove(arrowsLayer, POLYLINE_LAYER)
stopsLayerStarted = true
}
/**
* Setup the Map Layers
*/
private fun setupBusLayer(style: Style) {
// Buses source
busesSource = GeoJsonSource(BUSES_SOURCE_ID)
style.addSource(busesSource)
style.addImage("bus_symbol",ResourcesCompat.getDrawable(resources, R.drawable.map_bus_position_icon, activity?.theme)!!)
// Buses layer
val busesLayer = SymbolLayer(BUSES_LAYER_ID, BUSES_SOURCE_ID).apply {
withProperties(
PropertyFactory.iconImage("bus_symbol"),
//PropertyFactory.iconSize(1.2f),
PropertyFactory.iconAllowOverlap(true),
PropertyFactory.iconIgnorePlacement(true),
PropertyFactory.iconRotate(Expression.get("bearing")),
PropertyFactory.iconRotationAlignment(ICON_ROTATION_ALIGNMENT_MAP)
)
}
style.addLayerAbove(busesLayer, STOPS_LAYER_ID)
}
override fun onAttach(context: Context) {
super.onAttach(context)
if(context is CommonFragmentListener){
fragmentListener = context
} else throw RuntimeException("$context must implement CommonFragmentListener")
}
private fun stopAnimations(){
for(anim in animatorsByVeh.values){
anim.cancel()
}
}
+ /**
+ * Save the loaded pattern data, without the stops!
+ */
private fun savePatternsToShow(patterns: List){
currentPatterns = patterns.sortedWith(patternsSorter)
patternsAdapter?.let {
it.clear()
it.addAll(currentPatterns.map { p->"${p.pattern.directionId} - ${p.pattern.headsign}" })
it.notifyDataSetChanged()
}
+ // if we are loading from a stop, find it
+ val patternToShow = stopIDFromToShow?.let { sID ->
+ val stopGtfsID = "gtt:$sID"
+ var p: MatoPatternWithStops? = null
+ var pLength = 0
+ for(patt in currentPatterns){
+ for(pstop in patt.stopsIndices){
+ if(pstop.stopGtfsId == stopGtfsID){
+ //found
+ if (patt.stopsIndices.size>pLength){
+ p = patt
+ pLength = patt.stopsIndices.size
+ }
+ //break here, we have determined this pattern has the stop we're looking for
+ break
+ }
+ }
+ }
+ p
+ }
+ if(stopIDFromToShow!=null){
+ if(patternToShow==null)
+ Log.w(DEBUG_TAG, "We had to show the pattern from stop $stopIDFromToShow, but we didn't find it")
+ else
+ Log.d(DEBUG_TAG, "Requesting to show pattern from stop $stopIDFromToShow, found pattern ${patternToShow.pattern.code}")
+ }
+ //unset the stopID to show
+ if(patternToShow!=null) {
+
+ //showPattern(patternToShow)
+ patternShown = patternToShow
+ stopIDFromToShow = null
+ }
patternShown?.let {
showPattern(it)
}
}
/**
* Called when the position of the spinner is updated
*/
private fun setPatternAndReqStops(patternWithStops: MatoPatternWithStops){
Log.d(DEBUG_TAG, "Requesting stops for pattern ${patternWithStops.pattern.code}")
viewModel.selectedPatternLiveData.value = patternWithStops
viewModel.currentPatternStops.value = patternWithStops.stopsIndices.sortedBy { i-> i.order }
patternShown = patternWithStops
viewModel.requestStopsForPatternWithStops(patternWithStops)
}
private fun showPattern(patternWs: MatoPatternWithStops){
- Log.d(DEBUG_TAG, "Finding pattern to show: ${patternWs.pattern.code}")
+ //Log.d(DEBUG_TAG, "Finding pattern to show: ${patternWs.pattern.code}")
var pos = -2
val code = patternWs.pattern.code.trim()
for (k in currentPatterns.indices) {
if (currentPatterns[k].pattern.code.trim() == code) {
pos = k
break
}
}
- Log.d(DEBUG_TAG, "Found pattern $code in position: $pos")
- if (pos >= 0)
+ Log.d(DEBUG_TAG, "Requesting stops fro pattern $code in position: $pos")
+ if (pos !=-2)
patternsSpinner.setSelection(pos)
- //set pattern
- setPatternAndReqStops(patternWs)
+ else
+ Log.e(DEBUG_TAG, "Pattern with code $code not found!!")
+ //request pattern stops from DB
+ //setPatternAndReqStops(patternWs)
}
private fun zoomToCurrentPattern(){
if(polyline==null) return
val NULL_VALUE = -4000.0
var maxLat = NULL_VALUE
var minLat = NULL_VALUE
var minLong = NULL_VALUE
var maxLong = NULL_VALUE
polyline?.let {
for(p in it.coordinates()){
val lat = p.latitude()
val lon = p.longitude()
// get max latitude
if(maxLat == NULL_VALUE)
maxLat =lat
else if (maxLat < lat) maxLat = lat
// find min latitude
if (minLat ==NULL_VALUE)
minLat = lat
else if (minLat > lat) minLat = lat
if(maxLong == NULL_VALUE || maxLong < lon )
maxLong = lon
if (minLong == NULL_VALUE || minLong > lon)
minLong = lon
}
val padding = 50 // Pixel di padding intorno ai limiti
Log.d(DEBUG_TAG, "Setting limits of bounding box of line: $minLat -> $maxLat, $minLong -> $maxLong")
val bbox = LatLngBounds.from(maxLat,maxLong, minLat, minLong)
//map.zoomToBoundingBox(BoundingBox(maxLat+del, maxLong+del, minLat-del, minLong-del), false)
map?.animateCamera(CameraUpdateFactory.newLatLngBounds(bbox, padding))
}
}
private fun displayPatternWithStopsOnMap(patternWs: MatoPatternWithStops, stopsToSort: List, zoomToPattern: Boolean){
if(!mapInitialized.get()){
//set the runnable and do nothing else
Log.d(DEBUG_TAG, "Delaying pattern display to when map is Ready: ${patternWs.pattern.code}")
toRunWhenMapReady = Runnable {
displayPatternWithStopsOnMap(patternWs, stopsToSort, zoomToPattern)
}
return
}
Log.d(DEBUG_TAG, "Got the stops: ${stopsToSort.map { s->s.gtfsID }}}")
patternShown = patternWs
//Problem: stops are not sorted
val stopOrderD = patternWs.stopsIndices.withIndex().associate{it.value.stopGtfsId to it.index}
val stopsSorted = stopsToSort.sortedBy { s-> stopOrderD[s.gtfsID] }
val pattern = patternWs.pattern
val pointsList = PolylineParser.decodePolyline(pattern.patternGeometryPoly, pattern.patternGeometryLength)
val pointsToShow = pointsList.map { Point.fromLngLat(it.longitude, it.latitude) }
Log.d(DEBUG_TAG, "The polyline has ${pointsToShow.size} points to display")
polyline = LineString.fromLngLats(pointsToShow)
val lineFeature = Feature.fromGeometry(polyline)
//Log.d(DEBUG_TAG, "Polyline in JSON is: ${lineFeature.toJson()}")
// --- STOPS---
val features = ArrayList()
for (s in stopsSorted){
if (s.latitude!=null && s.longitude!=null) {
val loc = if (showOnTopOfLine) findOptimalPosition(s, pointsList)
else LatLng(s.latitude!!, s.longitude!!)
features.add(
Feature.fromGeometry(
Point.fromLngLat(loc.longitude, loc.latitude),
JsonObject().apply {
addProperty("id", s.ID)
addProperty("name", s.stopDefaultName)
//addProperty("routes", s.routesThatStopHereToString()) // Add routes array to JSON object
}
)
)
}
}
// -- ARROWS --
//val splitPolyline = MapLibreUtils.splitPolyWhenDistanceTooBig(pointsList, 200.0)
val arrowFeatures = ArrayList()
val pointsIndexToShowIcon = MapLibreUtils.findPointsToPutDirectionMarkers(pointsList, stopsSorted, 900.0)
for (idx in pointsIndexToShowIcon){
val pnow = pointsList[idx]
val otherp = if(idx>1) pointsList[idx-1] else pointsList[idx+1]
val bearing = if (idx>1) MapLibreUtils.getBearing(pointsList[idx-1], pnow) else MapLibreUtils.getBearing(pnow, pointsList[idx+1])
arrowFeatures.add(Feature.fromGeometry(
Point.fromLngLat((pnow.longitude+otherp.longitude)/2, (pnow.latitude+otherp.latitude)/2 ), //average
JsonObject().apply {
addProperty("bearing", bearing)
}
))
}
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 (stopsLayerStarted) {
stopsSource.setGeoJson(FeatureCollection.fromFeatures(features))
polylineSource.setGeoJson(lineFeature)
polyArrowSource.setGeoJson(FeatureCollection.fromFeatures(arrowFeatures))
lastStopsSizeShown = features.size
} else
map?.let {
Log.d(DEBUG_TAG, "Map stop layer is not started yet, init layer")
initStopsPolyLineLayers(mapStyle, FeatureCollection.fromFeatures(features),lineFeature, FeatureCollection.fromFeatures(arrowFeatures))
Log.d(DEBUG_TAG,"Started stops layer on map")
lastStopsSizeShown = features.size
stopsLayerStarted = true
} ?:{
Log.e(DEBUG_TAG, "Stops layer is not started!!")
}
/* OLD CODE
for(s in stops){
val gp =
val marker = MarkerUtils.makeMarker(
gp, s.ID, s.stopDefaultName,
s.routesThatStopHereToString(),
map,stopTouchResponder, stopIcon,
R.layout.linedetail_stop_infowindow,
R.color.line_drawn_poly
)
marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
stopsOverlay.add(marker)
}
*/
//POINTS LIST IS NOT IN ORDER ANY MORE
//if(!map.overlayManager.contains(stopsOverlay)){
// map.overlayManager.add(stopsOverlay)
//}
if(zoomToPattern) zoomToCurrentPattern()
//map.invalidate()
}
private fun initializeRecyclerView(){
val llManager = LinearLayoutManager(context)
llManager.orientation = LinearLayoutManager.VERTICAL
stopsRecyclerView.layoutManager = llManager
}
private fun showStopsInRecyclerView(stops: List){
Log.d(DEBUG_TAG, "Setting stops from: "+viewModel.currentPatternStops.value)
val orderBy = viewModel.currentPatternStops.value!!.withIndex().associate{it.value.stopGtfsId to it.index}
val stopsSorted = stops.sortedBy { s -> orderBy[s.gtfsID] }
val numStops = stopsSorted.size
Log.d(DEBUG_TAG, "RecyclerView adapter is: ${stopsRecyclerView.adapter}")
val setNewAdapter = true
if(setNewAdapter){
stopsRecyclerView.adapter = StopRecyclerAdapter(
stopsSorted, stopAdapterListener, StopRecyclerAdapter.Use.LINES,
NameCapitalize.FIRST
)
}
}
/**
* This method fixes the display of the pattern, to be used when clicking on a bus
*/
private fun showPatternWithCode(patternId: String){
//var index = 0
Log.d(DEBUG_TAG, "Showing pattern with code $patternId ")
for (i in currentPatterns.indices){
val pattStop = currentPatterns[i]
if(pattStop.pattern.code == patternId){
Log.d(DEBUG_TAG, "Pattern found in position $i")
//setPatternAndReqStops(pattStop)
patternsSpinner.setSelection(i)
break
}
}
}
/**
* Update function for the bus positions
* Takes the processed updates and saves them accordingly
* Copied from MapLibreFragment, removing the labels
*/
private fun updateBusPositionsInMap(incomingData: HashMap>){
val vehsNew = HashSet(incomingData.values.map { up -> up.first.vehicle })
val vehsOld = HashSet(updatesByVehDict.keys)
Log.d(DEBUG_TAG, "In fragment, have ${incomingData.size} updates to show")
var countUpds = 0
//val symbolsToUpdate = ArrayList()
for (upsWithTrp in incomingData.values){
val pos = upsWithTrp.first
val patternStops = upsWithTrp.second
val vehID = pos.vehicle
var animate = false
if (vehsOld.contains(vehID)){
//update position only if the starting or the stopping position of the animation are in the view
val oldPos = updatesByVehDict[vehID]?.posUpdate
var avoidShowingUpdateBecauseIsImpossible = false
oldPos?.let{
if(it.routeID!=pos.routeID) {
val dist = LatLng(it.latitude, it.longitude).distanceTo(LatLng(pos.latitude, pos.longitude))
val speed = dist*3.6 / (pos.timestamp - it.timestamp) //this should be in km/h
Log.w(DEBUG_TAG, "Vehicle $vehID changed route from ${oldPos.routeID} to ${pos.routeID}, distance: $dist, speed: $speed")
if (speed > 120 || speed < 0){
avoidShowingUpdateBecauseIsImpossible = true
}
}
}
if (avoidShowingUpdateBecauseIsImpossible){
// DO NOT SHOW THIS SHIT
Log.w(DEBUG_TAG, "Update for vehicle $vehID skipped")
continue
}
val samePosition = oldPos?.let { (it.latitude==pos.latitude)&&(it.longitude == pos.longitude) }?:false
if(!samePosition) {
//val isPositionInBounds = isInsideVisibleRegion(
// pos.latitude, pos.longitude, true
//) || (oldPos?.let { isInsideVisibleRegion(it.latitude,it.longitude,true) } ?: false)
val skip = true
if (skip) {
//animate = true
//this moves both the icon and the label
moveVehicleToNewPosition(pos)
} else {
//update
updatesByVehDict[vehID] = LivePositionTripPattern(pos,patternStops?.pattern)
/*busLabelSymbolsByVeh[vehID]?.let {
it.latLng = LatLng(pos.latitude, pos.longitude)
symbolsToUpdate.add(it)
}*/
//if(vehShowing==vehID)
// map?.animateCamera(CameraUpdateFactory.newLatLng(LatLng(pos.latitude, pos.longitude)),500)
//TODO: Follow the vehicle
}
}
countUpds++
}
else{
//not inside
// update it simply
updatesByVehDict[vehID] = LivePositionTripPattern(pos, patternStops?.pattern)
//createLabelForVehicle(pos)
//if(vehShowing==vehID)
// map?.animateCamera(CameraUpdateFactory.newLatLng(LatLng(pos.latitude, pos.longitude)),500)
}
if (vehID == vehShowing){
//update the data
showVehicleTripInBottomSheet(vehID)
}
}
//symbolManager.update(symbolsToUpdate)
//remove old positions
Log.d(DEBUG_TAG, "Updated $countUpds vehicles")
vehsOld.removeAll(vehsNew)
//now vehsOld contains the vehicles id for those that have NOT been updated
val currentTimeStamp = System.currentTimeMillis() /1000
for(vehID in vehsOld){
//remove after 2 minutes of inactivity
if (updatesByVehDict[vehID]!!.posUpdate.timestamp - currentTimeStamp > 2*60){
updatesByVehDict.remove(vehID)
//removeVehicleLabel(vehID)
}
}
//update UI
updatePositionsIcons(false)
}
/**
* This is the tricky part, animating the transitions
* Basically, we need to set the new positions with the data and redraw them all
*/
private fun moveVehicleToNewPosition(positionUpdate: LivePositionUpdate){
if (positionUpdate.vehicle !in updatesByVehDict.keys)
return
val vehID = positionUpdate.vehicle
val currentUpdate = updatesByVehDict[positionUpdate.vehicle]
currentUpdate?.let { it ->
//cancel current animation on vehicle
animatorsByVeh[vehID]?.cancel()
val posUp = it.posUpdate
val currentPos = LatLng(posUp.latitude, posUp.longitude)
val newPos = LatLng(positionUpdate.latitude, positionUpdate.longitude)
val valueAnimator = ValueAnimator.ofObject(MapLibreUtils.LatLngEvaluator(), currentPos, newPos)
valueAnimator.addUpdateListener(object : ValueAnimator.AnimatorUpdateListener {
private var latLng: LatLng? = null
override fun onAnimationUpdate(animation: ValueAnimator) {
latLng = animation.animatedValue as LatLng
//update position on animation
val update = updatesByVehDict[positionUpdate.vehicle]!!
latLng?.let { ll->
update.posUpdate.latitude = ll.latitude
update.posUpdate.longitude = ll.longitude
updatePositionsIcons(false)
}
}
})
/*valueAnimator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator) {
super.onAnimationStart(animation)
//val update = positionsByVehDict[positionUpdate.vehicle]!!
//remove the label at the start of the animation
//removeVehicleLabel(vehID)
val annot = busLabelSymbolsByVeh[vehID]
annot?.let { sym ->
sym.textOpacity = 0.0f
symbolsToUpdate.add(sym)
}
}
override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
/*val annot = busLabelSymbolsByVeh[vehID]
annot?.let { sym ->
sym.textOpacity = 1.0f
sym.latLng = newPos //LatLng(newPos)
symbolsToUpdate.add(sym)
}
*/
}
})
*/
animatorsByVeh[vehID]?.cancel()
//set the new position as the current one but with the old lat and lng
positionUpdate.latitude = posUp.latitude
positionUpdate.longitude = posUp.longitude
updatesByVehDict[vehID]!!.posUpdate = positionUpdate
valueAnimator.duration = 300
valueAnimator.interpolator = LinearInterpolator()
valueAnimator.start()
animatorsByVeh[vehID] = valueAnimator
} ?: {
Log.e(DEBUG_TAG, "Have to run animation for veh ${positionUpdate.vehicle} but not in the dict, adding")
//updatesByVehDict[positionUpdate.vehicle] = positionUpdate
}
}
/**
* Update the bus positions displayed on the map, from the existing data
*/
private fun updatePositionsIcons(forced: Boolean){
//avoid frequent updates
val currentTime = System.currentTimeMillis()
if(!forced && currentTime - lastUpdateTime < 60){
//DO NOT UPDATE THE MAP
return
}
val features = ArrayList()//stops.mapNotNull { stop ->
//stop.latitude?.let { lat ->
// stop.longitude?.let { lon ->
for (dat in updatesByVehDict.values){
//if (s.latitude!=null && s.longitude!=null)
val pos = dat.posUpdate
val point = Point.fromLngLat(pos.longitude, pos.latitude)
features.add(
Feature.fromGeometry(
point,
JsonObject().apply {
addProperty("veh", pos.vehicle)
addProperty("trip", pos.tripID)
addProperty("bearing", pos.bearing ?:0.0f)
addProperty("line", pos.routeID)
}
)
)
/*busLabelSymbolsByVeh[pos.vehicle]?.let {
it.latLng = LatLng(pos.latitude, pos.longitude)
symbolsToUpdate.add(it)
}
*/
}
busesSource.setGeoJson(FeatureCollection.fromFeatures(features))
//update labels, clear cache to be used
//symbolManager.update(symbolsToUpdate)
//symbolsToUpdate.clear()
lastUpdateTime = System.currentTimeMillis()
}
override fun onResume() {
super.onResume()
Log.d(DEBUG_TAG, "Resetting paused from onResume")
mapView.onResume()
pausedFragment = false
val keySourcePositions = getString(R.string.pref_positions_source)
useMQTTPositions = PreferenceManager.getDefaultSharedPreferences(requireContext())
.getString(keySourcePositions, "mqtt").contentEquals("mqtt")
//separate paths
if(useMQTTPositions)
liveBusViewModel.requestMatoPosUpdates(GtfsUtils.getLineNameFromGtfsID(lineID))
else
liveBusViewModel.requestGTFSUpdates()
if(mapViewModel.currentLat.value!=MapViewModel.INVALID) {
Log.d(DEBUG_TAG, "mapViewModel posi: ${mapViewModel.currentLat.value}, ${mapViewModel.currentLong.value}"+
" zoom ${mapViewModel.currentZoom.value}")
//THIS WAS A FIX FOR THE OLD OSMDROID MAP
/*val controller = map.controller
viewLifecycleOwner.lifecycleScope.launch {
delay(100)
Log.d(DEBUG_TAG, "zooming back to point")
controller.animateTo(GeoPoint(mapViewModel.currentLat.value!!, mapViewModel.currentLong.value!!),
mapViewModel.currentZoom.value!!,null,null)
//controller.setCenter(GeoPoint(mapViewModel.currentLat.value!!, mapViewModel.currentLong.value!!))
//controller.setZoom(mapViewModel.currentZoom.value!!)
}
*/
}
//initialize GUI here
fragmentListener.readyGUIfor(FragmentKind.LINES)
}
override fun onPause() {
super.onPause()
mapView.onPause()
if(useMQTTPositions) liveBusViewModel.stopMatoUpdates()
pausedFragment = true
//save map
val camera = map?.cameraPosition
camera?.let {cam->
mapViewModel.currentLat.value = cam.target?.latitude ?: -400.0
mapViewModel.currentLong.value = cam.target?.longitude ?: -400.0
mapViewModel.currentZoom.value = cam.zoom
}
}
override fun onStart() {
super.onStart()
mapView.onStart()
}
override fun onDestroy() {
super.onDestroy()
mapView.onDestroy()
}
override fun onStop() {
super.onStop()
mapView.onStop()
shownStopInBottomSheet?.let {
mapViewModel.stopShowing = it
}
shouldMapLocationBeReactivated = locationComponent.isLocationComponentEnabled
}
override fun onDestroyView() {
map?.run {
Log.d(DEBUG_TAG, "Saving camera position")
savedCameraPosition = cameraPosition
}
super.onDestroyView()
Log.d(DEBUG_TAG, "Destroying the views")
/*mapStyle.removeLayer(STOPS_LAYER_ID)
mapStyle?.removeSource(STOPS_SOURCE_ID)
mapStyle.removeLayer(POLYLINE_LAYER)
mapStyle.removeSource(POLYLINE_SOURCE)
*/
//stopsLayerStarted = false
}
override fun onMapDestroy() {
mapStyle.removeLayer(STOPS_LAYER_ID)
mapStyle.removeSource(STOPS_SOURCE_ID)
mapStyle.removeLayer(POLYLINE_LAYER)
mapStyle.removeSource(POLYLINE_SOURCE)
mapStyle.removeLayer(BUSES_LAYER_ID)
mapStyle.removeSource(BUSES_SOURCE_ID)
map?.locationComponent?.isLocationComponentEnabled = false
}
override fun getBaseViewForSnackBar(): View? {
return null
}
companion object {
private const val LINEID_KEY="lineID"
+ private const val STOPID_FROM_KEY="stopID"
private const val STOPS_SOURCE_ID = "stops-source"
private const val STOPS_LAYER_ID = "stops-layer"
private const val STOP_ACTIVE_IMG = "stop_active_img"
private const val STOP_IMAGE_ID = "stop-img"
private const val POLYLINE_LAYER = "polyline-layer"
private const val POLYLINE_SOURCE = "polyline-source"
private const val POLY_ARROWS_LAYER = "arrows-layer"
private const val POLY_ARROWS_SOURCE = "arrows-source"
private const val POLY_ARROW ="poly-arrow-img"
private const val DEBUG_TAG="BusTO-LineDetailFragment"
- fun makeArgs(lineID: String): Bundle{
+ fun makeArgs(lineID: String, stopIDFrom: String?): Bundle{
val b = Bundle()
b.putString(LINEID_KEY, lineID)
+ b.putString(STOPID_FROM_KEY, stopIDFrom)
return b
}
- fun newInstance(lineID: String?) = LinesDetailFragment().apply {
- lineID?.let { arguments = makeArgs(it) }
+ fun newInstance(lineID: String?, stopIDFrom: String?) = LinesDetailFragment().apply {
+ lineID?.let { arguments = makeArgs(it, stopIDFrom) }
}
@JvmStatic
private fun findOptimalPosition(stop: Stop, pointsList: MutableList): LatLng{
if(stop.latitude==null || stop.longitude ==null|| pointsList.isEmpty())
throw IllegalArgumentException()
val sLat = stop.latitude!!
val sLong = stop.longitude!!
if(pointsList.size < 2)
return pointsList[0]
pointsList.sortBy { utils.measuredistanceBetween(sLat, sLong, it.latitude, it.longitude) }
val p1 = pointsList[0]
val p2 = pointsList[1]
if (p1.longitude == p2.longitude){
//Log.e(DEBUG_TAG, "Same longitude")
return LatLng(sLat, p1.longitude)
} else if (p1.latitude == p2.latitude){
//Log.d(DEBUG_TAG, "Same latitude")
return LatLng(p2.latitude,sLong)
}
val m = (p1.latitude - p2.latitude) / (p1.longitude - p2.longitude)
val minv = (p1.longitude-p2.longitude)/(p1.latitude - p2.latitude)
val cR = p1.latitude - p1.longitude * m
val longNew = (minv * sLong + sLat -cR ) / (m+minv)
val latNew = (m*longNew + cR)
//Log.d(DEBUG_TAG,"Stop ${stop.ID} old pos: ($sLat, $sLong), new pos ($latNew,$longNew)")
return LatLng(latNew,longNew)
}
private const val DEFAULT_CENTER_LAT = 45.12
private const val DEFAULT_CENTER_LON = 7.6858
}
enum class BottomShowing{
STOP, VEHICLE
}
}
\ No newline at end of file
diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/LinesGridShowingFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/LinesGridShowingFragment.kt
index 7ab74d9..cb9870b 100644
--- a/app/src/main/java/it/reyboz/bustorino/fragments/LinesGridShowingFragment.kt
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/LinesGridShowingFragment.kt
@@ -1,337 +1,337 @@
package it.reyboz.bustorino.fragments
import android.content.Context
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.Animation
import android.view.animation.LinearInterpolator
import android.view.animation.RotateAnimation
import android.widget.ImageView
import android.widget.TextView
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.RecyclerView
import it.reyboz.bustorino.R
import it.reyboz.bustorino.adapters.RouteAdapter
import it.reyboz.bustorino.adapters.RouteOnlyLineAdapter
import it.reyboz.bustorino.backend.utils
import it.reyboz.bustorino.data.PreferencesHolder
import it.reyboz.bustorino.data.gtfs.GtfsRoute
import it.reyboz.bustorino.middleware.AutoFitGridLayoutManager
import it.reyboz.bustorino.util.LinesNameSorter
import it.reyboz.bustorino.util.ViewUtils
import it.reyboz.bustorino.viewmodels.LinesGridShowingViewModel
class LinesGridShowingFragment : ScreenBaseFragment() {
private val viewModel: LinesGridShowingViewModel by viewModels()
//private lateinit var gridLayoutManager: AutoFitGridLayoutManager
private lateinit var favoritesRecyclerView: RecyclerView
private lateinit var urbanRecyclerView: RecyclerView
private lateinit var extraurbanRecyclerView: RecyclerView
private lateinit var touristRecyclerView: RecyclerView
private lateinit var favoritesTitle: TextView
private lateinit var urbanLinesTitle: TextView
private lateinit var extrurbanLinesTitle: TextView
private lateinit var touristLinesTitle: TextView
private var routesByAgency = HashMap>()
/*hashMapOf(
AG_URBAN to ArrayList(),
AG_EXTRAURB to ArrayList(),
AG_TOUR to ArrayList()
)*/
private lateinit var fragmentListener: CommonFragmentListener
private val linesNameSorter = LinesNameSorter()
private val linesComparator = Comparator { a,b ->
return@Comparator linesNameSorter.compare(a.shortName, b.shortName)
}
private val routeClickListener = RouteAdapter.ItemClicker {
- fragmentListener.showLineOnMap(it.gtfsId)
+ fragmentListener.showLineOnMap(it.gtfsId, null)
}
private val arrows = HashMap()
private val durations = HashMap()
private var openRecyclerView = "AG_URBAN"
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val rootView = inflater.inflate(R.layout.fragment_lines_grid, container, false)
favoritesRecyclerView = rootView.findViewById(R.id.favoritesRecyclerView)
urbanRecyclerView = rootView.findViewById(R.id.urbanLinesRecyclerView)
extraurbanRecyclerView = rootView.findViewById(R.id.extraurbanLinesRecyclerView)
touristRecyclerView = rootView.findViewById(R.id.touristLinesRecyclerView)
favoritesTitle = rootView.findViewById(R.id.favoritesTitleView)
urbanLinesTitle = rootView.findViewById(R.id.urbanLinesTitleView)
extrurbanLinesTitle = rootView.findViewById(R.id.extraurbanLinesTitleView)
touristLinesTitle = rootView.findViewById(R.id.touristLinesTitleView)
arrows[AG_URBAN] = rootView.findViewById(R.id.arrowUrb)
arrows[AG_TOUR] = rootView.findViewById(R.id.arrowTourist)
arrows[AG_EXTRAURB] = rootView.findViewById(R.id.arrowExtraurban)
arrows[AG_FAV] = rootView.findViewById(R.id.arrowFavorites)
//show urban expanded by default
val recViews = listOf(urbanRecyclerView, extraurbanRecyclerView, touristRecyclerView)
for (recyView in recViews) {
val gridLayoutManager = AutoFitGridLayoutManager(
requireContext().applicationContext,
(utils.convertDipToPixels(context, COLUMN_WIDTH_DP.toFloat())).toInt()
)
recyView.layoutManager = gridLayoutManager
}
//init favorites recyclerview
val gridLayoutManager = AutoFitGridLayoutManager(
requireContext().applicationContext,
(utils.convertDipToPixels(context, 70f)).toInt()
)
favoritesRecyclerView.layoutManager = gridLayoutManager
viewModel.routesLiveData.observe(viewLifecycleOwner){
//routesList = ArrayList(it)
//routesList.sortWith(linesComparator)
routesByAgency.clear()
for(route in it){
val agency = route.agencyID
if(!routesByAgency.containsKey(agency)){
routesByAgency[agency] = ArrayList()
}
routesByAgency[agency]?.add(route)
}
//val adapter = RouteOnlyLineAdapter(routesByAgency.map { route-> route.shortName })
//zip agencies and recyclerviews
Companion.AGENCIES.zip(recViews) { ag, recView ->
routesByAgency[ag]?.let { routeList ->
routeList.sortWith(linesComparator)
//val adapter = RouteOnlyLineAdapter(it.map { rt -> rt.shortName })
val adapter = RouteAdapter(routeList,routeClickListener)
recView.adapter = adapter
durations[ag] = if(routeList.size < 20) ViewUtils.DEF_DURATION else 1000
}
}
}
viewModel.favoritesLines.observe(viewLifecycleOwner){ routes->
val routesNames = routes.map { it.shortName }
//create new item click listener every time
val adapter = RouteOnlyLineAdapter(routesNames){ pos, _ ->
val r = routes[pos]
- fragmentListener.showLineOnMap(r.gtfsId)
+ fragmentListener.showLineOnMap(r.gtfsId, null)
}
favoritesRecyclerView.adapter = adapter
}
//onClicks
urbanLinesTitle.setOnClickListener {
openLinesAndCloseOthersIfNeeded(AG_URBAN)
}
extrurbanLinesTitle.setOnClickListener {
openLinesAndCloseOthersIfNeeded(AG_EXTRAURB)
}
touristLinesTitle.setOnClickListener {
openLinesAndCloseOthersIfNeeded(AG_TOUR)
}
favoritesTitle.setOnClickListener {
closeOpenFavorites()
}
arrows[AG_FAV]?.setOnClickListener {
closeOpenFavorites()
}
//arrows onClicks
for(k in Companion.AGENCIES){
//k is either AG_TOUR, AG_EXTRAURBAN, AG_URBAN
arrows[k]?.setOnClickListener { openLinesAndCloseOthersIfNeeded(k) }
}
return rootView
}
private fun closeOpenFavorites(){
if(favoritesRecyclerView.visibility == View.VISIBLE){
//close it
favoritesRecyclerView.visibility = View.GONE
setOpen(arrows[AG_FAV]!!, false)
viewModel.favoritesExpanded.value = false
} else{
favoritesRecyclerView.visibility = View.VISIBLE
setOpen(arrows[AG_FAV]!!, true)
viewModel.favoritesExpanded.value = true
}
}
private fun openLinesAndCloseOthersIfNeeded(agency: String){
if(openRecyclerView!="" && openRecyclerView!= agency) {
switchRecyclerViewStatus(openRecyclerView)
}
switchRecyclerViewStatus(agency)
}
private fun switchRecyclerViewStatus(agency: String){
val recyclerView = when(agency){
AG_TOUR -> touristRecyclerView
AG_EXTRAURB -> extraurbanRecyclerView
AG_URBAN -> urbanRecyclerView
else -> throw IllegalArgumentException("$DEBUG_TAG: Agency Invalid")
}
val expandedLiveData = when(agency){
AG_TOUR -> viewModel.isTouristExpanded
AG_URBAN -> viewModel.isUrbanExpanded
AG_EXTRAURB -> viewModel.isExtraUrbanExpanded
else -> throw IllegalArgumentException("$DEBUG_TAG: Agency Invalid")
}
val duration = durations[agency]
val arrow = arrows[agency]
val durArrow = if(duration == null || duration==ViewUtils.DEF_DURATION) 500 else duration
if(duration!=null&&arrow!=null)
when (recyclerView.visibility){
View.GONE -> {
Log.d(DEBUG_TAG, "Open recyclerview $agency")
//val a =ViewUtils.expand(recyclerView, duration, 0)
recyclerView.visibility = View.VISIBLE
expandedLiveData.value = true
Log.d(DEBUG_TAG, "Arrow for $agency has rotation: ${arrow.rotation}")
setOpen(arrow, true)
//arrow.startAnimation(rotateArrow(true,durArrow))
openRecyclerView = agency
}
View.VISIBLE -> {
Log.d(DEBUG_TAG, "Close recyclerview $agency")
//ViewUtils.collapse(recyclerView, duration)
recyclerView.visibility = View.GONE
expandedLiveData.value = false
//arrow.rotation = 90f
Log.d(DEBUG_TAG, "Arrow for $agency has rotation ${arrow.rotation} pre-rotate")
setOpen(arrow, false)
//arrow.startAnimation(rotateArrow(false,durArrow))
openRecyclerView = ""
}
View.INVISIBLE -> {
TODO()
}
}
}
override fun onAttach(context: Context) {
super.onAttach(context)
if(context is CommonFragmentListener){
fragmentListener = context
} else throw RuntimeException("$context must implement CommonFragmentListener")
}
override fun getBaseViewForSnackBar(): View? {
return null
}
override fun onResume() {
super.onResume()
val pref = PreferencesHolder.getMainSharedPreferences(requireContext())
val res = pref.getStringSet(PreferencesHolder.PREF_FAVORITE_LINES, HashSet())
res?.let { viewModel.setFavoritesLinesIDs(HashSet(it))}
//restore state
viewModel.favoritesExpanded.value?.let {
if(!it){
//close it
favoritesRecyclerView.visibility = View.GONE
setOpen(arrows[AG_FAV]!!, false)
} else{
favoritesRecyclerView.visibility = View.VISIBLE
setOpen(arrows[AG_FAV]!!, true)
}
}
viewModel.isUrbanExpanded.value?.let {
if(it) {
urbanRecyclerView.visibility = View.VISIBLE
arrows[AG_URBAN]?.rotation= 90f
openRecyclerView = AG_URBAN
Log.d(DEBUG_TAG, "RecyclerView gtt:U is expanded")
}
else {
urbanRecyclerView.visibility = View.GONE
arrows[AG_URBAN]?.rotation= 0f
}
}
viewModel.isTouristExpanded.value?.let {
val recview = touristRecyclerView
if(it) {
recview.visibility = View.VISIBLE
arrows[AG_TOUR]?.rotation=90f
openRecyclerView = AG_TOUR
} else {
recview.visibility = View.GONE
arrows[AG_TOUR]?.rotation= 0f
}
}
viewModel.isExtraUrbanExpanded.value?.let {
val recview = extraurbanRecyclerView
if(it) {
openRecyclerView = AG_EXTRAURB
recview.visibility = View.VISIBLE
arrows[AG_EXTRAURB]?.rotation=90f
} else {
recview.visibility = View.GONE
arrows[AG_EXTRAURB]?.rotation=0f
}
}
fragmentListener.readyGUIfor(FragmentKind.LINES)
}
companion object {
private const val COLUMN_WIDTH_DP=200
private const val AG_FAV = "fav"
private const val AG_URBAN = "gtt:U"
private const val AG_EXTRAURB ="gtt:E"
private const val AG_TOUR ="gtt:T"
private const val DEBUG_TAG ="BusTO-LinesGridFragment"
const val FRAGMENT_TAG = "LinesGridShowingFragment"
private val AGENCIES = listOf(AG_URBAN, AG_EXTRAURB, AG_TOUR)
fun newInstance() = LinesGridShowingFragment()
@JvmStatic
fun setOpen(imageView: ImageView, value: Boolean){
if(value)
imageView.rotation = 90f
else
imageView.rotation = 0f
}
@JvmStatic
fun rotateArrow(toOpen: Boolean, duration: Long): RotateAnimation{
val start = if (toOpen) 0f else 90f
val stop = if(toOpen) 90f else 0f
Log.d(DEBUG_TAG, "Rotate arrow from $start to $stop")
val rotate = RotateAnimation(start, stop, Animation.RELATIVE_TO_SELF,
0.5f, Animation.RELATIVE_TO_SELF, 0.5f)
rotate.duration = duration
rotate.interpolator = LinearInterpolator()
//rotate.fillAfter = true
rotate.fillBefore = false
return rotate
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java
index 6d043e9..20d9817 100644
--- a/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java
@@ -1,881 +1,881 @@
package it.reyboz.bustorino.fragments;
import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.location.Criteria;
import android.location.Location;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatImageButton;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.app.ActivityCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import android.os.Handler;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ProgressBar;
import android.widget.Toast;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import java.util.List;
import java.util.Map;
import it.reyboz.bustorino.R;
import it.reyboz.bustorino.backend.*;
import it.reyboz.bustorino.data.PreferencesHolder;
import it.reyboz.bustorino.middleware.AppLocationManager;
import it.reyboz.bustorino.middleware.AsyncArrivalsSearcher;
import it.reyboz.bustorino.middleware.AsyncStopsSearcher;
import it.reyboz.bustorino.middleware.BarcodeScanContract;
import it.reyboz.bustorino.middleware.BarcodeScanOptions;
import it.reyboz.bustorino.middleware.BarcodeScanUtils;
import it.reyboz.bustorino.util.LocationCriteria;
import it.reyboz.bustorino.util.Permissions;
import org.jetbrains.annotations.NotNull;
import static it.reyboz.bustorino.backend.utils.getBusStopIDFromUri;
import static it.reyboz.bustorino.util.Permissions.LOCATION_PERMISSIONS;
/**
* A simple {@link Fragment} subclass.
* Use the {@link MainScreenFragment#newInstance} factory method to
* create an instance of this fragment.
*/
public class MainScreenFragment extends ScreenBaseFragment implements FragmentListenerMain{
private static final String SAVED_FRAGMENT="saved_fragment";
private static final String DEBUG_TAG = "BusTO - MainFragment";
public static final String PENDING_STOP_SEARCH="PendingStopSearch";
public final static String FRAGMENT_TAG = "MainScreenFragment";
private FragmentHelper fragmentHelper;
private SwipeRefreshLayout swipeRefreshLayout;
private EditText busStopSearchByIDEditText;
private EditText busStopSearchByNameEditText;
private ProgressBar progressBar;
private MenuItem actionHelpMenuItem;
private FloatingActionButton floatingActionButton;
private FrameLayout resultFrameLayout;
private boolean setupOnStart = true;
private boolean suppressArrivalsReload = false;
//private Snackbar snackbar;
/*
* Search mode
*/
private static final int SEARCH_BY_NAME = 0;
private static final int SEARCH_BY_ID = 1;
//private static final int SEARCH_BY_ROUTE = 2; // implement this -- DONE!
private int searchMode;
//private ImageButton addToFavorites;
//// HIDDEN BUT IMPORTANT ELEMENTS ////
FragmentManager childFragMan;
Handler mainHandler;
private final Runnable refreshStop = new Runnable() {
public void run() {
if(getContext() == null) return;
List fetcherList = utils.getDefaultArrivalsFetchers(getContext());
ArrivalsFetcher[] arrivalsFetchers = new ArrivalsFetcher[fetcherList.size()];
arrivalsFetchers = fetcherList.toArray(arrivalsFetchers);
if (childFragMan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) {
ArrivalsFragment fragment = (ArrivalsFragment) childFragMan.findFragmentById(R.id.resultFrame);
if (fragment == null){
//we create a new fragment, which is WRONG
Log.e("BusTO-RefreshStop", "Asking for refresh when there is no fragment");
// AsyncDataDownload(fragmentHelper, arrivalsFetchers,getContext()).execute();
} else{
String stopName = fragment.getStopID();
new AsyncArrivalsSearcher(fragmentHelper, fragment.getCurrentFetchersAsArray(), getContext()).execute(stopName);
}
} else //we create a new fragment, which is WRONG
new AsyncArrivalsSearcher(fragmentHelper, arrivalsFetchers, getContext()).execute();
}
};
//
private final ActivityResultLauncher barcodeLauncher = registerForActivityResult(new BarcodeScanContract(),
result -> {
if(result!=null && result.getContents()!=null) {
//Toast.makeText(MyActivity.this, "Cancelled", Toast.LENGTH_LONG).show();
Uri uri;
try {
uri = Uri.parse(result.getContents()); // this apparently prevents NullPointerException. Somehow.
} catch (NullPointerException e) {
if (getContext()!=null)
Toast.makeText(getContext().getApplicationContext(),
R.string.no_qrcode, Toast.LENGTH_SHORT).show();
return;
}
String busStopID = getBusStopIDFromUri(uri);
busStopSearchByIDEditText.setText(busStopID);
requestArrivalsForStopID(busStopID);
} else {
//Toast.makeText(MyActivity.this, "Scanned: " + result.getContents(), Toast.LENGTH_LONG).show();
if (getContext()!=null)
Toast.makeText(getContext().getApplicationContext(),
R.string.no_qrcode, Toast.LENGTH_SHORT).show();
}
});
/// LOCATION STUFF ///
boolean pendingIntroRun = false;
boolean pendingNearbyStopsFragmentRequest = false;
boolean locationPermissionGranted, locationPermissionAsked = false;
AppLocationManager locationManager;
private final ActivityResultLauncher requestPermissionLauncher =
registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback