diff --git a/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java b/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java index e593268..f2ef442 100644 --- a/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java +++ b/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java @@ -1,786 +1,786 @@ /* 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(); } } 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){ readyGUIfor(FragmentKind.LINES); FragmentTransaction tr = getSupportFragmentManager().beginTransaction(); tr.replace(R.id.mainActContentFrame, LinesDetailFragment.class, LinesDetailFragment.Companion.makeArgs(routeGtfsId)); 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){ FragmentManager fm = getSupportFragmentManager(); FragmentTransaction ft = fm.beginTransaction(); - MapFragment fragment = stop == null? MapFragment.getInstance(): MapFragment.getInstance(stop); - ft.replace(R.id.mainActContentFrame, fragment, MapFragment.FRAGMENT_TAG); + MapFragmentKt fragment = stop == null? MapFragmentKt.getInstance(): MapFragmentKt.getInstance(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; } if (edit){ editor.commit(); } } } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/MapFragmentKt.kt b/app/src/main/java/it/reyboz/bustorino/fragments/MapFragmentKt.kt new file mode 100644 index 0000000..08aa2fe --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/fragments/MapFragmentKt.kt @@ -0,0 +1,771 @@ +/* + BusTO - Fragments components + Copyright (C) 2020 Andrea Ugo + Copyright (C) 2021 Fabio Mazza + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ +package it.reyboz.bustorino.fragments + +import android.Manifest +import android.animation.ObjectAnimator +import android.annotation.SuppressLint +import android.content.Context +import android.location.LocationManager +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageButton +import android.widget.Toast +import androidx.activity.result.ActivityResultCallback +import androidx.activity.result.contract.ActivityResultContracts +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.content.res.ResourcesCompat +import androidx.lifecycle.ViewModelProvider +import androidx.preference.PreferenceManager +import it.reyboz.bustorino.R +import it.reyboz.bustorino.backend.Stop +import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate +import it.reyboz.bustorino.backend.mato.MQTTMatoClient +import it.reyboz.bustorino.backend.utils +import it.reyboz.bustorino.data.gtfs.MatoPattern +import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops +import it.reyboz.bustorino.map.BusInfoWindow +import it.reyboz.bustorino.map.CustomInfoWindow +import it.reyboz.bustorino.map.CustomInfoWindow.TouchResponder +import it.reyboz.bustorino.map.LocationOverlay +import it.reyboz.bustorino.map.LocationOverlay.OverlayCallbacks +import it.reyboz.bustorino.map.MarkerUtils +import it.reyboz.bustorino.middleware.GeneralActivity +import it.reyboz.bustorino.util.Permissions +import it.reyboz.bustorino.viewmodels.LivePositionsViewModel +import it.reyboz.bustorino.viewmodels.StopsMapViewModel +import org.osmdroid.config.Configuration +import org.osmdroid.events.DelayedMapListener +import org.osmdroid.events.MapListener +import org.osmdroid.events.ScrollEvent +import org.osmdroid.events.ZoomEvent +import org.osmdroid.tileprovider.tilesource.TileSourceFactory +import org.osmdroid.util.GeoPoint +import org.osmdroid.views.MapView +import org.osmdroid.views.overlay.FolderOverlay +import org.osmdroid.views.overlay.Marker +import org.osmdroid.views.overlay.infowindow.InfoWindow +import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider + +open class MapFragmentKt : ScreenBaseFragment() { + protected var listenerMain: FragmentListenerMain? = null + private var shownStops: HashSet? = null + private lateinit var map: MapView + var ctx: Context? = null + private lateinit var mLocationOverlay: LocationOverlay + private lateinit var stopsFolderOverlay: FolderOverlay + private var savedMapState: Bundle? = null + protected lateinit var btCenterMap: ImageButton + protected lateinit var btFollowMe: ImageButton + protected var coordLayout: CoordinatorLayout? = null + private var hasMapStartFinished = false + private var followingLocation = false + + //the ViewModel from which we get the stop to display in the map + private var stopsViewModel: StopsMapViewModel? = null + + //private GtfsPositionsViewModel gtfsPosViewModel; //= new ViewModelProvider(this).get(MapViewModel.class); + private var livePositionsViewModel: LivePositionsViewModel? = null + private var useMQTTViewModel = true + private val busPositionMarkersByTrip = HashMap() + private var busPositionsOverlay: FolderOverlay? = null + private val tripMarkersAnimators = HashMap() + protected val responder = TouchResponder { stopID, stopName -> + if (listenerMain != null) { + Log.d(DEBUG_TAG, "Asked to show arrivals for stop ID: $stopID") + listenerMain!!.requestArrivalsForStopID(stopID) + } + } + protected val locationCallbacks: OverlayCallbacks = object : OverlayCallbacks { + override fun onDisableFollowMyLocation() { + updateGUIForLocationFollowing(false) + followingLocation = false + } + + override fun onEnableFollowMyLocation() { + updateGUIForLocationFollowing(true) + followingLocation = true + } + } + private val positionRequestLauncher = + registerForActivityResult, Map>( + ActivityResultContracts.RequestMultiplePermissions(), + ActivityResultCallback { result -> + if (result == null) { + Log.w(DEBUG_TAG, "Got asked permission but request is null, doing nothing?") + } else if (java.lang.Boolean.TRUE == result[Manifest.permission.ACCESS_COARSE_LOCATION] && java.lang.Boolean.TRUE == result[Manifest.permission.ACCESS_FINE_LOCATION]) { + map.overlays.remove(mLocationOverlay) + startLocationOverlay(true, map) + if (context == null || requireContext().getSystemService(Context.LOCATION_SERVICE) == null) + return@ActivityResultCallback ///@registerForActivityResult + val locationManager = + requireContext().getSystemService(Context.LOCATION_SERVICE) as LocationManager + @SuppressLint("MissingPermission") val userLocation = + locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) + if (userLocation != null) { + map!!.controller.setZoom(POSITION_FOUND_ZOOM) + val startPoint = GeoPoint(userLocation) + setLocationFollowing(true) + map!!.controller.setCenter(startPoint) + } + } else Log.w(DEBUG_TAG, "No location permission") + }) + + //public static MapFragment getInstance(@NonNull Stop stop){ + // return getInstance(stop.getLatitude(), stop.getLongitude(), stop.getStopDisplayName(), stop.ID); + //} + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + //use the same layout as the activity + val root = inflater.inflate(R.layout.fragment_map, container, false) + val context = requireContext() + ctx = context.applicationContext + Configuration.getInstance().load(ctx, PreferenceManager.getDefaultSharedPreferences(context)) + map = root.findViewById(R.id.map) + map.setTileSource(TileSourceFactory.MAPNIK) + //map.setTilesScaledToDpi(true); + map.setFlingEnabled(true) + + // add ability to zoom with 2 fingers + map.setMultiTouchControls(true) + btCenterMap = root.findViewById(R.id.icon_center_map) + btFollowMe = root.findViewById(R.id.icon_follow) + coordLayout = root.findViewById(R.id.coord_layout) + + //setup FolderOverlay + stopsFolderOverlay = FolderOverlay() + //setup Bus Markers Overlay + busPositionsOverlay = FolderOverlay() + //reset shown bus updates + busPositionMarkersByTrip.clear() + tripMarkersAnimators.clear() + //set map not done + hasMapStartFinished = false + val keySourcePositions = getString(R.string.pref_positions_source) + useMQTTViewModel = PreferenceManager.getDefaultSharedPreferences(requireContext()) + .getString(keySourcePositions, SettingsFragment.LIVE_POSITIONS_PREF_MQTT_VALUE) + .contentEquals(SettingsFragment.LIVE_POSITIONS_PREF_MQTT_VALUE) + + + //Start map from bundle + if (savedInstanceState != null) startMap(arguments, savedInstanceState) else startMap( + arguments, savedMapState + ) + //set listeners + map.addMapListener(DelayedMapListener(object : MapListener { + override fun onScroll(paramScrollEvent: ScrollEvent): Boolean { + requestStopsToShow() + //Log.d(DEBUG_TAG, "Scrolling"); + //if (moveTriggeredByCode) moveTriggeredByCode =false; + //else setLocationFollowing(false); + return true + } + + override fun onZoom(event: ZoomEvent): Boolean { + requestStopsToShow() + return true + } + })) + btCenterMap.setOnClickListener(View.OnClickListener { v: View? -> + //Log.i(TAG, "centerMap clicked "); + if (Permissions.bothLocationPermissionsGranted(context)) { + val myPosition = mLocationOverlay!!.myLocation + map.getController().animateTo(myPosition) + } else Toast.makeText(context, R.string.enable_position_message_map, Toast.LENGTH_SHORT) + .show() + }) + btFollowMe.setOnClickListener(View.OnClickListener { v: View? -> + //Log.i(TAG, "btFollowMe clicked "); + if (Permissions.bothLocationPermissionsGranted(context)) setLocationFollowing(!followingLocation) else Toast.makeText( + context, R.string.enable_position_message_map, Toast.LENGTH_SHORT + ) + .show() + }) + return root + } + + override fun onAttach(context: Context) { + super.onAttach(context) + val provider = ViewModelProvider(this) + //gtfsPosViewModel = provider.get(GtfsPositionsViewModel.class); + livePositionsViewModel = provider.get(LivePositionsViewModel::class.java) + stopsViewModel = provider.get(StopsMapViewModel::class.java) + listenerMain = if (context is FragmentListenerMain) { + context + } else { + throw RuntimeException( + context.toString() + + " must implement FragmentListenerMain" + ) + } + } + + override fun onDetach() { + super.onDetach() + listenerMain = null + //stop animations + + // setupOnAttached = true; + Log.w(DEBUG_TAG, "Fragment detached") + } + + override fun onPause() { + super.onPause() + Log.w(DEBUG_TAG, "On pause called mapfrag") + saveMapState() + for (animator in tripMarkersAnimators.values) { + if (animator != null && animator.isRunning) { + animator.cancel() + } + } + tripMarkersAnimators.clear() + if (useMQTTViewModel) livePositionsViewModel!!.stopMatoUpdates() + } + + /** + * Save the map state inside the fragment + * (calls saveMapState(bundle)) + */ + private fun saveMapState() { + savedMapState = Bundle() + saveMapState(savedMapState!!) + } + + /** + * Save the state of the map to restore it to a later time + * @param bundle the bundle in which to save the data + */ + private fun saveMapState(bundle: Bundle) { + Log.d(DEBUG_TAG, "Saving state, location following: $followingLocation") + bundle.putBoolean(FOLLOWING_LOCAT_KEY, followingLocation) + if (map == null) { + //The map is null, it can happen? + Log.e(DEBUG_TAG, "Cannot save map center, map is null") + return + } + val loc = map!!.mapCenter + bundle.putDouble(MAP_CENTER_LAT_KEY, loc.latitude) + bundle.putDouble(MAP_CENTER_LON_KEY, loc.longitude) + bundle.putDouble(MAP_CURRENT_ZOOM_KEY, map!!.zoomLevelDouble) + } + + override fun onResume() { + super.onResume() + //TODO: cleanup duplicate code (maybe merging the positions classes?) + if (listenerMain != null) listenerMain!!.readyGUIfor(FragmentKind.MAP) + /// choose which to use + val keySourcePositions = getString(R.string.pref_positions_source) + useMQTTViewModel = PreferenceManager.getDefaultSharedPreferences(requireContext()) + .getString(keySourcePositions, SettingsFragment.LIVE_POSITIONS_PREF_MQTT_VALUE) + .contentEquals( + SettingsFragment.LIVE_POSITIONS_PREF_MQTT_VALUE + ) + if (livePositionsViewModel != null) { + //gtfsPosViewModel.requestUpdates(); + if (useMQTTViewModel) livePositionsViewModel!!.requestMatoPosUpdates(MQTTMatoClient.LINES_ALL) + else livePositionsViewModel!!.requestGTFSUpdates() + //mapViewModel.testCascade(); + livePositionsViewModel!!.isLastWorkResultGood.observe(this) { d: Boolean -> + Log.d( + DEBUG_TAG, "Last trip download result is $d" + ) + } + livePositionsViewModel!!.tripsGtfsIDsToQuery.observe(this) { dat: List -> + Log.i(DEBUG_TAG, "Have these trips IDs missing from the DB, to be queried: $dat") + livePositionsViewModel!!.downloadTripsFromMato(dat) + } + } /*else if(gtfsPosViewModel!=null){ + gtfsPosViewModel.requestUpdates(); + gtfsPosViewModel.getTripsGtfsIDsToQuery().observe(this, dat -> { + Log.i(DEBUG_TAG, "Have these trips IDs missing from the DB, to be queried: "+dat); + //gtfsPosViewModel.downloadTripsFromMato(dat); + MatoTripsDownloadWorker.Companion.downloadTripsFromMato(dat,getContext().getApplicationContext(), + "BusTO-MatoTripDownload"); + }); + } + */ else Log.e(DEBUG_TAG, "livePositionsViewModel is null at onResume") + + //rerequest stop + stopsViewModel!!.requestStopsInBoundingBox(map!!.boundingBox) + } + + private fun startRequestsPositions() { + if (livePositionsViewModel != null) { + //should always be the case + livePositionsViewModel!!.updatesWithTripAndPatterns.observe(viewLifecycleOwner) { data: HashMap> -> + Log.d( + DEBUG_TAG, + "Have " + data.size + " trip updates, has Map start finished: " + hasMapStartFinished + ) + if (hasMapStartFinished) updateBusPositionsInMap(data) + if (!isDetached && !useMQTTViewModel) livePositionsViewModel!!.requestDelayedGTFSUpdates( + 3000 + ) + } + } else { + Log.e(DEBUG_TAG, "PositionsViewModel is null") + } + } + + override fun onSaveInstanceState(outState: Bundle) { + saveMapState(outState) + super.onSaveInstanceState(outState) + } + //own methods + /** + * Switch following the location on and off + * @param value true if we want to follow location + */ + fun setLocationFollowing(value: Boolean) { + followingLocation = value + if (mLocationOverlay == null || context == null || map == null) //nothing else to do + return + if (value) { + mLocationOverlay!!.enableFollowLocation() + } else { + mLocationOverlay!!.disableFollowLocation() + } + } + + /** + * Do all the stuff you need to do on the gui, when parameter is changed to value + * @param following value + */ + protected fun updateGUIForLocationFollowing(following: Boolean) { + if (following) btFollowMe!!.setImageResource(R.drawable.ic_follow_me_on) else btFollowMe!!.setImageResource( + R.drawable.ic_follow_me + ) + } + + /** + * Build the location overlay. Enable only when + * a) we know we have the permission + * b) the location map is set + */ + private fun startLocationOverlay(enableLocation: Boolean, map: MapView?) { + checkNotNull(activity) { "Cannot enable LocationOverlay now" } + // Location Overlay + // from OpenBikeSharing (THANK GOD) + Log.d(DEBUG_TAG, "Starting position overlay") + val imlp = GpsMyLocationProvider(requireActivity().baseContext) + imlp.locationUpdateMinDistance = 5f + imlp.locationUpdateMinTime = 2000 + val overlay = LocationOverlay(imlp, map, locationCallbacks) + if (enableLocation) overlay.enableMyLocation() + overlay.isOptionsMenuEnabled = true + + //map.getOverlays().add(this.mLocationOverlay); + mLocationOverlay = overlay + map!!.overlays.add(mLocationOverlay) + } + + fun startMap(incoming: Bundle?, savedInstanceState: Bundle?) { + //Check that we're attached + val activity = if (activity is GeneralActivity) activity as GeneralActivity? else null + if (context == null || activity == null) { + //we are not attached + Log.e(DEBUG_TAG, "Calling startMap when not attached") + return + } else { + Log.d(DEBUG_TAG, "Starting map from scratch") + } + //clear previous overlays + map!!.overlays.clear() + + + //parse incoming bundle + var marker: GeoPoint? = null + var name: String? = null + var ID: String? = null + var routesStopping: String? = "" + if (incoming != null) { + val lat = incoming.getDouble(BUNDLE_LATIT) + val lon = incoming.getDouble(BUNDLE_LONGIT) + marker = GeoPoint(lat, lon) + name = incoming.getString(BUNDLE_NAME) + ID = incoming.getString(BUNDLE_ID) + routesStopping = incoming.getString(BUNDLE_ROUTES_STOPPING, "") + } + + + //ask for location permission + if (!Permissions.bothLocationPermissionsGranted(activity)) { + if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)) { + //TODO: show dialog for permission rationale + Toast.makeText(activity, R.string.enable_position_message_map, Toast.LENGTH_SHORT) + .show() + } + positionRequestLauncher.launch(Permissions.LOCATION_PERMISSIONS) + } + shownStops = HashSet() + // move the map on the marker position or on a default view point: Turin, Piazza Castello + // and set the start zoom + val mapController = map!!.controller + var startPoint: GeoPoint? = null + startLocationOverlay( + Permissions.bothLocationPermissionsGranted(activity), + map + ) + // set the center point + if (marker != null) { + //startPoint = marker; + mapController.setZoom(POSITION_FOUND_ZOOM) + setLocationFollowing(false) + // put the center a little bit off (animate later) + startPoint = GeoPoint(marker) + startPoint.latitude = marker.latitude + utils.angleRawDifferenceFromMeters(20.0) + startPoint.longitude = marker.longitude - utils.angleRawDifferenceFromMeters(20.0) + //don't need to do all the rest since we want to show a point + } else if (savedInstanceState != null && savedInstanceState.containsKey(MAP_CURRENT_ZOOM_KEY)) { + mapController.setZoom(savedInstanceState.getDouble(MAP_CURRENT_ZOOM_KEY)) + mapController.setCenter( + GeoPoint( + savedInstanceState.getDouble(MAP_CENTER_LAT_KEY), + savedInstanceState.getDouble(MAP_CENTER_LON_KEY) + ) + ) + Log.d( + DEBUG_TAG, + "Location following from savedInstanceState: " + savedInstanceState.getBoolean( + FOLLOWING_LOCAT_KEY + ) + ) + setLocationFollowing(savedInstanceState.getBoolean(FOLLOWING_LOCAT_KEY)) + } else { + Log.d(DEBUG_TAG, "No position found from intent or saved state") + var found = false + val locationManager = + requireContext().getSystemService(Context.LOCATION_SERVICE) as LocationManager + //check for permission + if (Permissions.bothLocationPermissionsGranted(activity)) { + @SuppressLint("MissingPermission") val userLocation = + locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) + if (userLocation != null) { + val distan = utils.measuredistanceBetween( + userLocation.latitude, userLocation.longitude, + DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON + ) + if (distan < 100000.0) { + mapController.setZoom(POSITION_FOUND_ZOOM) + startPoint = GeoPoint(userLocation) + found = true + setLocationFollowing(true) + } + } + } + if (!found) { + startPoint = GeoPoint(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON) + mapController.setZoom(NO_POSITION_ZOOM) + setLocationFollowing(false) + } + } + + // set the minimum zoom level + map!!.minZoomLevel = 15.0 + //add contingency check (shouldn't happen..., but) + if (startPoint != null) { + mapController.setCenter(startPoint) + } + + + //add stops overlay + //map.getOverlays().add(mLocationOverlay); + map!!.overlays.add(stopsFolderOverlay) + Log.d(DEBUG_TAG, "Requesting stops load") + // This is not necessary, by setting the center we already move + // the map and we trigger a stop request + //requestStopsToShow(); + if (marker != null) { + // make a marker with the info window open for the searched marker + //TODO: make Stop Bundle-able + val stopMarker = makeMarker(marker, ID, name, routesStopping, true) + map!!.controller.animateTo(marker) + } + //add the overlays with the bus stops + if (busPositionsOverlay == null) { + //Log.i(DEBUG_TAG, "Null bus positions overlay,redo"); + busPositionsOverlay = FolderOverlay() + } + startRequestsPositions() + if (stopsViewModel != null) { + stopsViewModel!!.stopsInBoundingBox.observe(viewLifecycleOwner) { stops: List? -> + showStopsMarkers( + stops + ) + } + } else Log.d(DEBUG_TAG, "Cannot observe new stops in map, stopsViewModel is null") + map!!.overlays.add(busPositionsOverlay) + //set map as started + hasMapStartFinished = true + } + + /** + * Start a request to load the stops that are in the current view + * from the database + */ + private fun requestStopsToShow() { + // get the top, bottom, left and right screen's coordinate + val bb = map!!.boundingBox + Log.d( + DEBUG_TAG, + "Requesting stops in bounding box, stopViewModel is null " + (stopsViewModel == null) + ) + if (stopsViewModel != null) { + stopsViewModel!!.requestStopsInBoundingBox(bb) + } + + } + + private fun updateBusMarker( + marker: Marker?, + posUpdate: LivePositionUpdate, + justCreated: Boolean + ) { + val position: GeoPoint + val updateID = posUpdate.tripID + if (!justCreated) { + position = marker!!.position + if (posUpdate.latitude != position.latitude || posUpdate.longitude != position.longitude) { + val newpos = GeoPoint(posUpdate.latitude, posUpdate.longitude) + val valueAnimator = MarkerUtils.makeMarkerAnimator( + map, marker, newpos, MarkerUtils.LINEAR_ANIMATION, 1200 + ) + valueAnimator.setAutoCancel(true) + tripMarkersAnimators[updateID] = valueAnimator + valueAnimator.start() + } + //marker.setPosition(new GeoPoint(posUpdate.getLatitude(), posUpdate.getLongitude())); + } else { + position = GeoPoint(posUpdate.latitude, posUpdate.longitude) + marker!!.position = position + } + if (posUpdate.bearing != null) marker.rotation = posUpdate.bearing * -1f + } + + private fun updateBusPositionsInMap(tripsPatterns: HashMap>) { + Log.d(DEBUG_TAG, "Updating positions of the buses") + //if(busPositionsOverlay == null) busPositionsOverlay = new FolderOverlay(); + val noPatternsTrips = ArrayList() + for (tripID in tripsPatterns.keys) { + val (update, tripWithPatternStops) = tripsPatterns[tripID] ?: continue + + + //check if Marker is already created + if (busPositionMarkersByTrip.containsKey(tripID)) { + //need to change the position of the marker + val marker = busPositionMarkersByTrip[tripID]!! + updateBusMarker(marker, update, false) + if (marker.infoWindow != null && marker.infoWindow is BusInfoWindow) { + val window = marker.infoWindow as BusInfoWindow + if (tripWithPatternStops != null) { + //Log.d(DEBUG_TAG, "Update pattern for trip: "+tripID); + window.setPatternAndDraw(tripWithPatternStops.pattern) + } + } + } else { + //marker is not there, need to make it + if (map == null) Log.e( + DEBUG_TAG, + "Creating marker with null map, things will explode" + ) + val marker = Marker(map) + + /*final Drawable mDrawable = DrawableUtils.Companion.getScaledDrawableResources( + getResources(), + R.drawable.point_heading_icon, + R.dimen.map_icons_size, R.dimen.map_icons_size); + + */ + //String route = GtfsUtils.getLineNameFromGtfsID(update.getRouteID()); + val mdraw = + ResourcesCompat.getDrawable(resources, R.drawable.map_bus_position_icon, null)!! + //mdraw.setBounds(0,0,28,28); + marker.icon = mdraw + if (tripWithPatternStops == null) { + noPatternsTrips.add(tripID) + } + var markerPattern: MatoPattern? = null + if (tripWithPatternStops != null && tripWithPatternStops.pattern != null) markerPattern = + tripWithPatternStops.pattern + marker.infoWindow = + BusInfoWindow(map!!, update, markerPattern, false) { pattern: MatoPattern? -> } + marker.setInfoWindowAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER) + marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER) + updateBusMarker(marker, update, true) + // the overlay is null when it's not attached yet?5 + // cannot recreate it because it becomes null very soon + // if(busPositionsOverlay == null) busPositionsOverlay = new FolderOverlay(); + //save the marker + if (busPositionsOverlay != null) { + busPositionsOverlay!!.add(marker) + busPositionMarkersByTrip[tripID] = marker + } + } + } + if (noPatternsTrips.size > 0) { + Log.i(DEBUG_TAG, "These trips have no matching pattern: $noPatternsTrips") + } + } + + /** + * Add stops as Markers on the map + * @param stops the list of stops that must be included + */ + protected fun showStopsMarkers(stops: List?) { + if (context == null || stops == null) { + //we are not attached + return + } + var good = true + for (stop in stops) { + if (shownStops!!.contains(stop.ID)) { + continue + } + if (stop.longitude == null || stop.latitude == null) continue + shownStops!!.add(stop.ID) + if (!map!!.isShown) { + if (good) Log.d( + DEBUG_TAG, + "Need to show stop but map is not shown, probably detached already" + ) + good = false + continue + } else if (map!!.repository == null) { + Log.e(DEBUG_TAG, "Map view repository is null") + } + val marker = GeoPoint(stop.latitude!!, stop.longitude!!) + val stopMarker = makeMarker(marker, stop, false) + stopsFolderOverlay!!.add(stopMarker) + if (!map!!.overlays.contains(stopsFolderOverlay)) { + Log.w(DEBUG_TAG, "Map doesn't have folder overlay") + } + good = true + } + //Log.d(DEBUG_TAG,"We have " +stopsFolderOverlay.getItems().size()+" stops in the folderOverlay"); + //force redraw of markers + map!!.invalidate() + } + + fun makeMarker(geoPoint: GeoPoint?, stop: Stop, isStartMarker: Boolean): Marker { + return makeMarker( + geoPoint, stop.ID, + stop.stopDefaultName, + stop.routesThatStopHereToString(), isStartMarker + ) + } + + fun makeMarker( + geoPoint: GeoPoint?, stopID: String?, stopName: String?, + routesStopping: String?, isStartMarker: Boolean + ): Marker { + + // add a marker + val marker = Marker(map) + + // set custom info window as info window + val popup = CustomInfoWindow( + map, stopID, stopName, routesStopping, + responder, R.layout.linedetail_stop_infowindow, R.color.red_darker + ) + marker.infoWindow = popup + + // make the marker clickable + marker.setOnMarkerClickListener { thisMarker: Marker, mapView: MapView? -> + if (thisMarker.isInfoWindowOpen) { + // on second click + Log.w(DEBUG_TAG, "Pressed on the click marker") + } else { + // on first click + + // hide all opened info window + InfoWindow.closeAllInfoWindowsOn(map) + // show this particular info window + thisMarker.showInfoWindow() + // move the map to its position + map!!.controller.animateTo(thisMarker.position) + } + true + } + + // set its position + marker.position = geoPoint + marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER) + // add to it an icon + //marker.setIcon(getResources().getDrawable(R.drawable.bus_marker)); + marker.icon = ResourcesCompat.getDrawable(resources, R.drawable.bus_stop, ctx!!.theme) + // add to it a title + marker.title = stopName + // set the description as the ID + marker.snippet = stopID + + // show popup info window of the searched marker + if (isStartMarker) { + marker.showInfoWindow() + //map.getController().animateTo(marker.getPosition()); + } + return marker + } + + override fun getBaseViewForSnackBar(): View? { + return coordLayout + } + + companion object { + //private static final String TAG = "Busto-MapActivity"; + private const val MAP_CURRENT_ZOOM_KEY = "map-current-zoom" + private const val MAP_CENTER_LAT_KEY = "map-center-lat" + private const val MAP_CENTER_LON_KEY = "map-center-lon" + private const val FOLLOWING_LOCAT_KEY = "following" + const val BUNDLE_LATIT = "lat" + const val BUNDLE_LONGIT = "lon" + const val BUNDLE_NAME = "name" + const val BUNDLE_ID = "ID" + const val BUNDLE_ROUTES_STOPPING = "routesStopping" + const val FRAGMENT_TAG = "BusTOMapFragment" + private const val DEFAULT_CENTER_LAT = 45.0708 + private const val DEFAULT_CENTER_LON = 7.6858 + private const val POSITION_FOUND_ZOOM = 18.3 + const val NO_POSITION_ZOOM = 17.1 + private const val DEBUG_TAG = FRAGMENT_TAG + + @JvmStatic + fun getInstance(): MapFragmentKt { + return MapFragmentKt() + } + @JvmStatic + fun getInstance(stop: Stop): MapFragmentKt { + val fragment = MapFragmentKt() + val args = Bundle() + args.putDouble(MapFragment.BUNDLE_LATIT, stop.latitude!!) + args.putDouble(MapFragment.BUNDLE_LONGIT, stop.longitude!!) + args.putString(MapFragment.BUNDLE_NAME, stop.stopDisplayName) + args.putString(MapFragment.BUNDLE_ID, stop.ID) + args.putString(MapFragment.BUNDLE_ROUTES_STOPPING, stop.routesThatStopHereToString()) + fragment.arguments = args + + return fragment + } + } +} diff --git a/app/src/main/java/it/reyboz/bustorino/map/BusPositionUtils.kt b/app/src/main/java/it/reyboz/bustorino/map/BusPositionUtils.kt index bd1af1d..c76d45d 100644 --- a/app/src/main/java/it/reyboz/bustorino/map/BusPositionUtils.kt +++ b/app/src/main/java/it/reyboz/bustorino/map/BusPositionUtils.kt @@ -1,41 +1,40 @@ package it.reyboz.bustorino.map import android.animation.ObjectAnimator import android.util.Log import androidx.core.content.res.ResourcesCompat import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate import it.reyboz.bustorino.data.gtfs.MatoPattern import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops -import it.reyboz.bustorino.fragments.MapFragment import org.osmdroid.util.GeoPoint import org.osmdroid.views.MapView import org.osmdroid.views.overlay.Marker class BusPositionUtils { companion object{ @JvmStatic public fun updateBusPositionMarker(map: MapView, marker: Marker?, posUpdate: LivePositionUpdate, tripMarkersAnimators: HashMap, justCreated: Boolean) { val position: GeoPoint val updateID = posUpdate.tripID if (!justCreated) { position = marker!!.position if (posUpdate.latitude != position.latitude || posUpdate.longitude != position.longitude) { val newpos = GeoPoint(posUpdate.latitude, posUpdate.longitude) val valueAnimator = MarkerUtils.makeMarkerAnimator( map, marker, newpos, MarkerUtils.LINEAR_ANIMATION, 1200 ) valueAnimator.setAutoCancel(true) tripMarkersAnimators.put(updateID, valueAnimator) valueAnimator.start() } //marker.setPosition(new GeoPoint(posUpdate.getLatitude(), posUpdate.getLongitude())); } else { position = GeoPoint(posUpdate.latitude, posUpdate.longitude) marker!!.position = position } if (posUpdate.bearing != null) marker.rotation = posUpdate.bearing * -1f } } } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c928bfc..dabc127 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,336 +1,338 @@ BusTO Libre BusTO BusTO dev BusTO git You\'re using the latest in technology when it comes to respecting your privacy. Search Scan QR Code Yes No Next Previous Install Barcode Scanner? This application requires an app to scan the QR codes. Would you like to install Barcode Scanner now? Bus stop number Bus stop name Insert bus stop number Insert bus stop name %1$s towards %2$s %s (unknown destination) Verify your Internet connection! Seems that no bus stop have this name No arrivals found for this stop Error parsing the 5T/GTT website (damn site!) Name too short, type more characters and retry Arrivals at: %1$s Choose the bus stop… Line Lines Urban lines Extra urban lines Tourist lines Destination: Lines: %1$s Line: %1$s No timetable found No QR code found, try using another app to scan Unexpected internal error, cannot extract data from GTT/5T website Help About the app More about Contribute https://gitpull.it/w/librebusto/en/ Source code Licence11 Meet the author Bus stop is now in your favorites Bus stop removed from your favorites Added line to favorites Remove line from favorites Favorites Favorites Favorites Map No favorites? Arghh! Press on a bus stop star to populate this list! Delete Rename Rename the bus stop Reset About the app Tap the star to add the bus stop to the favourites\n\nHow to read timelines:\n   12:56* Real-time arrivals\n   12:56   Scheduled arrivals\n\nPull down to refresh the timetable \n Long press on Arrivals source to change the source of the arrival times GOT IT! Arrival times No arrivals found for lines: Welcome!

Thanks for using BusTO, an open source and independent app useful to move around Torino using a Free/Libre software.


Why use this app?

- You\'ll never be tracked
- You\'ll never see boring ads
- We\'ll always respect your privacy
- Moreover, it\'s lightweight!


Introductory tutorial

If you want to see the introduction again, use the button below:

]]>
News and Updates

