diff --git a/app/build.gradle b/app/build.gradle index 4ddf53e..8fa8951 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,128 +1,135 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' android { compileSdkVersion 33 buildToolsVersion '33.0.2' namespace "it.reyboz.bustorino" defaultConfig { applicationId "it.reyboz.bustorino" - minSdkVersion 16 + minSdkVersion 21 targetSdkVersion 33 versionCode 48 versionName "1.19.1" vectorDrawables.useSupportLibrary = true multiDexEnabled true javaCompileOptions { annotationProcessorOptions { arguments = ["room.schemaLocation": "$projectDir/assets/schemas/".toString()] } } testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } sourceSets { androidTest.assets.srcDirs += files("$projectDir/assets/schemas/".toString()) } buildTypes { debug { applicationIdSuffix ".debug" versionNameSuffix "-dev" } gitpull{ applicationIdSuffix ".gitdev" versionNameSuffix "-gitdev" } } lintOptions { abortOnError false } repositories { mavenCentral() mavenLocal() } dependencies { //new libraries } } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation "androidx.fragment:fragment-ktx:$fragment_version" implementation "androidx.activity:activity:$activity_version" - implementation "androidx.annotation:annotation:1.3.0" + implementation "androidx.annotation:annotation:1.6.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation "androidx.appcompat:appcompat:$appcompat_version" implementation "androidx.appcompat:appcompat-resources:$appcompat_version" implementation "androidx.preference:preference:$preference_version" + implementation "androidx.work:work-runtime:$work_version" + implementation "androidx.work:work-runtime-ktx:$work_version" + - implementation "com.google.android.material:material:1.5.0" - implementation 'androidx.constraintlayout:constraintlayout:2.1.3' + implementation "com.google.android.material:material:1.9.0" + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0" implementation 'org.jsoup:jsoup:1.13.1' implementation 'com.readystatesoftware.sqliteasset:sqliteassethelper:2.0.1' implementation 'com.android.volley:volley:1.2.1' implementation 'org.osmdroid:osmdroid-android:6.1.10' // remember to enable maven repo jitpack.io when wanting to use osmbonuspack //implementation 'com.github.MKergall:osmbonuspack:6.9.0' // ACRA implementation "ch.acra:acra-mail:$acra_version" implementation "ch.acra:acra-dialog:$acra_version" // google transit realtime - implementation 'com.google.protobuf:protobuf-java:3.14.0' + implementation 'com.google.protobuf:protobuf-java:3.17.2' + // mqtt library + implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5' + implementation 'com.github.hannesa2:paho.mqtt.android:3.5.3' + // ViewModel implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" // LiveData implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" // Lifecycles only (without ViewModel or LiveData) implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version" // Legacy implementation 'androidx.legacy:legacy-support-v4:1.0.0' // Room components + implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-ktx:$room_version" - implementation "androidx.work:work-runtime-ktx:$work_version" kapt "androidx.room:room-compiler:$room_version" //multidex - we need this to build the app implementation "androidx.multidex:multidex:$multidex_version" implementation 'de.siegmar:fastcsv:2.0.0' testImplementation 'junit:junit:4.12' implementation 'junit:junit:4.12' - implementation "androidx.test.ext:junit:1.1.3" + implementation "androidx.test.ext:junit:1.1.5" implementation "androidx.test:core:$androidXTestVersion" implementation "androidx.test:runner:$androidXTestVersion" implementation "androidx.room:room-testing:$room_version" - androidTestImplementation "androidx.test.ext:junit:1.1.3" + androidTestImplementation "androidx.test.ext:junit:1.1.5" androidTestImplementation "androidx.test:core:$androidXTestVersion" androidTestImplementation "androidx.test:runner:$androidXTestVersion" androidTestImplementation "androidx.test:rules:$androidXTestVersion" androidTestImplementation "androidx.room:room-testing:$room_version" } diff --git a/app/src/androidTest/java/it/reyboz/bustorino/data/gtfs/GtfsDBMigrationsTest.java b/app/src/androidTest/java/it/reyboz/bustorino/data/gtfs/GtfsDBMigrationsTest.java index 494be13..2f7d448 100644 --- a/app/src/androidTest/java/it/reyboz/bustorino/data/gtfs/GtfsDBMigrationsTest.java +++ b/app/src/androidTest/java/it/reyboz/bustorino/data/gtfs/GtfsDBMigrationsTest.java @@ -1,54 +1,53 @@ package it.reyboz.bustorino.data.gtfs; import androidx.room.Room; import androidx.room.migration.Migration; import androidx.room.testing.MigrationTestHelper; import androidx.sqlite.db.SupportSQLiteDatabase; import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import java.io.IOException; import it.reyboz.bustorino.data.gtfs.GtfsDatabase; //@RunWith(AndroidJUnit4.class) public class GtfsDBMigrationsTest { private static final String TEST_DB = "migration-test"; @Rule public MigrationTestHelper helper; public GtfsDBMigrationsTest() { helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(), - GtfsDatabase.class.getCanonicalName(), - new FrameworkSQLiteOpenHelperFactory()); + GtfsDatabase.class.getCanonicalName()); } @Test public void migrateAll() throws IOException { // Create earliest version of the database. SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1); db.close(); // Open latest version of the database. Room will validate the schema // once all migrations execute. GtfsDatabase appDb = Room.databaseBuilder( InstrumentationRegistry.getInstrumentation().getTargetContext(), GtfsDatabase.class, TEST_DB) .addMigrations(ALL_MIGRATIONS).build(); appDb.getOpenHelper().getWritableDatabase(); appDb.close(); } // Array of all migrations private static final Migration[] ALL_MIGRATIONS = new Migration[]{ GtfsDatabase.Companion.getMIGRATION_1_2(), }; } diff --git a/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java b/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java index f2cc194..bb21d62 100644 --- a/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java +++ b/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java @@ -1,54 +1,92 @@ /* 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.appcompat.app.ActionBar; -import it.reyboz.bustorino.fragments.LinesDetailFragment; -import it.reyboz.bustorino.fragments.TestRealtimeGtfsFragment; +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 { +public class ActivityExperiments extends GeneralActivity implements CommonFragmentListener { final static String DEBUG_TAG = "ExperimentsGTFS"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_experiments); ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); 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:56U")) - .commit(); - */ - .add(R.id.fragment_container_view, LinesDetailFragment.class, - LinesDetailFragment.Companion.makeArgs("gtt:10U")) + /* .add(R.id.fragment_container_view, LinesDetailFragment.class, + + LinesDetailFragment.Companion.makeArgs("gtt:4U")) + + */ + .add(R.id.fragment_container_view, LinesGridShowingFragment.class, null) .commit(); + + //.add(R.id.fragment_container_view, LinesDetailFragment.class, + // LinesDetailFragment.Companion.makeArgs("gtt:4U")) + //.add(R.id.fragment_container_view, TestRealtimeGtfsFragment.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){ + + readyGUIfor(FragmentKind.LINES); + FragmentTransaction tr = getSupportFragmentManager().beginTransaction(); + tr.replace(R.id.fragment_container_view, LinesDetailFragment.class, + LinesDetailFragment.Companion.makeArgs(routeGtfsId)); + 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 03360a4..cb3ea6f 100644 --- a/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java +++ b/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java @@ -1,748 +1,762 @@ /* 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); final SharedPreferences theShPr = getMainSharedPreferences(); boolean showingArrivalsFromIntent = false; //database check GtfsDatabase gtfsDB = GtfsDatabase.Companion.getGtfsDatabase(this); final int db_version = gtfsDB.getOpenHelper().getReadableDatabase().getVersion(); boolean dataUpdateRequested = false; 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); } 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; } //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(); } 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); } @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(LinesFragment.FRAGMENT_TAG); + Fragment f = fragmentManager.findFragmentByTag(LinesGridShowingFragment.FRAGMENT_TAG); if(f!=null){ - ft.replace(R.id.mainActContentFrame, f, LinesFragment.FRAGMENT_TAG); + ft.replace(R.id.mainActContentFrame, f, LinesGridShowingFragment.FRAGMENT_TAG); }else{ //use new method - ft.replace(R.id.mainActContentFrame,LinesFragment.class,fragArgs,LinesFragment.FRAGMENT_TAG); + ft.replace(R.id.mainActContentFrame,LinesGridShowingFragment.class,fragArgs, + LinesGridShowingFragment.FRAGMENT_TAG); } if (addToBackStack) - ft.addToBackStack("lines"); + 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); if (addToBackStack) ft.addToBackStack(null); ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); ft.commit(); } class ToolbarItemClickListener implements Toolbar.OnMenuItemClickListener{ private final Context activityContext; public ToolbarItemClickListener(Context activityContext) { this.activityContext = activityContext; } @Override public boolean onMenuItemClick(MenuItem item) { switch (item.getItemId()) { case R.id.action_about: startActivity(new Intent(ActivityPrincipal.this, ActivityAbout.class)); return true; case R.id.action_hack: openIceweasel(getString(R.string.hack_url), activityContext); return true; case R.id.action_source: openIceweasel("https://gitpull.it/source/libre-busto/", activityContext); return true; case R.id.action_licence: openIceweasel("https://www.gnu.org/licenses/gpl-3.0.html", activityContext); return true; case R.id.action_experiments: startActivity(new Intent(ActivityPrincipal.this, ActivityExperiments.class)); default: } 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; } if (edit){ editor.commit(); } } } diff --git a/app/src/main/java/it/reyboz/bustorino/adapters/RouteAdapter.kt b/app/src/main/java/it/reyboz/bustorino/adapters/RouteAdapter.kt new file mode 100644 index 0000000..8577422 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/adapters/RouteAdapter.kt @@ -0,0 +1,59 @@ +package it.reyboz.bustorino.adapters + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.cardview.widget.CardView +import androidx.recyclerview.widget.RecyclerView +import it.reyboz.bustorino.R +import it.reyboz.bustorino.data.gtfs.GtfsRoute +import java.lang.ref.WeakReference + +class RouteAdapter(val routes: List, + click: onItemClick, + private val layoutId: Int = R.layout.line_title_header) : + RecyclerView.Adapter() +{ + val clickreference: WeakReference + init { + clickreference = WeakReference(click) + } + + class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { + val descrptionTextView: TextView + val nameTextView : TextView + val innerCardView : CardView? + init { + // Define click listener for the ViewHolder's View + nameTextView = view.findViewById(R.id.lineShortNameTextView) + descrptionTextView = view.findViewById(R.id.lineDirectionTextView) + innerCardView = view.findViewById(R.id.innerCardView) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(layoutId, parent, false) + + return ViewHolder(view) + } + + override fun getItemCount() = routes.size + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + // Get element from your dataset at this position and replace the + // contents of the view with that element + val route = routes[position] + holder.nameTextView.text = route.shortName + holder.descrptionTextView.text = route.longName + + holder.itemView.setOnClickListener{ + clickreference.get()?.onRouteItemClicked(route) + } + } + + fun interface onItemClick{ + fun onRouteItemClicked(gtfsRoute: GtfsRoute) + } +} \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/adapters/SquareStopAdapter.java b/app/src/main/java/it/reyboz/bustorino/adapters/SquareStopAdapter.java index 0892a9e..82e042b 100644 --- a/app/src/main/java/it/reyboz/bustorino/adapters/SquareStopAdapter.java +++ b/app/src/main/java/it/reyboz/bustorino/adapters/SquareStopAdapter.java @@ -1,128 +1,128 @@ /* BusTO - UI components Copyright (C) 2017 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.adapters; import android.location.Location; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.util.StopSorterByDistance; import it.reyboz.bustorino.fragments.FragmentListenerMain; import java.util.Collections; import java.util.List; public class SquareStopAdapter extends RecyclerView.Adapter { private final static int layoutRes = R.layout.stop_card; //private List stops; private @Nullable Location userPosition; private FragmentListenerMain listener; private List stops; public SquareStopAdapter(@Nullable List stopList, FragmentListenerMain fragmentListener, @Nullable Location pos) { listener = fragmentListener; userPosition = pos; stops = stopList; } @Override public SquareViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { final View view = LayoutInflater.from(parent.getContext()).inflate(layoutRes, parent, false); //sort the stops by distance if(stops != null && stops.size() > 0) - Collections.sort(stops,new StopSorterByDistance(userPosition)); + Collections.sort(stops,new StopSorterByDistance(userPosition)); return new SquareViewHolder(view); } @Override public void onBindViewHolder(SquareViewHolder holder, int position) { //DO THE ACTUAL WORK TO PUT THE DATA if(stops==null || stops.size() == 0) return; //NO STOPS final Stop stop = stops.get(position); if(stop!=null){ if(stop.getDistanceFromLocation(userPosition)!=Double.POSITIVE_INFINITY){ Double distance = stop.getDistanceFromLocation(userPosition); holder.distancetextView.setText(distance.intValue()+" m"); } else { holder.distancetextView.setVisibility(View.GONE); } holder.stopNameView.setText(stop.getStopDisplayName()); holder.stopIDView.setText(stop.ID); String whatStopsHere = stop.routesThatStopHereToString(); if(whatStopsHere == null) { holder.routesView.setVisibility(View.GONE); } else { holder.routesView.setText(whatStopsHere); holder.routesView.setVisibility(View.VISIBLE); // might be GONE due to View Holder Pattern } holder.stopID =stop.ID; } else { Log.w("SquareStopAdapter","!! The selected stop is null !!"); } } @Override public int getItemCount() { return stops.size(); } class SquareViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { TextView stopIDView; TextView stopNameView; TextView routesView; TextView distancetextView; String stopID; SquareViewHolder(View holdView){ super(holdView); holdView.setOnClickListener(this); stopIDView = (TextView) holdView.findViewById(R.id.stop_numberText); stopNameView = (TextView) holdView.findViewById(R.id.stop_nameText); routesView = (TextView) holdView.findViewById(R.id.stop_linesText); distancetextView = (TextView) holdView.findViewById(R.id.stop_distanceTextView); } @Override public void onClick(View v) { listener.requestArrivalsForStopID(stopID); } } public void setStops(List stops) { this.stops = stops; } public void setUserPosition(@Nullable Location userPosition) { this.userPosition = userPosition; } /* @Override public Stop getItem(int position) { return stops.get(position); } */ } diff --git a/app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsRtPositionsRequest.kt b/app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsRtPositionsRequest.kt index f2e0bf2..5a054e9 100644 --- a/app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsRtPositionsRequest.kt +++ b/app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsRtPositionsRequest.kt @@ -1,72 +1,71 @@ /* BusTO - Backend 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.backend.gtfs import com.android.volley.NetworkResponse import com.android.volley.Request import com.android.volley.Response import com.android.volley.VolleyError import com.android.volley.toolbox.HttpHeaderParser import com.google.transit.realtime.GtfsRealtime -import com.google.transit.realtime.GtfsRealtime.VehiclePosition class GtfsRtPositionsRequest( errorListener: Response.ErrorListener?, val listener: RequestListener) : - Request>(Method.GET, URL_POSITION, errorListener) { - override fun parseNetworkResponse(response: NetworkResponse?): Response> { + Request>(Method.GET, URL_POSITION, errorListener) { + override fun parseNetworkResponse(response: NetworkResponse?): Response> { if (response == null){ return Response.error(VolleyError("Null response")) } if (response.statusCode != 200) return Response.error(VolleyError("Error code is ${response.statusCode}")) val gtfsreq = GtfsRealtime.FeedMessage.parseFrom(response.data) - val positionList = ArrayList() + val positionList = ArrayList() if (gtfsreq.hasHeader() && gtfsreq.entityCount>0){ for (i in 0 until gtfsreq.entityCount){ val entity = gtfsreq.getEntity(i) if (entity.hasVehicle()){ - positionList.add(GtfsPositionUpdate(entity.vehicle)) + positionList.add(LivePositionUpdate(entity.vehicle)) } } } return Response.success(positionList, HttpHeaderParser.parseCacheHeaders(response)) } - override fun deliverResponse(response: ArrayList?) { + override fun deliverResponse(response: ArrayList?) { listener.onResponse(response) } companion object{ const val URL_POSITION = "http://percorsieorari.gtt.to.it/das_gtfsrt/vehicle_position.aspx" const val URL_TRIP_UPDATES ="http://percorsieorari.gtt.to.it/das_gtfsrt/trip_update.aspx" const val URL_ALERTS = "http://percorsieorari.gtt.to.it/das_gtfsrt/alerts.aspx" public interface RequestListener{ - fun onResponse(response: ArrayList?) + fun onResponse(response: ArrayList?) } } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsPositionUpdate.kt b/app/src/main/java/it/reyboz/bustorino/backend/gtfs/LivePositionUpdate.kt similarity index 75% rename from app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsPositionUpdate.kt rename to app/src/main/java/it/reyboz/bustorino/backend/gtfs/LivePositionUpdate.kt index 2936912..a604eff 100644 --- a/app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsPositionUpdate.kt +++ b/app/src/main/java/it/reyboz/bustorino/backend/gtfs/LivePositionUpdate.kt @@ -1,59 +1,65 @@ /* BusTO - Backend 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.backend.gtfs import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate.ScheduleRelationship import com.google.transit.realtime.GtfsRealtime.VehiclePosition import com.google.transit.realtime.GtfsRealtime.VehiclePosition.OccupancyStatus -data class GtfsPositionUpdate( - val tripID: String, - val startTime: String, - val startDate: String, +data class LivePositionUpdate( + val tripID: String, //tripID WITHOUT THE "gtt:" prefix + val startTime: String?, + val startDate: String?, val routeID: String, + val vehicle: String, - val latitude: Float, - val longitude: Float, - val bearing: Float, + val latitude: Double, + val longitude: Double, + val bearing: Float?, val timestamp: Long, - val vehicleInfo: VehicleInfo, + val nextStop: String?, + + /*val vehicleInfo: VehicleInfo, val occupancyStatus: OccupancyStatus?, val scheduleRelationship: ScheduleRelationship? + */ ){ constructor(position: VehiclePosition) : this( position.trip.tripId, position.trip.startTime, position.trip.startDate, position.trip.routeId, - position.position.latitude, - position.position.longitude, + position.vehicle.label, + + position.position.latitude.toDouble(), + position.position.longitude.toDouble(), position.position.bearing, position.timestamp, - VehicleInfo(position.vehicle.id, position.vehicle.label), - position.occupancyStatus, null ) - data class VehicleInfo( + /*data class VehicleInfo( val id: String, val label:String ) + + */ } diff --git a/app/src/main/java/it/reyboz/bustorino/backend/mato/MQTTMatoClient.kt b/app/src/main/java/it/reyboz/bustorino/backend/mato/MQTTMatoClient.kt new file mode 100644 index 0000000..385de81 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/backend/mato/MQTTMatoClient.kt @@ -0,0 +1,323 @@ +package it.reyboz.bustorino.backend.mato + +import android.content.Context +import android.util.Log +import androidx.lifecycle.LifecycleOwner +import info.mqtt.android.service.Ack +import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import org.eclipse.paho.client.mqttv3.* +import info.mqtt.android.service.MqttAndroidClient +import info.mqtt.android.service.QoS + +import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence +import org.json.JSONArray +import org.json.JSONException +import java.lang.ref.WeakReference +import java.util.ArrayList +import java.util.Properties + +typealias PositionsMap = HashMap > + +class MQTTMatoClient private constructor(): MqttCallbackExtended{ + + private var isStarted = false + private var subscribedToAll = false + + private lateinit var client: MqttAndroidClient + //private var clientID = "" + + private val respondersMap = HashMap>>() + + private val currentPositions = PositionsMap() + + private lateinit var lifecycle: LifecycleOwner + private var context: Context?= null + + private fun connect(context: Context, iMqttActionListener: IMqttActionListener?){ + + val clientID = "mqttjs_${getRandomString(8)}" + + client = MqttAndroidClient(context,SERVER_ADDR,clientID,Ack.AUTO_ACK) + + val options = MqttConnectOptions() + //options.sslProperties = + options.isCleanSession = true + val headersPars = Properties() + headersPars.setProperty("Origin","https://mato.muoversiatorino.it") + headersPars.setProperty("Host","mapi.5t.torino.it") + options.customWebSocketHeaders = headersPars + + //actually connect + client.connect(options,null, iMqttActionListener) + isStarted = true + client.setCallback(this) + + if (this.context ==null) + this.context = context.applicationContext + } + + + override fun connectComplete(reconnect: Boolean, serverURI: String?) { + Log.d(DEBUG_TAG, "Connected to server, reconnect: $reconnect") + Log.d(DEBUG_TAG, "Have listeners: $respondersMap") + } + + fun startAndSubscribe(lineId: String, responder: MQTTMatoListener, context: Context): Boolean{ + //start the client, and then subscribe to the topic + val topic = mapTopic(lineId) + synchronized(this) { + if(!isStarted){ + connect(context, object : IMqttActionListener{ + override fun onSuccess(asyncActionToken: IMqttToken?) { + val disconnectedBufferOptions = DisconnectedBufferOptions() + disconnectedBufferOptions.isBufferEnabled = true + disconnectedBufferOptions.bufferSize = 100 + disconnectedBufferOptions.isPersistBuffer = false + disconnectedBufferOptions.isDeleteOldestMessages = false + client.setBufferOpts(disconnectedBufferOptions) + client.subscribe(topic, QoS.AtMostOnce.value) + } + + override fun onFailure(asyncActionToken: IMqttToken?, exception: Throwable?) { + Log.e(DEBUG_TAG, "FAILED To connect to the server") + } + + }) + //wait for connection + } else { + client.subscribe(topic, QoS.AtMostOnce.value) + } + } + + + + synchronized(this){ + if (!respondersMap.contains(lineId)) + respondersMap[lineId] = ArrayList() + respondersMap[lineId]!!.add(WeakReference(responder)) + Log.d(DEBUG_TAG, "Add MQTT Listener for line $lineId, topic $topic") + } + + return true + } + + fun desubscribe(responder: MQTTMatoListener){ + var removed = false + for ((line,v)in respondersMap.entries){ + var done = false + for (el in v){ + if (el.get()==null){ + v.remove(el) + } else if(el.get() == responder){ + v.remove(el) + done = true + } + if (done) + break + } + if(done) Log.d(DEBUG_TAG, "Removed one listener for line $line, listeners: $v") + //if (done) break + if (v.isEmpty()){ + //actually unsubscribe + client.unsubscribe( mapTopic(line)) + } + removed = done || removed + } + Log.d(DEBUG_TAG, "Removed: $removed, respondersMap: $respondersMap") + } + fun getPositions(): PositionsMap{ + return currentPositions + } + + fun sendUpdateToResponders(responders: ArrayList>): Boolean{ + var sent = false + for (wrD in responders) + if (wrD.get() == null) + responders.remove(wrD) + else { + wrD.get()!!.onUpdateReceived(currentPositions) + sent = true + } + return sent + } + + override fun connectionLost(cause: Throwable?) { + Log.w(DEBUG_TAG, "Lost connection in MQTT Mato Client") + + + synchronized(this){ + // isStarted = false + //var i = 0 + // while(i < 20 && !isStarted) { + connect(context!!, object: IMqttActionListener{ + override fun onSuccess(asyncActionToken: IMqttToken?) { + //relisten to messages + for ((line,elms) in respondersMap.entries){ + val topic = mapTopic(line) + if(elms.isEmpty()) + respondersMap.remove(line) + else + client.subscribe(topic, QoS.AtMostOnce.value, null, null) + } + Log.d(DEBUG_TAG, "Reconnected to MQTT Mato Client") + + } + + override fun onFailure(asyncActionToken: IMqttToken?, exception: Throwable?) { + Log.w(DEBUG_TAG, "Failed to reconnect to MQTT server") + } + }) + + } + + + } + + override fun messageArrived(topic: String?, message: MqttMessage?) { + if (topic==null || message==null) return + + parseMessageAndAddToList(topic, message) + //GlobalScope.launch { } + + } + + private fun parseMessageAndAddToList(topic: String, message: MqttMessage){ + + val vals = topic.split("/") + val lineId = vals[1] + val vehicleId = vals[2] + val timestamp = (System.currentTimeMillis() / 1000 ) as Long + + val messString = String(message.payload) + + + try { + val jsonList = JSONArray(messString) + //val full = if(jsonList.length()>7) { + // if (jsonList.get(7).equals(null)) null else jsonList.getInt(7) + //}else null + /*val posUpdate = MQTTPositionUpdate(lineId+"U", vehicleId, + jsonList.getDouble(0), + jsonList.getDouble(1), + if(jsonList.get(2).equals(null)) null else jsonList.getInt(2), + if(jsonList.get(3).equals(null)) null else jsonList.getInt(3), + if(jsonList.get(4).equals(null)) null else jsonList.getString(4)+"U", + if(jsonList.get(5).equals(null)) null else jsonList.getInt(5), + if(jsonList.get(6).equals(null)) null else jsonList.getInt(6), + //full + ) + + */ + if(jsonList.get(4)==null){ + Log.d(DEBUG_TAG, "We have null tripId: line $lineId veh $vehicleId: $jsonList") + return + } + val posUpdate = LivePositionUpdate( + jsonList.getString(4)+"U", + null, + null, + lineId+"U", + vehicleId, + jsonList.getDouble(0), //latitude + jsonList.getDouble(1), //longitude + if(jsonList.get(2).equals(null)) null else jsonList.getInt(2).toFloat(), //"heading" (same as bearing?) + timestamp, + if(jsonList.get(6).equals(null)) null else jsonList.getInt(6).toString() //nextStop + ) + + //add update + var valid = false + if(!currentPositions.contains(lineId)) + currentPositions[lineId] = HashMap() + currentPositions[lineId]?.let{ + it[vehicleId] = posUpdate + valid = true + } + var sent = false + if (LINES_ALL in respondersMap.keys) { + sent = sendUpdateToResponders(respondersMap[LINES_ALL]!!) + + + } + if(lineId in respondersMap.keys){ + sent = sendUpdateToResponders(respondersMap[lineId]!!) or sent + + } + if(!sent){ + Log.w(DEBUG_TAG, "We have received an update but apparently there is no one to send it") + var emptyResp = true + for(en in respondersMap.values){ + if(!en.isEmpty()){ + emptyResp=false + break + } + } + //try unsubscribing to all + if(emptyResp) { + Log.d(DEBUG_TAG, "Unsubscribe all") + client.unsubscribe(LINES_ALL) + } + } + //Log.d(DEBUG_TAG, "We have update on line $lineId, vehicle $vehicleId") + } catch (e: JSONException){ + Log.e(DEBUG_TAG,"Cannot decipher message on topic $topic, line $lineId, veh $vehicleId") + e.printStackTrace() + } + } + + + override fun deliveryComplete(token: IMqttDeliveryToken?) { + //NOT USED (we're not sending any messages) + } + + + companion object{ + + const val SERVER_ADDR="wss://mapi.5t.torino.it:443/scre" + const val LINES_ALL="ALL" + private const val DEBUG_TAG="BusTO-MatoMQTT" + @Volatile + private var instance: MQTTMatoClient? = null + + fun getInstance() = instance?: synchronized(this){ + instance?: MQTTMatoClient().also { instance= it } + } + + @JvmStatic + fun mapTopic(lineId: String): String{ + return if(lineId== LINES_ALL || lineId == "#") + "#" + else{ + "/${lineId}/#" + } + } + + fun getRandomString(length: Int) : String { + val allowedChars = ('a'..'f') + ('0'..'9') + return (1..length) + .map { allowedChars.random() } + .joinToString("") + } + + + fun interface MQTTMatoListener{ + //positionsMap is a dict with line -> vehicle -> Update + fun onUpdateReceived(posUpdates: PositionsMap) + } + } +} + +data class MQTTPositionUpdate( + val lineId: String, + val vehicleId: String, + val latitude: Double, + val longitude: Double, + val heading: Int?, + val speed: Int?, + val tripId: String?, + val direct: Int?, + val nextStop: Int?, + //val full: Int? +) \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt b/app/src/main/java/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt index f870d26..29cbb6f 100644 --- a/app/src/main/java/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt +++ b/app/src/main/java/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt @@ -1,419 +1,419 @@ /* BusTO - Backend 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.backend.mato import android.content.Context import android.util.Log import com.android.volley.DefaultRetryPolicy import com.android.volley.toolbox.RequestFuture import it.reyboz.bustorino.BuildConfig import it.reyboz.bustorino.backend.* import it.reyboz.bustorino.data.gtfs.GtfsAgency import it.reyboz.bustorino.data.gtfs.GtfsFeed import it.reyboz.bustorino.data.gtfs.GtfsRoute import it.reyboz.bustorino.data.gtfs.MatoPattern import org.json.JSONArray import org.json.JSONException import org.json.JSONObject import java.util.* import java.util.concurrent.ExecutionException import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException import java.util.concurrent.atomic.AtomicReference import kotlin.collections.ArrayList open class MatoAPIFetcher( private val minNumPassaggi: Int ) : ArrivalsFetcher { var appContext: Context? = null set(value) { field = value!!.applicationContext } constructor(): this(DEF_MIN_NUMPASSAGGI) override fun ReadArrivalTimesAll(stopID: String?, res: AtomicReference?): Palina { stopID!! val now = Calendar.getInstance().time var numMinutes = 30 var palina = Palina(stopID) var numPassaggi = 0 var trials = 0 val numDepartures = 8 while (numPassaggi < minNumPassaggi && trials < 2) { //numDepartures+=2 numMinutes += 20 val future = RequestFuture.newFuture() val request = MapiArrivalRequest(stopID, now, numMinutes * 60, numDepartures, res, future, future) if (appContext == null || res == null) { Log.e("BusTO:MatoAPIFetcher", "ERROR: Given null context or null result ref") return Palina(stopID) } val requestQueue = NetworkVolleyManager.getInstance(appContext).requestQueue request.setTag(getVolleyReqTag(MatoQueries.QueryType.ARRIVALS)) requestQueue.add(request) try { val palinaResult = future.get(5, TimeUnit.SECONDS) if (palinaResult!=null) { /*if (BuildConfig.DEBUG) for (r in palinaResult.queryAllRoutes()){ Log.d(DEBUG_TAG, "route " + r.gtfsId + " has " + r.passaggi.size + " passaggi: "+ r.passaggiToString) }*/ palina = palinaResult numPassaggi = palina.minNumberOfPassages } else{ Log.d(DEBUG_TAG, "Result palina is null") } } catch (e: InterruptedException) { e.printStackTrace() res.set(Fetcher.Result.PARSER_ERROR) } catch (e: ExecutionException) { e.printStackTrace() if (res.get() == Fetcher.Result.OK) res.set(Fetcher.Result.SERVER_ERROR) } catch (e: TimeoutException) { res.set(Fetcher.Result.CONNECTION_ERROR) e.printStackTrace() } trials++ } return palina } override fun getSourceForFetcher(): Passaggio.Source { return Passaggio.Source.MatoAPI } companion object{ const val VOLLEY_TAG = "MatoAPIFetcher" const val DEBUG_TAG = "BusTO:MatoAPIFetcher" const val DEF_MIN_NUMPASSAGGI=2 val REQ_PARAMETERS = mapOf( "Content-Type" to "application/json; charset=utf-8", "DNT" to "1", "Host" to "mapi.5t.torino.it") private val longRetryPolicy = DefaultRetryPolicy(10000,5,DefaultRetryPolicy.DEFAULT_BACKOFF_MULT) fun getVolleyReqTag(type: MatoQueries.QueryType): String{ return when (type){ MatoQueries.QueryType.ALL_STOPS -> VOLLEY_TAG +"_AllStops" MatoQueries.QueryType.ARRIVALS -> VOLLEY_TAG+"_Arrivals" MatoQueries.QueryType.FEEDS -> VOLLEY_TAG +"_Feeds" MatoQueries.QueryType.ROUTES -> VOLLEY_TAG +"_AllRoutes" MatoQueries.QueryType.PATTERNS_FOR_ROUTES -> VOLLEY_TAG + "_PatternsForRoute" MatoQueries.QueryType.TRIP -> VOLLEY_TAG+"_Trip" } } /** * Get stops from the MatoAPI, set [res] accordingly */ fun getAllStopsGTT(context: Context, res: AtomicReference?): List{ val requestQueue = NetworkVolleyManager.getInstance(context).requestQueue val future = RequestFuture.newFuture>() val request = VolleyAllStopsRequest(future, future) request.tag = getVolleyReqTag(MatoQueries.QueryType.ALL_STOPS) request.retryPolicy = longRetryPolicy requestQueue.add(request) var palinaList:List = mutableListOf() try { palinaList = future.get(120, TimeUnit.SECONDS) res?.set(Fetcher.Result.OK) }catch (e: InterruptedException) { e.printStackTrace() res?.set(Fetcher.Result.PARSER_ERROR) } catch (e: ExecutionException) { e.printStackTrace() res?.set(Fetcher.Result.SERVER_ERROR) } catch (e: TimeoutException) { res?.set(Fetcher.Result.CONNECTION_ERROR) e.printStackTrace() } return palinaList } /* fun makeRequest(type: QueryType?, variables: JSONObject) : String{ type.let { val requestData = JSONObject() when (it){ QueryType.ARRIVALS ->{ requestData.put("operationName","AllStopsDirect") requestData.put("variables", variables) requestData.put("query", MatoQueries.QUERY_ARRIVALS) } else -> { //TODO all other cases } } //todo make the request... //https://pablobaxter.github.io/volley-docs/com/android/volley/toolbox/RequestFuture.html //https://stackoverflow.com/questions/16904741/can-i-do-a-synchronous-request-with-volley } return "" } */ fun parseStopJSON(jsonStop: JSONObject): Palina{ val latitude = jsonStop.getDouble("lat") val longitude = jsonStop.getDouble("lon") val palina = Palina( jsonStop.getString("code"), jsonStop.getString("name"), null, null, latitude, longitude, jsonStop.getString("gtfsId") ) val routesStoppingJSON = jsonStop.getJSONArray("routes") val baseRoutes = mutableListOf() // get all the possible routes for (i in 0 until routesStoppingJSON.length()){ val routeBaseInfo = routesStoppingJSON.getJSONObject(i) val r = Route(routeBaseInfo.getString("shortName"), Route.Type.UNKNOWN,"") r.setGtfsId(routeBaseInfo.getString("gtfsId").trim()) baseRoutes.add(r) } if (jsonStop.has("desc")){ palina.location = jsonStop.getString("desc") } //there is also "zoneId" which is the zone of the stop (0-> city, etc) if(jsonStop.has("stoptimesForPatterns")) { val routesStopTimes = jsonStop.getJSONArray("stoptimesForPatterns") for (i in 0 until routesStopTimes.length()) { val patternJSON = routesStopTimes.getJSONObject(i) val mRoute = parseRouteStoptimesJSON(patternJSON) //Log.d("BusTO-MapiFetcher") //val directionId = patternJSON.getJSONObject("pattern").getInt("directionId") //TODO: use directionId palina.addRoute(mRoute) for (r in baseRoutes) { if (mRoute.gtfsId != null && r.gtfsId.equals(mRoute.gtfsId)) { baseRoutes.remove(r) break } } } } for (noArrivalRoute in baseRoutes){ palina.addRoute(noArrivalRoute) } //val gtfsRoutes = mutableListOf<>() return palina } fun parseRouteStoptimesJSON(jsonPatternWithStops: JSONObject): Route{ val patternJSON = jsonPatternWithStops.getJSONObject("pattern") val routeJSON = patternJSON.getJSONObject("route") val passaggiJSON = jsonPatternWithStops.getJSONArray("stoptimes") val gtfsId = routeJSON.getString("gtfsId").trim() val passages = mutableListOf() for( i in 0 until passaggiJSON.length()){ val stoptime = passaggiJSON.getJSONObject(i) val scheduledTime = stoptime.getInt("scheduledArrival") val realtimeTime = stoptime.getInt("realtimeArrival") val realtime = stoptime.getBoolean("realtime") passages.add( Passaggio(realtimeTime,realtime, realtimeTime-scheduledTime, Passaggio.Source.MatoAPI) ) } var routeType = Route.Type.UNKNOWN if (gtfsId[gtfsId.length-1] == 'E') routeType = Route.Type.LONG_DISTANCE_BUS else when( routeJSON.getString("mode").trim()){ "BUS" -> routeType = Route.Type.BUS "TRAM" -> routeType = Route.Type.TRAM } val route = Route( routeJSON.getString("shortName"), patternJSON.getString("headsign"), routeType, passages, ) route.setGtfsId(gtfsId) return route } fun makeRequestParameters(requestName:String, variables: JSONObject, query: String): JSONObject{ val data = JSONObject() data.put("operationName", requestName) data.put("variables", variables) data.put("query", query) return data } fun getFeedsAndAgencies(context: Context, res: AtomicReference?): Pair, ArrayList> { val requestQueue = NetworkVolleyManager.getInstance(context).requestQueue val future = RequestFuture.newFuture() val request = MatoVolleyJSONRequest(MatoQueries.QueryType.FEEDS, JSONObject(), future, future) request.setRetryPolicy(longRetryPolicy) request.tag = getVolleyReqTag(MatoQueries.QueryType.FEEDS) requestQueue.add(request) val feeds = ArrayList() val agencies = ArrayList() var outObj = "" try { val resObj = future.get(120,TimeUnit.SECONDS) outObj = resObj.toString(1) val feedsJSON = resObj.getJSONArray("feeds") for (i in 0 until feedsJSON.length()){ val resTup = ResponseParsing.parseFeedJSON(feedsJSON.getJSONObject(i)) feeds.add(resTup.first) agencies.addAll(resTup.second) } } catch (e: InterruptedException) { e.printStackTrace() res?.set(Fetcher.Result.PARSER_ERROR) } catch (e: ExecutionException) { e.printStackTrace() res?.set(Fetcher.Result.SERVER_ERROR) } catch (e: TimeoutException) { res?.set(Fetcher.Result.CONNECTION_ERROR) e.printStackTrace() } catch (e: JSONException){ e.printStackTrace() res?.set(Fetcher.Result.PARSER_ERROR) Log.e(DEBUG_TAG, "Downloading feeds: $outObj") } return Pair(feeds,agencies) } fun getRoutes(context: Context, res: AtomicReference?): ArrayList{ val requestQueue = NetworkVolleyManager.getInstance(context).requestQueue val future = RequestFuture.newFuture() val params = JSONObject() params.put("feeds","gtt") val request = MatoVolleyJSONRequest(MatoQueries.QueryType.ROUTES, params, future, future) request.tag = getVolleyReqTag(MatoQueries.QueryType.ROUTES) request.retryPolicy = longRetryPolicy requestQueue.add(request) val routes = ArrayList() var outObj = "" try { val resObj = future.get(120,TimeUnit.SECONDS) outObj = resObj.toString(1) val routesJSON = resObj.getJSONArray("routes") for (i in 0 until routesJSON.length()){ val route = ResponseParsing.parseRouteJSON(routesJSON.getJSONObject(i)) routes.add(route) } } catch (e: InterruptedException) { e.printStackTrace() res?.set(Fetcher.Result.PARSER_ERROR) } catch (e: ExecutionException) { e.printStackTrace() res?.set(Fetcher.Result.SERVER_ERROR) } catch (e: TimeoutException) { res?.set(Fetcher.Result.CONNECTION_ERROR) e.printStackTrace() } catch (e: JSONException){ e.printStackTrace() res?.set(Fetcher.Result.PARSER_ERROR) Log.e(DEBUG_TAG, "Downloading feeds: $outObj") } return routes } fun getPatternsWithStops(context: Context, routesGTFSIds: MutableCollection, res: AtomicReference?): ArrayList{ val requestQueue = NetworkVolleyManager.getInstance(context).requestQueue val future = RequestFuture.newFuture() val params = JSONObject() for (r in routesGTFSIds){ if(r.isEmpty()) routesGTFSIds.remove(r) } val routes = JSONArray(routesGTFSIds) params.put("routes",routes) val request = MatoVolleyJSONRequest(MatoQueries.QueryType.PATTERNS_FOR_ROUTES, params, future, future) request.retryPolicy = longRetryPolicy request.tag = getVolleyReqTag(MatoQueries.QueryType.PATTERNS_FOR_ROUTES) requestQueue.add(request) val patterns = ArrayList() - //var outObj = "" + var resObj = JSONObject() try { - val resObj = future.get(60,TimeUnit.SECONDS) + resObj = future.get(60,TimeUnit.SECONDS) //outObj = resObj.toString(1) val routesJSON = resObj.getJSONArray("routes") for (i in 0 until routesJSON.length()){ val patternList = ResponseParsing.parseRoutePatternsStopsJSON(routesJSON.getJSONObject(i)) patterns.addAll(patternList) } } catch (e: InterruptedException) { e.printStackTrace() res?.set(Fetcher.Result.PARSER_ERROR) } catch (e: ExecutionException) { e.printStackTrace() res?.set(Fetcher.Result.SERVER_ERROR) } catch (e: TimeoutException) { res?.set(Fetcher.Result.CONNECTION_ERROR) e.printStackTrace() } catch (e: JSONException){ e.printStackTrace() res?.set(Fetcher.Result.PARSER_ERROR) - //Log.e(DEBUG_TAG, "Downloading feeds: $outObj") + Log.e(DEBUG_TAG, "Got result: $resObj") } return patterns } } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/backend/mato/ResponseParsing.kt b/app/src/main/java/it/reyboz/bustorino/backend/mato/ResponseParsing.kt index 62e9833..450dfa8 100644 --- a/app/src/main/java/it/reyboz/bustorino/backend/mato/ResponseParsing.kt +++ b/app/src/main/java/it/reyboz/bustorino/backend/mato/ResponseParsing.kt @@ -1,146 +1,162 @@ /* BusTO - Backend components Copyright (C) 2022 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.backend.mato import android.util.Log import it.reyboz.bustorino.data.gtfs.* import org.json.JSONException import org.json.JSONObject import kotlin.jvm.Throws /** * Class to hold the code for the parsing of responses from the Mato API, * from the JSON Object */ abstract class ResponseParsing{ companion object { final val DEBUG_TAG = "BusTO:MatoResponseParse" fun parseAgencyJSON(jsonObject: JSONObject): GtfsAgency { return GtfsAgency( jsonObject.getString("gtfsId"), jsonObject.getString("name"), jsonObject.getString("url"), jsonObject.getString("fareUrl"), jsonObject.getString("phone"), null ) } /** * Parse a feed request json, containing the GTFS agencies it is served by */ fun parseFeedJSON(jsonObject: JSONObject): Pair> { val agencies = ArrayList() val feed = GtfsFeed(jsonObject.getString("feedId")) val oo = jsonObject.getJSONArray("agencies") agencies.ensureCapacity(oo.length()) for (i in 0 until oo.length()) { val agObj = oo.getJSONObject(i) agencies.add( GtfsAgency( agObj.getString("gtfsId"), agObj.getString("name"), agObj.getString("url"), agObj.getString("fareUrl"), agObj.getString("phone"), feed ) ) } return Pair(feed, agencies) } fun parseRouteJSON(jsonObject: JSONObject): GtfsRoute { val agencyJSON = jsonObject.getJSONObject("agency") val agencyId = agencyJSON.getString("gtfsId") return GtfsRoute( jsonObject.getString("gtfsId"), agencyId, jsonObject.getString("shortName"), jsonObject.getString("longName"), jsonObject.getString("desc"), GtfsMode.getByValue(jsonObject.getInt("type"))!!, jsonObject.getString("color"), jsonObject.getString("textColor") ) } /** * Parse a route pattern from the JSON response of the MaTO server */ fun parseRoutePatternsStopsJSON(jsonObject: JSONObject): ArrayList { val routeGtfsId = jsonObject.getString("gtfsId") val patternsJSON = jsonObject.getJSONArray("patterns") val patternsOut = ArrayList(patternsJSON.length()) var mPatternJSON: JSONObject for (i in 0 until patternsJSON.length()) { mPatternJSON = patternsJSON.getJSONObject(i) val stopsJSON = mPatternJSON.getJSONArray("stops") val stopsCodes = ArrayList(stopsJSON.length()) for (k in 0 until stopsJSON.length()) { stopsCodes.add( stopsJSON.getJSONObject(k).getString("gtfsId") ) } val geometry = mPatternJSON.getJSONObject("patternGeometry") val numGeo = geometry.getInt("length") val polyline = geometry.getString("points") patternsOut.add( MatoPattern( mPatternJSON.getString("name"), mPatternJSON.getString("code"), mPatternJSON.getString("semanticHash"), mPatternJSON.getInt("directionId"), - routeGtfsId, mPatternJSON.getString("headsign"), polyline, numGeo, stopsCodes + routeGtfsId, + sanitize( mPatternJSON.getString("headsign")), + polyline, numGeo, stopsCodes ) ) } return patternsOut } @Throws(JSONException::class) fun parseTripInfo(jsonData: JSONObject): GtfsTrip { val jsonTrip = jsonData.getJSONObject("trip") val routeId = jsonTrip.getJSONObject("route").getString("gtfsId") val patternId = jsonTrip.getJSONObject("pattern").getString("code") // still have "activeDates" which are the days in which the pattern is active //Log.d("BusTO:RequestParsing", "Making GTFS trip for: $jsonData") val trip = GtfsTrip( - routeId, jsonTrip.getString("serviceId"), jsonTrip.getString("gtfsId"), - jsonTrip.getString("tripHeadsign"), -1, "", "", + routeId, jsonTrip.getString("serviceId"), + jsonTrip.getString("gtfsId"), + sanitize(jsonTrip.getString("tripHeadsign")), + -1, "", "", Converters.wheelchairFromString(jsonTrip.getString("wheelchairAccessible")), false, patternId, jsonTrip.getString("semanticHash") ) return trip } + + @JvmStatic + fun sanitize(dir: String): String{ + var str = dir.trim() + val lastChar = str[str.length-1] + if(lastChar==','|| lastChar==';') { + Log.d(DEBUG_TAG, "Sanitization: removing last char from $str") + str = str.dropLast(1) + } + + return str + } } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/backend/utils.java b/app/src/main/java/it/reyboz/bustorino/backend/utils.java index 19d28dd..55e6755 100644 --- a/app/src/main/java/it/reyboz/bustorino/backend/utils.java +++ b/app/src/main/java/it/reyboz/bustorino/backend/utils.java @@ -1,355 +1,356 @@ /* BusTO (backend components) Copyright (C) 2019 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.backend; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.util.Log; import android.util.TypedValue; import android.view.View; import androidx.annotation.Nullable; import androidx.preference.PreferenceManager; import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import it.reyboz.bustorino.backend.mato.MatoAPIFetcher; import it.reyboz.bustorino.fragments.SettingsFragment; public abstract class utils { private static final double EarthRadius = 6371e3; public static Double measuredistanceBetween(double lat1,double long1,double lat2,double long2){ final double phi1 = Math.toRadians(lat1); final double phi2 = Math.toRadians(lat2); final double deltaPhi = Math.toRadians(lat2-lat1); final double deltaTheta = Math.toRadians(long2-long1); final double a = Math.sin(deltaPhi/2)*Math.sin(deltaPhi/2)+ Math.cos(phi1)*Math.cos(phi2)*Math.sin(deltaTheta/2)*Math.sin(deltaTheta/2); final double c = 2*Math.atan2(Math.sqrt(a),Math.sqrt(1-a)); return Math.abs(EarthRadius*c); } public static Double angleRawDifferenceFromMeters(double distanceInMeters){ return Math.toDegrees(distanceInMeters/EarthRadius); } - /* - public static int convertDipToPixels(Context con,float dips) + + public static int convertDipToPixelsInt(Context con,double dips) { return (int) (dips * con.getResources().getDisplayMetrics().density + 0.5f); } - */ + public static float convertDipToPixels(Context con, float dp){ return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,con.getResources().getDisplayMetrics()); } + /* public static int calculateNumColumnsFromSize(View containerView, int pixelsize){ int width = containerView.getWidth(); float ncols = ((float)width)/pixelsize; return (int) Math.floor(ncols); } */ /** * Check if there is an internet connection * @param con context object to get the system service * @return true if we are */ public static boolean isConnected(Context con) { ConnectivityManager connMgr = (ConnectivityManager) con.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = connMgr.getActiveNetworkInfo(); return networkInfo != null && networkInfo.isConnected(); } ///////////////////// INTENT HELPER //////////////////////////////////////////////////////////// /** * Try to extract the bus stop ID from a URi * * @param uri The URL * @return bus stop ID or null */ public static String getBusStopIDFromUri(Uri uri) { String busStopID; // everithing catches fire when passing null to a switch. String host = uri.getHost(); if (host == null) { Log.e("ActivityMain", "Not an URL: " + uri); return null; } switch (host) { case "m.gtt.to.it": // http://m.gtt.to.it/m/it/arrivi.jsp?n=1254 busStopID = uri.getQueryParameter("n"); if (busStopID == null) { Log.e("ActivityMain", "Expected ?n from: " + uri); } break; case "www.gtt.to.it": case "gtt.to.it": // http://www.gtt.to.it/cms/percorari/arrivi?palina=1254 busStopID = uri.getQueryParameter("palina"); if (busStopID == null) { Log.e("ActivityMain", "Expected ?palina from: " + uri); } break; default: Log.e("ActivityMain", "Unexpected intent URL: " + uri); busStopID = null; } return busStopID; } final static Pattern ROMAN_PATTERN = Pattern.compile( "^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$"); private static boolean isRomanNumber(String str){ if(str.isEmpty()) return false; final Matcher matcher = ROMAN_PATTERN.matcher(str); return matcher.find(); } public static String toTitleCase(String givenString, boolean lowercaseRest) { String[] arr = givenString.trim().split(" "); StringBuilder sb = new StringBuilder(); //Log.d("BusTO chars", "String parsing: "+givenString+" in array: "+ Arrays.toString(arr)); for (String s : arr) { if (s.length() > 0) { String[] allsubs = s.split("\\."); boolean addPoint = s.contains("."); /*if (s.contains(".lli")|| s.contains(".LLI")) //Fratelli { DOESN'T ALWAYS WORK addPoint = false; allsubs = new String[]{s}; }*/ boolean first = true; for (String subs : allsubs) { if(first) first=false; else { if (addPoint) sb.append("."); sb.append(" "); } if(isRomanNumber(subs)){ //add and skip the rest sb.append(subs); continue; } //SPLIT ON ', check if contains "D'" if(subs.toLowerCase(Locale.ROOT).startsWith("d'")){ sb.append("D'"); subs = subs.substring(2); } int index = 0; char c = subs.charAt(index); if(subs.length() > 1 && c=='('){ sb.append(c); index += 1; c = subs.charAt(index); } sb.append(Character.toUpperCase(c)); if (lowercaseRest) sb.append(subs.substring(index+1).toLowerCase(Locale.ROOT)); else sb.append(subs.substring(index+1)); } if(addPoint && allsubs.length == 1) sb.append('.'); sb.append(" "); /*sb.append(Character.toUpperCase(arr[i].charAt(0))); if (lowercaseRest) sb.append(arr[i].substring(1).toLowerCase(Locale.ROOT)); else sb.append(arr[i].substring(1)); sb.append(" "); */ } else sb.append(s); } return sb.toString().trim(); } /** * Open an URL in the default browser. * * @param url URL */ public static void openIceweasel(String url, Context context) { Intent browserIntent1 = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); if (browserIntent1.resolveActivity(context.getPackageManager()) != null) { //check we have an activity ready to receive intents (otherwise, there will be a crash) context.startActivity(browserIntent1); } else{ Log.e("BusTO","openIceweasel can't find a browser"); } } /** * Get the default list of fetchers for arrival times * @return array of ArrivalsFetchers to use */ public static ArrivalsFetcher[] getDefaultArrivalsFetchers(){ return new ArrivalsFetcher[]{ new MatoAPIFetcher(), new GTTJSONFetcher(), new FiveTScraperFetcher()}; } /** * Get the default list of fetchers for arrival times * @return array of ArrivalsFetchers to use */ public static List getDefaultArrivalsFetchers(Context context){ SharedPreferences defSharPref = PreferenceManager.getDefaultSharedPreferences(context); final Set setSelected = new HashSet<>(defSharPref.getStringSet(SettingsFragment.KEY_ARRIVALS_FETCHERS_USE, new HashSet<>())); if (setSelected.isEmpty()) { return Arrays.asList(new MatoAPIFetcher(), new GTTJSONFetcher(), new FiveTScraperFetcher()); }else{ ArrayList outFetchers = new ArrayList<>(4); /*for(String s: setSelected){ switch (s){ case "matofetcher": outFetchers.add(new MatoAPIFetcher()); break; case "fivetapifetcher": outFetchers.add(new FiveTAPIFetcher()); break; case "gttjsonfetcher": outFetchers.add(new GTTJSONFetcher()); break; case "fivetscraper": outFetchers.add(new FiveTScraperFetcher()); break; default: throw new IllegalArgumentException(); } }*/ if (setSelected.contains("matofetcher")) { outFetchers.add(new MatoAPIFetcher()); setSelected.remove("matofetcher"); } if (setSelected.contains("fivetapifetcher")) { outFetchers.add(new FiveTAPIFetcher()); setSelected.remove("fivetapifetcher"); } if (setSelected.contains("gttjsonfetcher")){ outFetchers.add(new GTTJSONFetcher()); setSelected.remove("gttjsonfetcher"); } if (setSelected.contains("fivetscraper")) { outFetchers.add(new FiveTScraperFetcher()); setSelected.remove("fivetscraper"); } if(!setSelected.isEmpty()){ Log.e("BusTO-Utils","Getting some fetchers values which are not contemplated: "+setSelected); } return outFetchers; } } /*public String getShorterDirection(String headSign){ String[] parts = headSign.split(","); if (parts.length<=1){ return headSign.trim(); } String first = parts[0].trim(); String second = parts[1].trim(); String firstLower = first.toLowerCase(Locale.ITALIAN); switch (firstLower){ case "circolare destra": case "circolare sinistra": case } }*/ /** * Print the first i lines of the the trace of an exception * https://stackoverflow.com/questions/21706722/fetch-only-first-n-lines-of-a-stack-trace */ /* public static String traceCaller(Exception ex, int i) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); StringBuilder sb = new StringBuilder(); ex.printStackTrace(pw); String ss = sw.toString(); String[] splitted = ss.split("\n"); sb.append("\n"); if(splitted.length > 2 + i) { for(int x = 2; x < i+2; x++) { sb.append(splitted[x].trim()); sb.append("\n"); } return sb.toString(); } return "Trace too Short."; } */ public static String joinList(@Nullable List dat, String separator){ StringBuilder sb = new StringBuilder(); if(dat==null || dat.size()==0) return ""; else if(dat.size()==1) return dat.get(0); sb.append(dat.get(0)); for (int i=1; i Set convertArrayToSet(T[] array) { // Create an empty Set Set set = new HashSet<>(); // Add each element into the set set.addAll(Arrays.asList(array)); // Return the converted Set return set; } public static String giveClassesForArray(T[] array){ StringBuilder sb = new StringBuilder(); for (T f: array){ sb.append(""); sb.append(f.getClass().getSimpleName()); sb.append("; "); } return sb.toString(); } } diff --git a/app/src/main/java/it/reyboz/bustorino/data/GtfsRepository.kt b/app/src/main/java/it/reyboz/bustorino/data/GtfsRepository.kt index d3c310c..bba97c0 100644 --- a/app/src/main/java/it/reyboz/bustorino/data/GtfsRepository.kt +++ b/app/src/main/java/it/reyboz/bustorino/data/GtfsRepository.kt @@ -1,34 +1,38 @@ package it.reyboz.bustorino.data import android.content.Context import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import it.reyboz.bustorino.data.gtfs.* class GtfsRepository( val gtfsDao: GtfsDBDao ) { constructor(context: Context) : this(GtfsDatabase.getGtfsDatabase(context).gtfsDao()) fun getLinesLiveDataForFeed(feed: String): LiveData>{ //return withContext(Dispatchers.IO){ return gtfsDao.getRoutesForFeed(feed) //} } fun getPatternsForRouteID(routeID: String): LiveData>{ return if(routeID.isNotEmpty()) gtfsDao.getPatternsLiveDataByRouteID(routeID) else MutableLiveData(listOf()) } /** * Get the patterns with the stops lists (gtfsIDs only) */ fun getPatternsWithStopsForRouteID(routeID: String): LiveData>{ return if(routeID.isNotEmpty()) gtfsDao.getPatternsWithStopsByRouteID(routeID) else MutableLiveData(listOf()) } + + fun getAllRoutes(): LiveData>{ + return gtfsDao.getAllRoutes() + } } \ No newline at end of file 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 1b60a2b..03e6831 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/CommonFragmentListener.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/CommonFragmentListener.java @@ -1,40 +1,45 @@ package it.reyboz.bustorino.fragments; import android.location.Location; import android.view.View; import it.reyboz.bustorino.backend.Stop; 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); } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/MapViewModel.kt b/app/src/main/java/it/reyboz/bustorino/fragments/GTFSPositionsViewModel.kt similarity index 89% rename from app/src/main/java/it/reyboz/bustorino/fragments/MapViewModel.kt rename to app/src/main/java/it/reyboz/bustorino/fragments/GTFSPositionsViewModel.kt index bdff94f..3dcbb76 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/MapViewModel.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/GTFSPositionsViewModel.kt @@ -1,209 +1,206 @@ /* BusTO - View Models 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.app.Application import android.util.Log import androidx.lifecycle.* import com.android.volley.Response import it.reyboz.bustorino.backend.NetworkVolleyManager -import it.reyboz.bustorino.backend.gtfs.GtfsPositionUpdate +import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate import it.reyboz.bustorino.backend.gtfs.GtfsRtPositionsRequest import it.reyboz.bustorino.data.* import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import java.util.concurrent.Executors /** * View Model for the map. For containing the stops, the trips and whatever */ -class MapViewModel(application: Application): AndroidViewModel(application) { +class GTFSPositionsViewModel(application: Application): AndroidViewModel(application) { private val gtfsRepo = GtfsRepository(application) private val netVolleyManager = NetworkVolleyManager.getInstance(application) - val positionsLiveData = MutableLiveData>() + val positionsLiveData = MutableLiveData>() private val positionsRequestRunning = MutableLiveData() private val positionRequestListener = object: GtfsRtPositionsRequest.Companion.RequestListener{ - override fun onResponse(response: ArrayList?) { + override fun onResponse(response: ArrayList?) { Log.i(DEBUG_TI,"Got response from the GTFS RT server") - response?.let {it:ArrayList -> + response?.let {it:ArrayList -> if (it.size == 0) { Log.w(DEBUG_TI,"No position updates from the server") return } else { //Log.i(DEBUG_TI, "Posting value to positionsLiveData") viewModelScope.launch { positionsLiveData.postValue(it) } } } //whatever the result, launch again the update TODO } } private val positionRequestErrorListener = Response.ErrorListener { //error listener, it->VolleyError Log.e(DEBUG_TI, "Could not download the update, error:\n"+it.stackTrace) //TODO: launch again if needed } fun requestUpdates(){ if(positionsRequestRunning.value == null || !positionsRequestRunning.value!!) { val request = GtfsRtPositionsRequest(positionRequestErrorListener, positionRequestListener) netVolleyManager.requestQueue.add(request) Log.i(DEBUG_TI, "Requested GTFS realtime position updates") positionsRequestRunning.value = true } } /*suspend fun requestDelayedUpdates(timems: Long){ delay(timems) requestUpdates() } */ fun requestDelayedUpdates(timems: Long){ viewModelScope.launch { delay(timems) requestUpdates() } } // TRIPS IDS that have to be queried to the DB val tripsIDsInUpdates : LiveData> = positionsLiveData.map { Log.i(DEBUG_TI, "positionsLiveData changed") //allow new requests for the positions of buses positionsRequestRunning.value = false //add "gtt:" prefix because it's implicit in GTFS Realtime API return@map it.map { pos -> "gtt:"+pos.tripID } } //this holds the trips that have been downloaded but for which we have no pattern /*private val gtfsTripsInDBMissingPattern = tripsIDsInUpdates.map { tripsIDs -> val tripsInDB = gtfsRepo.gtfsDao.getTripsFromIDs(tripsIDs) val tripsPatternCodes = tripsInDB.map { tr -> tr.patternId } val codesInDB = gtfsRepo.gtfsDao.getPatternsCodesInTheDB(tripsPatternCodes) tripsInDB.filter { !(codesInDB.contains(it.patternId)) } }*/ //private val patternsCodesInDB = tripsDBPatterns.map { gtfsRepo.gtfsDao.getPatternsCodesInTheDB(it) } // trips that are in the DB, together with the pattern. If the pattern is not present in the DB, it's null val gtfsTripsPatternsInDB = tripsIDsInUpdates.switchMap { //Log.i(DEBUG_TI, "tripsIds in updates changed: ${it.size}") gtfsRepo.gtfsDao.getTripPatternStops(it) } //trip IDs to query, which are not present in the DB val tripsGtfsIDsToQuery: LiveData> = gtfsTripsPatternsInDB.map { tripswithPatterns -> val tripNames=tripswithPatterns.map { twp-> twp.trip.tripID } Log.i(DEBUG_TI, "Have ${tripswithPatterns.size} trips in the DB") if (tripsIDsInUpdates.value!=null) - return@map tripsIDsInUpdates.value!!.filter { !tripNames.contains(it) } + return@map tripsIDsInUpdates.value!!.filter { !tripNames.contains(it) } else { Log.e(DEBUG_TI,"Got results for gtfsTripsInDB but not tripsIDsInUpdates??") return@map ArrayList() } } val updatesWithTripAndPatterns = gtfsTripsPatternsInDB.map { tripPatterns-> Log.i(DEBUG_TI, "Mapping trips and patterns") - val mdict = HashMap>() + val mdict = HashMap>() //missing patterns val routesToDownload = HashSet() if(positionsLiveData.value!=null) for(update in positionsLiveData.value!!){ val trID = update.tripID var found = false for(trip in tripPatterns){ if (trip.pattern == null){ //pattern is null, which means we have to download // the pattern data from MaTO routesToDownload.add(trip.trip.routeID) } if (trip.trip.tripID == "gtt:$trID"){ found = true //insert directly mdict[trID] = Pair(update,trip) break } } if (!found){ //Log.d(DEBUG_TI, "Cannot find pattern ${tr}") //give the update anyway mdict[trID] = Pair(update,null) } } //have to request download of missing Patterns if (routesToDownload.size > 0){ Log.d(DEBUG_TI, "Have ${routesToDownload.size} missing patterns from the DB: $routesToDownload") downloadMissingPatterns(ArrayList(routesToDownload)) } return@map mdict } /* There are two strategies for the queries, since we cannot query a bunch of tripIDs all together to Mato API -> we need to query each separately: 1 -> wait until they are all queried to insert in the DB 2 -> after each request finishes, insert it into the DB Keep in mind that trips DO CHANGE often, and so do the Patterns */ fun downloadTripsFromMato(trips: List): Boolean{ return MatoTripsDownloadWorker.downloadTripsFromMato(trips,getApplication(), DEBUG_TI) } - fun downloadMissingPatterns(routeIds: List): Boolean{ + private fun downloadMissingPatterns(routeIds: List): Boolean{ return MatoPatternsDownloadWorker.downloadPatternsForRoutes(routeIds, getApplication()) } init { Log.d(DEBUG_TI, "MapViewModel created") Log.d(DEBUG_TI, "Observers of positionsLiveData ${positionsLiveData.hasActiveObservers()}") positionsRequestRunning.value = false; } fun testCascade(){ - val n = ArrayList() - n.add(GtfsPositionUpdate("22920721U","lala","lalal","lol",1000.0f,1000.0f, 9000.0f, - 378192810192, GtfsPositionUpdate.VehicleInfo("aj","a"), - null, null - + val n = ArrayList() + n.add(LivePositionUpdate("22920721U","lala","lalal","lol","ASD", + 1000.0,1000.0, 9000.0f, 21838191, null )) positionsLiveData.value = n } /** * Start downloading missing GtfsTrips and Insert them in the DB */ companion object{ - const val DEBUG_TI="BusTO-MapViewModel" + private const val DEBUG_TI="BusTO-GTFSRTViewModel" const val DEFAULT_DELAY_REQUESTS: Long=4000 } } \ No newline at end of file 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 1007c3e..c9d6786 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt @@ -1,195 +1,725 @@ +/* + 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.animation.ObjectAnimator +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Paint import android.os.Bundle -import android.os.Parcelable import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.AdapterView -import android.widget.ArrayAdapter -import android.widget.Spinner -import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModelProvider +import android.widget.* +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView 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.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.gtfs.MatoPattern import it.reyboz.bustorino.data.gtfs.MatoPatternWithStops -import it.reyboz.bustorino.data.gtfs.PatternStop -import org.osmdroid.tileprovider.tilesource.OnlineTileSourceBase +import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops +import it.reyboz.bustorino.map.BusInfoWindow +import it.reyboz.bustorino.map.BusPositionUtils +import it.reyboz.bustorino.map.CustomInfoWindow.TouchResponder +import it.reyboz.bustorino.map.MapViewModel +import it.reyboz.bustorino.map.MarkerUtils +import it.reyboz.bustorino.viewmodels.MQTTPositionsViewModel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.osmdroid.config.Configuration import org.osmdroid.tileprovider.tilesource.TileSourceFactory +import org.osmdroid.util.BoundingBox import org.osmdroid.util.GeoPoint -import org.osmdroid.util.MapTileIndex import org.osmdroid.views.MapView +import org.osmdroid.views.overlay.FolderOverlay +import org.osmdroid.views.overlay.Marker import org.osmdroid.views.overlay.Polyline +import org.osmdroid.views.overlay.advancedpolyline.MonochromaticPaintList -class LinesDetailFragment() : Fragment() { - - private lateinit var lineID: String +class LinesDetailFragment() : ScreenBaseFragment() { + private lateinit var lineID: String private lateinit var patternsSpinner: Spinner private var patternsAdapter: ArrayAdapter? = null - private var patternsSpinnerState: Parcelable? = null + //private var patternsSpinnerState: Parcelable? = null private lateinit var currentPatterns: List - private lateinit var gtfsStopsForCurrentPattern: List private lateinit var map: MapView - private lateinit var viewingPattern: MatoPatternWithStops + private var viewingPattern: 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 lateinit var stopsRecyclerView: RecyclerView + //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") + } + } - private lateinit var viewModel: LinesViewModel + override fun onLongPressOnStop(stop: Stop?): Boolean { + TODO("Not yet implemented") + } - private var polyline = Polyline(); - private var stopPosList = ArrayList() + } - companion object { - private const val LINEID_KEY="lineID" - fun newInstance() = LinesDetailFragment() - const val DEBUG_TAG="LinesDetailFragment" - fun makeArgs(lineID: String): Bundle{ - val b = Bundle() - b.putString(LINEID_KEY, lineID) - return b - } - private const val DEFAULT_CENTER_LAT = 45.0708 - private const val DEFAULT_CENTER_LON = 7.6858 + private var polyline: Polyline? = null + //private var stopPosList = ArrayList() + + private lateinit var stopsOverlay: FolderOverlay + //fragment actions + private lateinit var fragmentListener: CommonFragmentListener + + private val stopTouchResponder = TouchResponder { stopID, stopName -> + Log.d(DEBUG_TAG, "Asked to show arrivals for stop ID: $stopID") + fragmentListener.requestArrivalsForStopID(stopID) } + private var showOnTopOfLine = true + private var recyclerInitDone = false + + //position of live markers + private val busPositionMarkersByTrip = HashMap() + private var busPositionsOverlay = FolderOverlay() + private val tripMarkersAnimators = HashMap() + private val liveBusViewModel: MQTTPositionsViewModel by viewModels() + @SuppressLint("SetTextI18n") override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { val rootView = inflater.inflate(R.layout.fragment_lines_detail, container, false) lineID = requireArguments().getString(LINEID_KEY, "") + switchButton = rootView.findViewById(R.id.switchImageButton) + stopsRecyclerView = rootView.findViewById(R.id.patternStopsRecyclerView) + + val titleTextView = rootView.findViewById(R.id.titleTextView) + + titleTextView.text = getString(R.string.line)+" "+GtfsUtils.getLineNameFromGtfsID(lineID) patternsSpinner = rootView.findViewById(R.id.patternsSpinner) patternsAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, ArrayList()) patternsSpinner.adapter = patternsAdapter - map = rootView.findViewById(R.id.lineMap) - val USGS_SAT: OnlineTileSourceBase = object : OnlineTileSourceBase( - "USGS National Map Sat", - 0, - 15, - 256, - "", - arrayOf("https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryTopo/MapServer/tile/"), - "USGS" - ) { - override fun getTileURLString(pMapTileIndex: Long): String { - return baseUrl + MapTileIndex.getZoom(pMapTileIndex) + "/" + MapTileIndex.getY(pMapTileIndex) + "/" + MapTileIndex.getX( - pMapTileIndex - ) - } - } - map.setTileSource(TileSourceFactory.MAPNIK) - /* - object : OnlineTileSourceBase("USGS Topo", 0, 18, 256, "", - arrayOf("https://basemap.nationalmap.gov/ArcGIS/rest/services/USGSTopo/MapServer/tile/" )) { - override fun getTileURLString(pMapTileIndex: Long) : String{ - return baseUrl + - MapTileIndex.getZoom(pMapTileIndex)+"/" + MapTileIndex.getY(pMapTileIndex) + - "/" + MapTileIndex.getX(pMapTileIndex)+ mImageFilenameEnding; - } - } - */ - //map.setTilesScaledToDpi(true); - //map.setTilesScaledToDpi(true); - map.setFlingEnabled(true) - map.setUseDataConnection(true) + initializeMap(rootView) + + initializeRecyclerView() - // add ability to zoom with 2 fingers - map.setMultiTouchControls(true) - map.minZoomLevel = 10.0 + switchButton.setOnClickListener{ + if(map.visibility == View.VISIBLE){ + map.visibility = View.GONE + stopsRecyclerView.visibility = View.VISIBLE - //map controller setup - val mapController = map.controller - mapController.setZoom(12.0) - mapController.setCenter(GeoPoint(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON)) - map.invalidate() + viewModel.setMapShowing(false) + liveBusViewModel.stopPositionsListening() + switchButton.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.ic_map_white_30)) + } else{ + stopsRecyclerView.visibility = View.GONE + map.visibility = View.VISIBLE + viewModel.setMapShowing(true) + liveBusViewModel.requestPosUpdates(lineID) + switchButton.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.ic_list_30)) + } + } + viewModel.setRouteIDQuery(lineID) viewModel.patternsWithStopsByRouteLiveData.observe(viewLifecycleOwner){ patterns -> savePatternsToShow(patterns) } - - /* We have the pattern and the stops here, time to display them */ viewModel.stopsForPatternLiveData.observe(viewLifecycleOwner) { stops -> - Log.d(DEBUG_TAG, "Got the stops: ${stops.map { s->s.gtfsID }}}") - - val pattern = viewingPattern.pattern - - val pointsList = PolylineParser.decodePolyline(pattern.patternGeometryPoly, pattern.patternGeometryLength) - //val polyLine=Polyline(map) - //polyLine.setPoints(pointsList) - //save points - if(map.overlayManager.contains(polyline)){ - map.overlayManager.remove(polyline) + if(map.visibility ==View.VISIBLE) + showPatternWithStopsOnMap(stops) + else{ + if(stopsRecyclerView.visibility==View.VISIBLE) + showStopsAsList(stops) } - polyline = Polyline(map) - polyline.setPoints(pointsList) - - map.overlayManager.add(polyline) - map.controller.animateTo(pointsList[0]) - map.invalidate() } - - viewModel.setRouteIDQuery(lineID) + if(pausedFragment && viewModel.selectedPatternLiveData.value!=null){ + val patt = viewModel.selectedPatternLiveData.value!! + Log.d(DEBUG_TAG, "Recreating views on resume, setting pattern: ${patt.pattern.code}") + showPattern(patt) + pausedFragment = false + } 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 patternWithStops = currentPatterns.get(position) + //viewModel.setPatternToDisplay(patternWithStops) setPatternAndReqStops(patternWithStops) + + Log.d(DEBUG_TAG, "item Selected, cleaning bus markers") + if(map?.visibility == View.VISIBLE) { + busPositionsOverlay.closeAllInfoWindows() + busPositionsOverlay.items.clear() + busPositionMarkersByTrip.clear() + + stopAnimations() + tripMarkersAnimators.clear() + liveBusViewModel.retriggerPositionUpdate() + } } override fun onNothingSelected(p0: AdapterView<*>?) { } } + //live bus positions + liveBusViewModel.updatesWithTripAndPatterns.observe(viewLifecycleOwner){ + if(map.visibility == View.GONE || viewingPattern ==null){ + //DO NOTHING + return@observe + } + //filter buses with direction, show those only with the same direction + val outmap = HashMap>() + val currentPattern = viewingPattern!!.pattern + val numUpds = it.entries.size + Log.d(DEBUG_TAG, "Got $numUpds updates, current pattern is: ${currentPattern.name}, directionID: ${currentPattern.directionId}") + val patternsDirections = HashMap() + for((tripId, pair) in it.entries){ + + if(pair.second!=null && pair.second?.pattern !=null){ + val dir = pair.second?.pattern?.directionId + if(dir !=null && dir == currentPattern.directionId){ + outmap.set(tripId, pair) + } + patternsDirections.set(tripId,if (dir!=null) dir else -10) + } else{ + outmap[tripId] = pair + //Log.d(DEBUG_TAG, "No pattern for tripID: $tripId") + patternsDirections.set(tripId, -10) + } + } + Log.d(DEBUG_TAG, " Filtered updates are ${outmap.keys.size}") // Original updates directs: $patternsDirections\n + updateBusPositionsInMap(outmap) + } + + //download missing tripIDs + liveBusViewModel.tripsGtfsIDsToQuery.observe(viewLifecycleOwner){ + //gtfsPosViewModel.downloadTripsFromMato(dat); + MatoTripsDownloadWorker.downloadTripsFromMato( + it, requireContext().applicationContext, + "BusTO-MatoTripDownload" + ) + } + + return rootView } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - viewModel = ViewModelProvider(this).get(LinesViewModel::class.java) + private fun initializeMap(rootView : View){ + val ctx = requireContext().applicationContext + Configuration.getInstance().load(ctx, PreferenceManager.getDefaultSharedPreferences(ctx)) + + map = rootView.findViewById(R.id.lineMap) + map.let { + it.setTileSource(TileSourceFactory.MAPNIK) + /* + object : OnlineTileSourceBase("USGS Topo", 0, 18, 256, "", + arrayOf("https://basemap.nationalmap.gov/ArcG IS/rest/services/USGSTopo/MapServer/tile/" )) { + override fun getTileURLString(pMapTileIndex: Long) : String{ + return baseUrl + + MapTileIndex.getZoom(pMapTileIndex)+"/" + MapTileIndex.getY(pMapTileIndex) + + "/" + MapTileIndex.getX(pMapTileIndex)+ mImageFilenameEnding; + } + } + */ + stopsOverlay = FolderOverlay() + busPositionsOverlay = FolderOverlay() + //map.setTilesScaledToDpi(true); + //map.setTilesScaledToDpi(true); + it.setFlingEnabled(true) + it.setUseDataConnection(true) + + // add ability to zoom with 2 fingers + it.setMultiTouchControls(true) + it.minZoomLevel = 11.0 + + //map controller setup + val mapController = it.controller + var zoom = 12.0 + var centerMap = GeoPoint(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON) + if(mapViewModel.currentLat.value!=MapViewModel.INVALID) { + Log.d(DEBUG_TAG, "mapViewModel posi: ${mapViewModel.currentLat.value}, ${mapViewModel.currentLong.value}"+ + " zoom ${mapViewModel.currentZoom.value}") + zoom = mapViewModel.currentZoom.value!! + centerMap = GeoPoint(mapViewModel.currentLat.value!!, mapViewModel.currentLong.value!!) + /*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!!) + + */ + + } + mapController.setZoom(zoom) + mapController.setCenter(centerMap) + Log.d(DEBUG_TAG, "Initializing map, first init $firstInit") + //map.invalidate() + + it.overlayManager.add(stopsOverlay) + it.overlayManager.add(busPositionsOverlay) + + zoomToCurrentPattern() + firstInit = false + + } + + + } + + override fun onAttach(context: Context) { + super.onAttach(context) + if(context is CommonFragmentListener){ + fragmentListener = context + } else throw RuntimeException("$context must implement CommonFragmentListener") + + fragmentListener.readyGUIfor(FragmentKind.LINES) + } + + + private fun stopAnimations(){ + for(anim in tripMarkersAnimators.values){ + anim.cancel() + } } private fun savePatternsToShow(patterns: List){ currentPatterns = patterns.sortedBy { p-> p.pattern.code } patternsAdapter?.let { it.clear() it.addAll(currentPatterns.map { p->"${p.pattern.directionId} - ${p.pattern.headsign}" }) it.notifyDataSetChanged() } - - val pos = patternsSpinner.selectedItemPosition - //might be possible that the selectedItem is different (larger than list size) - if(pos!= AdapterView.INVALID_POSITION && pos >= 0 && (pos < currentPatterns.size)){ - val p = currentPatterns[pos] - Log.d(LinesFragment.DEBUG_TAG, "Setting patterns with pos $pos and p gtfsID ${p.pattern.code}") - setPatternAndReqStops(currentPatterns[pos]) + viewingPattern?.let { + showPattern(it) } - Log.d(DEBUG_TAG, "Patterns changed") } + /** + * 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}") - gtfsStopsForCurrentPattern = patternWithStops.stopsIndices.sortedBy { i-> i.order } + viewModel.selectedPatternLiveData.value = patternWithStops + viewModel.currentPatternStops.value = patternWithStops.stopsIndices.sortedBy { i-> i.order } viewingPattern = patternWithStops viewModel.requestStopsForPatternWithStops(patternWithStops) } + private fun showPattern(patternWs: MatoPatternWithStops){ + 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) + patternsSpinner.setSelection(pos) + //set pattern + setPatternAndReqStops(patternWs) + } + + private fun zoomToCurrentPattern(){ + var pointsList: List + if(viewingPattern==null) { + Log.e(DEBUG_TAG, "asked to zoom to pattern but current viewing pattern is null") + if(polyline!=null) + pointsList = polyline!!.actualPoints + else { + Log.d(DEBUG_TAG, "The polyline is null") + return + } + }else{ + val pattern = viewingPattern!!.pattern + + pointsList = PolylineParser.decodePolyline(pattern.patternGeometryPoly, pattern.patternGeometryLength) + } + + var maxLat = -4000.0 + var minLat = -4000.0 + var minLong = -4000.0 + var maxLong = -4000.0 + for (p in pointsList){ + // get max latitude + if(maxLat == -4000.0) + maxLat = p.latitude + else if (maxLat < p.latitude) maxLat = p.latitude + // find min latitude + if (minLat == -4000.0) + minLat = p.latitude + else if (minLat > p.latitude) minLat = p.latitude + if(maxLong == -4000.0 || maxLong < p.longitude ) + maxLong = p.longitude + if (minLong == -4000.0 || minLong > p.longitude) + minLong = p.longitude + } + + val del = 0.008 + //map.controller.c + Log.d(DEBUG_TAG, "Setting limits of bounding box of line: $minLat -> $maxLat, $minLong -> $maxLong") + map.zoomToBoundingBox(BoundingBox(maxLat+del, maxLong+del, minLat-del, minLong-del), false) + } + + private fun showPatternWithStopsOnMap(stops: List){ + Log.d(DEBUG_TAG, "Got the stops: ${stops.map { s->s.gtfsID }}}") + if(viewingPattern==null || map == null) return + + val pattern = viewingPattern!!.pattern + + val pointsList = PolylineParser.decodePolyline(pattern.patternGeometryPoly, pattern.patternGeometryLength) + + var maxLat = -4000.0 + var minLat = -4000.0 + var minLong = -4000.0 + var maxLong = -4000.0 + for (p in pointsList){ + // get max latitude + if(maxLat == -4000.0) + maxLat = p.latitude + else if (maxLat < p.latitude) maxLat = p.latitude + // find min latitude + if (minLat == -4000.0) + minLat = p.latitude + else if (minLat > p.latitude) minLat = p.latitude + if(maxLong == -4000.0 || maxLong < p.longitude ) + maxLong = p.longitude + if (minLong == -4000.0 || minLong > p.longitude) + minLong = p.longitude + } + //val polyLine=Polyline(map) + //polyLine.setPoints(pointsList) + //save points + if(map.overlayManager.contains(polyline)){ + map.overlayManager.remove(polyline) + } + polyline = Polyline(map, false) + polyline!!.setPoints(pointsList) + //polyline.color = ContextCompat.getColor(context!!,R.color.brown_vd) + polyline!!.infoWindow = null + val paint = Paint() + paint.color = ContextCompat.getColor(requireContext(),R.color.line_drawn_poly) + paint.isAntiAlias = true + paint.strokeWidth = 16f + paint.style = Paint.Style.FILL_AND_STROKE + paint.strokeJoin = Paint.Join.ROUND + paint.strokeCap = Paint.Cap.ROUND + polyline!!.outlinePaintLists.add(MonochromaticPaintList(paint)) + + map.overlayManager.add(0,polyline!!) + + stopsOverlay.closeAllInfoWindows() + stopsOverlay.items.clear() + val stopIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ball) + + for(s in stops){ + val gp = if (showOnTopOfLine) + findOptimalPosition(s,pointsList) + else GeoPoint(s.latitude!!,s.longitude!!) + + 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) + //} + polyline!!.setOnClickListener(Polyline.OnClickListener { polyline, mapView, eventPos -> + Log.d(DEBUG_TAG, "clicked") + true + }) + + //map.controller.zoomToB//#animateTo(pointsList[0]) + val del = 0.008 + map.zoomToBoundingBox(BoundingBox(maxLat+del, maxLong+del, minLat-del, minLong-del), true) + //map.invalidate() + } + + private fun initializeRecyclerView(){ + val llManager = LinearLayoutManager(context) + llManager.orientation = LinearLayoutManager.VERTICAL + + stopsRecyclerView.layoutManager = llManager + } + private fun showStopsAsList(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 + ) + + } + + + + } + + + /** + * Remove bus marker from overlay associated with tripID + */ + private fun removeBusMarker(tripID: String){ + if(!busPositionMarkersByTrip.containsKey(tripID)){ + Log.e(DEBUG_TAG, "Asked to remove veh with tripID $tripID but it's supposedly not shown") + return + } + val marker = busPositionMarkersByTrip[tripID] + busPositionsOverlay.remove(marker) + busPositionMarkersByTrip.remove(tripID) + + val animator = tripMarkersAnimators[tripID] + animator?.let{ + it.cancel() + tripMarkersAnimators.remove(tripID) + } + + } + + private fun showPatternWithStop(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 + } + } + } + /** + * draw the position of the buses in the map. Copied from MapFragment + */ + private fun updateBusPositionsInMap(tripsPatterns: java.util.HashMap> + ) { + //Log.d(MapFragment.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 + + var marker: Marker? = null + //check if Marker is already created + if (busPositionMarkersByTrip.containsKey(tripID)) { + + //check if the trip direction ID is the same, if not remove + if(tripWithPatternStops?.pattern != null && + tripWithPatternStops.pattern.directionId != viewingPattern?.pattern?.directionId){ + removeBusMarker(tripID) + + } else { + //need to change the position of the marker + marker = busPositionMarkersByTrip.get(tripID)!! + BusPositionUtils.updateBusPositionMarker(map, marker, update, tripMarkersAnimators, false) + // Set the pattern to add the info + if (marker.infoWindow != null && marker.infoWindow is BusInfoWindow) { + val window = marker.infoWindow as BusInfoWindow + if (window.pattern == null && 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 (mapView == null) Log.e(MapFragment.DEBUG_TAG, "Creating marker with null map, things will explode") + marker = Marker(map) + + //String route = GtfsUtils.getLineNameFromGtfsID(update.getRouteID()); + val mdraw = ResourcesCompat.getDrawable(getResources(), R.drawable.map_bus_position_icon, null)!! + //mdraw.setBounds(0,0,28,28); + + marker.icon = mdraw + var markerPattern: MatoPattern? = null + if (tripWithPatternStops != null) { + if (tripWithPatternStops.pattern != null) + markerPattern = tripWithPatternStops.pattern + } + marker.infoWindow = BusInfoWindow(map, update, markerPattern, true) { + // set pattern to show + if(it!=null) + showPatternWithStop(it.code) + } + //marker.infoWindow as BusInfoWindow + marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER) + BusPositionUtils.updateBusPositionMarker(map,marker, update, tripMarkersAnimators,true) + // the overlay is null when it's not attached yet? + // cannot recreate it because it becomes null very soon + // if(busPositionsOverlay == null) busPositionsOverlay = new FolderOverlay(); + //save the marker + if (busPositionsOverlay != null) { + busPositionsOverlay.add(marker) + busPositionMarkersByTrip.put(tripID, marker) + } + } + } + if (noPatternsTrips.size > 0) { + Log.i(DEBUG_TAG, "These trips have no matching pattern: $noPatternsTrips") + } + } + + override fun onResume() { + super.onResume() + Log.d(DEBUG_TAG, "Resetting paused from onResume") + pausedFragment = false + + liveBusViewModel.requestPosUpdates(GtfsUtils.getLineNameFromGtfsID(lineID)) + + if(mapViewModel.currentLat.value!=MapViewModel.INVALID) { + Log.d(DEBUG_TAG, "mapViewModel posi: ${mapViewModel.currentLat.value}, ${mapViewModel.currentLong.value}"+ + " zoom ${mapViewModel.currentZoom.value}") + 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!!) + } + //controller.setZoom() + } + } + + override fun onPause() { + super.onPause() + liveBusViewModel.stopPositionsListening() + pausedFragment = true + //save map + val center = map.mapCenter + mapViewModel.currentLat.value = center.latitude + mapViewModel.currentLong.value = center.longitude + mapViewModel.currentZoom.value = map.zoomLevel.toDouble() + } + + override fun getBaseViewForSnackBar(): View? { + return null + } + + companion object { + private const val LINEID_KEY="lineID" + fun newInstance() = LinesDetailFragment() + const val DEBUG_TAG="LinesDetailFragment" + + fun makeArgs(lineID: String): Bundle{ + val b = Bundle() + b.putString(LINEID_KEY, lineID) + return b + } + @JvmStatic + private fun findOptimalPosition(stop: Stop, pointsList: MutableList): GeoPoint{ + 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 GeoPoint(sLat, p1.longitude) + } else if (p1.latitude == p2.latitude){ + //Log.d(DEBUG_TAG, "Same latitude") + return GeoPoint(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 GeoPoint(latNew,longNew) + } + + private const val DEFAULT_CENTER_LAT = 45.12 + private const val DEFAULT_CENTER_LON = 7.6858 + } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/LinesFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/LinesFragment.kt index b379f6b..e9dbabf 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/LinesFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/LinesFragment.kt @@ -1,418 +1,418 @@ /* BusTO - Fragments components Copyright (C) 2022 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.os.Bundle import android.os.Parcelable import android.util.Log import android.view.* import android.widget.* import android.widget.AdapterView.INVALID_POSITION import android.widget.AdapterView.OnItemSelectedListener import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView 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.Stop import it.reyboz.bustorino.data.gtfs.GtfsRoute import it.reyboz.bustorino.data.gtfs.MatoPatternWithStops import it.reyboz.bustorino.data.gtfs.PatternStop import it.reyboz.bustorino.util.LinesNameSorter import it.reyboz.bustorino.util.PatternWithStopsSorter class LinesFragment : ScreenBaseFragment() { companion object { fun newInstance(){ LinesFragment() } - const val DEBUG_TAG="BusTO-LinesFragment" + private const val DEBUG_TAG="BusTO-LinesFragment" const val FRAGMENT_TAG="LinesFragment" val patternStopsComparator = PatternWithStopsSorter() } private lateinit var viewModel: LinesViewModel private lateinit var linesSpinner: Spinner private lateinit var patternsSpinner: Spinner private lateinit var currentRoutes: List private lateinit var selectedPatterns: List private lateinit var routeDescriptionTextView: TextView private lateinit var stopsRecyclerView: RecyclerView private var linesAdapter: ArrayAdapter? = null private var patternsAdapter: ArrayAdapter? = null private var mListener: CommonFragmentListener? = null private val linesNameSorter = LinesNameSorter() private val linesComparator = Comparator { a,b -> return@Comparator linesNameSorter.compare(a.shortName, b.shortName) } private var firstClick = true private var recyclerViewState:Parcelable? = null private var patternsSpinnerState:Parcelable? = null private val adapterListener = object : StopAdapterListener { override fun onTappedStop(stop: Stop?) { //var r = "" //stop?.let { r= it.stopDisplayName.toString() } if(viewModel.shouldShowMessage) { Toast.makeText(context, R.string.long_press_stop_4_options, Toast.LENGTH_SHORT).show() viewModel.shouldShowMessage=false } stop?.let { mListener?.requestArrivalsForStopID(it.ID) } if(stop == null){ Log.e(DEBUG_TAG,"Passed wrong stop") } if(mListener == null){ Log.e(DEBUG_TAG, "Listener is null") } } override fun onLongPressOnStop(stop: Stop?): Boolean { Log.d(DEBUG_TAG, "LongPressOnStop") return true } } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) Log.d(DEBUG_TAG, "saveInstanceState bundle: $outState") } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val rootView = inflater.inflate(R.layout.fragment_lines, container, false) linesSpinner = rootView.findViewById(R.id.linesSpinner) patternsSpinner = rootView.findViewById(R.id.patternsSpinner) routeDescriptionTextView = rootView.findViewById(R.id.routeDescriptionTextView) stopsRecyclerView = rootView.findViewById(R.id.patternStopsRecyclerView) val llManager = LinearLayoutManager(context) llManager.orientation = LinearLayoutManager.VERTICAL stopsRecyclerView.layoutManager = llManager //allow the context menu to be opened registerForContextMenu(stopsRecyclerView) Log.d(DEBUG_TAG, "Called onCreateView for LinesFragment") Log.d(DEBUG_TAG, "OnCreateView, selected line spinner pos: ${linesSpinner.selectedItemPosition}") Log.d(DEBUG_TAG, "OnCreateView, selected patterns spinner pos: ${patternsSpinner.selectedItemPosition}") //set requests viewModel.routesGTTLiveData.observe(viewLifecycleOwner) { setRoutes(it) } viewModel.patternsWithStopsByRouteLiveData.observe(viewLifecycleOwner){ patterns -> run { selectedPatterns = patterns.sortedBy { p-> p.pattern.code } //patterns. //sortedBy {-1*it.stopsIndices.size}// "${p.pattern.directionId} - ${p.pattern.headsign}" } patternsAdapter?.let { it.clear() it.addAll(selectedPatterns.map { p->"${p.pattern.directionId} - ${p.pattern.headsign}" }) it.notifyDataSetChanged() } viewModel.selectedPatternLiveData.value?.let { setSelectedPattern(it) } val pos = patternsSpinner.selectedItemPosition //might be possible that the selectedItem is different (larger than list size) if(pos!= INVALID_POSITION && pos >= 0 && (pos < selectedPatterns.size)){ val p = selectedPatterns[pos] Log.d(DEBUG_TAG, "Setting patterns with pos $pos and p gtfsID ${p.pattern.code}") setPatternAndReqStops(selectedPatterns[pos]) } } } viewModel.stopsForPatternLiveData.observe(viewLifecycleOwner){stops-> Log.d("BusTO-LinesFragment", "Setting stops from DB") setCurrentStops(stops) } if(context!=null) { patternsAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, ArrayList()) patternsSpinner.adapter = patternsAdapter linesAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, ArrayList()) linesSpinner.adapter = linesAdapter if (linesSpinner.onItemSelectedListener != null){ Log.d(DEBUG_TAG, "linesSpinner listener != null") } //listener linesSpinner.onItemSelectedListener = object: OnItemSelectedListener{ override fun onItemSelected(p0: AdapterView<*>?, p1: View?, pos: Int, p3: Long) { val selRoute = currentRoutes.get(pos) routeDescriptionTextView.text = selRoute.longName val oldRoute = viewModel.getRouteIDQueried() val resetSpinner = (oldRoute != null) && (oldRoute.trim() != selRoute.gtfsId.trim()) Log.d(DEBUG_TAG, "Selected route: ${selRoute.gtfsId}, reset spinner: $resetSpinner, oldRoute: $oldRoute") //launch query for this gtfsID viewModel.setRouteIDQuery(selRoute.gtfsId) //reset spinner position if(resetSpinner) patternsSpinner.setSelection(0) } override fun onNothingSelected(p0: AdapterView<*>?) { } } patternsSpinner.onItemSelectedListener = object : OnItemSelectedListener{ override fun onItemSelected(p0: AdapterView<*>?, p1: View?, position: Int, p3: Long) { val patternWithStops = selectedPatterns.get(position) // setPatternAndReqStops(patternWithStops) //viewModel.currentPositionInPatterns.value = position } override fun onNothingSelected(p0: AdapterView<*>?) { } } } return rootView } override fun onAttach(context: Context) { super.onAttach(context) if(context is CommonFragmentListener) mListener = context else throw RuntimeException(context.toString() + " must implement CommonFragmentListener") } override fun onResume() { super.onResume() mListener?.readyGUIfor(FragmentKind.LINES) Log.d(DEBUG_TAG, "Resuming lines fragment") //Log.d(DEBUG_TAG, "OnResume, selected line spinner pos: ${linesSpinner.selectedItemPosition}") //Log.d(DEBUG_TAG, "OnResume, selected patterns spinner pos: ${patternsSpinner.selectedItemPosition}") } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewModel = ViewModelProvider(this).get(LinesViewModel::class.java) Log.d(DEBUG_TAG, "Fragment onCreate") } override fun getBaseViewForSnackBar(): View? { return null } private fun setSelectedPattern(patternWs: MatoPatternWithStops){ Log.d(DEBUG_TAG, "Finding pattern to show: ${patternWs.pattern.code}") var pos = -2 val code = patternWs.pattern.code.trim() for(k in selectedPatterns.indices){ if(selectedPatterns[k].pattern.code.trim() == code){ pos = k break } } Log.d(DEBUG_TAG, "Found pattern $code in position: $pos") if(pos>=0){ patternsSpinner.setSelection(pos) } } private fun setRoutes(routes: List){ Log.d(DEBUG_TAG, "Resetting routes") currentRoutes = routes.sortedWith(linesComparator) if (linesAdapter!=null){ var selGtfsRoute = viewModel.getRouteIDQueried() var selRouteIdx = 0 if(selGtfsRoute == null){ selGtfsRoute ="" } Log.d(DEBUG_TAG, "Setting routes, selected route gtfsID: $selGtfsRoute") val adapter = linesAdapter!! if (adapter.isEmpty) { Log.d(DEBUG_TAG, "Lines adapter is empty") } else{ adapter.clear() } adapter.addAll(currentRoutes.map { r -> r.shortName }) adapter.notifyDataSetChanged() for(j in currentRoutes.indices){ val route = currentRoutes[j] if (route.gtfsId == selGtfsRoute) { selRouteIdx = j Log.d(DEBUG_TAG, "Route $selGtfsRoute has index $j") } } linesSpinner.setSelection(selRouteIdx) // } /* linesAdapter?.clear() linesAdapter?.addAll(currentRoutes.map { r -> r.shortName }) linesAdapter?.notifyDataSetChanged() */ } private fun setCurrentStops(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}") var setNewAdapter = true if(stopsRecyclerView.adapter is StopRecyclerAdapter){ val adapter = stopsRecyclerView.adapter as StopRecyclerAdapter if(adapter.stops.size == stopsSorted.size && (adapter.stops.get(0).gtfsID == stopsSorted.get(0).gtfsID) && (adapter.stops.get(numStops-1).gtfsID == stopsSorted.get(numStops-1).gtfsID) ){ Log.d(DEBUG_TAG, "Found same stops on recyclerview") setNewAdapter = false } /*else { Log.d(DEBUG_TAG, "Found adapter on recyclerview, but not the same stops") adapter.stops = stopsSorted adapter.notifyDataSetChanged() }*/ } if(setNewAdapter){ stopsRecyclerView.adapter = StopRecyclerAdapter( stopsSorted, adapterListener, StopRecyclerAdapter.Use.LINES, NameCapitalize.FIRST ) } } private fun setPatternAndReqStops(patternWithStops: MatoPatternWithStops){ Log.d(DEBUG_TAG, "Requesting stops for pattern ${patternWithStops.pattern.code}") //currentPatternStops = patternWithStops.stopsIndices.sortedBy { i-> i.order } viewModel.currentPatternStops.value = patternWithStops.stopsIndices.sortedBy { i-> i.order } viewModel.selectedPatternLiveData.value = patternWithStops viewModel.requestStopsForPatternWithStops(patternWithStops) } override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) { super.onCreateContextMenu(menu, v, menuInfo) Log.d("BusTO-LinesFragment", "Creating context menu ") if (v.id == R.id.patternStopsRecyclerView) { // if we aren't attached to activity, return null if (activity == null) return val inflater = requireActivity().menuInflater inflater.inflate(R.menu.menu_line_item, menu) } } override fun onContextItemSelected(item: MenuItem): Boolean { if (stopsRecyclerView.getAdapter() !is StopRecyclerAdapter) return false val adapter =stopsRecyclerView.adapter as StopRecyclerAdapter val stop = adapter.stops.get(adapter.getPosition()) val acId = item.itemId if(acId == R.id.action_view_on_map){ // view on the map if ((stop.latitude == null) or (stop.longitude == null) or (mListener == null) ) { Toast.makeText(context, R.string.cannot_show_on_map_no_position, Toast.LENGTH_SHORT).show() return true } mListener!!.showMapCenteredOnStop(stop) return true } else if (acId == R.id.action_show_arrivals){ mListener?.requestArrivalsForStopID(stop.ID) return true } return false } override fun onStop() { super.onStop() Log.d(DEBUG_TAG, "Fragment stopped") recyclerViewState = stopsRecyclerView.layoutManager?.onSaveInstanceState() patternsSpinnerState = patternsSpinner.onSaveInstanceState() } override fun onStart() { super.onStart() Log.d(DEBUG_TAG, "OnStart, selected line spinner pos: ${linesSpinner.selectedItemPosition}") Log.d(DEBUG_TAG, "OnStart, selected patterns spinner pos: ${patternsSpinner.selectedItemPosition}") if (recyclerViewState!=null){ stopsRecyclerView.layoutManager?.onRestoreInstanceState(recyclerViewState) } if(patternsSpinnerState!=null){ patternsSpinner.onRestoreInstanceState(patternsSpinnerState) } } /* override fun onDestroyView() { super.onDestroyView() Log.d(DEBUG_TAG, "Fragment view destroyed") } override fun onDestroy() { super.onDestroy() Log.d(DEBUG_TAG, "Fragment destroyed") } */ override fun onViewStateRestored(savedInstanceState: Bundle?) { super.onViewStateRestored(savedInstanceState) Log.d(DEBUG_TAG, "OnViewStateRes, bundled saveinstancestate: $savedInstanceState") Log.d(DEBUG_TAG, "OnViewStateRes, selected line spinner pos: ${linesSpinner.selectedItemPosition}") Log.d(DEBUG_TAG, "OnViewStateRes, selected patterns spinner pos: ${patternsSpinner.selectedItemPosition}") } } \ 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 new file mode 100644 index 0000000..0c69a4d --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/fragments/LinesGridShowingFragment.kt @@ -0,0 +1,277 @@ +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.backend.utils +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 urbanRecyclerView: RecyclerView + private lateinit var extraurbanRecyclerView: RecyclerView + private lateinit var touristRecyclerView: RecyclerView + + 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.onItemClick { + fragmentListener.showLineOnMap(it.gtfsId) + } + 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) + + urbanRecyclerView = rootView.findViewById(R.id.urbanLinesRecyclerView) + extraurbanRecyclerView = rootView.findViewById(R.id.extraurbanLinesRecyclerView) + touristRecyclerView = rootView.findViewById(R.id.touristLinesRecyclerView) + + 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) + //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 + } + + 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 + } + } + + } + + //onClicks + urbanLinesTitle.setOnClickListener { + if(openRecyclerView!=""&& openRecyclerView!= AG_URBAN){ + openCloseRecyclerView(openRecyclerView) + openCloseRecyclerView(AG_URBAN) + } + } + extrurbanLinesTitle.setOnClickListener { + if(openRecyclerView!=""&& openRecyclerView!= AG_EXTRAURB){ + openCloseRecyclerView(openRecyclerView) + openCloseRecyclerView(AG_EXTRAURB) + + } + } + touristLinesTitle.setOnClickListener { + if(openRecyclerView!="" && openRecyclerView!= AG_TOUR) { + openCloseRecyclerView(openRecyclerView) + openCloseRecyclerView(AG_TOUR) + } + } + + return rootView + } + + private fun openCloseRecyclerView(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") + + fragmentListener.readyGUIfor(FragmentKind.LINES) + } + + override fun getBaseViewForSnackBar(): View? { + return null + } + + override fun onResume() { + super.onResume() + 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 + } + } + } + + + companion object { + private const val COLUMN_WIDTH_DP=200 + 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/LinesViewModel.kt b/app/src/main/java/it/reyboz/bustorino/fragments/LinesViewModel.kt index 90437f4..94442f9 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/LinesViewModel.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/LinesViewModel.kt @@ -1,95 +1,107 @@ package it.reyboz.bustorino.fragments import android.app.Application import android.util.Log import androidx.lifecycle.* import it.reyboz.bustorino.backend.Stop import it.reyboz.bustorino.data.GtfsRepository import it.reyboz.bustorino.data.NextGenDB import it.reyboz.bustorino.data.OldDataRepository import it.reyboz.bustorino.data.gtfs.GtfsDatabase import it.reyboz.bustorino.data.gtfs.GtfsRoute import it.reyboz.bustorino.data.gtfs.MatoPatternWithStops import it.reyboz.bustorino.data.gtfs.PatternStop import java.util.concurrent.Executors class LinesViewModel(application: Application) : AndroidViewModel(application) { private val gtfsRepo: GtfsRepository private val oldRepo: OldDataRepository //val patternsByRouteLiveData: LiveData> private val routeIDToSearch = MutableLiveData() private var lastShownPatternStops = ArrayList() val currentPatternStops = MutableLiveData>() val selectedPatternLiveData = MutableLiveData() val stopsForPatternLiveData = MutableLiveData>() private val executor = Executors.newFixedThreadPool(2) + val mapShowing = MutableLiveData(true) + fun setMapShowing(yes: Boolean){ + mapShowing.value = yes + //retrigger redraw + stopsForPatternLiveData.postValue(stopsForPatternLiveData.value) + } init { val gtfsDao = GtfsDatabase.getGtfsDatabase(application).gtfsDao() gtfsRepo = GtfsRepository(gtfsDao) oldRepo = OldDataRepository(executor, NextGenDB.getInstance(application)) } + val routesGTTLiveData: LiveData> by lazy{ gtfsRepo.getLinesLiveDataForFeed("gtt") } val patternsWithStopsByRouteLiveData = routeIDToSearch.switchMap { gtfsRepo.getPatternsWithStopsForRouteID(it) } + fun setRouteIDQuery(routeID: String){ routeIDToSearch.value = routeID } fun getRouteIDQueried(): String?{ return routeIDToSearch.value } var shouldShowMessage = true + fun setPatternToDisplay(patternStops: MatoPatternWithStops){ + + selectedPatternLiveData.value = patternStops + } /** * Find the */ private fun requestStopsForGTFSIDs(gtfsIDs: List){ if (gtfsIDs.equals(lastShownPatternStops)){ //nothing to do return } oldRepo.requestStopsWithGtfsIDs(gtfsIDs) { if (it.isSuccess) { stopsForPatternLiveData.postValue(it.result) } else { Log.e("BusTO-LinesVM", "Got error on callback with stops for gtfsID") it.exception?.printStackTrace() } } lastShownPatternStops.clear() for(id in gtfsIDs) lastShownPatternStops.add(id) } fun requestStopsForPatternWithStops(patternStops: MatoPatternWithStops){ val gtfsIDs = ArrayList() for(pat in patternStops.stopsIndices){ gtfsIDs.add(pat.stopGtfsId) } requestStopsForGTFSIDs(gtfsIDs) } /*fun getLinesGTT(): MutableLiveData> { val routesData = MutableLiveData>() viewModelScope.launch { val routes=gtfsRepo.getLinesForFeed("gtt") routesData.postValue(routes) } return routesData }*/ } \ 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 0d7c18d..fc1e0d1 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java @@ -1,885 +1,891 @@ 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.Button; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.ProgressBar; import android.widget.TextView; 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.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 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 OPTION_SHOW_LEGEND = "show_legend"; 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"; /// UI ELEMENTS // private ImageButton addToFavorites; private FragmentHelper fragmentHelper; private SwipeRefreshLayout swipeRefreshLayout; private EditText busStopSearchByIDEditText; private EditText busStopSearchByNameEditText; private ProgressBar progressBar; private TextView howDoesItWorkTextView; private Button hideHintButton; private MenuItem actionHelpMenuItem; private FloatingActionButton floatingActionButton; private FrameLayout resultFrameLayout; private boolean setupOnStart = true; private boolean suppressArrivalsReload = false; private boolean instanceStateSaved = 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; // TODO: implement this -- https://gitpull.it/T12 private int searchMode; //private ImageButton addToFavorites; //// HIDDEN BUT IMPORTANT ELEMENTS //// FragmentManager fragMan; 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 (fragMan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) { ArrivalsFragment fragment = (ArrivalsFragment) fragMan.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(), + Toast.makeText(getContext().getApplicationContext(), R.string.no_qrcode, Toast.LENGTH_SHORT).show(); } }); /// LOCATION STUFF /// boolean pendingNearbyStopsRequest = false; boolean locationPermissionGranted, locationPermissionAsked = false; AppLocationManager locationManager; private final ActivityResultLauncher requestPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback>() { @Override public void onActivityResult(Map result) { if(result==null || result.get(Manifest.permission.ACCESS_COARSE_LOCATION) == null ||result.get(Manifest.permission.ACCESS_FINE_LOCATION) == null) return; if(result.get(Manifest.permission.ACCESS_COARSE_LOCATION) == null || result.get(Manifest.permission.ACCESS_FINE_LOCATION) == null) return; boolean resCoarse = result.get(Manifest.permission.ACCESS_COARSE_LOCATION); boolean resFine = result.get(Manifest.permission.ACCESS_FINE_LOCATION); Log.d(DEBUG_TAG, "Permissions for location are: "+result); if(result.get(Manifest.permission.ACCESS_COARSE_LOCATION) && result.get(Manifest.permission.ACCESS_FINE_LOCATION)){ locationPermissionGranted = true; Log.w(DEBUG_TAG, "Starting position"); if (mListener!= null && getContext()!=null){ if (locationManager==null) locationManager = AppLocationManager.getInstance(getContext()); locationManager.addLocationRequestFor(requester); } // show nearby fragment //showNearbyStopsFragment(); Log.d(DEBUG_TAG, "We have location permission"); if(pendingNearbyStopsRequest){ showNearbyFragmentIfNeeded(cr); pendingNearbyStopsRequest = false; } } if(pendingNearbyStopsRequest) pendingNearbyStopsRequest=false; } }); private final LocationCriteria cr = new LocationCriteria(2000, 10000); //Location private AppLocationManager.LocationRequester requester = new AppLocationManager.LocationRequester() { @Override public void onLocationChanged(Location loc) { } @Override public void onLocationStatusChanged(int status) { if(status == AppLocationManager.LOCATION_GPS_AVAILABLE && !isNearbyFragmentShown() && checkLocationPermission()){ //request Stops //pendingNearbyStopsRequest = false; if (getContext()!= null && !isNearbyFragmentShown()) //mainHandler.post(new NearbyStopsRequester(getContext(), cr)); showNearbyFragmentIfNeeded(cr); } } @Override public long getLastUpdateTimeMillis() { return 50; } @Override public LocationCriteria getLocationCriteria() { return cr; } @Override public void onLocationProviderAvailable() { //Log.w(DEBUG_TAG, "pendingNearbyStopRequest: "+pendingNearbyStopsRequest); if(!isNearbyFragmentShown() && getContext()!=null){ // we should have the location permission if(!checkLocationPermission()) Log.e(DEBUG_TAG, "Asking to show nearbystopfragment when " + "we have no location permission"); pendingNearbyStopsRequest = true; //mainHandler.post(new NearbyStopsRequester(getContext(), cr)); showNearbyFragmentIfNeeded(cr); } } @Override public void onLocationDisabled() { } }; //// ACTIVITY ATTACHED (LISTENER /// private CommonFragmentListener mListener; private String pendingStopID = null; private CoordinatorLayout coordLayout; public MainScreenFragment() { // Required empty public constructor } public static MainScreenFragment newInstance() { return new MainScreenFragment(); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { //do nothing Log.d(DEBUG_TAG, "ARGS ARE NOT NULL: "+getArguments()); if (getArguments().getString(PENDING_STOP_SEARCH)!=null) pendingStopID = getArguments().getString(PENDING_STOP_SEARCH); } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment View root = inflater.inflate(R.layout.fragment_main_screen, container, false); addToFavorites = root.findViewById(R.id.addToFavorites); busStopSearchByIDEditText = root.findViewById(R.id.busStopSearchByIDEditText); busStopSearchByNameEditText = root.findViewById(R.id.busStopSearchByNameEditText); progressBar = root.findViewById(R.id.progressBar); howDoesItWorkTextView = root.findViewById(R.id.howDoesItWorkTextView); hideHintButton = root.findViewById(R.id.hideHintButton); swipeRefreshLayout = root.findViewById(R.id.listRefreshLayout); floatingActionButton = root.findViewById(R.id.floatingActionButton); resultFrameLayout = root.findViewById(R.id.resultFrame); busStopSearchByIDEditText.setSelectAllOnFocus(true); busStopSearchByIDEditText .setOnEditorActionListener((v, actionId, event) -> { // IME_ACTION_SEARCH alphabetical option if (actionId == EditorInfo.IME_ACTION_SEARCH) { onSearchClick(v); return true; } return false; }); busStopSearchByNameEditText .setOnEditorActionListener((v, actionId, event) -> { // IME_ACTION_SEARCH alphabetical option if (actionId == EditorInfo.IME_ACTION_SEARCH) { onSearchClick(v); return true; } return false; }); swipeRefreshLayout .setOnRefreshListener(() -> mainHandler.post(refreshStop)); swipeRefreshLayout.setColorSchemeResources(R.color.blue_500, R.color.orange_500); coordLayout = root.findViewById(R.id.coord_layout); floatingActionButton.setOnClickListener((this::onToggleKeyboardLayout)); hideHintButton.setOnClickListener(this::onHideHint); AppCompatImageButton qrButton = root.findViewById(R.id.QRButton); qrButton.setOnClickListener(this::onQRButtonClick); AppCompatImageButton searchButton = root.findViewById(R.id.searchButton); searchButton.setOnClickListener(this::onSearchClick); // Fragment stuff fragMan = getChildFragmentManager(); fragMan.addOnBackStackChangedListener(() -> Log.d("BusTO Main Fragment", "BACK STACK CHANGED")); fragmentHelper = new FragmentHelper(this, getChildFragmentManager(), getContext(), R.id.resultFrame); setSearchModeBusStopID(); cr.setAccuracy(Criteria.ACCURACY_FINE); cr.setAltitudeRequired(false); cr.setBearingRequired(false); cr.setCostAllowed(true); cr.setPowerRequirement(Criteria.NO_REQUIREMENT); locationManager = AppLocationManager.getInstance(getContext()); Log.d(DEBUG_TAG, "OnCreateView, savedInstanceState null: "+(savedInstanceState==null)); return root; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); Log.d(DEBUG_TAG, "onViewCreated, SwipeRefreshLayout visible: "+(swipeRefreshLayout.getVisibility()==View.VISIBLE)); Log.d(DEBUG_TAG, "Saved instance state is: "+savedInstanceState); //Restore instance state /*if (savedInstanceState!=null){ Fragment fragment = getChildFragmentManager().getFragment(savedInstanceState, SAVED_FRAGMENT); if (fragment!=null){ getChildFragmentManager().beginTransaction().add(R.id.resultFrame, fragment).commit(); setupOnStart = false; } } */ if (getChildFragmentManager().findFragmentById(R.id.resultFrame)!= null){ swipeRefreshLayout.setVisibility(View.VISIBLE); } instanceStateSaved = false; } @Override public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); Log.d(DEBUG_TAG, "Saving instance state"); Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if (fragment!=null) getChildFragmentManager().putFragment(outState, SAVED_FRAGMENT, fragment); if (fragmentHelper!=null) fragmentHelper.setBlockAllActivities(true); instanceStateSaved = true; } public void setSuppressArrivalsReload(boolean value){ suppressArrivalsReload = value; // we have to suppress the reloading of the (possible) ArrivalsFragment /*if(value) { Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if (fragment instanceof ArrivalsFragment) { ArrivalsFragment frag = (ArrivalsFragment) fragment; frag.setReloadOnResume(false); } } */ } /** * Cancel the reload of the arrival times * because we are going to pop the fragment */ public void cancelReloadArrivalsIfNeeded(){ if(getContext()==null) return; //we are not attached //Fragment fr = getChildFragmentManager().findFragmentById(R.id.resultFrame); fragmentHelper.stopLastRequestIfNeeded(true); toggleSpinner(false); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); Log.d(DEBUG_TAG, "OnAttach called, setupOnAttach: "+ setupOnStart); mainHandler = new Handler(); if (context instanceof CommonFragmentListener) { mListener = (CommonFragmentListener) context; } else { throw new RuntimeException(context + " must implement CommonFragmentListener"); } } @Override public void onDetach() { super.onDetach(); mListener = null; // setupOnAttached = true; } @Override public void onStart() { super.onStart(); Log.d(DEBUG_TAG, "onStart called, setupOnStart: "+setupOnStart); if (setupOnStart) { if (pendingStopID==null){ //We want the nearby bus stops! //mainHandler.post(new NearbyStopsRequester(getContext(), cr)); Log.d(DEBUG_TAG, "Showing nearby stops"); if(!checkLocationPermission()){ requestLocationPermission(); pendingNearbyStopsRequest = true; } else { showNearbyFragmentIfNeeded(cr); } } else{ ///TODO: if there is a stop displayed, we need to hold the update } setupOnStart = false; } } @Override public void onResume() { final Context con = getContext(); Log.w(DEBUG_TAG, "OnResume called, setupOnStart: "+ setupOnStart); if (con != null) { if(locationManager==null) locationManager = AppLocationManager.getInstance(con); if(Permissions.locationPermissionGranted(con)){ Log.d(DEBUG_TAG, "Location permission OK"); if(!locationManager.isRequesterRegistered(requester)) locationManager.addLocationRequestFor(requester); } //don't request permission } else { Log.w(DEBUG_TAG, "Context is null at onResume"); } super.onResume(); // if we have a pending stopID request, do it Log.d(DEBUG_TAG, "Pending stop ID for arrivals: "+pendingStopID); //this is the second time we are attaching this fragment Log.d(DEBUG_TAG, "Waiting for new stop request: "+ suppressArrivalsReload); if (suppressArrivalsReload){ // we have to suppress the reloading of the (possible) ArrivalsFragment Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if (fragment instanceof ArrivalsFragment){ ArrivalsFragment frag = (ArrivalsFragment) fragment; frag.setReloadOnResume(false); } //deactivate suppressArrivalsReload = false; } if(pendingStopID!=null){ Log.d(DEBUG_TAG, "Pending request for arrivals at stop ID: "+pendingStopID); requestArrivalsForStopID(pendingStopID); pendingStopID = null; } mListener.readyGUIfor(FragmentKind.MAIN_SCREEN_FRAGMENT); fragmentHelper.setBlockAllActivities(false); } @Override public void onPause() { //mainHandler = null; locationManager.removeLocationRequestFor(requester); super.onPause(); fragmentHelper.setBlockAllActivities(true); fragmentHelper.stopLastRequestIfNeeded(true); } /* GUI METHODS */ /** * QR scan button clicked * * @param v View QRButton clicked */ public void onQRButtonClick(View v) { BarcodeScanOptions scanOptions = new BarcodeScanOptions(); Intent intent = scanOptions.createScanIntent(); if(!BarcodeScanUtils.checkTargetPackageExists(getContext(), intent)){ BarcodeScanUtils.showDownloadDialog(null, this); }else { barcodeLauncher.launch(scanOptions); } } public void onHideHint(View v) { hideHints(); setOption(OPTION_SHOW_LEGEND, false); } /** * OK this is pure shit * * @param v View clicked */ public void onSearchClick(View v) { final StopsFinderByName[] stopsFinderByNames = new StopsFinderByName[]{new GTTStopsFetcher(), new FiveTStopsFetcher()}; if (searchMode == SEARCH_BY_ID) { String busStopID = busStopSearchByIDEditText.getText().toString(); fragmentHelper.stopLastRequestIfNeeded(true); requestArrivalsForStopID(busStopID); } else { // searchMode == SEARCH_BY_NAME String query = busStopSearchByNameEditText.getText().toString(); query = query.trim(); if(getContext()!=null) { if (query.length() < 1) { Toast.makeText(getContext(), R.string.insert_bus_stop_name_error, Toast.LENGTH_SHORT).show(); } else if(query.length()< 2){ Toast.makeText(getContext(), R.string.query_too_short, Toast.LENGTH_SHORT).show(); } else { fragmentHelper.stopLastRequestIfNeeded(true); new AsyncStopsSearcher(fragmentHelper, stopsFinderByNames).execute(query); } } } } public void onToggleKeyboardLayout(View v) { if (searchMode == SEARCH_BY_NAME) { setSearchModeBusStopID(); if (busStopSearchByIDEditText.requestFocus()) { showKeyboard(); } } else { // searchMode == SEARCH_BY_ID setSearchModeBusStopName(); if (busStopSearchByNameEditText.requestFocus()) { showKeyboard(); } } } @Override public void enableRefreshLayout(boolean yes) { swipeRefreshLayout.setEnabled(yes); } ////////////////////////////////////// GUI HELPERS ///////////////////////////////////////////// public void showKeyboard() { if(getActivity() == null) return; InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); View view = searchMode == SEARCH_BY_ID ? busStopSearchByIDEditText : busStopSearchByNameEditText; imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); } private void setSearchModeBusStopID() { searchMode = SEARCH_BY_ID; busStopSearchByNameEditText.setVisibility(View.GONE); busStopSearchByNameEditText.setText(""); busStopSearchByIDEditText.setVisibility(View.VISIBLE); floatingActionButton.setImageResource(R.drawable.alphabetical); } private void setSearchModeBusStopName() { searchMode = SEARCH_BY_NAME; busStopSearchByIDEditText.setVisibility(View.GONE); busStopSearchByIDEditText.setText(""); busStopSearchByNameEditText.setVisibility(View.VISIBLE); floatingActionButton.setImageResource(R.drawable.numeric); } protected boolean isNearbyFragmentShown(){ Fragment fragment = getChildFragmentManager().findFragmentByTag(NearbyStopsFragment.FRAGMENT_TAG); return (fragment!= null && fragment.isVisible()); } /** * Having that cursor at the left of the edit text makes me cancer. * * @param busStopID bus stop ID */ private void setBusStopSearchByIDEditText(String busStopID) { busStopSearchByIDEditText.setText(busStopID); busStopSearchByIDEditText.setSelection(busStopID.length()); } 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); } @Nullable @org.jetbrains.annotations.Nullable @Override public View getBaseViewForSnackBar() { return coordLayout; } @Override public void toggleSpinner(boolean enable) { if (enable) { //already set by the RefreshListener when needed //swipeRefreshLayout.setRefreshing(true); progressBar.setVisibility(View.VISIBLE); } else { swipeRefreshLayout.setRefreshing(false); progressBar.setVisibility(View.GONE); } } private void prepareGUIForBusLines() { swipeRefreshLayout.setEnabled(true); swipeRefreshLayout.setVisibility(View.VISIBLE); //actionHelpMenuItem.setVisible(true); } private void prepareGUIForBusStops() { swipeRefreshLayout.setEnabled(false); swipeRefreshLayout.setVisibility(View.VISIBLE); //actionHelpMenuItem.setVisible(false); } private void actuallyShowNearbyStopsFragment(){ swipeRefreshLayout.setVisibility(View.VISIBLE); final Fragment existingFrag = fragMan.findFragmentById(R.id.resultFrame); // fragment; if (!(existingFrag instanceof NearbyStopsFragment)){ Log.d(DEBUG_TAG, "actually showing Nearby Stops Fragment"); //there is no fragment showing final NearbyStopsFragment fragment = NearbyStopsFragment.newInstance(NearbyStopsFragment.TYPE_STOPS); FragmentTransaction ft = fragMan.beginTransaction(); ft.replace(R.id.resultFrame, fragment, NearbyStopsFragment.FRAGMENT_TAG); if (getActivity()!=null && !getActivity().isFinishing() &&!instanceStateSaved) ft.commit(); else Log.e(DEBUG_TAG, "Not showing nearby fragment because we saved instanceState"); } } @Override public void showFloatingActionButton(boolean yes) { mListener.showFloatingActionButton(yes); } /** * This provides a temporary fix to make the transition * to a single asynctask go smoother * * @param fragmentType the type of fragment created */ @Override public void readyGUIfor(FragmentKind fragmentType) { //if we are getting results, already, stop waiting for nearbyStops if (fragmentType == FragmentKind.ARRIVALS || fragmentType == FragmentKind.STOPS) { hideKeyboard(); if (pendingNearbyStopsRequest) { locationManager.removeLocationRequestFor(requester); pendingNearbyStopsRequest = false; } } if (fragmentType == null) Log.e("ActivityMain", "Problem with fragmentType"); else switch (fragmentType) { case ARRIVALS: prepareGUIForBusLines(); if (getOption(OPTION_SHOW_LEGEND, true)) { showHints(); } break; case STOPS: prepareGUIForBusStops(); break; default: Log.d(DEBUG_TAG, "Fragment type is unknown"); return; } // Shows hints } + @Override + public void showLineOnMap(String routeGtfsId) { + //pass to activity + mListener.showLineOnMap(routeGtfsId); + } + @Override public void showMapCenteredOnStop(Stop stop) { if(mListener!=null) mListener.showMapCenteredOnStop(stop); } /** * Main method for stops requests * @param ID the Stop ID */ @Override public void requestArrivalsForStopID(String ID) { if (!isResumed()){ //defer request pendingStopID = ID; Log.d(DEBUG_TAG, "Deferring update for stop "+ID+ " saved: "+pendingStopID); return; } final boolean delayedRequest = !(pendingStopID==null); final FragmentManager framan = getChildFragmentManager(); if (getContext()==null){ Log.e(DEBUG_TAG, "Asked for arrivals with null context"); return; } ArrivalsFetcher[] fetchers = utils.getDefaultArrivalsFetchers(getContext()).toArray(new ArrivalsFetcher[0]); if (ID == null || ID.length() <= 0) { // we're still in UI thread, no need to mess with Progress showToastMessage(R.string.insert_bus_stop_number_error, true); toggleSpinner(false); } else if (framan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) { ArrivalsFragment fragment = (ArrivalsFragment) framan.findFragmentById(R.id.resultFrame); if (fragment != null && fragment.getStopID() != null && fragment.getStopID().equals(ID)){ // Run with previous fetchers //fragment.getCurrentFetchers().toArray() new AsyncArrivalsSearcher(fragmentHelper,fragment.getCurrentFetchersAsArray(), getContext()).execute(ID); } else{ new AsyncArrivalsSearcher(fragmentHelper, fetchers, getContext()).execute(ID); } } else { Log.d(DEBUG_TAG, "This is probably the first arrivals search, preparing GUI"); prepareGUIForBusLines(); new AsyncArrivalsSearcher(fragmentHelper,fetchers, getContext()).execute(ID); Log.d(DEBUG_TAG, "Started search for arrivals of stop " + ID); } } private boolean checkLocationPermission(){ final Context context = getContext(); if(context==null) return false; final boolean isOldVersion = Build.VERSION.SDK_INT < Build.VERSION_CODES.M; final boolean noPermission = ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED; return isOldVersion || !noPermission; } private void requestLocationPermission(){ requestPermissionLauncher.launch(LOCATION_PERMISSIONS); } private void showNearbyFragmentIfNeeded(Criteria cr){ if(isNearbyFragmentShown()) { //nothing to do Log.w(DEBUG_TAG, "launched nearby fragment request but we already are showing"); return; } if(getContext()==null){ Log.e(DEBUG_TAG, "Wanting to show nearby fragment but context is null"); return; } AppLocationManager appLocationManager = AppLocationManager.getInstance(getContext()); final boolean haveProviders = appLocationManager.anyLocationProviderMatchesCriteria(cr); if (haveProviders && fragmentHelper.getLastSuccessfullySearchedBusStop() == null && !fragMan.isDestroyed()) { //Go ahead with the request Log.d("mainActivity", "Recreating stop fragment"); actuallyShowNearbyStopsFragment(); pendingNearbyStopsRequest = false; } else if(!haveProviders){ Log.e(DEBUG_TAG, "NO PROVIDERS FOR POSITION"); } } /////////// LOCATION METHODS ////////// /* private void startStopRequest(String provider) { Log.d(DEBUG_TAG, "Provider " + provider + " got enabled"); if (locmgr != null && mainHandler != null && pendingNearbyStopsRequest && locmgr.getProvider(provider).meetsCriteria(cr)) { } } */ /* * Run location requests separately and asynchronously class NearbyStopsRequester implements Runnable { Context appContext; Criteria cr; public NearbyStopsRequester(Context appContext, Criteria criteria) { this.appContext = appContext.getApplicationContext(); this.cr = criteria; } @Override public void run() { if(isNearbyFragmentShown()) { //nothing to do Log.w(DEBUG_TAG, "launched nearby fragment request but we already are showing"); return; } final boolean isOldVersion = Build.VERSION.SDK_INT < Build.VERSION_CODES.M; final boolean noPermission = ActivityCompat.checkSelfPermission(appContext, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(appContext, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED; //if we don't have the permission, we have to ask for it, if we haven't // asked too many times before if (noPermission) { if (!isOldVersion) { pendingNearbyStopsRequest = true; //Permissions.assertLocationPermissions(appContext,getActivity()); requestPermissionLauncher.launch(LOCATION_PERMISSIONS); Log.w(DEBUG_TAG, "Cannot get position: Asking permission, noPositionFromSys: " + noPermission); return; } else { Toast.makeText(appContext, "Asked for permission position too many times", Toast.LENGTH_LONG).show(); } } else setOption(LOCATION_PERMISSION_GIVEN, true); AppLocationManager appLocationManager = AppLocationManager.getInstance(appContext); final boolean haveProviders = appLocationManager.anyLocationProviderMatchesCriteria(cr); if (haveProviders && fragmentHelper.getLastSuccessfullySearchedBusStop() == null && !fragMan.isDestroyed()) { //Go ahead with the request Log.d("mainActivity", "Recreating stop fragment"); showNearbyStopsFragment(); pendingNearbyStopsRequest = false; } else if(!haveProviders){ Log.e(DEBUG_TAG, "NO PROVIDERS FOR POSITION"); } } } */ } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java index 3d38da1..0c9669a 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java @@ -1,825 +1,836 @@ /* BusTO - Fragments components Copyright (C) 2020 Andrea Ugo Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.fragments; import android.Manifest; import android.animation.ObjectAnimator; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.drawable.Drawable; import android.location.Location; import android.location.LocationManager; import android.os.AsyncTask; -import android.os.Build; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.Toast; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.res.ResourcesCompat; import androidx.lifecycle.ViewModelProvider; import androidx.preference.PreferenceManager; -import it.reyboz.bustorino.backend.gtfs.GtfsPositionUpdate; -import it.reyboz.bustorino.backend.gtfs.GtfsUtils; +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.MatoTripsDownloadWorker; import it.reyboz.bustorino.data.gtfs.MatoPattern; import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops; import it.reyboz.bustorino.map.*; +import it.reyboz.bustorino.viewmodels.MQTTPositionsViewModel; import org.osmdroid.api.IGeoPoint; import org.osmdroid.api.IMapController; import org.osmdroid.config.Configuration; import org.osmdroid.events.DelayedMapListener; import org.osmdroid.events.MapListener; import org.osmdroid.events.ScrollEvent; import org.osmdroid.events.ZoomEvent; import org.osmdroid.tileprovider.tilesource.TileSourceFactory; import org.osmdroid.util.BoundingBox; import org.osmdroid.util.GeoPoint; import org.osmdroid.views.MapView; import org.osmdroid.views.overlay.FolderOverlay; import org.osmdroid.views.overlay.Marker; import org.osmdroid.views.overlay.infowindow.InfoWindow; import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider; import java.lang.ref.WeakReference; import java.util.*; import kotlin.Pair; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.data.NextGenDB; import it.reyboz.bustorino.middleware.GeneralActivity; import it.reyboz.bustorino.util.Permissions; public class MapFragment extends ScreenBaseFragment { - private static final String TAG = "Busto-MapActivity"; + //private static final String TAG = "Busto-MapActivity"; private static final String MAP_CURRENT_ZOOM_KEY = "map-current-zoom"; private static final String MAP_CENTER_LAT_KEY = "map-center-lat"; private static final String MAP_CENTER_LON_KEY = "map-center-lon"; private static final String FOLLOWING_LOCAT_KEY ="following"; public static final String BUNDLE_LATIT = "lat"; public static final String BUNDLE_LONGIT = "lon"; public static final String BUNDLE_NAME = "name"; public static final String BUNDLE_ID = "ID"; public static final String BUNDLE_ROUTES_STOPPING = "routesStopping"; public static final String FRAGMENT_TAG="BusTOMapFragment"; private static final double DEFAULT_CENTER_LAT = 45.0708; private static final double DEFAULT_CENTER_LON = 7.6858; private static final double POSITION_FOUND_ZOOM = 18.3; public static final double NO_POSITION_ZOOM = 17.1; private static final String DEBUG_TAG=FRAGMENT_TAG; protected FragmentListenerMain listenerMain; private HashSet shownStops = null; //the asynctask used to get the stops from the database private AsyncStopFetcher stopFetcher = null; private MapView map = null; public Context ctx; private LocationOverlay mLocationOverlay = null; private FolderOverlay stopsFolderOverlay = null; private Bundle savedMapState = null; protected ImageButton btCenterMap; protected ImageButton btFollowMe; private boolean hasMapStartFinished = false; private boolean followingLocation = false; - private MapViewModel mapViewModel ; //= new ViewModelProvider(this).get(MapViewModel.class); + //private GTFSPositionsViewModel gtfsPosViewModel; //= new ViewModelProvider(this).get(MapViewModel.class); + private MQTTPositionsViewModel positionsViewModel; private final HashMap busPositionMarkersByTrip = new HashMap<>(); private FolderOverlay busPositionsOverlay = null; private final HashMap tripMarkersAnimators = new HashMap<>(); protected final CustomInfoWindow.TouchResponder responder = new CustomInfoWindow.TouchResponder() { @Override public void onActionUp(@NonNull String stopID, @Nullable String stopName) { if (listenerMain!= null){ Log.d(DEBUG_TAG, "Asked to show arrivals for stop ID: "+stopID); listenerMain.requestArrivalsForStopID(stopID); } } }; protected final LocationOverlay.OverlayCallbacks locationCallbacks = new LocationOverlay.OverlayCallbacks() { @Override public void onDisableFollowMyLocation() { updateGUIForLocationFollowing(false); followingLocation=false; } @Override public void onEnableFollowMyLocation() { updateGUIForLocationFollowing(true); followingLocation=true; } }; private final ActivityResultLauncher positionRequestLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), result -> { if (result == null){ Log.w(DEBUG_TAG, "Got asked permission but request is null, doing nothing?"); } else if(Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_COARSE_LOCATION)) && Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_FINE_LOCATION))){ map.getOverlays().remove(mLocationOverlay); startLocationOverlay(true, map); if(getContext()==null || getContext().getSystemService(Context.LOCATION_SERVICE)==null) return; LocationManager locationManager = (LocationManager) getContext().getSystemService(Context.LOCATION_SERVICE); @SuppressLint("MissingPermission") Location userLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); if (userLocation != null) { map.getController().setZoom(POSITION_FOUND_ZOOM); GeoPoint startPoint = new GeoPoint(userLocation); setLocationFollowing(true); map.getController().setCenter(startPoint); } } else Log.w(DEBUG_TAG,"No location permission"); }); public MapFragment() { } public static MapFragment getInstance(){ return new MapFragment(); } public static MapFragment getInstance(@NonNull Stop stop){ MapFragment fragment= new MapFragment(); Bundle args = new Bundle(); args.putDouble(BUNDLE_LATIT, stop.getLatitude()); args.putDouble(BUNDLE_LONGIT, stop.getLongitude()); args.putString(BUNDLE_NAME, stop.getStopDisplayName()); args.putString(BUNDLE_ID, stop.ID); args.putString(BUNDLE_ROUTES_STOPPING, stop.routesThatStopHereToString()); fragment.setArguments(args); return fragment; } //public static MapFragment getInstance(@NonNull Stop stop){ // return getInstance(stop.getLatitude(), stop.getLongitude(), stop.getStopDisplayName(), stop.ID); //} @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { //use the same layout as the activity View root = inflater.inflate(R.layout.activity_map, container, false); if (getContext() == null){ throw new IllegalStateException(); } ctx = getContext().getApplicationContext(); Configuration.getInstance().load(ctx, PreferenceManager.getDefaultSharedPreferences(ctx)); map = root.findViewById(R.id.map); map.setTileSource(TileSourceFactory.MAPNIK); //map.setTilesScaledToDpi(true); map.setFlingEnabled(true); // add ability to zoom with 2 fingers map.setMultiTouchControls(true); btCenterMap = root.findViewById(R.id.icon_center_map); btFollowMe = root.findViewById(R.id.icon_follow); //setup FolderOverlay stopsFolderOverlay = new FolderOverlay(); //setup Bus Markers Overlay busPositionsOverlay = new FolderOverlay(); //reset shown bus updates busPositionMarkersByTrip.clear(); tripMarkersAnimators.clear(); //set map not done hasMapStartFinished = false; //Start map from bundle if (savedInstanceState !=null) startMap(getArguments(), savedInstanceState); else startMap(getArguments(), savedMapState); //set listeners map.addMapListener(new DelayedMapListener(new MapListener() { @Override public boolean onScroll(ScrollEvent paramScrollEvent) { requestStopsToShow(); //Log.d(DEBUG_TAG, "Scrolling"); //if (moveTriggeredByCode) moveTriggeredByCode =false; //else setLocationFollowing(false); return true; } @Override public boolean onZoom(ZoomEvent event) { requestStopsToShow(); return true; } })); btCenterMap.setOnClickListener(v -> { //Log.i(TAG, "centerMap clicked "); if(Permissions.locationPermissionGranted(getContext())) { final GeoPoint myPosition = mLocationOverlay.getMyLocation(); map.getController().animateTo(myPosition); } else Toast.makeText(getContext(), R.string.enable_position_message_map, Toast.LENGTH_SHORT) .show(); }); btFollowMe.setOnClickListener(v -> { //Log.i(TAG, "btFollowMe clicked "); if(Permissions.locationPermissionGranted(getContext())) setLocationFollowing(!followingLocation); else Toast.makeText(getContext(), R.string.enable_position_message_map, Toast.LENGTH_SHORT) .show(); }); + return root; } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); - mapViewModel = new ViewModelProvider(this).get(MapViewModel.class); + //gtfsPosViewModel = new ViewModelProvider(this).get(GTFSPositionsViewModel.class); + //viewModel + positionsViewModel = new ViewModelProvider(this).get(MQTTPositionsViewModel.class); if (context instanceof FragmentListenerMain) { listenerMain = (FragmentListenerMain) context; } else { throw new RuntimeException(context.toString() + " must implement FragmentListenerMain"); } } @Override public void onDetach() { super.onDetach(); listenerMain = null; //stop animations // setupOnAttached = true; Log.w(DEBUG_TAG, "Fragment detached"); } @Override public void onPause() { super.onPause(); Log.w(DEBUG_TAG, "On pause called mapfrag"); saveMapState(); for (ObjectAnimator animator : tripMarkersAnimators.values()) { if(animator!=null && animator.isRunning()){ animator.cancel(); } } tripMarkersAnimators.clear(); + positionsViewModel.stopPositionsListening(); if (stopFetcher!= null) stopFetcher.cancel(true); } /** * Save the map state inside the fragment * (calls saveMapState(bundle)) */ private void saveMapState(){ savedMapState = new Bundle(); saveMapState(savedMapState); } /** * Save the state of the map to restore it to a later time * @param bundle the bundle in which to save the data */ private void saveMapState(Bundle bundle){ Log.d(DEBUG_TAG, "Saving state, location following: "+followingLocation); bundle.putBoolean(FOLLOWING_LOCAT_KEY, followingLocation); if (map == null){ //The map is null, it can happen? Log.e(DEBUG_TAG, "Cannot save map center, map is null"); return; } final IGeoPoint loc = map.getMapCenter(); bundle.putDouble(MAP_CENTER_LAT_KEY, loc.getLatitude()); bundle.putDouble(MAP_CENTER_LON_KEY, loc.getLongitude()); bundle.putDouble(MAP_CURRENT_ZOOM_KEY, map.getZoomLevelDouble()); } @Override public void onResume() { super.onResume(); if(listenerMain!=null) listenerMain.readyGUIfor(FragmentKind.MAP); - if(mapViewModel!=null) { - mapViewModel.requestUpdates(); + if(positionsViewModel !=null) { + //gtfsPosViewModel.requestUpdates(); + positionsViewModel.requestPosUpdates(MQTTMatoClient.LINES_ALL); //mapViewModel.testCascade(); - mapViewModel.getTripsGtfsIDsToQuery().observe(this, dat -> { + positionsViewModel.getTripsGtfsIDsToQuery().observe(this, dat -> { Log.i(DEBUG_TAG, "Have these trips IDs missing from the DB, to be queried: "+dat); - mapViewModel.downloadTripsFromMato(dat); + //gtfsPosViewModel.downloadTripsFromMato(dat); + MatoTripsDownloadWorker.Companion.downloadTripsFromMato(dat,getContext().getApplicationContext(), + "BusTO-MatoTripDownload"); }); } } @Override public void onSaveInstanceState(@NonNull Bundle outState) { saveMapState(outState); super.onSaveInstanceState(outState); } //own methods /** * Switch following the location on and off * @param value true if we want to follow location */ public void setLocationFollowing(Boolean value){ followingLocation = value; if(mLocationOverlay==null || getContext() == null || map ==null) //nothing else to do return; if (value){ mLocationOverlay.enableFollowLocation(); } else { mLocationOverlay.disableFollowLocation(); } } /** * Do all the stuff you need to do on the gui, when parameter is changed to value * @param following value */ protected void updateGUIForLocationFollowing(boolean following){ if (following) btFollowMe.setImageResource(R.drawable.ic_follow_me_on); else btFollowMe.setImageResource(R.drawable.ic_follow_me); } /** * Build the location overlay. Enable only when * a) we know we have the permission * b) the location map is set */ private void startLocationOverlay(boolean enableLocation, MapView map){ if(getActivity()== null) throw new IllegalStateException("Cannot enable LocationOverlay now"); // Location Overlay // from OpenBikeSharing (THANK GOD) Log.d(DEBUG_TAG, "Starting position overlay"); GpsMyLocationProvider imlp = new GpsMyLocationProvider(getActivity().getBaseContext()); imlp.setLocationUpdateMinDistance(5); imlp.setLocationUpdateMinTime(2000); final LocationOverlay overlay = new LocationOverlay(imlp,map, locationCallbacks); if (enableLocation) overlay.enableMyLocation(); overlay.setOptionsMenuEnabled(true); //map.getOverlays().add(this.mLocationOverlay); this.mLocationOverlay = overlay; map.getOverlays().add(mLocationOverlay); } public void startMap(Bundle incoming, Bundle savedInstanceState) { //Check that we're attached GeneralActivity activity = getActivity() instanceof GeneralActivity ? (GeneralActivity) getActivity() : null; if(getContext()==null|| activity==null){ //we are not attached Log.e(DEBUG_TAG, "Calling startMap when not attached"); return; }else{ Log.d(DEBUG_TAG, "Starting map from scratch"); } //clear previous overlays map.getOverlays().clear(); //parse incoming bundle GeoPoint marker = null; String name = null; String ID = null; String routesStopping = ""; if (incoming != null) { double lat = incoming.getDouble(BUNDLE_LATIT); double lon = incoming.getDouble(BUNDLE_LONGIT); marker = new GeoPoint(lat, lon); name = incoming.getString(BUNDLE_NAME); ID = incoming.getString(BUNDLE_ID); routesStopping = incoming.getString(BUNDLE_ROUTES_STOPPING, ""); } //ask for location permission if(!Permissions.locationPermissionGranted(activity)){ if(shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)){ //TODO: show dialog for permission rationale Toast.makeText(activity, R.string.enable_position_message_map, Toast.LENGTH_SHORT).show(); } positionRequestLauncher.launch(Permissions.LOCATION_PERMISSIONS); } shownStops = new HashSet<>(); // move the map on the marker position or on a default view point: Turin, Piazza Castello // and set the start zoom IMapController mapController = map.getController(); GeoPoint startPoint = null; startLocationOverlay(Permissions.locationPermissionGranted(activity), map); // set the center point if (marker != null) { //startPoint = marker; mapController.setZoom(POSITION_FOUND_ZOOM); setLocationFollowing(false); // put the center a little bit off (animate later) startPoint = new GeoPoint(marker); startPoint.setLatitude(marker.getLatitude()+ utils.angleRawDifferenceFromMeters(20)); startPoint.setLongitude(marker.getLongitude()-utils.angleRawDifferenceFromMeters(20)); //don't need to do all the rest since we want to show a point } else if (savedInstanceState != null && savedInstanceState.containsKey(MAP_CURRENT_ZOOM_KEY)) { mapController.setZoom(savedInstanceState.getDouble(MAP_CURRENT_ZOOM_KEY)); mapController.setCenter(new GeoPoint(savedInstanceState.getDouble(MAP_CENTER_LAT_KEY), savedInstanceState.getDouble(MAP_CENTER_LON_KEY))); Log.d(DEBUG_TAG, "Location following from savedInstanceState: "+savedInstanceState.getBoolean(FOLLOWING_LOCAT_KEY)); setLocationFollowing(savedInstanceState.getBoolean(FOLLOWING_LOCAT_KEY)); } else { Log.d(DEBUG_TAG, "No position found from intent or saved state"); boolean found = false; LocationManager locationManager = (LocationManager) getContext().getSystemService(Context.LOCATION_SERVICE); //check for permission if (locationManager != null && Permissions.locationPermissionGranted(activity)) { @SuppressLint("MissingPermission") Location userLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); + if (userLocation != null) { - mapController.setZoom(POSITION_FOUND_ZOOM); - startPoint = new GeoPoint(userLocation); - found = true; - setLocationFollowing(true); + double distan = utils.measuredistanceBetween(userLocation.getLatitude(), userLocation.getLongitude(), + DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON); + if (distan < 100_000.0) { + mapController.setZoom(POSITION_FOUND_ZOOM); + startPoint = new GeoPoint(userLocation); + found = true; + setLocationFollowing(true); + } } } if(!found){ startPoint = new GeoPoint(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON); mapController.setZoom(NO_POSITION_ZOOM); setLocationFollowing(false); } } // set the minimum zoom level map.setMinZoomLevel(15.0); //add contingency check (shouldn't happen..., but) if (startPoint != null) { mapController.setCenter(startPoint); } //add stops overlay //map.getOverlays().add(mLocationOverlay); map.getOverlays().add(this.stopsFolderOverlay); Log.d(DEBUG_TAG, "Requesting stops load"); // This is not necessary, by setting the center we already move // the map and we trigger a stop request //requestStopsToShow(); if (marker != null) { // make a marker with the info window open for the searched marker //TODO: make Stop Bundle-able Marker stopMarker = makeMarker(marker, ID , name, routesStopping,true); map.getController().animateTo(marker); } //add the overlays with the bus stops if(busPositionsOverlay == null){ //Log.i(DEBUG_TAG, "Null bus positions overlay,redo"); busPositionsOverlay = new FolderOverlay(); } - if(mapViewModel!=null){ + if(positionsViewModel !=null){ //should always be the case - mapViewModel.getUpdatesWithTripAndPatterns().observe(this, data->{ + positionsViewModel.getUpdatesWithTripAndPatterns().observe(getViewLifecycleOwner(), data->{ Log.d(DEBUG_TAG, "Have "+data.size()+" trip updates, has Map start finished: "+hasMapStartFinished); if (hasMapStartFinished) updateBusPositionsInMap(data); - if(!isDetached()) - mapViewModel.requestDelayedUpdates(4000); + //if(!isDetached()) + // gtfsPosViewModel.requestDelayedUpdates(4000); }); + } else { + Log.e(DEBUG_TAG, "PositionsViewModel is null"); } map.getOverlays().add(this.busPositionsOverlay); //set map as started hasMapStartFinished = true; } /** * Start a request to load the stops that are in the current view * from the database */ private void requestStopsToShow(){ // get the top, bottom, left and right screen's coordinate BoundingBox bb = map.getBoundingBox(); double latFrom = bb.getLatSouth(); double latTo = bb.getLatNorth(); double lngFrom = bb.getLonWest(); double lngTo = bb.getLonEast(); if (stopFetcher!= null && stopFetcher.getStatus()!= AsyncTask.Status.FINISHED) stopFetcher.cancel(true); stopFetcher = new AsyncStopFetcher(this); stopFetcher.execute( new AsyncStopFetcher.BoundingBoxLimit(lngFrom,lngTo,latFrom, latTo)); } - private void updateBusMarker(final Marker marker,final GtfsPositionUpdate posUpdate,@Nullable boolean justCreated){ + private void updateBusMarker(final Marker marker, final LivePositionUpdate posUpdate, @Nullable boolean justCreated){ GeoPoint position; final String updateID = posUpdate.getTripID(); if(!justCreated){ position = marker.getPosition(); if(posUpdate.getLatitude()!=position.getLatitude() || posUpdate.getLongitude()!=position.getLongitude()){ GeoPoint newpos = new GeoPoint(posUpdate.getLatitude(), posUpdate.getLongitude()); - ObjectAnimator valueAnimator = MarkerAnimation.makeMarkerAnimator(map, marker, newpos, new GeoPointInterpolator.LinearFixed(), 2500); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { - valueAnimator.setAutoCancel(true); - } else if(tripMarkersAnimators.containsKey(updateID)) { - ObjectAnimator otherAnim = tripMarkersAnimators.get(updateID); - assert otherAnim != null; - otherAnim.cancel(); - } + ObjectAnimator valueAnimator = MarkerUtils.makeMarkerAnimator( + map, marker, newpos, MarkerUtils.LINEAR_ANIMATION, 1200); + valueAnimator.setAutoCancel(true); tripMarkersAnimators.put(updateID,valueAnimator); valueAnimator.start(); } //marker.setPosition(new GeoPoint(posUpdate.getLatitude(), posUpdate.getLongitude())); } else { position = new GeoPoint(posUpdate.getLatitude(), posUpdate.getLongitude()); marker.setPosition(position); } - marker.setRotation(posUpdate.getBearing()*(-1.f)); + if(posUpdate.getBearing()!=null) + marker.setRotation(posUpdate.getBearing()*(-1.f)); } - private void updateBusPositionsInMap(HashMap> tripsPatterns){ + private void updateBusPositionsInMap(HashMap> tripsPatterns){ Log.d(DEBUG_TAG, "Updating positions of the buses"); //if(busPositionsOverlay == null) busPositionsOverlay = new FolderOverlay(); final ArrayList noPatternsTrips = new ArrayList<>(); for(String tripID: tripsPatterns.keySet()) { - final Pair pair = tripsPatterns.get(tripID); + final Pair pair = tripsPatterns.get(tripID); if (pair == null) continue; - final GtfsPositionUpdate update = pair.getFirst(); + final LivePositionUpdate update = pair.getFirst(); final TripAndPatternWithStops tripWithPatternStops = pair.getSecond(); //check if Marker is already created if (busPositionMarkersByTrip.containsKey(tripID)){ //need to change the position of the marker final Marker marker = busPositionMarkersByTrip.get(tripID); assert marker!=null; updateBusMarker(marker, update, false); if(marker.getInfoWindow()!=null && marker.getInfoWindow() instanceof BusInfoWindow){ BusInfoWindow window = (BusInfoWindow) marker.getInfoWindow(); if(tripWithPatternStops != null) { //Log.d(DEBUG_TAG, "Update pattern for trip: "+tripID); window.setPatternAndDraw(tripWithPatternStops.getPattern()); } } } else{ //marker is not there, need to make it if(map==null) Log.e(DEBUG_TAG, "Creating marker with null map, things will explode"); final Marker marker = new Marker(map); /*final Drawable mDrawable = DrawableUtils.Companion.getScaledDrawableResources( getResources(), R.drawable.point_heading_icon, R.dimen.map_icons_size, R.dimen.map_icons_size); */ - String route = GtfsUtils.getLineNameFromGtfsID(update.getRouteID()); - final Drawable mdraw = ResourcesCompat.getDrawable(getResources(),R.drawable.point_heading_icon, null); + //String route = GtfsUtils.getLineNameFromGtfsID(update.getRouteID()); + final Drawable mdraw = ResourcesCompat.getDrawable(getResources(),R.drawable.map_bus_position_icon, null); /*final Drawable mdraw = DrawableUtils.Companion.writeOnDrawable(getResources(), R.drawable.point_heading_icon, R.color.white, route,12); */ assert mdraw != null; //mdraw.setBounds(0,0,28,28); marker.setIcon(mdraw); if(tripWithPatternStops == null){ noPatternsTrips.add(tripID); } MatoPattern markerPattern = null; if(tripWithPatternStops != null && tripWithPatternStops.getPattern()!=null) markerPattern = tripWithPatternStops.getPattern(); - marker.setInfoWindow(new BusInfoWindow(map, update, markerPattern , () -> { - - })); + marker.setInfoWindow(new BusInfoWindow(map, update, markerPattern , false, (pattern) -> { })); + marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER); updateBusMarker(marker, update, true); - // the overlay is null when it's not attached yet? + // the overlay is null when it's not attached yet?5 // cannot recreate it because it becomes null very soon // if(busPositionsOverlay == null) busPositionsOverlay = new FolderOverlay(); //save the marker if(busPositionsOverlay!=null) { busPositionsOverlay.add(marker); busPositionMarkersByTrip.put(tripID, marker); } } } if(noPatternsTrips.size()>0){ Log.i(DEBUG_TAG, "These trips have no matching pattern: "+noPatternsTrips); } } /** * Add stops as Markers on the map * @param stops the list of stops that must be included */ protected void showStopsMarkers(List stops){ if (getContext() == null || stops == null){ //we are not attached return; } boolean good = true; for (Stop stop : stops) { if (shownStops.contains(stop.ID)){ continue; } if(stop.getLongitude()==null || stop.getLatitude()==null) continue; shownStops.add(stop.ID); if(!map.isShown()){ if(good) Log.d(DEBUG_TAG, "Need to show stop but map is not shown, probably detached already"); good = false; continue; } else if(map.getRepository() == null){ Log.e(DEBUG_TAG, "Map view repository is null"); } GeoPoint marker = new GeoPoint(stop.getLatitude(), stop.getLongitude()); Marker stopMarker = makeMarker(marker, stop, false); stopsFolderOverlay.add(stopMarker); if (!map.getOverlays().contains(stopsFolderOverlay)) { Log.w(DEBUG_TAG, "Map doesn't have folder overlay"); } good=true; } //Log.d(DEBUG_TAG,"We have " +stopsFolderOverlay.getItems().size()+" stops in the folderOverlay"); //force redraw of markers map.invalidate(); } public Marker makeMarker(GeoPoint geoPoint, Stop stop, boolean isStartMarker){ return makeMarker(geoPoint,stop.ID, stop.getStopDefaultName(), stop.routesThatStopHereToString(), isStartMarker); } public Marker makeMarker(GeoPoint geoPoint, String stopID, String stopName, String routesStopping, boolean isStartMarker) { // add a marker final Marker marker = new Marker(map); // set custom info window as info window CustomInfoWindow popup = new CustomInfoWindow(map, stopID, stopName, routesStopping, - responder); + responder, R.layout.linedetail_stop_infowindow, R.color.red_darker); marker.setInfoWindow(popup); // make the marker clickable marker.setOnMarkerClickListener((thisMarker, mapView) -> { if (thisMarker.isInfoWindowOpen()) { // on second click Log.w(DEBUG_TAG, "Pressed on the click marker"); } else { // on first click // hide all opened info window InfoWindow.closeAllInfoWindowsOn(map); // show this particular info window thisMarker.showInfoWindow(); // move the map to its position map.getController().animateTo(thisMarker.getPosition()); } return true; }); // set its position marker.setPosition(geoPoint); - marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM); + marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER); // add to it an icon //marker.setIcon(getResources().getDrawable(R.drawable.bus_marker)); marker.setIcon(ResourcesCompat.getDrawable(getResources(), R.drawable.bus_stop, ctx.getTheme())); // add to it a title marker.setTitle(stopName); // set the description as the ID marker.setSnippet(stopID); // show popup info window of the searched marker if (isStartMarker) { marker.showInfoWindow(); //map.getController().animateTo(marker.getPosition()); } return marker; } @Nullable @org.jetbrains.annotations.Nullable @Override public View getBaseViewForSnackBar() { return null; } /** * Simple asyncTask class to load the stops in the background * Holds a weak reference to the fragment to do callbacks */ static class AsyncStopFetcher extends AsyncTask>{ final WeakReference fragmentWeakReference; public AsyncStopFetcher(MapFragment fragment) { this.fragmentWeakReference = new WeakReference<>(fragment); } @Override protected List doInBackground(BoundingBoxLimit... limits) { if(fragmentWeakReference.get()==null || fragmentWeakReference.get().getContext() == null){ Log.w(DEBUG_TAG, "AsyncLoad fragmentWeakreference null"); return null; } final BoundingBoxLimit limit = limits[0]; //Log.d(DEBUG_TAG, "Async Stop Fetcher started working"); NextGenDB dbHelper = NextGenDB.getInstance(fragmentWeakReference.get().getContext()); ArrayList stops = dbHelper.queryAllInsideMapView(limit.latitFrom, limit.latitTo, limit.longFrom, limit.latitTo); dbHelper.close(); return stops; } @Override protected void onPostExecute(List stops) { super.onPostExecute(stops); //Log.d(DEBUG_TAG, "Async Stop Fetcher has finished working"); if(fragmentWeakReference.get()==null) { Log.w(DEBUG_TAG, "AsyncLoad fragmentWeakreference null"); return; } if (stops!=null) Log.d(DEBUG_TAG, "AsyncLoad number of stops: "+stops.size()); fragmentWeakReference.get().showStopsMarkers(stops); } private static class BoundingBoxLimit{ final double longFrom, longTo, latitFrom, latitTo; public BoundingBoxLimit(double longFrom, double longTo, double latitFrom, double latitTo) { this.longFrom = longFrom; this.longTo = longTo; this.latitFrom = latitFrom; this.latitTo = latitTo; } } } } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.java index d8747b6..a42e371 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.java @@ -1,674 +1,636 @@ /* 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.content.SharedPreferences; import android.database.Cursor; import android.location.Location; import android.net.Uri; import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.lifecycle.Observer; import androidx.loader.app.LoaderManager; import androidx.loader.content.CursorLoader; import androidx.loader.content.Loader; import androidx.core.util.Pair; import androidx.preference.PreferenceManager; import androidx.appcompat.widget.AppCompatButton; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.work.WorkInfo; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ProgressBar; import android.widget.TextView; import com.android.volley.*; import it.reyboz.bustorino.BuildConfig; import it.reyboz.bustorino.R; import it.reyboz.bustorino.adapters.ArrivalsStopAdapter; import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.backend.FiveTAPIFetcher.QueryType; import it.reyboz.bustorino.backend.mato.MapiArrivalRequest; import it.reyboz.bustorino.data.DatabaseUpdate; import it.reyboz.bustorino.data.NextGenDB; import it.reyboz.bustorino.middleware.AppLocationManager; import it.reyboz.bustorino.data.AppDataProvider; import it.reyboz.bustorino.data.NextGenDB.Contract.*; import it.reyboz.bustorino.adapters.SquareStopAdapter; +import it.reyboz.bustorino.middleware.AutoFitGridLayoutManager; import it.reyboz.bustorino.util.LocationCriteria; import it.reyboz.bustorino.util.StopSorterByDistance; import java.util.*; public class NearbyStopsFragment extends Fragment implements LoaderManager.LoaderCallbacks { private FragmentListenerMain mListener; private FragmentLocationListener fragmentLocationListener; private final static String DEBUG_TAG = "NearbyStopsFragment"; private final static String FRAGMENT_TYPE_KEY = "FragmentType"; public final static int TYPE_STOPS = 19, TYPE_ARRIVALS = 20; private int fragment_type; public final static String FRAGMENT_TAG="NearbyStopsFrag"; //data Bundle private final String BUNDLE_LOCATION = "location"; private final int LOADER_ID = 0; private RecyclerView gridRecyclerView; private SquareStopAdapter dataAdapter; private AutoFitGridLayoutManager gridLayoutManager; private Location lastReceivedLocation = null; private ProgressBar circlingProgressBar,flatProgressBar; private int distance; protected SharedPreferences globalSharedPref; private SharedPreferences.OnSharedPreferenceChangeListener preferenceChangeListener; private TextView messageTextView,titleTextView; private CommonScrollListener scrollListener; private AppCompatButton switchButton; private boolean firstLocForStops = true,firstLocForArrivals = true; public static final int COLUMN_WIDTH_DP = 250; private Integer MAX_DISTANCE = -3; private int MIN_NUM_STOPS = -1; private int TIME_INTERVAL_REQUESTS = -1; private AppLocationManager locManager; //These are useful for the case of nearby arrivals private ArrivalsManager arrivalsManager = null; private ArrivalsStopAdapter arrivalsStopAdapter = null; private boolean dbUpdateRunning = false; private ArrayList currentNearbyStops = new ArrayList<>(); public NearbyStopsFragment() { // Required empty public constructor } /** * Use this factory method to create a new instance of * this fragment using the provided parameters. * @return A new instance of fragment NearbyStopsFragment. */ public static NearbyStopsFragment newInstance(int fragmentType) { if(fragmentType != TYPE_STOPS && fragmentType != TYPE_ARRIVALS ) throw new IllegalArgumentException("WRONG KIND OF FRAGMENT USED"); NearbyStopsFragment fragment = new NearbyStopsFragment(); final Bundle args = new Bundle(1); args.putInt(FRAGMENT_TYPE_KEY,fragmentType); fragment.setArguments(args); return fragment; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { setFragmentType(getArguments().getInt(FRAGMENT_TYPE_KEY)); } locManager = AppLocationManager.getInstance(getContext()); fragmentLocationListener = new FragmentLocationListener(this); if (getContext()!=null) { globalSharedPref = getContext().getSharedPreferences(getString(R.string.mainSharedPreferences), Context.MODE_PRIVATE); globalSharedPref.registerOnSharedPreferenceChangeListener(preferenceChangeListener); } } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment if (getContext() == null) throw new RuntimeException(); View root = inflater.inflate(R.layout.fragment_nearby_stops, container, false); gridRecyclerView = root.findViewById(R.id.stopGridRecyclerView); gridLayoutManager = new AutoFitGridLayoutManager(getContext().getApplicationContext(), Float.valueOf(utils.convertDipToPixels(getContext(),COLUMN_WIDTH_DP)).intValue()); gridRecyclerView.setLayoutManager(gridLayoutManager); gridRecyclerView.setHasFixedSize(false); circlingProgressBar = root.findViewById(R.id.loadingBar); flatProgressBar = root.findViewById(R.id.horizontalProgressBar); messageTextView = root.findViewById(R.id.messageTextView); titleTextView = root.findViewById(R.id.titleTextView); switchButton = root.findViewById(R.id.switchButton); scrollListener = new CommonScrollListener(mListener,false); switchButton.setOnClickListener(v -> switchFragmentType()); Log.d(DEBUG_TAG, "onCreateView"); DatabaseUpdate.watchUpdateWorkStatus(getContext(), this, new Observer>() { @Override public void onChanged(List workInfos) { if(workInfos.isEmpty()) return; WorkInfo wi = workInfos.get(0); if (wi.getState() == WorkInfo.State.RUNNING && locManager.isRequesterRegistered(fragmentLocationListener)) { locManager.removeLocationRequestFor(fragmentLocationListener); dbUpdateRunning = true; } else if(!locManager.isRequesterRegistered(fragmentLocationListener)){ locManager.addLocationRequestFor(fragmentLocationListener); dbUpdateRunning = false; } } }); return root; } /** * Use this method to set the fragment type * @param type the type, TYPE_ARRIVALS or TYPE_STOPS */ private void setFragmentType(int type){ if(type!=TYPE_ARRIVALS && type !=TYPE_STOPS) throw new IllegalArgumentException("type not recognized"); this.fragment_type = type; switch(type){ case TYPE_ARRIVALS: TIME_INTERVAL_REQUESTS = 5*1000; break; case TYPE_STOPS: TIME_INTERVAL_REQUESTS = 1000; } } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); /// TODO: RISOLVERE PROBLEMA: il context qui e' l'Activity non il Fragment if (context instanceof FragmentListenerMain) { mListener = (FragmentListenerMain) context; } else { throw new RuntimeException(context + " must implement OnFragmentInteractionListener"); } Log.d(DEBUG_TAG, "OnAttach called"); } @Override public void onPause() { super.onPause(); gridRecyclerView.setAdapter(null); locManager.removeLocationRequestFor(fragmentLocationListener); Log.d(DEBUG_TAG,"On paused called"); } @Override public void onResume() { super.onResume(); try{ if(!dbUpdateRunning && !locManager.isRequesterRegistered(fragmentLocationListener)) locManager.addLocationRequestFor(fragmentLocationListener); } catch (SecurityException ex){ //ignored //try another location provider } switch(fragment_type){ case TYPE_STOPS: if(dataAdapter!=null){ gridRecyclerView.setAdapter(dataAdapter); circlingProgressBar.setVisibility(View.GONE); } break; case TYPE_ARRIVALS: if(arrivalsStopAdapter!=null){ gridRecyclerView.setAdapter(arrivalsStopAdapter); circlingProgressBar.setVisibility(View.GONE); } } mListener.enableRefreshLayout(false); Log.d(DEBUG_TAG,"OnResume called"); if(getContext()==null){ Log.e(DEBUG_TAG, "NULL CONTEXT, everything is going to crash now"); MIN_NUM_STOPS = 5; MAX_DISTANCE = 600; return; } //Re-read preferences SharedPreferences shpr = PreferenceManager.getDefaultSharedPreferences(getContext().getApplicationContext()); //For some reason, they are all saved as strings MAX_DISTANCE = shpr.getInt(getString(R.string.pref_key_radius_recents),600); boolean isMinStopInt = true; try{ MIN_NUM_STOPS = shpr.getInt(getString(R.string.pref_key_num_recents), 5); } catch (ClassCastException ex){ isMinStopInt = false; } if(!isMinStopInt) try { MIN_NUM_STOPS = Integer.parseInt(shpr.getString(getString(R.string.pref_key_num_recents), "5")); } catch (NumberFormatException ex){ MIN_NUM_STOPS = 5; } if(BuildConfig.DEBUG) Log.d(DEBUG_TAG, "Max distance for stops: "+MAX_DISTANCE+ ", Min number of stops: "+MIN_NUM_STOPS); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); gridRecyclerView.setVisibility(View.INVISIBLE); gridRecyclerView.addOnScrollListener(scrollListener); } @Override public void onDetach() { super.onDetach(); mListener = null; if(arrivalsManager!=null) arrivalsManager.cancelAllRequests(); } @NonNull @Override public Loader onCreateLoader(int id, Bundle args) { //BUILD URI if (args!=null) lastReceivedLocation = args.getParcelable(BUNDLE_LOCATION); Uri.Builder builder = new Uri.Builder(); builder.scheme("content").authority(AppDataProvider.AUTHORITY) .appendPath("stops").appendPath("location") .appendPath(String.valueOf(lastReceivedLocation.getLatitude())) .appendPath(String.valueOf(lastReceivedLocation.getLongitude())) .appendPath(String.valueOf(distance)); //distance CursorLoader cl = new CursorLoader(getContext(),builder.build(),NextGenDB.QUERY_COLUMN_stops_all,null,null,null); cl.setUpdateThrottle(2000); return cl; } @Override public void onLoadFinished(@NonNull Loader loader, Cursor cursor) { if (0 > MAX_DISTANCE) throw new AssertionError(); //Cursor might be null if (cursor == null) { Log.e(DEBUG_TAG, "Null cursor, something really wrong happened"); return; } Log.d(DEBUG_TAG, "Num stops found: " + cursor.getCount() + ", Current distance: " + distance); if (!dbUpdateRunning && (cursor.getCount() < MIN_NUM_STOPS && distance <= MAX_DISTANCE)) { distance = distance * 2; Bundle d = new Bundle(); d.putParcelable(BUNDLE_LOCATION, lastReceivedLocation); getLoaderManager().restartLoader(LOADER_ID, d, this); //Log.d(DEBUG_TAG, "Doubling distance now!"); return; } Log.d("LoadFromCursor", "Number of nearby stops: " + cursor.getCount()); //////// if(cursor.getCount()>0) currentNearbyStops = NextGenDB.getStopsFromCursorAllFields(cursor); showCurrentStops(); } /** * Display the stops, or run new set of requests for arrivals */ private void showCurrentStops(){ if (currentNearbyStops.isEmpty()) { setNoStopsLayout(); return; } double minDistance = Double.POSITIVE_INFINITY; for(Stop s: currentNearbyStops){ minDistance = Math.min(minDistance, s.getDistanceFromLocation(lastReceivedLocation)); } //quick trial to hopefully always get the stops in the correct order Collections.sort(currentNearbyStops,new StopSorterByDistance(lastReceivedLocation)); switch (fragment_type){ case TYPE_STOPS: showStopsInRecycler(currentNearbyStops); break; case TYPE_ARRIVALS: arrivalsManager = new ArrivalsManager(currentNearbyStops); flatProgressBar.setVisibility(View.VISIBLE); flatProgressBar.setProgress(0); flatProgressBar.setIndeterminate(false); //for the moment, be satisfied with only one location //AppLocationManager.getInstance(getContext()).removeLocationRequestFor(fragmentLocationListener); break; default: } } @Override public void onLoaderReset(@NonNull Loader loader) { } /** * To enable targeting from the Button */ public void switchFragmentType(View v){ switchFragmentType(); } /** * Call when you need to switch the type of fragment */ private void switchFragmentType(){ if(fragment_type==TYPE_ARRIVALS){ setFragmentType(TYPE_STOPS); switchButton.setText(getString(R.string.show_arrivals)); titleTextView.setText(getString(R.string.nearby_stops_message)); if(arrivalsManager!=null) arrivalsManager.cancelAllRequests(); if(dataAdapter!=null) gridRecyclerView.setAdapter(dataAdapter); } else if (fragment_type==TYPE_STOPS){ setFragmentType(TYPE_ARRIVALS); titleTextView.setText(getString(R.string.nearby_arrivals_message)); switchButton.setText(getString(R.string.show_stops)); if(arrivalsStopAdapter!=null) gridRecyclerView.setAdapter(arrivalsStopAdapter); } fragmentLocationListener.lastUpdateTime = -1; //locManager.removeLocationRequestFor(fragmentLocationListener); //locManager.addLocationRequestFor(fragmentLocationListener); showCurrentStops(); } //useful methods /////// GUI METHODS //////// private void showStopsInRecycler(List stops){ if(firstLocForStops) { dataAdapter = new SquareStopAdapter(stops, mListener, lastReceivedLocation); gridRecyclerView.setAdapter(dataAdapter); firstLocForStops = false; }else { dataAdapter.setStops(stops); dataAdapter.setUserPosition(lastReceivedLocation); } dataAdapter.notifyDataSetChanged(); //showRecyclerHidingLoadMessage(); if (gridRecyclerView.getVisibility() != View.VISIBLE) { circlingProgressBar.setVisibility(View.GONE); gridRecyclerView.setVisibility(View.VISIBLE); } messageTextView.setVisibility(View.GONE); if(mListener!=null) mListener.readyGUIfor(FragmentKind.NEARBY_STOPS); } private void showArrivalsInRecycler(List palinas){ Collections.sort(palinas,new StopSorterByDistance(lastReceivedLocation)); final ArrayList> routesPairList = new ArrayList<>(10); //int maxNum = Math.min(MAX_STOPS, stopList.size()); for(Palina p: palinas){ //if there are no routes available, skip stop if(p.queryAllRoutes().size() == 0) continue; for(Route r: p.queryAllRoutes()){ //if there are no routes, should not do anything if (r.passaggi != null && !r.passaggi.isEmpty()) routesPairList.add(new Pair<>(p,r)); } } if (getContext()==null){ Log.e(DEBUG_TAG, "Trying to show arrivals in Recycler but we're not attached"); return; } if(firstLocForArrivals){ arrivalsStopAdapter = new ArrivalsStopAdapter(routesPairList,mListener,getContext(),lastReceivedLocation); gridRecyclerView.setAdapter(arrivalsStopAdapter); firstLocForArrivals = false; } else { arrivalsStopAdapter.setRoutesPairListAndPosition(routesPairList,lastReceivedLocation); } //arrivalsStopAdapter.notifyDataSetChanged(); showRecyclerHidingLoadMessage(); if(mListener!=null) mListener.readyGUIfor(FragmentKind.NEARBY_ARRIVALS); } private void setNoStopsLayout(){ messageTextView.setVisibility(View.VISIBLE); messageTextView.setText(R.string.no_stops_nearby); circlingProgressBar.setVisibility(View.GONE); } /** * Does exactly what is says on the tin */ private void showRecyclerHidingLoadMessage(){ if (gridRecyclerView.getVisibility() != View.VISIBLE) { circlingProgressBar.setVisibility(View.GONE); gridRecyclerView.setVisibility(View.VISIBLE); } messageTextView.setVisibility(View.GONE); } class ArrivalsManager implements Response.Listener, Response.ErrorListener{ final HashMap palinasDone = new HashMap<>(); //final Map> routesToAdd = new HashMap<>(); final static String REQUEST_TAG = "NearbyArrivals"; final NetworkVolleyManager volleyManager; int activeRequestCount = 0,reqErrorCount = 0, reqSuccessCount=0; ArrivalsManager(List stops){ volleyManager = NetworkVolleyManager.getInstance(getContext()); int MAX_ARRIVAL_STOPS = 35; Date currentDate = new Date(); int timeRange = 3600; int departures = 10; int numreq = 0; for(Stop s: stops.subList(0,Math.min(stops.size(), MAX_ARRIVAL_STOPS))){ final MapiArrivalRequest req = new MapiArrivalRequest(s.ID, currentDate, timeRange, departures, this, this); req.setTag(REQUEST_TAG); volleyManager.addToRequestQueue(req); activeRequestCount++; numreq++; } flatProgressBar.setMax(numreq); } @Override public void onErrorResponse(VolleyError error) { if(error instanceof ParseError){ //TODO Log.w(DEBUG_TAG,"Parsing error for stop request"); } else if (error instanceof NetworkError){ String s; if(error.networkResponse!=null) s = new String(error.networkResponse.data); else s=""; Log.w(DEBUG_TAG,"Network error: "+s); }else { Log.w(DEBUG_TAG,"Volley Error: "+error.getMessage()); } if(error.networkResponse!=null){ Log.w(DEBUG_TAG, "Error status code: "+error.networkResponse.statusCode); } //counters activeRequestCount--; reqErrorCount++; flatProgressBar.setProgress(reqErrorCount+reqSuccessCount); } @Override public void onResponse(Palina result) { //counter for requests activeRequestCount--; reqSuccessCount++; //final Palina palinaInMap = palinasDone.get(result.ID); //palina cannot be null here //sorry for the brutal crash when it happens //if(palinaInMap == null) throw new IllegalStateException("Cannot get the palina from the map"); //add the palina to the successful one //TODO: Avoid redoing everything every time a new Result arrives palinasDone.put(result.ID, result); final ArrayList outList = new ArrayList<>(); for(Palina p: palinasDone.values()){ final List routes = p.queryAllRoutes(); if(routes!=null && routes.size()>0) outList.add(p); } showArrivalsInRecycler(outList); flatProgressBar.setProgress(reqErrorCount+reqSuccessCount); if(activeRequestCount==0) { flatProgressBar.setIndeterminate(true); flatProgressBar.setVisibility(View.GONE); } } void cancelAllRequests(){ volleyManager.getRequestQueue().cancelAll(REQUEST_TAG); flatProgressBar.setVisibility(View.GONE); } } /** * Local locationListener, to use for the GPS */ class FragmentLocationListener implements AppLocationManager.LocationRequester{ LoaderManager.LoaderCallbacks callbacks; private int oldLocStatus = -2; private LocationCriteria cr; private long lastUpdateTime = -1; public FragmentLocationListener(LoaderManager.LoaderCallbacks callbacks) { this.callbacks = callbacks; } @Override public void onLocationChanged(Location location) { //set adapter float accuracy = location.getAccuracy(); if(accuracy<100 && !dbUpdateRunning) { distance = 20; final Bundle msgBundle = new Bundle(); msgBundle.putParcelable(BUNDLE_LOCATION,location); getLoaderManager().restartLoader(LOADER_ID,msgBundle,callbacks); } lastUpdateTime = System.currentTimeMillis(); Log.d("BusTO:NearPositListen","can start loader "+ !dbUpdateRunning); } @Override public void onLocationStatusChanged(int status) { switch(status){ case AppLocationManager.LOCATION_GPS_AVAILABLE: messageTextView.setVisibility(View.GONE); break; case AppLocationManager.LOCATION_UNAVAILABLE: messageTextView.setText(R.string.enableGpsText); messageTextView.setVisibility(View.VISIBLE); break; default: Log.e(DEBUG_TAG,"Location status not recognized"); } } @Override public LocationCriteria getLocationCriteria() { return new LocationCriteria(120,TIME_INTERVAL_REQUESTS); } @Override public long getLastUpdateTimeMillis() { return lastUpdateTime; } void resetUpdateTime(){ lastUpdateTime = -1; } @Override public void onLocationProviderAvailable() { } @Override public void onLocationDisabled() { } } - - /** - * Simple trick to get an automatic number of columns (from https://www.journaldev.com/13792/android-gridlayoutmanager-example) - * - */ - class AutoFitGridLayoutManager extends GridLayoutManager { - - private int columnWidth; - private boolean columnWidthChanged = true; - - public AutoFitGridLayoutManager(Context context, int columnWidth) { - super(context, 1); - - setColumnWidth(columnWidth); - } - - public void setColumnWidth(int newColumnWidth) { - if (newColumnWidth > 0 && newColumnWidth != columnWidth) { - columnWidth = newColumnWidth; - columnWidthChanged = true; - } - } - - @Override - public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { - if (columnWidthChanged && columnWidth > 0) { - int totalSpace; - if (getOrientation() == VERTICAL) { - totalSpace = getWidth() - getPaddingRight() - getPaddingLeft(); - } else { - totalSpace = getHeight() - getPaddingTop() - getPaddingBottom(); - } - int spanCount = Math.max(1, totalSpace / columnWidth); - setSpanCount(spanCount); - columnWidthChanged = false; - } - super.onLayoutChildren(recycler, state); - } - } } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/TestRealtimeGtfsFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/TestRealtimeGtfsFragment.kt index e11de63..a9e0852 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/TestRealtimeGtfsFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/TestRealtimeGtfsFragment.kt @@ -1,93 +1,124 @@ package it.reyboz.bustorino.fragments import android.os.Bundle +import android.util.Log import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Button +import android.widget.EditText import android.widget.TextView -import android.widget.Toast -import com.android.volley.Response -import com.google.transit.realtime.GtfsRealtime +import androidx.fragment.app.viewModels import it.reyboz.bustorino.R -import it.reyboz.bustorino.backend.NetworkVolleyManager -import it.reyboz.bustorino.backend.gtfs.GtfsPositionUpdate -import it.reyboz.bustorino.backend.gtfs.GtfsRtPositionsRequest +import it.reyboz.bustorino.backend.mato.MQTTMatoClient +import it.reyboz.bustorino.viewmodels.MQTTPositionsViewModel -// TODO: Rename parameter arguments, choose names that match -// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER -private const val ARG_PARAM1 = "param1" -private const val ARG_PARAM2 = "param2" /** * A simple [Fragment] subclass. * Use the [TestRealtimeGtfsFragment.newInstance] factory method to * create an instance of this fragment. */ class TestRealtimeGtfsFragment : Fragment() { private lateinit var buttonLaunch: Button private lateinit var messageTextView: TextView - private val requestListener = object: GtfsRtPositionsRequest.Companion.RequestListener{ + private var subscribed = false + private lateinit var mqttMatoClient: MQTTMatoClient + + private lateinit var lineEditText: EditText + + private val mqttViewModel: MQTTPositionsViewModel by viewModels() + + /*private val requestListener = object: GtfsRtPositionsRequest.Companion.RequestListener{ override fun onResponse(response: ArrayList?) { if (response == null) return if (response.size == 0) { messageTextView.text = "No entities in the message" return } val position = response[0] //position. messageTextView.text = "Entity message 0: ${position}" } + + } + */ + + private val listener = MQTTMatoClient.Companion.MQTTMatoListener{ + + messageTextView.text = "Update: ${it}" + Log.d("BUSTO-TestMQTT", "Received update $it") } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) arguments?.let { } } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // Inflate the layout for this fragment val rootView= inflater.inflate(R.layout.fragment_test_realtime_gtfs, container, false) buttonLaunch = rootView.findViewById(R.id.btn_download_data) + buttonLaunch.text="Start" messageTextView = rootView.findViewById(R.id.gtfsMessageTextView) + lineEditText = rootView.findViewById(R.id.lineEditText) + + mqttViewModel.updatesWithTripAndPatterns.observe(viewLifecycleOwner){ + val upds = it.entries.map { it.value.first } + messageTextView.text = "$upds" + } buttonLaunch.setOnClickListener { context?.let {cont-> - val req = GtfsRtPositionsRequest( + /*val req = GtfsRtPositionsRequest( Response.ErrorListener { Toast.makeText(cont, "Error: ${it.message}",Toast.LENGTH_SHORT) }, requestListener ) NetworkVolleyManager.getInstance(cont).addToRequestQueue(req) + + */ + subscribed = if(subscribed){ + //mqttMatoClient.desubscribe(listener) + mqttViewModel.stopPositionsListening() + buttonLaunch.text="Start" + false + } else{ + //mqttMatoClient.startAndSubscribe(lineEditText.text.trim().toString(), listener) + mqttViewModel.requestPosUpdates(lineEditText.text.trim().toString()) + buttonLaunch.text="Stop" + true + } + } } return rootView } companion object { /** * Use this factory method to create a new instance of * this fragment using the provided parameters. * * @return A new instance of fragment TestRealtimeGtfsFragment. */ @JvmStatic fun newInstance() = TestRealtimeGtfsFragment().apply { } } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/map/BusInfoWindow.kt b/app/src/main/java/it/reyboz/bustorino/map/BusInfoWindow.kt index dced2ec..4f00313 100644 --- a/app/src/main/java/it/reyboz/bustorino/map/BusInfoWindow.kt +++ b/app/src/main/java/it/reyboz/bustorino/map/BusInfoWindow.kt @@ -1,82 +1,110 @@ /* BusTO - Map 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.map import android.annotation.SuppressLint -import android.view.MotionEvent -import android.view.View import android.view.View.* +import android.widget.ImageView import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.marginEnd import it.reyboz.bustorino.R -import it.reyboz.bustorino.backend.gtfs.GtfsPositionUpdate +import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate import it.reyboz.bustorino.backend.gtfs.GtfsUtils -import it.reyboz.bustorino.data.gtfs.GtfsTrip +import it.reyboz.bustorino.backend.utils import it.reyboz.bustorino.data.gtfs.MatoPattern import org.osmdroid.views.MapView import org.osmdroid.views.overlay.infowindow.BasicInfoWindow @SuppressLint("ClickableViewAccessibility") class BusInfoWindow(map: MapView, - val update: GtfsPositionUpdate, + private val routeName: String, + private val vehicleLabel: String, var pattern: MatoPattern?, - private val touchUp: onTouchUp): + val showClose: Boolean, + private val touchUp: onTouchUp + ): BasicInfoWindow(R.layout.bus_info_window,map) { init { mView.setOnTouchListener { view, motionEvent -> - touchUp.onActionUp() + touchUp.onActionUp(pattern) close() //mView.performClick() true } } + constructor(map: MapView, update: LivePositionUpdate, pattern: MatoPattern?, showClose: Boolean, touchUp: onTouchUp, ): + this(map, + GtfsUtils.getLineNameFromGtfsID(update.routeID), + update.vehicle, + pattern, + showClose, + touchUp + ) + override fun onOpen(item: Any?) { // super.onOpen(item) val titleView = mView.findViewById(R.id.businfo_title) val descrView = mView.findViewById(R.id.businfo_description) val subdescrView = mView.findViewById(R.id.businfo_subdescription) - val nameRoute = GtfsUtils.getLineNameFromGtfsID(update.routeID) - titleView.text = (mView.resources.getString(R.string.line_fill, nameRoute) + val iconClose = mView.findViewById(R.id.closeIcon) + + //val nameRoute = GtfsUtils.getLineNameFromGtfsID(update.lineGtfsId) + + titleView.text = (mView.resources.getString(R.string.line_fill, routeName) ) - subdescrView.text = update.vehicleInfo.label + subdescrView.text = vehicleLabel if(pattern!=null){ descrView.text = pattern!!.headsign descrView.visibility = VISIBLE } else{ descrView.visibility = GONE } + if(!showClose){ + iconClose.visibility = GONE + val ctx = titleView.context + val layPars = (titleView.layoutParams as ConstraintLayout.LayoutParams).apply { + marginStart= 0 //utils.convertDipToPixelsInt(ctx, 8.0)//8.dpToPixels() + topMargin=utils.convertDipToPixelsInt(ctx, 4.0) + marginEnd=0 + bottomMargin=0 + } + //titleView.layoutParams = layPars + } } + fun setPatternAndDraw(pattern: MatoPattern?){ if(pattern==null){ return } this.pattern = pattern if(isOpen){ onOpen(pattern) } } fun interface onTouchUp{ - fun onActionUp() + fun onActionUp(pattern: MatoPattern?) } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/map/BusPositionUtils.kt b/app/src/main/java/it/reyboz/bustorino/map/BusPositionUtils.kt new file mode 100644 index 0000000..bd1af1d --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/map/BusPositionUtils.kt @@ -0,0 +1,41 @@ +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/java/it/reyboz/bustorino/map/CustomInfoWindow.java b/app/src/main/java/it/reyboz/bustorino/map/CustomInfoWindow.java index ce6d19f..0a8fd47 100644 --- a/app/src/main/java/it/reyboz/bustorino/map/CustomInfoWindow.java +++ b/app/src/main/java/it/reyboz/bustorino/map/CustomInfoWindow.java @@ -1,95 +1,120 @@ /* BusTO - Map 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.map; import android.annotation.SuppressLint; import android.os.Build; import android.view.MotionEvent; import android.view.View; +import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; import org.osmdroid.views.MapView; import org.osmdroid.views.overlay.infowindow.BasicInfoWindow; import it.reyboz.bustorino.R; public class CustomInfoWindow extends BasicInfoWindow { //TODO: Make the action on the Click customizable private final TouchResponder touchResponder; private final String stopID, name, routesStopping; + + private final int colorResID; //final DisplayMetrics metrics; @Override public void onOpen(Object item) { super.onOpen(item); TextView descr_textView = mView.findViewById(R.id.bubble_description); CharSequence text = descr_textView.getText(); TextView titleTV = mView.findViewById(R.id.bubble_title); + titleTV.setTextColor(ContextCompat.getColor(mView.getContext(),colorResID)); //Log.d("BusTO-MapInfoWindow", "Descrip: "+text+", title "+(titleTV==null? "null": titleTV.getText())); if (text==null || !text.toString().isEmpty()){ descr_textView.setVisibility(View.VISIBLE); } else descr_textView.setVisibility(View.GONE); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { mView.setElevation(3.2f); } TextView subDescriptTextView = mView.findViewById(R.id.bubble_subdescription); if (routesStopping!=null && !routesStopping.isEmpty()){ subDescriptTextView.setText(routesStopping); subDescriptTextView.setVisibility(View.VISIBLE); } + //check if there is a close image + ImageView image = mView.findViewById(R.id.closeIcon); + if (image != null) { + image.setOnClickListener( view -> close()); + } + + } + public CustomInfoWindow(MapView mapView, String stopID, String name, String routesStopping, + TouchResponder responder){ + + this(mapView, stopID, name, routesStopping, responder,R.layout.map_popup, R.color.red_darker); } @SuppressLint("ClickableViewAccessibility") - public CustomInfoWindow(MapView mapView, String stopID, String name, String routesStopping, TouchResponder responder) { + public CustomInfoWindow(MapView mapView, + String stopID, + String name, + String routesStopping, + TouchResponder responder, + int layoutId, + int colorResId) { // get the personalized layout - super(R.layout.map_popup, mapView); + super(layoutId, mapView); touchResponder =responder; this.stopID = stopID; this.name = name; this.routesStopping = routesStopping; + colorResID = colorResId; //metrics = Resources.getSystem().getDisplayMetrics(); // make clickable mView.setOnTouchListener((View v, MotionEvent e) -> { if (e.getAction() == MotionEvent.ACTION_UP) { // on click touchResponder.onActionUp(this.stopID, this.name); } return true; }); + + } public interface TouchResponder{ /** * React to a click on the stop View * @param stopID the stop id * @param stopName the stop name */ void onActionUp(@NonNull String stopID, @Nullable String stopName); } } diff --git a/app/src/main/java/it/reyboz/bustorino/map/MapViewModel.kt b/app/src/main/java/it/reyboz/bustorino/map/MapViewModel.kt new file mode 100644 index 0000000..87651c3 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/map/MapViewModel.kt @@ -0,0 +1,15 @@ +package it.reyboz.bustorino.map + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel + +class MapViewModel : ViewModel() { + + val currentLat = MutableLiveData(INVALID) + val currentLong = MutableLiveData(INVALID) + val currentZoom = MutableLiveData(-10.0) + + companion object{ + const val INVALID = -1000.0 + } +} \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/map/MarkerAnimation.java b/app/src/main/java/it/reyboz/bustorino/map/MarkerAnimation.java deleted file mode 100644 index 7e7ebb9..0000000 --- a/app/src/main/java/it/reyboz/bustorino/map/MarkerAnimation.java +++ /dev/null @@ -1,31 +0,0 @@ -package it.reyboz.bustorino.map; - -/* Copyright 2013 Google Inc. - Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0.html */ - - - import android.animation.ObjectAnimator; - import android.animation.TypeEvaluator; - import android.util.Property; - - import org.osmdroid.util.GeoPoint; - import org.osmdroid.views.MapView; - import org.osmdroid.views.overlay.Marker; - -public class MarkerAnimation { - - - public static ObjectAnimator makeMarkerAnimator(final MapView map, Marker marker, GeoPoint finalPosition, final GeoPointInterpolator GeoPointInterpolator, int durationMs) { - TypeEvaluator typeEvaluator = new TypeEvaluator() { - @Override - public GeoPoint evaluate(float fraction, GeoPoint startValue, GeoPoint endValue) { - return GeoPointInterpolator.interpolate(fraction, startValue, endValue); - } - }; - Property property = Property.of(Marker.class, GeoPoint.class, "position"); - ObjectAnimator animator = ObjectAnimator.ofObject(marker, property, typeEvaluator, finalPosition); - animator.setDuration(durationMs); - //animator.start(); - return animator; - } -} diff --git a/app/src/main/java/it/reyboz/bustorino/map/MarkerUtils.java b/app/src/main/java/it/reyboz/bustorino/map/MarkerUtils.java new file mode 100644 index 0000000..962898f --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/map/MarkerUtils.java @@ -0,0 +1,102 @@ +package it.reyboz.bustorino.map; + +import android.animation.ObjectAnimator; +import android.animation.TypeEvaluator; +import android.graphics.drawable.Drawable; +import android.util.Log; +import android.util.Property; + + +import android.view.animation.LinearInterpolator; +import it.reyboz.bustorino.R; +import org.osmdroid.util.GeoPoint; +import org.osmdroid.views.MapView; +import org.osmdroid.views.overlay.Marker; +import org.osmdroid.views.overlay.infowindow.InfoWindow; + +public class MarkerUtils { + + public static final int LINEAR_ANIMATION = 1; + + /* Copyright 2013 Google Inc. + Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0.html */ + public static ObjectAnimator makeMarkerAnimator(final MapView map, Marker marker, GeoPoint finalPosition, int animationType, int durationMs) { + + GeoPointInterpolator interpolator; + switch (animationType){ + case LINEAR_ANIMATION: + interpolator = new GeoPointInterpolator.Linear(); + break; + default: + throw new IllegalArgumentException("Value "+animationType+ " for animationType is invalid"); + } + TypeEvaluator typeEvaluator = (fraction, startValue, endValue) -> + interpolator.interpolate(fraction, startValue, endValue); + Property property = Property.of(Marker.class, GeoPoint.class, "position"); + ObjectAnimator animator = ObjectAnimator.ofObject(marker, property, typeEvaluator, finalPosition); + switch (animationType){ + case LINEAR_ANIMATION: + + animator.setInterpolator(new LinearInterpolator()); + default: + } + animator.setDuration(durationMs); + //animator.start(); + return animator; + } + + public static Marker makeMarker(GeoPoint geoPoint, String stopID, String stopName, + String routesStopping, + MapView map, + CustomInfoWindow.TouchResponder responder, + Drawable icon, + int infoWindowLayout, + int titleColorId) { + + // add a marker + final Marker marker = new Marker(map); + + // set custom info window as info window + CustomInfoWindow popup = new CustomInfoWindow(map, stopID, stopName, routesStopping, responder, infoWindowLayout, titleColorId); + marker.setInfoWindow(popup); + + // make the marker clickable + marker.setOnMarkerClickListener((thisMarker, mapView) -> { + if (thisMarker.isInfoWindowOpen()) { + // on second click + Log.w("BusTO-OsmMap", "Pressed on the click marker"); + } else { + // on first click + + // hide all opened info window + InfoWindow.closeAllInfoWindowsOn(map); + // show this particular info window + thisMarker.showInfoWindow(); + // move the map to its position + map.getController().animateTo(thisMarker.getPosition()); + } + + return true; + }); + + // set its position + marker.setPosition(geoPoint); + marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM); + // add to it an icon + //marker.setIcon(getResources().getDrawable(R.drawable.bus_marker)); + + marker.setIcon(icon); + // add to it a title + marker.setTitle(stopName); + // set the description as the ID + marker.setSnippet(stopID); + + // show popup info window of the searched marker + /*if (isStartMarker) { + marker.showInfoWindow(); + //map.getController().animateTo(marker.getPosition()); + }*/ + + return marker; + } +} diff --git a/app/src/main/java/it/reyboz/bustorino/middleware/AutoFitGridLayoutManager.kt b/app/src/main/java/it/reyboz/bustorino/middleware/AutoFitGridLayoutManager.kt new file mode 100644 index 0000000..6412496 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/middleware/AutoFitGridLayoutManager.kt @@ -0,0 +1,41 @@ +package it.reyboz.bustorino.middleware + +import android.content.Context +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.Recycler + +/** + * Simple trick to get an automatic number of columns (from https://www.journaldev.com/13792/android-gridlayoutmanager-example) + * + */ +class AutoFitGridLayoutManager(context: Context?, columnWidth: Int): + GridLayoutManager(context, 1) { + private var columnWidth = 0 + private var columnWidthChanged = true + + init { + setColumnWidth(columnWidth) + } + + fun setColumnWidth(newColumnWidth: Int) { + if (newColumnWidth > 0 && newColumnWidth != columnWidth) { + columnWidth = newColumnWidth + columnWidthChanged = true + } + } + + override fun onLayoutChildren(recycler: Recycler, state: RecyclerView.State) { + if (columnWidthChanged && columnWidth > 0) { + val totalSpace: Int = if (orientation == VERTICAL) { + width - paddingRight - paddingLeft + } else { + height - paddingTop - paddingBottom + } + val spanCount = Math.max(1, totalSpace / columnWidth) + setSpanCount(spanCount) + columnWidthChanged = false + } + super.onLayoutChildren(recycler, state) + } +} diff --git a/app/src/main/java/it/reyboz/bustorino/util/ViewUtils.kt b/app/src/main/java/it/reyboz/bustorino/util/ViewUtils.kt index ad693d8..78891bd 100644 --- a/app/src/main/java/it/reyboz/bustorino/util/ViewUtils.kt +++ b/app/src/main/java/it/reyboz/bustorino/util/ViewUtils.kt @@ -1,33 +1,95 @@ package it.reyboz.bustorino.util import android.graphics.Rect import android.util.Log - import android.view.View +import android.view.WindowManager +import android.view.animation.Animation +import android.view.animation.Transformation import androidx.core.widget.NestedScrollView class ViewUtils { companion object{ const val DEBUG_TAG="BusTO:ViewUtils" fun isViewFullyVisibleInScroll(view: View, scrollView: NestedScrollView): Boolean { val scrollBounds = Rect() scrollView.getDrawingRect(scrollBounds) val top = view.y val bottom = top + view.height Log.d(DEBUG_TAG, "Scroll bounds are $scrollBounds, top:${view.y}, bottom $bottom") return (scrollBounds.top < top && scrollBounds.bottom > bottom) } fun isViewPartiallyVisibleInScroll(view: View, scrollView: NestedScrollView): Boolean{ val scrollBounds = Rect() scrollView.getHitRect(scrollBounds) Log.d(DEBUG_TAG, "Scroll bounds are $scrollBounds") if (view.getLocalVisibleRect(scrollBounds)) { return true } else { return false } } + + //from https://stackoverflow.com/questions/4946295/android-expand-collapse-animation + fun expand(v: View,duration: Long, layoutHeight: Int = WindowManager.LayoutParams.WRAP_CONTENT) { + val matchParentMeasureSpec = + View.MeasureSpec.makeMeasureSpec((v.parent as View).width, View.MeasureSpec.EXACTLY) + val wrapContentMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) + v.measure(matchParentMeasureSpec, wrapContentMeasureSpec) + val targetHeight = v.measuredHeight + + // Older versions of android (pre API 21) cancel animations for views with a height of 0. + v.layoutParams.height = 1 + v.visibility = View.VISIBLE + val a: Animation = object : Animation() { + override fun applyTransformation(interpolatedTime: Float, t: Transformation?) { + v.layoutParams.height = + if (interpolatedTime == 1f) layoutHeight + else (targetHeight * interpolatedTime).toInt() + v.requestLayout() + } + + override fun willChangeBounds(): Boolean { + return true + } + } + + // Expansion speed of 1dp/ms + if(duration == DEF_DURATION) + a.duration = (targetHeight / v.context.resources.displayMetrics.density).toInt().toLong() + else + a.duration = duration + v.startAnimation(a) + } + + fun collapse(v: View, duration: Long): Animation { + val initialHeight = v.measuredHeight + val a: Animation = object : Animation() { + override fun applyTransformation(interpolatedTime: Float, t: Transformation?) { + if (interpolatedTime == 1f) { + v.visibility = View.GONE + } else { + v.layoutParams.height = initialHeight - (initialHeight * interpolatedTime).toInt() + v.requestLayout() + } + } + + override fun willChangeBounds(): Boolean { + return true + } + } + + // Collapse speed of 1dp/ms + if (duration == DEF_DURATION) + a.duration = (initialHeight / v.context.resources.displayMetrics.density).toInt().toLong() + else + a.duration = duration + v.startAnimation(a) + return a + } + + const val DEF_DURATION: Long = -2 } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesGridShowingViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesGridShowingViewModel.kt new file mode 100644 index 0000000..b5b5bbd --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesGridShowingViewModel.kt @@ -0,0 +1,27 @@ +package it.reyboz.bustorino.viewmodels + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import it.reyboz.bustorino.data.GtfsRepository +import it.reyboz.bustorino.data.NextGenDB +import it.reyboz.bustorino.data.OldDataRepository +import it.reyboz.bustorino.data.gtfs.GtfsDatabase + +class LinesGridShowingViewModel(application: Application) : AndroidViewModel(application) { + + private val gtfsRepo: GtfsRepository + + init { + val gtfsDao = GtfsDatabase.getGtfsDatabase(application).gtfsDao() + gtfsRepo = GtfsRepository(gtfsDao) + + } + + val routesLiveData = gtfsRepo.getAllRoutes() + + val isUrbanExpanded = MutableLiveData(true) + val isExtraUrbanExpanded = MutableLiveData(false) + val isTouristExpanded = MutableLiveData(false) +} \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/MQTTPositionsViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/MQTTPositionsViewModel.kt new file mode 100644 index 0000000..618784e --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/MQTTPositionsViewModel.kt @@ -0,0 +1,165 @@ +/* + BusTO - ViewModel 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.viewmodels + +import android.app.Application +import android.util.Log +import androidx.lifecycle.* +import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate +import it.reyboz.bustorino.backend.mato.MQTTMatoClient +import it.reyboz.bustorino.data.GtfsRepository +import it.reyboz.bustorino.data.MatoPatternsDownloadWorker +import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops +import it.reyboz.bustorino.fragments.GTFSPositionsViewModel +import kotlinx.coroutines.launch + + +typealias UpdatesMap = HashMap + +class MQTTPositionsViewModel(application: Application): AndroidViewModel(application) { + + private val gtfsRepo = GtfsRepository(application) + + //private val updates = UpdatesMap() + private val updatesLiveData = MutableLiveData>() + + private var mqttClient = MQTTMatoClient.getInstance() + + private var lineListening = "" + private var lastTimeReceived: Long = 0 + + private val positionListener = MQTTMatoClient.Companion.MQTTMatoListener{ + + val mupds = ArrayList() + if(lineListening==MQTTMatoClient.LINES_ALL){ + for(sdic in it.values){ + for(update in sdic.values){ + mupds.add(update) + } + } + } else{ + //we're listening to one + if (it.containsKey(lineListening.trim()) ){ + for(up in it[lineListening]?.values!!){ + mupds.add(up) + } + } + } + val time = System.currentTimeMillis() + if(lastTimeReceived == (0.toLong()) || (time-lastTimeReceived)>500){ + updatesLiveData.value = (mupds) + lastTimeReceived = time + } + + } + + //find the trip IDs in the updates + private val tripsIDsInUpdates = updatesLiveData.map { it -> + //Log.d(DEBUG_TI, "Updates map has keys ${upMap.keys}") + it.map { pos -> "gtt:"+pos.tripID } + + } + // get the trip IDs in the DB + private val gtfsTripsPatternsInDB = tripsIDsInUpdates.switchMap { + Log.i(DEBUG_TI, "tripsIds in updates changed: ${it.size}") + gtfsRepo.gtfsDao.getTripPatternStops(it) + } + //trip IDs to query, which are not present in the DB + //REMEMBER TO OBSERVE THIS IN THE MAP + val tripsGtfsIDsToQuery: LiveData> = gtfsTripsPatternsInDB.map { tripswithPatterns -> + val tripNames=tripswithPatterns.map { twp-> twp.trip.tripID } + Log.i(DEBUG_TI, "Have ${tripswithPatterns.size} trips in the DB") + if (tripsIDsInUpdates.value!=null) + return@map tripsIDsInUpdates.value!!.filter { !(tripNames.contains(it) || it.contains("null"))} + else { + Log.e(DEBUG_TI,"Got results for gtfsTripsInDB but not tripsIDsInUpdates??") + return@map ArrayList() + } + } + + // unify trips with updates + val updatesWithTripAndPatterns = gtfsTripsPatternsInDB.map { tripPatterns-> + Log.i(DEBUG_TI, "Mapping trips and patterns") + val mdict = HashMap>() + //missing patterns + val routesToDownload = HashSet() + if(updatesLiveData.value!=null) + for(update in updatesLiveData.value!!){ + + val trID:String = update.tripID + var found = false + for(trip in tripPatterns){ + if (trip.pattern == null){ + //pattern is null, which means we have to download + // the pattern data from MaTO + routesToDownload.add(trip.trip.routeID) + } + if (trip.trip.tripID == "gtt:$trID"){ + found = true + //insert directly + mdict[trID] = Pair(update,trip) + break + } + } + if (!found){ + //Log.d(DEBUG_TI, "Cannot find pattern ${tr}") + //give the update anyway + mdict[trID] = Pair(update,null) + } + } + //have to request download of missing Patterns + if (routesToDownload.size > 0){ + Log.d(DEBUG_TI, "Have ${routesToDownload.size} missing patterns from the DB: $routesToDownload") + //downloadMissingPatterns (ArrayList(routesToDownload)) + MatoPatternsDownloadWorker.downloadPatternsForRoutes(routesToDownload.toList(), getApplication()) + } + + return@map mdict + } + + + fun requestPosUpdates(line: String){ + lineListening = line + viewModelScope.launch { + mqttClient.startAndSubscribe(line,positionListener, getApplication()) + } + + + //updatePositions(1000) + } + + fun stopPositionsListening(){ + viewModelScope.launch { + val tt = System.currentTimeMillis() + mqttClient.desubscribe(positionListener) + val time = System.currentTimeMillis() -tt + Log.d(DEBUG_TI, "Took $time ms to unsubscribe") + } + + } + + fun retriggerPositionUpdate(){ + if(updatesLiveData.value!=null){ + updatesLiveData.postValue(updatesLiveData.value) + } + } + + companion object{ + private const val DEBUG_TI = "BusTO-MQTTLiveData" + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ball.xml b/app/src/main/res/drawable/ball.xml new file mode 100644 index 0000000..bf55b28 --- /dev/null +++ b/app/src/main/res/drawable/ball.xml @@ -0,0 +1,6 @@ + + + diff --git a/app/src/main/res/drawable/baseline_chevron_right_24.xml b/app/src/main/res/drawable/baseline_chevron_right_24.xml new file mode 100644 index 0000000..e8bff86 --- /dev/null +++ b/app/src/main/res/drawable/baseline_chevron_right_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/baseline_close_16.xml b/app/src/main/res/drawable/baseline_close_16.xml new file mode 100644 index 0000000..74f00d1 --- /dev/null +++ b/app/src/main/res/drawable/baseline_close_16.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_list_30.xml b/app/src/main/res/drawable/ic_list_30.xml new file mode 100644 index 0000000..d8dcad4 --- /dev/null +++ b/app/src/main/res/drawable/ic_list_30.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_map_white_30.xml b/app/src/main/res/drawable/ic_map_white_30.xml new file mode 100644 index 0000000..e753026 --- /dev/null +++ b/app/src/main/res/drawable/ic_map_white_30.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/map_bus_position_icon.xml b/app/src/main/res/drawable/map_bus_position_icon.xml new file mode 100644 index 0000000..7cfc769 --- /dev/null +++ b/app/src/main/res/drawable/map_bus_position_icon.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/drawable/point_heading_icon.xml b/app/src/main/res/drawable/point_heading_icon.xml deleted file mode 100644 index 1b9fb58..0000000 --- a/app/src/main/res/drawable/point_heading_icon.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - diff --git a/app/src/main/res/layout/bus_info_window.xml b/app/src/main/res/layout/bus_info_window.xml index ce00e42..97b2e25 100644 --- a/app/src/main/res/layout/bus_info_window.xml +++ b/app/src/main/res/layout/bus_info_window.xml @@ -1,64 +1,86 @@ - + + app:layout_constraintLeft_toRightOf="@id/businfo_title" + + android:layout_alignParentTop="true" + app:layout_constraintHorizontal_bias="0.5" + + app:layout_constraintStart_toEndOf="@+id/businfo_title" + android:layout_marginTop="4dp" + android:layout_marginEnd="2dp" + android:layout_marginStart="6dp" + /> + + + app:layout_constraintRight_toRightOf="parent" + android:layout_below="@id/businfo_title" + android:text="BUCAGLIONE GIANGI" + android:gravity="center" + android:textAlignment="center" + android:layout_marginTop="2dp" + app:layout_constraintBottom_toTopOf="@+id/businfo_subdescription" + android:layout_marginLeft="4dp" android:layout_marginRight="4dp"/> - - + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintHorizontal_bias="0.5" + android:layout_marginStart="8dp" android:layout_marginEnd="8dp" android:layout_marginTop="2dp" + android:layout_marginBottom="3dp" + app:layout_constraintBottom_toBottomOf="parent"/> + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_lines_detail.xml b/app/src/main/res/layout/fragment_lines_detail.xml index 4e1e2f2..9690103 100644 --- a/app/src/main/res/layout/fragment_lines_detail.xml +++ b/app/src/main/res/layout/fragment_lines_detail.xml @@ -1,83 +1,124 @@ - + app:layout_constraintTop_toTopOf="parent" + android:layout_marginTop="8dp" android:gravity="center_horizontal|center_vertical" + app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" + android:layout_marginStart="8dp" android:layout_marginEnd="8dp"/> + app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/routeDescrTextView" + android:layout_marginTop="8dp" app:layout_constraintTop_toBottomOf="@+id/titleTextView" + android:layout_marginStart="4dp"/> + app:layout_constraintTop_toTopOf="@+id/patternsSpinner" + app:layout_constraintBottom_toBottomOf="@+id/patternsSpinner" + /> - + />--> + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_lines_grid.xml b/app/src/main/res/layout/fragment_lines_grid.xml new file mode 100644 index 0000000..25e0144 --- /dev/null +++ b/app/src/main/res/layout/fragment_lines_grid.xml @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_test_realtime_gtfs.xml b/app/src/main/res/layout/fragment_test_realtime_gtfs.xml index 5437404..7ae0d0b 100644 --- a/app/src/main/res/layout/fragment_test_realtime_gtfs.xml +++ b/app/src/main/res/layout/fragment_test_realtime_gtfs.xml @@ -1,27 +1,43 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context=".fragments.TestRealtimeGtfsFragment"> + + android:layout_margin="20dp" app:layout_constraintStart_toStartOf="parent" + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintBottom_toBottomOf="parent" + />