diff --git a/app/build.gradle b/app/build.gradle
index 391bf48..a688f75 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,134 +1,138 @@
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 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
}
+ testOptions {
+ unitTests.returnDefaultValues = true
+ }
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.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.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.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"
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.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.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/main/java/it/reyboz/bustorino/data/FavoritesLiveData.java b/app/src/main/java/it/reyboz/bustorino/data/FavoritesLiveData.java
index 4f9d2fa..57ad63d 100644
--- a/app/src/main/java/it/reyboz/bustorino/data/FavoritesLiveData.java
+++ b/app/src/main/java/it/reyboz/bustorino/data/FavoritesLiveData.java
@@ -1,222 +1,226 @@
/*
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.data;
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.os.Handler;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import it.reyboz.bustorino.BuildConfig;
import it.reyboz.bustorino.backend.Stop;
public class FavoritesLiveData extends LiveData> implements CustomAsyncQueryHandler.AsyncQueryListener {
private static final String TAG = "BusTO-FavoritesLiveData";
private final boolean notifyChangesDescendants;
@NonNull
private final Context mContext;
@NonNull
private final FavoritesLiveData.ForceLoadContentObserver mObserver;
private final CustomAsyncQueryHandler queryHandler;
private final Uri FAVORITES_URI = AppDataProvider.getUriBuilderToComplete().appendPath(
AppDataProvider.FAVORITES).build();
private final int FAV_TOKEN = 23, STOPS_TOKEN_BASE=220;
@Nullable
private List stopsFromFavorites, stopsDone;
private boolean isQueryRunning = false;
private int stopNeededCount = 0;
public FavoritesLiveData(@NonNull Context context, boolean notifyDescendantsChanges) {
super();
mContext = context.getApplicationContext();
mObserver = new FavoritesLiveData.ForceLoadContentObserver();
notifyChangesDescendants = notifyDescendantsChanges;
queryHandler = new CustomAsyncQueryHandler(mContext.getContentResolver(),this);
}
private void loadData() {
loadData(false);
}
private static Uri.Builder getStopsBuilder(){
return AppDataProvider.getUriBuilderToComplete().appendPath("stop");
}
private void loadData(boolean forceQuery) {
Log.d(TAG, "loadData() force: "+forceQuery);
if (!forceQuery){
if (getValue()!= null){
//Data already loaded
Log.d(TAG, "Data already loaded");
return;
}
}
if (isQueryRunning){
//we are waiting for data, we will get an update soon
Log.d(TAG, "Query is running, abort");
return;
}
isQueryRunning = true;
queryHandler.startQuery(FAV_TOKEN,null, FAVORITES_URI, UserDB.getFavoritesColumnNamesAsArray, null, null, null);
}
+ public void forceReload(){
+ loadData(true);
+ }
+
@Override
protected void onActive() {
//Log.d(TAG, "onActive()");
loadData(true);
}
/**
* Clear the data for the cursor
*/
public void onClear(){
ContentResolver resolver = mContext.getContentResolver();
resolver.unregisterContentObserver(mObserver);
}
@Override
protected void setValue(List stops) {
//Log.d("BusTO-FavoritesLiveData","Setting the new values for the stops, have "+
// stops.size()+" stops");
ContentResolver resolver = mContext.getContentResolver();
resolver.registerContentObserver(FAVORITES_URI, notifyChangesDescendants,mObserver);
super.setValue(stops);
}
@Override
public void onQueryComplete(int token, Object cookie, Cursor cursor) {
if (cursor == null){
//Nothing to do
Log.e(TAG, "Null cursor for token "+token);
return;
}
if (token == FAV_TOKEN) {
stopsFromFavorites = UserDB.getFavoritesFromCursor(cursor, UserDB.getFavoritesColumnNamesAsArray);
cursor.close();
//reset counters
stopNeededCount = stopsFromFavorites.size();
stopsDone = new ArrayList<>();
if(stopsFromFavorites.size() == 0){
//we don't need to call the other query
setValue(stopsDone);
isQueryRunning = false;
} else
for (int i = 0; i < stopsFromFavorites.size(); i++) {
Stop s = stopsFromFavorites.get(i);
queryHandler.startQuery(STOPS_TOKEN_BASE + i, null,
getStopsBuilder().appendPath(s.ID).build(),
NextGenDB.QUERY_COLUMN_stops_all, null, null, null);
}
} else if(token >= STOPS_TOKEN_BASE){
final int index = token - STOPS_TOKEN_BASE;
assert stopsFromFavorites != null;
Stop stopUpdate = stopsFromFavorites.get(index);
Stop finalStop;
List result = NextGenDB.getStopsFromCursorAllFields(cursor);
cursor.close();
if (result.size() < 1){
// stop is not in the DB
finalStop = stopUpdate;
} else {
finalStop = result.get(0);
if (BuildConfig.DEBUG && !(finalStop.ID.equals(stopUpdate.ID))) {
throw new AssertionError("Assertion failed");
}
finalStop.setStopUserName(stopUpdate.getStopUserName());
}
if (stopsDone!=null)
stopsDone.add(finalStop);
stopNeededCount--;
if (stopNeededCount == 0) {
// we have finished the queries
isQueryRunning = false;
Collections.sort(stopsDone);
setValue(stopsDone);
}
}
}
/**
* Content Observer that forces reload of cursor when data changes
* On different thread (new Handler)
*/
public final class ForceLoadContentObserver
extends ContentObserver {
public ForceLoadContentObserver() {
super(new Handler());
}
@Override
public boolean deliverSelfNotifications() {
return true;
}
@Override
public void onChange(boolean selfChange) {
Log.d(TAG, "ForceLoadContentObserver.onChange()");
loadData(true);
}
}
}
diff --git a/app/src/main/java/it/reyboz/bustorino/data/FavoritesViewModel.java b/app/src/main/java/it/reyboz/bustorino/data/FavoritesViewModel.java
index 9b78f0a..e71f98f 100644
--- a/app/src/main/java/it/reyboz/bustorino/data/FavoritesViewModel.java
+++ b/app/src/main/java/it/reyboz/bustorino/data/FavoritesViewModel.java
@@ -1,36 +1,35 @@
package it.reyboz.bustorino.data;
import android.app.Application;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import java.util.List;
import it.reyboz.bustorino.backend.Stop;
public class FavoritesViewModel extends AndroidViewModel {
FavoritesLiveData favoritesLiveData;
- final Context appContext;
public FavoritesViewModel(@NonNull Application application) {
super(application);
- appContext = application.getApplicationContext();
+ //appContext = application.getApplicationContext();
}
@Override
protected void onCleared() {
favoritesLiveData.onClear();
super.onCleared();
}
- public LiveData> getFavorites(){
+ public FavoritesLiveData getFavorites(){
if (favoritesLiveData==null){
- favoritesLiveData= new FavoritesLiveData(appContext, true);
+ favoritesLiveData= new FavoritesLiveData(getApplication(), true);
}
return favoritesLiveData;
}
}
diff --git a/app/src/main/java/it/reyboz/bustorino/data/NextGenDB.java b/app/src/main/java/it/reyboz/bustorino/data/NextGenDB.java
index ad6ebb6..fb07af4 100644
--- a/app/src/main/java/it/reyboz/bustorino/data/NextGenDB.java
+++ b/app/src/main/java/it/reyboz/bustorino/data/NextGenDB.java
@@ -1,491 +1,497 @@
/*
BusTO (middleware)
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.data;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteConstraintException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.provider.BaseColumns;
import android.util.Log;
import it.reyboz.bustorino.backend.Palina;
import it.reyboz.bustorino.backend.Route;
import it.reyboz.bustorino.backend.Stop;
import java.util.*;
import java.util.stream.Collectors;
import static it.reyboz.bustorino.data.NextGenDB.Contract.*;
public class NextGenDB extends SQLiteOpenHelper{
public static final String DATABASE_NAME = "bustodatabase.db";
public static final int DATABASE_VERSION = 3;
public static final String DEBUG_TAG = "NextGenDB-BusTO";
//NO Singleton instance
//private static volatile NextGenDB instance = null;
//Some generating Strings
private static final String SQL_CREATE_LINES_TABLE="CREATE TABLE "+Contract.LinesTable.TABLE_NAME+" ("+
Contract.LinesTable._ID +" INTEGER PRIMARY KEY AUTOINCREMENT, "+ Contract.LinesTable.COLUMN_NAME +" TEXT, "+
Contract.LinesTable.COLUMN_DESCRIPTION +" TEXT, "+Contract.LinesTable.COLUMN_TYPE +" TEXT, "+
"UNIQUE ("+LinesTable.COLUMN_NAME+","+LinesTable.COLUMN_DESCRIPTION+","+LinesTable.COLUMN_TYPE+" ) "+" )";
private static final String SQL_CREATE_BRANCH_TABLE="CREATE TABLE "+Contract.BranchesTable.TABLE_NAME+" ("+
Contract.BranchesTable._ID +" INTEGER, "+ Contract.BranchesTable.COL_BRANCHID +" INTEGER PRIMARY KEY, "+
Contract.BranchesTable.COL_LINE +" INTEGER, "+ Contract.BranchesTable.COL_DESCRIPTION +" TEXT, "+
Contract.BranchesTable.COL_DIRECTION+" TEXT, "+ Contract.BranchesTable.COL_TYPE +" INTEGER, "+
//SERVICE DAYS: 0 => FERIALE,1=>FESTIVO,-1=>UNKNOWN,add others if necessary
Contract.BranchesTable.COL_FESTIVO +" INTEGER, "+
//DAYS COLUMNS. IT'S SO TEDIOUS I TRIED TO KILL MYSELF
BranchesTable.COL_LUN+" INTEGER, "+BranchesTable.COL_MAR+" INTEGER, "+BranchesTable.COL_MER+" INTEGER, "+BranchesTable.COL_GIO+" INTEGER, "+
BranchesTable.COL_VEN+" INTEGER, "+ BranchesTable.COL_SAB+" INTEGER, "+BranchesTable.COL_DOM+" INTEGER, "+
"FOREIGN KEY("+ Contract.BranchesTable.COL_LINE +") references "+ Contract.LinesTable.TABLE_NAME+"("+ Contract.LinesTable._ID+") "
+")";
private static final String SQL_CREATE_CONNECTIONS_TABLE="CREATE TABLE "+Contract.ConnectionsTable.TABLE_NAME+" ("+
Contract.ConnectionsTable.COLUMN_BRANCH+" INTEGER, "+ Contract.ConnectionsTable.COLUMN_STOP_ID+" TEXT, "+
Contract.ConnectionsTable.COLUMN_ORDER+" INTEGER, "+
"PRIMARY KEY ("+ Contract.ConnectionsTable.COLUMN_BRANCH+","+ Contract.ConnectionsTable.COLUMN_STOP_ID + "), "+
"FOREIGN KEY("+ Contract.ConnectionsTable.COLUMN_BRANCH+") references "+ Contract.BranchesTable.TABLE_NAME+"("+ Contract.BranchesTable.COL_BRANCHID +"), "+
"FOREIGN KEY("+ Contract.ConnectionsTable.COLUMN_STOP_ID+") references "+ Contract.StopsTable.TABLE_NAME+"("+ Contract.StopsTable.COL_ID +") "
+")";
private static final String SQL_CREATE_STOPS_TABLE="CREATE TABLE "+Contract.StopsTable.TABLE_NAME+" ("+
Contract.StopsTable.COL_ID+" TEXT PRIMARY KEY, "+ Contract.StopsTable.COL_TYPE+" INTEGER, "+Contract.StopsTable.COL_LAT+" REAL NOT NULL, "+
Contract.StopsTable.COL_LONG+" REAL NOT NULL, "+ Contract.StopsTable.COL_NAME+" TEXT NOT NULL, "+
StopsTable.COL_GTFS_ID+" TEXT, "+
Contract.StopsTable.COL_LOCATION+" TEXT, "+Contract.StopsTable.COL_PLACE+" TEXT, "+
Contract.StopsTable.COL_LINES_STOPPING +" TEXT )";
private static final String SQL_CREATE_STOPS_TABLE_TO_COMPLETE = " ("+
Contract.StopsTable.COL_ID+" TEXT PRIMARY KEY, "+ Contract.StopsTable.COL_TYPE+" INTEGER, "+Contract.StopsTable.COL_LAT+" REAL NOT NULL, "+
Contract.StopsTable.COL_LONG+" REAL NOT NULL, "+ Contract.StopsTable.COL_NAME+" TEXT NOT NULL, "+
Contract.StopsTable.COL_LOCATION+" TEXT, "+Contract.StopsTable.COL_PLACE+" TEXT, "+
Contract.StopsTable.COL_LINES_STOPPING +" TEXT )";
public static final String[] QUERY_COLUMN_stops_all = {
StopsTable.COL_ID, StopsTable.COL_NAME, StopsTable.COL_GTFS_ID, StopsTable.COL_LOCATION,
StopsTable.COL_TYPE, StopsTable.COL_LAT, StopsTable.COL_LONG, StopsTable.COL_LINES_STOPPING};
public static final String QUERY_WHERE_LAT_AND_LNG_IN_RANGE = StopsTable.COL_LAT + " >= ? AND " +
StopsTable.COL_LAT + " <= ? AND "+ StopsTable.COL_LONG +
" >= ? AND "+ StopsTable.COL_LONG + " <= ?";
public static final String QUERY_FROM_GTFS_ID_IN_TO_COMPLETE= StopsTable.COL_GTFS_ID +" IN ";
public static String QUERY_WHERE_ID = StopsTable.COL_ID+" = ?";
private final Context appContext;
private static NextGenDB INSTANCE;
private NextGenDB(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
appContext = context.getApplicationContext();
}
public static NextGenDB getInstance(Context context) {
if (INSTANCE == null){
INSTANCE = new NextGenDB(context);
}
return INSTANCE;
}
@Override
public void onCreate(SQLiteDatabase db) {
Log.d("BusTO-AppDB","Lines creating database:\n"+SQL_CREATE_LINES_TABLE+"\n"+
SQL_CREATE_STOPS_TABLE+"\n"+SQL_CREATE_BRANCH_TABLE+"\n"+SQL_CREATE_CONNECTIONS_TABLE);
db.execSQL(SQL_CREATE_LINES_TABLE);
db.execSQL(SQL_CREATE_STOPS_TABLE);
//tables with constraints
db.execSQL(SQL_CREATE_BRANCH_TABLE);
db.execSQL(SQL_CREATE_CONNECTIONS_TABLE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if(oldVersion<2 && newVersion == 2){
//DROP ALL TABLES
db.execSQL("DROP TABLE "+ConnectionsTable.TABLE_NAME);
db.execSQL("DROP TABLE "+BranchesTable.TABLE_NAME);
db.execSQL("DROP TABLE "+LinesTable.TABLE_NAME);
db.execSQL("DROP TABLE "+ StopsTable.TABLE_NAME);
//RECREATE THE TABLES WITH THE NEW SCHEMA
db.execSQL(SQL_CREATE_LINES_TABLE);
db.execSQL(SQL_CREATE_STOPS_TABLE);
//tables with constraints
db.execSQL(SQL_CREATE_BRANCH_TABLE);
db.execSQL(SQL_CREATE_CONNECTIONS_TABLE);
DatabaseUpdate.requestDBUpdateWithWork(appContext, true, true);
}
if(oldVersion < 3 && newVersion == 3){
Log.d("BusTO-Database", "Running upgrades for version 3");
//add the new column
db.execSQL("ALTER TABLE "+StopsTable.TABLE_NAME+
" ADD COLUMN "+StopsTable.COL_GTFS_ID+" TEXT ");
// DatabaseUpdate.requestDBUpdateWithWork(appContext, true);
}
}
@Override
public void onConfigure(SQLiteDatabase db) {
super.onConfigure(db);
db.execSQL("PRAGMA foreign_keys=ON");
}
public static String getSqlCreateStopsTable(String tableName){
return "CREATE TABLE "+tableName+" ("+
Contract.StopsTable.COL_ID+" TEXT PRIMARY KEY, "+ Contract.StopsTable.COL_TYPE+" INTEGER, "+Contract.StopsTable.COL_LAT+" REAL NOT NULL, "+
Contract.StopsTable.COL_LONG+" REAL NOT NULL, "+ Contract.StopsTable.COL_NAME+" TEXT NOT NULL, "+
Contract.StopsTable.COL_LOCATION+" TEXT, "+Contract.StopsTable.COL_PLACE+" TEXT, "+
Contract.StopsTable.COL_LINES_STOPPING +" TEXT )";
}
/**
* Query some bus stops inside a map view
+ * @return stoplist, if empty it means that an error occurred
*
* You can obtain the coordinates from OSMDroid using something like this:
* BoundingBoxE6 bb = mMapView.getBoundingBox();
* double latFrom = bb.getLatSouthE6() / 1E6;
* double latTo = bb.getLatNorthE6() / 1E6;
* double lngFrom = bb.getLonWestE6() / 1E6;
* double lngTo = bb.getLonEastE6() / 1E6;
*/
public synchronized ArrayList queryAllInsideMapView(double minLat, double maxLat, double minLng, double maxLng) {
ArrayList stops = new ArrayList<>();
SQLiteDatabase db = this.getReadableDatabase();
// coordinates must be strings in the where condition
String minLatRaw = String.valueOf(minLat);
String maxLatRaw = String.valueOf(maxLat);
String minLngRaw = String.valueOf(minLng);
String maxLngRaw = String.valueOf(maxLng);
if(db == null) {
return stops;
}
try {
final Cursor result = db.query(StopsTable.TABLE_NAME, QUERY_COLUMN_stops_all, QUERY_WHERE_LAT_AND_LNG_IN_RANGE,
new String[] {minLatRaw, maxLatRaw, minLngRaw, maxLngRaw},
null, null, null);
stops = getStopsFromCursorAllFields(result);
result.close();
} catch(SQLiteException e) {
Log.e(DEBUG_TAG, "SQLiteException occurred");
e.printStackTrace();
return stops;
- }finally {
+ }catch (Exception e){
+ Log.e(DEBUG_TAG, "Exception occurred when getting stops");
+ e.printStackTrace();
+ return stops;
+ }
+ finally {
db.close();
}
return stops;
}
/**
* Query stops in the database having these IDs
* REMEMBER TO CLOSE THE DB CONNECTION AFTERWARDS
* @param bustoDB readable database instance
* @param gtfsIDs gtfs IDs to query
* @return list of stops
*/
public static synchronized ArrayList queryAllStopsWithGtfsIDs(SQLiteDatabase bustoDB, List gtfsIDs){
final ArrayList stops = new ArrayList<>();
if(bustoDB == null){
Log.e(DEBUG_TAG, "Asked query for IDs but database is null");
return stops;
} else if (gtfsIDs == null || gtfsIDs.isEmpty()) {
return stops;
}
final StringBuilder builder = new StringBuilder(QUERY_FROM_GTFS_ID_IN_TO_COMPLETE);
boolean first = true;
builder.append(" ( ");
for(int i=0; i< gtfsIDs.size(); i++){
if(first){
first = false;
} else{
builder.append(", ");
}
builder.append("?");//.append("\"").append(id).append("\"");
}
builder.append(") ");
final String whereClause = builder.toString();
final String[] idsQuery = gtfsIDs.toArray(new String[0]);
try {
final Cursor result = bustoDB.query(StopsTable.TABLE_NAME,QUERY_COLUMN_stops_all, whereClause,
idsQuery,
null, null, null);
stops.addAll(getStopsFromCursorAllFields(result));
result.close();
} catch(SQLiteException e) {
Log.e(DEBUG_TAG, "SQLiteException occurred");
e.printStackTrace();
}
return stops;
}
/**
* Get the list of stop in the query, with all the possible fields {NextGenDB.QUERY_COLUMN_stops_all}
* @param result cursor from query
* @return an Array of the stops found in the query
*/
public static ArrayList getStopsFromCursorAllFields(Cursor result){
final int colID = result.getColumnIndex(StopsTable.COL_ID);
final int colName = result.getColumnIndex(StopsTable.COL_NAME);
final int colLocation = result.getColumnIndex(StopsTable.COL_LOCATION);
final int colType = result.getColumnIndex(StopsTable.COL_TYPE);
final int colLat = result.getColumnIndex(StopsTable.COL_LAT);
final int colGtfsID = result.getColumnIndex(StopsTable.COL_GTFS_ID);
final int colLon = result.getColumnIndex(StopsTable.COL_LONG);
final int colLines = result.getColumnIndex(StopsTable.COL_LINES_STOPPING);
int count = result.getCount();
ArrayList stops = new ArrayList<>(count);
int i = 0;
while(result.moveToNext()) {
final String stopID = result.getString(colID).trim();
Route.Type type;
//if(result.getString(colType) == null) type = Route.Type.BUS;
//else type = Route.getTypeFromSymbol(result.getString(colType));
//if(result.getInt(colType) == null) type = Route.Type.BUS;
try{
type = Route.Type.fromCode(result.getInt(colType));
} catch (Exception e){
type = Route.Type.BUS;
}
String lines = result.getString(colLines).trim();
String locationSometimesEmpty = result.getString(colLocation);
if (locationSometimesEmpty!= null && locationSometimesEmpty.length() <= 0) {
locationSometimesEmpty = null;
}
stops.add(new Stop(stopID, result.getString(colName), null,
locationSometimesEmpty, type, splitLinesString(lines),
result.getDouble(colLat), result.getDouble(colLon),
result.getString(colGtfsID))
);
}
return stops;
}
public static synchronized int writeLinesStoppingHere(SQLiteDatabase db, HashMap> linesStoppingBy){
int rowsUpdated = 0;
for (String stopGtfsID : linesStoppingBy.keySet()){
if (linesStoppingBy.get(stopGtfsID)==null) continue;
if (linesStoppingBy.get(stopGtfsID).isEmpty()) continue;
ArrayList ll = new ArrayList<>(linesStoppingBy.get(stopGtfsID));
String stringForStops = Palina.buildRoutesStringFromNames(ll);
ContentValues cv = new ContentValues();
cv.put(StopsTable.COL_LINES_STOPPING, stringForStops);
// Which row to update, based on the title
String selection = StopsTable.COL_GTFS_ID + " LIKE ?";
String[] selectionArgs = { stopGtfsID };
int count = db.update(
StopsTable.TABLE_NAME,
cv,
selection,
selectionArgs);
if (count > 1){
Log.e(DEBUG_TAG, "Updated the linesStoppingBy for more than one stop");
}
rowsUpdated += count;
}
return rowsUpdated;
}
/*
static ArrayList createStopListFromCursor(Cursor data){
ArrayList stopList = new ArrayList<>();
final int col_id = data.getColumnIndex(StopsTable.COL_ID);
final int latInd = data.getColumnIndex(StopsTable.COL_LAT);
final int lonInd = data.getColumnIndex(StopsTable.COL_LONG);
final int nameindex = data.getColumnIndex(StopsTable.COL_NAME);
final int typeIndex = data.getColumnIndex(StopsTable.COL_TYPE);
final int linesIndex = data.getColumnIndex(StopsTable.COL_LINES_STOPPING);
data.moveToFirst();
for(int i=0; i stops){
return 0;
}
public static List splitLinesString(String linesStr){
return Arrays.asList(linesStr.split("\\s*,\\s*"));
}
public static final class Contract{
//Ok, I get it, it really is a pain in the ass..
// But it's the only way to have maintainable code
public interface DataTables {
String getTableName();
String[] getFields();
}
public static final class LinesTable implements BaseColumns, DataTables {
//The fields
public static final String TABLE_NAME = "lines";
public static final String COLUMN_NAME = "line_name";
public static final String COLUMN_DESCRIPTION = "line_description";
public static final String COLUMN_TYPE = "line_bacino";
@Override
public String getTableName() {
return TABLE_NAME;
}
@Override
public String[] getFields() {
return new String[]{COLUMN_NAME,COLUMN_DESCRIPTION,COLUMN_TYPE};
}
}
public static final class BranchesTable implements BaseColumns, DataTables {
public static final String TABLE_NAME = "branches";
public static final String COL_BRANCHID = "branchid";
public static final String COL_LINE = "lineid";
public static final String COL_DESCRIPTION = "branch_description";
public static final String COL_DIRECTION = "branch_direzione";
public static final String COL_FESTIVO = "branch_festivo";
public static final String COL_TYPE = "branch_type";
public static final String COL_LUN="runs_lun";
public static final String COL_MAR="runs_mar";
public static final String COL_MER="runs_mer";
public static final String COL_GIO="runs_gio";
public static final String COL_VEN="runs_ven";
public static final String COL_SAB="runs_sab";
public static final String COL_DOM="runs_dom";
@Override
public String getTableName() {
return TABLE_NAME;
}
@Override
public String[] getFields() {
return new String[]{COL_BRANCHID,COL_LINE,COL_DESCRIPTION,
COL_DIRECTION,COL_FESTIVO,COL_TYPE,
COL_LUN,COL_MAR,COL_MER,COL_GIO,COL_VEN,COL_SAB,COL_DOM
};
}
}
public static final class ConnectionsTable implements DataTables {
public static final String TABLE_NAME = "connections";
public static final String COLUMN_BRANCH = "branchid";
public static final String COLUMN_STOP_ID = "stopid";
public static final String COLUMN_ORDER = "ordine";
@Override
public String getTableName() {
return TABLE_NAME;
}
@Override
public String[] getFields() {
return new String[]{COLUMN_STOP_ID,COLUMN_BRANCH,COLUMN_ORDER};
}
}
public static final class StopsTable implements DataTables {
public static final String TABLE_NAME = "stops";
public static final String COL_ID = "stopid"; //integer
public static final String COL_TYPE = "stop_type";
public static final String COL_NAME = "stop_name";
public static final String COL_GTFS_ID = "gtfs_id";
public static final String COL_LAT = "stop_latitude";
public static final String COL_LONG = "stop_longitude";
public static final String COL_LOCATION = "stop_location";
public static final String COL_PLACE = "stop_placeName";
public static final String COL_LINES_STOPPING = "stop_lines";
@Override
public String getTableName() {
return TABLE_NAME;
}
@Override
public String[] getFields() {
return new String[]{COL_ID,COL_TYPE,COL_NAME,COL_GTFS_ID,COL_LAT,COL_LONG,COL_LOCATION,COL_PLACE,COL_LINES_STOPPING};
}
}
}
public static final class DBUpdatingException extends Exception{
public DBUpdatingException(String message) {
super(message);
}
}
}
diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/FavoritesFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/FavoritesFragment.java
index 31364a2..900e401 100644
--- a/app/src/main/java/it/reyboz/bustorino/fragments/FavoritesFragment.java
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/FavoritesFragment.java
@@ -1,306 +1,333 @@
/*
BusTO - Fragments 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.fragments;
import android.app.AlertDialog;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
+import androidx.work.WorkInfo;
import it.reyboz.bustorino.*;
import it.reyboz.bustorino.adapters.StopAdapterListener;
import it.reyboz.bustorino.adapters.StopRecyclerAdapter;
import it.reyboz.bustorino.backend.Stop;
+import it.reyboz.bustorino.data.DatabaseUpdate;
import it.reyboz.bustorino.data.FavoritesViewModel;
import it.reyboz.bustorino.middleware.AsyncStopFavoriteAction;
public class FavoritesFragment extends ScreenBaseFragment {
private RecyclerView favoriteRecyclerView;
private EditText busStopNameText;
private TextView favoriteTipTextView;
private ImageView angeryBusImageView;
+ private boolean dbUpdateRunning = false;
+ private FavoritesViewModel model;
+
+
@Nullable
private CommonFragmentListener mListener;
public static final String FRAGMENT_TAG = "BusTOFavFragment";
+ private final static String DEBUG_TAG = FRAGMENT_TAG;
+
private final StopAdapterListener adapterListener = new StopAdapterListener() {
@Override
public void onTappedStop(Stop stop) {
mListener.requestArrivalsForStopID(stop.ID);
}
@Override
public boolean onLongPressOnStop(Stop stop) {
Log.d("BusTO-FavoritesFrag", "LongPressOnStop");
return true;
}
};
public static FavoritesFragment newInstance() {
FavoritesFragment fragment = new FavoritesFragment();
Bundle args = new Bundle();
//args.putString(ARG_PARAM1, param1);
//args.putString(ARG_PARAM2, param2);
fragment.setArguments(args);
return fragment;
}
public FavoritesFragment(){
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
//do nothing
}
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_favorites, container, false);
favoriteRecyclerView = root.findViewById(R.id.favoritesRecyclerView);
//favoriteListView = root.findViewById(R.id.favoriteListView);
/*favoriteRecyclerView.setOn((parent, view, position, id) -> {
/*
* Casting because of Javamerda
* @url http://stackoverflow.com/questions/30549485/androids-list-view-parameterized-type-in-adapterview-onitemclicklistener
*/
/*
Stop busStop = (Stop) parent.getItemAtPosition(position);
if(mListener!=null){
mListener.requestArrivalsForStopID(busStop.ID);
}
});
*/
LinearLayoutManager llManager = new LinearLayoutManager(getContext());
llManager.setOrientation(LinearLayoutManager.VERTICAL);
favoriteRecyclerView.setLayoutManager(llManager);
DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(favoriteRecyclerView.getContext(),
llManager.getOrientation());
favoriteRecyclerView.addItemDecoration(dividerItemDecoration);
angeryBusImageView = root.findViewById(R.id.angeryBusImageView);
favoriteTipTextView = root.findViewById(R.id.favoriteTipTextView);
//register for the context menu
registerForContextMenu(favoriteRecyclerView);
- FavoritesViewModel model = new ViewModelProvider(this).get(FavoritesViewModel.class);
+
model.getFavorites().observe(getViewLifecycleOwner(), this::showStops);
+ // watch the DB update
+ DatabaseUpdate.watchUpdateWorkStatus(getContext(), this, workInfos -> {
+ if(workInfos.isEmpty()) return;
+
+ WorkInfo wi = workInfos.get(0);
+ if(wi.getState() == WorkInfo.State.RUNNING){
+ dbUpdateRunning = true;
+ } else {
+ //force reload if it was previously running
+ if(model!=null && dbUpdateRunning) {
+ Log.d(DEBUG_TAG,"DB Finished updating, reload favorites");
+ model.getFavorites().forceReload();
+ }
+ dbUpdateRunning = false;
+ }
+ });
+
showStops(new ArrayList<>());
return root;
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (context instanceof CommonFragmentListener) {
mListener = (CommonFragmentListener) context;
} else {
throw new RuntimeException(context
+ " must implement CommonFragmentListener");
}
+ model = new ViewModelProvider(this).get(FavoritesViewModel.class);
}
@Override
public void onDetach() {
super.onDetach();
mListener = null;
}
/*
This method is apparently NOT CALLED ANYMORE
Called on Android 6
*/
@Override
public void onCreateContextMenu(@NonNull ContextMenu menu, @NonNull View v,
ContextMenu.ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
Log.d("Favorites Fragment", "Creating context menu ");
if (v.getId() == R.id.favoritesRecyclerView) {
// if we aren't attached to activity, return null
if (getActivity()==null) return;
MenuInflater inflater = getActivity().getMenuInflater();
inflater.inflate(R.menu.menu_favourites_entry, menu);
}
}
@Override
public void onResume() {
super.onResume();
if (mListener!=null) mListener.readyGUIfor(FragmentKind.FAVORITES);
}
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item
.getMenuInfo();
if(!(favoriteRecyclerView.getAdapter() instanceof StopRecyclerAdapter))
return false;
StopRecyclerAdapter adapter = (StopRecyclerAdapter) favoriteRecyclerView.getAdapter();
Stop busStop = adapter.getStops().get(adapter.getPosition());
switch (item.getItemId()) {
case R.id.action_favourite_entry_delete:
if (getContext()!=null)
new AsyncStopFavoriteAction(getContext().getApplicationContext(), AsyncStopFavoriteAction.Action.REMOVE,
result -> {
}).execute(busStop);
return true;
case R.id.action_rename_bus_stop_username:
showBusStopUsernameInputDialog(busStop);
return true;
case R.id.action_view_on_map:
if (busStop.getLatitude() == null | busStop.getLongitude() == null |
mListener==null
) {
Toast.makeText(getContext(), R.string.cannot_show_on_map_no_position, Toast.LENGTH_SHORT).show();
return true;
}
//GeoPoint point = new GeoPoint(busStop.getLatitude(), busStop.getLongitude());
mListener.showMapCenteredOnStop(busStop);
return true;
default:
return super.onContextItemSelected(item);
}
}
@Nullable
@Override
public View getBaseViewForSnackBar() {
- return null;
+ return favoriteRecyclerView;
}
void showStops(List busStops){
// If no data is found show a friendly message
if(BuildConfig.DEBUG)
Log.d("BusTO - Favorites", "We have "+busStops.size()+" favorites in the list");
if (busStops.size() == 0) {
favoriteRecyclerView.setVisibility(View.INVISIBLE);
// TextView favoriteTipTextView = (TextView) findViewById(R.id.favoriteTipTextView);
//assert favoriteTipTextView != null;
favoriteTipTextView.setVisibility(View.VISIBLE);
//ImageView angeryBusImageView = (ImageView) findViewById(R.id.angeryBusImageView);
angeryBusImageView.setVisibility(View.VISIBLE);
} else {
favoriteRecyclerView.setVisibility(View.VISIBLE);
favoriteTipTextView.setVisibility(View.INVISIBLE);
angeryBusImageView.setVisibility(View.INVISIBLE);
}
/* There's a nice method called notifyDataSetChanged() to avoid building the ListView
* all over again. This method exists in a billion answers on Stack Overflow, but
* it's nowhere to be seen around here, Android Studio can't find it no matter what.
* Anyway, it only works from Android 2.3 onward (which is why it refuses to appear, I
* guess) and requires to modify the list with .add() and .clear() and some other
* methods, so to update a single stop we need to completely rebuild the list for no
* reason. It would probably end up as "slow" as throwing away the old ListView and
* redrwaing everything.
*/
// Show results
favoriteRecyclerView.setAdapter(new StopRecyclerAdapter(busStops,adapterListener, StopRecyclerAdapter.Use.FAVORITES));
}
public void showBusStopUsernameInputDialog(final Stop busStop) {
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
LayoutInflater inflater = this.getLayoutInflater();
View renameDialogLayout = inflater.inflate(R.layout.rename_dialog, null);
busStopNameText = (EditText) renameDialogLayout.findViewById(R.id.rename_dialog_bus_stop_name);
busStopNameText.setText(busStop.getStopDisplayName());
busStopNameText.setHint(busStop.getStopDefaultName());
builder.setTitle(getString(R.string.dialog_rename_bus_stop_username_title));
builder.setView(renameDialogLayout);
builder.setPositiveButton(getString(android.R.string.ok), (dialog, which) -> {
String busStopUsername = busStopNameText.getText().toString();
String oldUserName = busStop.getStopUserName();
// changed to none
if(busStopUsername.length() == 0) {
// unless it was already empty, set new
if(oldUserName != null) {
busStop.setStopUserName(null);
}
} else { // changed to something
// something different?
if(!busStopUsername.equals(oldUserName)) {
busStop.setStopUserName(busStopUsername);
}
}
launchUpdate(busStop);
});
builder.setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.cancel());
builder.setNeutralButton(R.string.dialog_rename_bus_stop_username_reset_button, (dialog, which) -> {
// delete user name from database
busStop.setStopUserName(null);
launchUpdate(busStop);
});
builder.show();
}
private void launchUpdate(Stop busStop){
if (getContext()!=null)
new AsyncStopFavoriteAction(getContext().getApplicationContext(), AsyncStopFavoriteAction.Action.UPDATE,
result -> {
//Toast.makeText(getApplicationContext(), R.string.tip_add_favorite, Toast.LENGTH_SHORT).show();
}).execute(busStop);
}
}
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 5b02a66..52959c5 100644
--- a/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java
@@ -1,795 +1,795 @@
/*
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.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.content.res.ResourcesCompat;
import androidx.lifecycle.ViewModelProvider;
import androidx.preference.PreferenceManager;
import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate;
import it.reyboz.bustorino.backend.mato.MQTTMatoClient;
import it.reyboz.bustorino.backend.utils;
import it.reyboz.bustorino.data.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 it.reyboz.bustorino.viewmodels.StopsMapViewModel;
import org.osmdroid.api.IGeoPoint;
import org.osmdroid.api.IMapController;
import org.osmdroid.config.Configuration;
import org.osmdroid.events.DelayedMapListener;
import org.osmdroid.events.MapListener;
import org.osmdroid.events.ScrollEvent;
import org.osmdroid.events.ZoomEvent;
import org.osmdroid.tileprovider.tilesource.TileSourceFactory;
import org.osmdroid.util.BoundingBox;
import org.osmdroid.util.GeoPoint;
import org.osmdroid.views.MapView;
import org.osmdroid.views.overlay.FolderOverlay;
import org.osmdroid.views.overlay.Marker;
import org.osmdroid.views.overlay.infowindow.InfoWindow;
import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider;
-import java.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 MAP_CURRENT_ZOOM_KEY = "map-current-zoom";
private static final String MAP_CENTER_LAT_KEY = "map-center-lat";
private static final String MAP_CENTER_LON_KEY = "map-center-lon";
private static final String FOLLOWING_LOCAT_KEY ="following";
public static final String BUNDLE_LATIT = "lat";
public static final String BUNDLE_LONGIT = "lon";
public static final String BUNDLE_NAME = "name";
public static final String BUNDLE_ID = "ID";
public static final String BUNDLE_ROUTES_STOPPING = "routesStopping";
public static final String FRAGMENT_TAG="BusTOMapFragment";
private static final double DEFAULT_CENTER_LAT = 45.0708;
private static final double DEFAULT_CENTER_LON = 7.6858;
private static final double POSITION_FOUND_ZOOM = 18.3;
public static final double NO_POSITION_ZOOM = 17.1;
private static final String DEBUG_TAG=FRAGMENT_TAG;
protected FragmentListenerMain listenerMain;
private HashSet shownStops = null;
private MapView map = null;
public Context ctx;
private LocationOverlay mLocationOverlay = null;
private FolderOverlay stopsFolderOverlay = null;
private Bundle savedMapState = null;
protected ImageButton btCenterMap;
protected ImageButton btFollowMe;
+
+ protected CoordinatorLayout coordLayout;
private boolean hasMapStartFinished = false;
private boolean followingLocation = false;
//the ViewModel from which we get the stop to display in the map
private StopsMapViewModel stopsViewModel;
//private GTFSPositionsViewModel gtfsPosViewModel; //= new ViewModelProvider(this).get(MapViewModel.class);
private 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);
+ View root = inflater.inflate(R.layout.fragment_map, container, false);
if (getContext() == null){
throw new IllegalStateException();
}
ctx = getContext().getApplicationContext();
Configuration.getInstance().load(ctx, PreferenceManager.getDefaultSharedPreferences(ctx));
map = root.findViewById(R.id.map);
map.setTileSource(TileSourceFactory.MAPNIK);
//map.setTilesScaledToDpi(true);
map.setFlingEnabled(true);
// add ability to zoom with 2 fingers
map.setMultiTouchControls(true);
btCenterMap = root.findViewById(R.id.icon_center_map);
btFollowMe = root.findViewById(R.id.icon_follow);
+ coordLayout = root.findViewById(R.id.coord_layout);
//setup FolderOverlay
stopsFolderOverlay = new FolderOverlay();
//setup Bus Markers Overlay
busPositionsOverlay = new FolderOverlay();
//reset shown bus updates
busPositionMarkersByTrip.clear();
tripMarkersAnimators.clear();
//set map not done
hasMapStartFinished = false;
//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);
//gtfsPosViewModel = new ViewModelProvider(this).get(GTFSPositionsViewModel.class);
//viewModel
ViewModelProvider provider = new ViewModelProvider(this);
positionsViewModel = provider.get(MQTTPositionsViewModel.class);
stopsViewModel = provider.get(StopsMapViewModel.class);
if (context instanceof FragmentListenerMain) {
listenerMain = (FragmentListenerMain) context;
} else {
throw new RuntimeException(context.toString()
+ " must implement FragmentListenerMain");
}
}
@Override
public void onDetach() {
super.onDetach();
listenerMain = null;
//stop animations
// setupOnAttached = true;
Log.w(DEBUG_TAG, "Fragment detached");
}
@Override
public void onPause() {
super.onPause();
Log.w(DEBUG_TAG, "On pause called mapfrag");
saveMapState();
for (ObjectAnimator animator : tripMarkersAnimators.values()) {
if(animator!=null && animator.isRunning()){
animator.cancel();
}
}
tripMarkersAnimators.clear();
positionsViewModel.stopPositionsListening();
}
/**
* 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(positionsViewModel !=null) {
//gtfsPosViewModel.requestUpdates();
positionsViewModel.requestPosUpdates(MQTTMatoClient.LINES_ALL);
//mapViewModel.testCascade();
positionsViewModel.getTripsGtfsIDsToQuery().observe(this, dat -> {
Log.i(DEBUG_TAG, "Have these trips IDs missing from the DB, to be queried: "+dat);
//gtfsPosViewModel.downloadTripsFromMato(dat);
MatoTripsDownloadWorker.Companion.downloadTripsFromMato(dat,getContext().getApplicationContext(),
"BusTO-MatoTripDownload");
});
}
}
@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) {
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(positionsViewModel !=null){
//should always be the case
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())
// gtfsPosViewModel.requestDelayedUpdates(4000);
});
} else {
Log.e(DEBUG_TAG, "PositionsViewModel is null");
}
if(stopsViewModel !=null){
stopsViewModel.getStopsInBoundingBox().observe(getViewLifecycleOwner(),
this::showStopsMarkers
);
} else Log.d(DEBUG_TAG, "Cannot observe new stops in map, stopsViewModel is null");
map.getOverlays().add(this.busPositionsOverlay);
//set map as started
hasMapStartFinished = true;
}
/**
* Start a request to load the stops that are in the current view
* from the database
*/
private void requestStopsToShow(){
// get the top, bottom, left and right screen's coordinate
BoundingBox bb = map.getBoundingBox();
Log.d(DEBUG_TAG, "Requesting stops in bounding box, stopViewModel is null "+(stopsViewModel==null));
if(stopsViewModel!=null){
stopsViewModel.requestStopsInBoundingBox(bb);
}
/*double latFrom = bb.getLatSouth();
double latTo = bb.getLatNorth();
double lngFrom = bb.getLonWest();
double lngTo = bb.getLonEast();
if (stopFetcher!= null && stopFetcher.getStatus()!= AsyncTask.Status.FINISHED)
stopFetcher.cancel(true);
stopFetcher = new AsyncStopFetcher(this);
stopFetcher.execute(
new AsyncStopFetcher.BoundingBoxLimit(lngFrom,lngTo,latFrom, latTo));
*/
}
private void updateBusMarker(final Marker marker, final LivePositionUpdate posUpdate, @Nullable boolean justCreated){
GeoPoint position;
final String updateID = posUpdate.getTripID();
if(!justCreated){
position = marker.getPosition();
if(posUpdate.getLatitude()!=position.getLatitude() || posUpdate.getLongitude()!=position.getLongitude()){
GeoPoint newpos = new GeoPoint(posUpdate.getLatitude(), posUpdate.getLongitude());
ObjectAnimator valueAnimator = MarkerUtils.makeMarkerAnimator(
map, marker, newpos, MarkerUtils.LINEAR_ANIMATION, 1200);
valueAnimator.setAutoCancel(true);
tripMarkersAnimators.put(updateID,valueAnimator);
valueAnimator.start();
}
//marker.setPosition(new GeoPoint(posUpdate.getLatitude(), posUpdate.getLongitude()));
} else {
position = new GeoPoint(posUpdate.getLatitude(), posUpdate.getLongitude());
marker.setPosition(position);
}
if(posUpdate.getBearing()!=null)
marker.setRotation(posUpdate.getBearing()*(-1.f));
}
private void updateBusPositionsInMap(HashMap> tripsPatterns){
Log.d(DEBUG_TAG, "Updating positions of the buses");
//if(busPositionsOverlay == null) busPositionsOverlay = new FolderOverlay();
final ArrayList noPatternsTrips = new ArrayList<>();
for(String tripID: tripsPatterns.keySet()) {
final Pair pair = tripsPatterns.get(tripID);
if (pair == null) continue;
final LivePositionUpdate update = pair.getFirst();
final TripAndPatternWithStops tripWithPatternStops = pair.getSecond();
//check if Marker is already created
if (busPositionMarkersByTrip.containsKey(tripID)){
//need to change the position of the marker
final Marker marker = busPositionMarkersByTrip.get(tripID);
assert marker!=null;
updateBusMarker(marker, update, false);
if(marker.getInfoWindow()!=null && marker.getInfoWindow() instanceof BusInfoWindow){
BusInfoWindow window = (BusInfoWindow) marker.getInfoWindow();
if(tripWithPatternStops != null) {
//Log.d(DEBUG_TAG, "Update pattern for trip: "+tripID);
window.setPatternAndDraw(tripWithPatternStops.getPattern());
}
}
} else{
//marker is not there, need to make it
if(map==null) Log.e(DEBUG_TAG, "Creating marker with null map, things will explode");
final Marker marker = new Marker(map);
/*final Drawable mDrawable = DrawableUtils.Companion.getScaledDrawableResources(
getResources(),
R.drawable.point_heading_icon,
R.dimen.map_icons_size, R.dimen.map_icons_size);
*/
//String route = GtfsUtils.getLineNameFromGtfsID(update.getRouteID());
final Drawable mdraw = ResourcesCompat.getDrawable(getResources(),R.drawable.map_bus_position_icon, null);
/*final Drawable mdraw = DrawableUtils.Companion.writeOnDrawable(getResources(),
R.drawable.point_heading_icon,
R.color.white,
route,12);
*/
assert mdraw != null;
//mdraw.setBounds(0,0,28,28);
marker.setIcon(mdraw);
if(tripWithPatternStops == null){
noPatternsTrips.add(tripID);
}
MatoPattern markerPattern = null;
if(tripWithPatternStops != null && tripWithPatternStops.getPattern()!=null)
markerPattern = tripWithPatternStops.getPattern();
marker.setInfoWindow(new BusInfoWindow(map, update, markerPattern , false, (pattern) -> { }));
marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER);
updateBusMarker(marker, update, true);
// the overlay is null when it's not attached yet?5
// cannot recreate it because it becomes null very soon
// if(busPositionsOverlay == null) busPositionsOverlay = new FolderOverlay();
//save the marker
if(busPositionsOverlay!=null) {
busPositionsOverlay.add(marker);
busPositionMarkersByTrip.put(tripID, marker);
}
}
}
if(noPatternsTrips.size()>0){
Log.i(DEBUG_TAG, "These trips have no matching pattern: "+noPatternsTrips);
}
}
/**
* Add stops as Markers on the map
* @param stops the list of stops that must be included
*/
protected void showStopsMarkers(List stops){
if (getContext() == null || stops == null){
//we are not attached
return;
}
boolean good = true;
for (Stop stop : stops) {
if (shownStops.contains(stop.ID)){
continue;
}
if(stop.getLongitude()==null || stop.getLatitude()==null)
continue;
shownStops.add(stop.ID);
if(!map.isShown()){
if(good)
Log.d(DEBUG_TAG, "Need to show stop but map is not shown, probably detached already");
good = false;
continue;
} else if(map.getRepository() == null){
Log.e(DEBUG_TAG, "Map view repository is null");
}
GeoPoint marker = new GeoPoint(stop.getLatitude(), stop.getLongitude());
Marker stopMarker = makeMarker(marker, stop, false);
stopsFolderOverlay.add(stopMarker);
if (!map.getOverlays().contains(stopsFolderOverlay)) {
Log.w(DEBUG_TAG, "Map doesn't have folder overlay");
}
good=true;
}
//Log.d(DEBUG_TAG,"We have " +stopsFolderOverlay.getItems().size()+" stops in the folderOverlay");
//force redraw of markers
map.invalidate();
}
public Marker makeMarker(GeoPoint geoPoint, Stop stop, boolean isStartMarker){
return makeMarker(geoPoint,stop.ID,
stop.getStopDefaultName(),
stop.routesThatStopHereToString(), isStartMarker);
}
public Marker makeMarker(GeoPoint geoPoint, String stopID, String stopName,
String routesStopping, boolean isStartMarker) {
// add a marker
final Marker marker = new Marker(map);
// set custom info window as info window
CustomInfoWindow popup = new CustomInfoWindow(map, stopID, stopName, routesStopping,
responder, R.layout.linedetail_stop_infowindow, R.color.red_darker);
marker.setInfoWindow(popup);
// make the marker clickable
marker.setOnMarkerClickListener((thisMarker, mapView) -> {
if (thisMarker.isInfoWindowOpen()) {
// on second click
Log.w(DEBUG_TAG, "Pressed on the click marker");
} else {
// on first click
// hide all opened info window
InfoWindow.closeAllInfoWindowsOn(map);
// show this particular info window
thisMarker.showInfoWindow();
// move the map to its position
map.getController().animateTo(thisMarker.getPosition());
}
return true;
});
// set its position
marker.setPosition(geoPoint);
marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER);
// add to it an icon
//marker.setIcon(getResources().getDrawable(R.drawable.bus_marker));
marker.setIcon(ResourcesCompat.getDrawable(getResources(), R.drawable.bus_stop, ctx.getTheme()));
// add to it a title
marker.setTitle(stopName);
// set the description as the ID
marker.setSnippet(stopID);
// show popup info window of the searched marker
if (isStartMarker) {
marker.showInfoWindow();
//map.getController().animateTo(marker.getPosition());
}
return marker;
}
@Nullable
- @org.jetbrains.annotations.Nullable
@Override
public View getBaseViewForSnackBar() {
- return null;
+ return coordLayout;
}
}
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 8883a2e..3ef2815 100644
--- a/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.java
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.java
@@ -1,624 +1,625 @@
/*
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.lifecycle.ViewModelProvider;
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.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.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.adapters.SquareStopAdapter;
import it.reyboz.bustorino.middleware.AutoFitGridLayoutManager;
import it.reyboz.bustorino.util.LocationCriteria;
import it.reyboz.bustorino.util.StopSorterByDistance;
import it.reyboz.bustorino.viewmodels.NearbyStopsViewModel;
import org.jetbrains.annotations.NotNull;
import java.util.*;
public class NearbyStopsFragment extends Fragment {
public enum FragType{
STOPS(1), ARRIVALS(2);
private final int num;
FragType(int num){
this.num = num;
}
public static FragType fromNum(int i){
switch (i){
case 1: return STOPS;
case 2: return ARRIVALS;
default:
throw new IllegalArgumentException("type not recognized");
}
}
}
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 FragType fragment_type = FragType.STOPS;
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 GPSPoint lastPosition = null;
private ProgressBar circlingProgressBar,flatProgressBar;
private int distance = 10;
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<>();
//ViewModel
private NearbyStopsViewModel viewModel;
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(FragType type) {
//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,type.num);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
setFragmentType(FragType.fromNum(getArguments().getInt(FRAGMENT_TYPE_KEY)));
}
locManager = AppLocationManager.getInstance(getContext());
fragmentLocationListener = new FragmentLocationListener();
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{
//start the request
if(!locManager.isRequesterRegistered(fragmentLocationListener))
locManager.addLocationRequestFor(fragmentLocationListener);
dbUpdateRunning = false;
}
}
});
//observe the livedata
viewModel.getStopsAtDistance().observe(getViewLifecycleOwner(), stops -> {
if (!dbUpdateRunning && (stops.size() < MIN_NUM_STOPS && distance <= MAX_DISTANCE)) {
distance = distance + 40;
viewModel.requestStopsAtDistance(distance, true);
//Log.d(DEBUG_TAG, "Doubling distance now!");
return;
}
if(!stops.isEmpty()) {
+ Log.d(DEBUG_TAG, "Showing "+stops.size()+" stops nearby");
currentNearbyStops =stops;
showStopsInViews(currentNearbyStops, lastPosition);
}
});
return root;
}
/**
* Use this method to set the fragment type
* @param type the type, TYPE_ARRIVALS or TYPE_STOPS
*/
private void setFragmentType(FragType type){
this.fragment_type = type;
switch(type){
case ARRIVALS:
TIME_INTERVAL_REQUESTS = 5*1000;
break;
case STOPS:
TIME_INTERVAL_REQUESTS = 1000;
}
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (context instanceof FragmentListenerMain) {
mListener = (FragmentListenerMain) context;
} else {
throw new RuntimeException(context
+ " must implement OnFragmentInteractionListener");
}
Log.d(DEBUG_TAG, "OnAttach called");
viewModel = new ViewModelProvider(this).get(NearbyStopsViewModel.class);
}
@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 STOPS:
if(dataAdapter!=null){
gridRecyclerView.setAdapter(dataAdapter);
circlingProgressBar.setVisibility(View.GONE);
}
break;
case 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();
}
/**
* Display the stops, or run new set of requests for arrivals
*/
private void showStopsInViews(ArrayList stops, GPSPoint location){
if (stops.isEmpty()) {
setNoStopsLayout();
return;
}
double minDistance = Double.POSITIVE_INFINITY;
for(Stop s: stops){
minDistance = Math.min(minDistance, s.getDistanceFromLocation(location.getLatitude(), location.getLongitude()));
}
//quick trial to hopefully always get the stops in the correct order
Collections.sort(stops,new StopSorterByDistance(location));
switch (fragment_type){
case STOPS:
showStopsInRecycler(stops);
break;
case ARRIVALS:
arrivalsManager = new ArrivalsManager(stops);
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:
}
}
/**
* 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==FragType.ARRIVALS){
setFragmentType(FragType.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==FragType.STOPS){
setFragmentType(FragType.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);
showStopsInViews(currentNearbyStops, lastPosition);
}
//useful methods
/////// GUI METHODS ////////
private void showStopsInRecycler(List stops){
if(firstLocForStops) {
dataAdapter = new SquareStopAdapter(stops, mListener, lastPosition);
gridRecyclerView.setAdapter(dataAdapter);
firstLocForStops = false;
}else {
dataAdapter.setStops(stops);
dataAdapter.setUserPosition(lastPosition);
}
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(lastPosition));
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(),lastPosition);
gridRecyclerView.setAdapter(arrivalsStopAdapter);
firstLocForArrivals = false;
} else {
arrivalsStopAdapter.setRoutesPairListAndPosition(routesPairList,lastPosition);
}
//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{
private int oldLocStatus = -2;
private LocationCriteria cr;
private long lastUpdateTime = -1;
@Override
public void onLocationChanged(Location location) {
//set adapter
if(location==null){
Log.e(DEBUG_TAG, "Location is null, cannot request stops");
return;
} else if(viewModel==null){
return;
}
if(location.getAccuracy()<100 && !dbUpdateRunning) {
if(viewModel.getDistanceMtLiveData().getValue()==null){
//never run request
distance = 40;
}
lastPosition = new GPSPoint(location.getLatitude(), location.getLongitude());
viewModel.requestStopsAtDistance(location.getLatitude(), location.getLongitude(), distance, true);
}
lastUpdateTime = System.currentTimeMillis();
Log.d("BusTO:NearPositListen","can start request for stops: "+ !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 @NotNull LocationCriteria getLocationCriteria() {
return new LocationCriteria(200,TIME_INTERVAL_REQUESTS);
}
@Override
public long getLastUpdateTimeMillis() {
return lastUpdateTime;
}
void resetUpdateTime(){
lastUpdateTime = -1;
}
@Override
public void onLocationProviderAvailable() {
}
@Override
public void onLocationDisabled() {
}
}
}
diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/ScreenBaseFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/ScreenBaseFragment.java
index 25971d4..9624939 100644
--- a/app/src/main/java/it/reyboz/bustorino/fragments/ScreenBaseFragment.java
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/ScreenBaseFragment.java
@@ -1,65 +1,65 @@
package it.reyboz.bustorino.fragments;
import android.content.Context;
import android.content.SharedPreferences;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import it.reyboz.bustorino.BuildConfig;
import static android.content.Context.MODE_PRIVATE;
public abstract class ScreenBaseFragment extends Fragment {
protected final static String PREF_FILE= BuildConfig.APPLICATION_ID+".fragment_prefs";
protected void setOption(String optionName, boolean value) {
Context mContext = getContext();
SharedPreferences.Editor editor = mContext.getSharedPreferences(PREF_FILE, MODE_PRIVATE).edit();
editor.putBoolean(optionName, value);
editor.commit();
}
protected boolean getOption(String optionName, boolean optDefault) {
Context mContext = getContext();
assert mContext != null;
return getOption(mContext, optionName, optDefault);
}
protected void showToastMessage(int messageID, boolean short_lenght) {
final int length = short_lenght ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG;
Toast.makeText(getContext(), messageID, length).show();
}
public void hideKeyboard() {
if (getActivity()==null) return;
View view = getActivity().getCurrentFocus();
if (view != null) {
((InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE))
.hideSoftInputFromWindow(view.getWindowToken(),
InputMethodManager.HIDE_NOT_ALWAYS);
}
}
/**
* Find the view on which the snackbar should be shown
- * @return
+ * @return a view or null if you don't want the snackbar shown
*/
@Nullable
public abstract View getBaseViewForSnackBar();
public static boolean getOption(Context context, String optionName, boolean optDefault){
SharedPreferences preferences = context.getSharedPreferences(PREF_FILE, MODE_PRIVATE);
return preferences.getBoolean(optionName, optDefault);
}
public static void setOption(Context context,String optionName, boolean value) {
SharedPreferences.Editor editor = context.getSharedPreferences(PREF_FILE, MODE_PRIVATE).edit();
editor.putBoolean(optionName, value);
editor.apply();
}
}
diff --git a/app/src/main/res/layout/fragment_main_screen.xml b/app/src/main/res/layout/fragment_main_screen.xml
index b0f61e7..8e96e9e 100644
--- a/app/src/main/res/layout/fragment_main_screen.xml
+++ b/app/src/main/res/layout/fragment_main_screen.xml
@@ -1,146 +1,147 @@
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_map.xml b/app/src/main/res/layout/fragment_map.xml
similarity index 71%
rename from app/src/main/res/layout/activity_map.xml
rename to app/src/main/res/layout/fragment_map.xml
index 7a3229a..6c12770 100644
--- a/app/src/main/res/layout/activity_map.xml
+++ b/app/src/main/res/layout/fragment_map.xml
@@ -1,34 +1,45 @@
+
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
\ No newline at end of file
diff --git a/app/src/test/java/it/reyboz/bustorino/util/DistanceTest.java b/app/src/test/java/it/reyboz/bustorino/util/DistanceTest.java
index bdb474f..c78a419 100644
--- a/app/src/test/java/it/reyboz/bustorino/util/DistanceTest.java
+++ b/app/src/test/java/it/reyboz/bustorino/util/DistanceTest.java
@@ -1,13 +1,22 @@
package it.reyboz.bustorino.util;
+import android.location.Location;
import it.reyboz.bustorino.backend.utils;
import org.junit.Test;
import static org.junit.Assert.*;
public class DistanceTest {
@Test
public void testDistance(){
double dist = utils.measuredistanceBetween(44.161957,8.302445, 44.645321, 7.656055);
- assertEquals(dist,74333.9, 0.05);
+ assertEquals(dist,74334.0, 0.05);
+ }
+ //@Test
+ //this tests fails as distance compute by Location.distanceBetween is always zero
+ public void testDistanceLocation(){
+ float[] result = new float[1];
+ float[] actualRes = new float[]{74333.9f}; // ,939.0f,0.01f};
+ Location.distanceBetween(44.161957,8.302445, 44.645321, 7.656055, result);
+ assertArrayEquals(actualRes, result, 0.01f);
}
}