On the Telegram channel, you can find information about the latest app updates

]]>
How does it work?

This app is able to do all the amazing things it does by pulling data from www.gtt.to.it, www.5t.torino.it or muoversiatorino.it "for personal use", along with open data from the AperTO (aperto.comune.torino.it) website.


The work of several people is behind this app, in particular:
- Fabio Mazza, current senior rockstar developer.
- Andrea Ugo, current junior rockstar developer.
- Silviu Chiriac, designer of the 2021 logo.
- Marco M, rockstar tester and bug hunter.
- Ludovico Pavesi, previous senior rockstar developer (asd).
- Valerio Bozzolan, maintainer and infrastructure (sponsor).
- Marco Gagino, contributor and first icon creator.
- JSoup web scraper library.
- makovkastar floating buttons.
- Google for icons and support and design libraries.
- Other icons from Bootstrap, Feather, and Hero Icons.
- All the contributors, and the beta testers, too!


If you want more technical information or to contribute to development, use the buttons below! ]]>
Licenses

The app and the related source code are released by Valerio Bozzolan and the other authors under the terms of the GNU General Public License v3+). So everyone is allowed to use, to study, to improve and to share this app by any kind of means and for any purpose: under the conditions of maintaining this rights and of attributing the original work to Valerio Bozzolan.


Notes

This app has been developed with the hope to be useful to everyone, but comes without ANY warranty of any kind.

The data used by the app comes directly from GTT and other public agencies: if you find any errors, please take it up to them, not to us.

This translation is kindly provided by Riccardo Caniato, Marco Gagino and Fabio Mazza.

Now you can hack public transport, too! :)

]]>
Cannot add to favorites (storage full or corrupted database?)! View on a map Cannot find any application to show it in Cannot find the position of the stop ListFragment - BusTO it.reyboz.bustorino.preferences db_is_updating Nearby stops Nearby connections App version The number of stops to show in the recent stops is invalid Invalid value, put a valid number Finding location No stops nearby Minimum number of stops Preferences Settings Settings General Experimental features Maximum distance (meters) Recent stops General settings Database management Launch manual database update Allow access to location to show it on the map Allow access to location to show stops nearby Please enable location on the device Database update in progress… Updating the database Force database update Touch to update the app database now is arriving at at the stop %1$s - %2$s Show arrivals Show stops Join Telegram channel Show introduction Center on my location Follow me Enable or disable location Location enabled Location disabled Location is disabled on device Arrivals source: %1$s GTT App GTT Website 5T Torino website Muoversi a Torino app Undetermined Changing arrival times source… Long press to change the source of arrivals @string/source_mato @string/fivetapifetcher @string/gttjsonfetcher @string/fivetscraper Sources of arrival times Select which sources of arrival times to use Default Default channel for notifications Database operations Updates of the app database BusTO - live position service Live positions Showing activity related to the live positions service MaTO live bus positions service is running Downloading trips from MaTO server Asked for %1$s permission too many times Cannot use the map with the storage permission! storage The application has crashed because you encountered a bug. \nIf you want, you can help the developers by sending the crash report via email. \nNote that no sensitive data is contained in the report, just small bits of info on your phone and app configuration/state. The application crashed and the crash report is in the attachments. Please describe what you were doing before the crash: \n Arrivals Map Favorites Open navigation drawer Close navigation drawer Experiments Buy us a coffee Map Search by stop Launching database update Downloading data from MaTO server Capitalize directions Do not change arrivals directions Capitalize everything Capitalize only first letter KEEP CAPITALIZE_ALL CAPITALIZE_FIRST Section to show on startup Touch to change it Show arrivals touching on stop Enable experiments Long press the stop for options @string/nav_arrivals_text @string/nav_favorites_text @string/nav_map_text @string/lines Source of real time positions for buses and trams MaTO (updated more frequently, might be offline) GTFS RT (more stable, less frequently updated) Remove trips data (free up space) All GTFS trips have been removed from the database Show tutorial open source app for Turin public transport. This is an independent app, with no ads and no tracking whatsoever.]]> favorites by touching the star next to its name]]> blue)]]> Settings to customize the app behaviour, and in the About the app section if you want to know more about the app and the developers.]]> Notifications permission to show the information about background processing. Press the button below to grant it]]> Grant location permission Location permission granted Location permission has not been granted OK, close the tutorial Close the tutorial Enable notifications Notifications enabled Backup and restore Import/export preferences Data saved Backup to file Import data from backup Backup has been imported Check at least one item to import! Import favorites from backup Import preferences from backup + + Hello blank fragment