diff --git a/app/src/main/java/it/reyboz/bustorino/backend/Notifications.java b/app/src/main/java/it/reyboz/bustorino/backend/Notifications.java
index 4bcfb7b..47c5496 100644
--- a/app/src/main/java/it/reyboz/bustorino/backend/Notifications.java
+++ b/app/src/main/java/it/reyboz/bustorino/backend/Notifications.java
@@ -1,47 +1,80 @@
package it.reyboz.bustorino.backend;
+import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.os.Build;
+import androidx.core.app.NotificationCompat;
import it.reyboz.bustorino.R;
public class Notifications {
public static final String DEFAULT_CHANNEL_ID ="Default";
public static final String DB_UPDATE_CHANNELS_ID ="Database Update";
public static void createDefaultNotificationChannel(Context context) {
// Create the NotificationChannel, but only on API 26+ because
// the NotificationChannel class is new and not in the support library
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
CharSequence name = context.getString(R.string.default_notification_channel);
String description = context.getString(R.string.default_notification_channel_description);
int importance = NotificationManager.IMPORTANCE_DEFAULT;
NotificationChannel channel = new NotificationChannel(DEFAULT_CHANNEL_ID, name, importance);
channel.setDescription(description);
// Register the channel with the system; you can't change the importance
// or other notification behaviors after this
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
notificationManager.createNotificationChannel(channel);
}
}
/**
* Register a notification channel on Android Oreo and above
* @param con a Context
* @param name channel name
* @param description channel description
* @param importance channel importance (from NotificationManager)
* @param ID channel ID
*/
public static void createNotificationChannel(Context con, String name, String description, int importance, String ID){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(ID, name, importance);
channel.setDescription(description);
// Register the channel with the system; you can't change the importance
// or other notification behaviors after this
NotificationManager notificationManager = con.getSystemService(NotificationManager.class);
notificationManager.createNotificationChannel(channel);
}
}
+
+
+ public static Notification makeMatoDownloadNotification(Context context,String title){
+ return new NotificationCompat.Builder(context, Notifications.DB_UPDATE_CHANNELS_ID)
+ //.setContentIntent(PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java), Constants.PENDING_INTENT_FLAG_IMMUTABLE))
+ .setSmallIcon(R.drawable.bus)
+ .setOngoing(true)
+ .setAutoCancel(true)
+ .setOnlyAlertOnce(true)
+ .setPriority(NotificationCompat.PRIORITY_MIN)
+ .setContentTitle(context.getString(R.string.app_name))
+ .setLocalOnly(true)
+ .setVisibility(NotificationCompat.VISIBILITY_SECRET)
+ .setContentText(title)
+ .build();
+ }
+ public static Notification makeMatoDownloadNotification(Context context){
+ return makeMatoDownloadNotification(context, "Downloading data from MaTO");
+ }
+
+ public static void createDBNotificationChannel(Context context){
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ NotificationChannel channel = new NotificationChannel(
+ Notifications.DB_UPDATE_CHANNELS_ID,
+ context.getString(R.string.database_notification_channel),
+ NotificationManager.IMPORTANCE_MIN
+ );
+ NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
+ notificationManager.createNotificationChannel(channel);
+ }
+ }
}
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 32acc65..f870d26 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,434 +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 = ""
try {
val 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")
}
- /*
- var numRequests = 0
- for(routeName in routesGTFSIds){
- if (!routeName.isEmpty()) numRequests++
- }
- val countDownForRequests = CountDownLatch(numRequests)
- val lockSave = ReentrantLock()
- //val countDownFor
- for (routeName in routesGTFSIds){
- val pars = JSONObject()
- pars.put("")
- }
- val goodResponseListener = Response.Listener { }
- val errorResponseListener = Response.ErrorListener { }
- */
return patterns
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/it/reyboz/bustorino/backend/mato/MatoQueries.kt b/app/src/main/java/it/reyboz/bustorino/backend/mato/MatoQueries.kt
index a69155e..2797ff1 100644
--- a/app/src/main/java/it/reyboz/bustorino/backend/mato/MatoQueries.kt
+++ b/app/src/main/java/it/reyboz/bustorino/backend/mato/MatoQueries.kt
@@ -1,189 +1,189 @@
/*
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
class MatoQueries {
companion object{
const val QUERY_ARRIVALS="""query AllStopsDirect(
${'$'}name: String
${'$'}startTime: Long
${'$'}timeRange: Int
${'$'}numberOfDepartures: Int
) {
stops(name: ${'$'}name) {
__typename
lat
lon
gtfsId
code
name
desc
wheelchairBoarding
routes {
__typename
gtfsId
shortName
}
stoptimesForPatterns(
startTime: ${'$'}startTime
timeRange: ${'$'}timeRange
numberOfDepartures: ${'$'}numberOfDepartures
) {
__typename
pattern {
__typename
headsign
directionId
route {
__typename
gtfsId
shortName
mode
}
}
stoptimes {
__typename
scheduledArrival
realtimeArrival
realtime
realtimeState
}
}
}
}
"""
const val ALL_STOPS_BY_FEEDS="""
query AllStops(${'$'}feeds: [String!]){
stops(feeds: ${'$'}feeds) {
lat
lon
gtfsId
code
name
desc
routes {
gtfsId
shortName
}
}
}
"""
const val ALL_FEEDS="""
query AllFeeds{
feeds{
feedId
agencies{
gtfsId
name
url
fareUrl
phone
}
}
}
"""
const val ROUTES_BY_FEED="""
query AllRoutes(${'$'}feeds: [String]){
routes(feeds: ${'$'}feeds) {
agency{
gtfsId
}
gtfsId
shortName
longName
type
desc
color
textColor
}
}
"""
-
+ //Query for the patterns, with the associated route
const val ROUTES_WITH_PATTERNS="""
query RoutesWithPatterns(${'$'}routes: [String]) {
routes(ids: ${'$'}routes) {
gtfsId
shortName
longName
type
patterns{
name
code
semanticHash
directionId
headsign
stops{
gtfsId
lat
lon
}
patternGeometry{
length
points
}
}
}
}
"""
const val TRIP_DETAILS="""
query TripInfo(${'$'}field: String!){
trip(id: ${'$'}field){
gtfsId
serviceId
route{
gtfsId
}
pattern{
name
code
headsign
}
wheelchairAccessible
activeDates
tripShortName
tripHeadsign
bikesAllowed
semanticHash
}
}
"""
fun getNameAndRequest(type: QueryType): Pair{
return when (type){
QueryType.FEEDS -> Pair("AllFeeds", ALL_FEEDS)
QueryType.ALL_STOPS -> Pair("AllStops", ALL_STOPS_BY_FEEDS)
QueryType.ARRIVALS -> Pair("AllStopsDirect", QUERY_ARRIVALS)
QueryType.ROUTES -> Pair("AllRoutes", ROUTES_BY_FEED)
QueryType.PATTERNS_FOR_ROUTES -> Pair("RoutesWithPatterns", ROUTES_WITH_PATTERNS)
QueryType.TRIP -> Pair("TripInfo", TRIP_DETAILS)
}
}
}
enum class QueryType {
ARRIVALS, ALL_STOPS, FEEDS, ROUTES, PATTERNS_FOR_ROUTES, TRIP
}
}
\ No newline at end of file
diff --git a/app/src/main/java/it/reyboz/bustorino/data/DatabaseUpdate.java b/app/src/main/java/it/reyboz/bustorino/data/DatabaseUpdate.java
index 5314ccd..4036afe 100644
--- a/app/src/main/java/it/reyboz/bustorino/data/DatabaseUpdate.java
+++ b/app/src/main/java/it/reyboz/bustorino/data/DatabaseUpdate.java
@@ -1,307 +1,330 @@
/*
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.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.Observer;
import androidx.work.*;
import it.reyboz.bustorino.R;
import it.reyboz.bustorino.backend.Fetcher;
import it.reyboz.bustorino.backend.FiveTAPIFetcher;
import it.reyboz.bustorino.backend.Palina;
-import it.reyboz.bustorino.backend.Route;
import it.reyboz.bustorino.backend.mato.MatoAPIFetcher;
import it.reyboz.bustorino.data.gtfs.GtfsAgency;
import it.reyboz.bustorino.data.gtfs.GtfsDatabase;
import it.reyboz.bustorino.data.gtfs.GtfsDBDao;
import it.reyboz.bustorino.data.gtfs.GtfsFeed;
import it.reyboz.bustorino.data.gtfs.GtfsRoute;
import it.reyboz.bustorino.data.gtfs.MatoPattern;
import it.reyboz.bustorino.data.gtfs.PatternStop;
import kotlin.Pair;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import static android.content.Context.MODE_PRIVATE;
public class DatabaseUpdate {
public static final String DEBUG_TAG = "BusTO-DBUpdate";
public static final int VERSION_UNAVAILABLE = -2;
public static final int JSON_PARSING_ERROR = -4;
public static final String DB_VERSION_KEY = "NextGenDB.GTTVersion";
public static final String DB_LAST_UPDATE_KEY = "NextGenDB.LastDBUpdate";
enum Result {
DONE, ERROR_STOPS_DOWNLOAD, ERROR_LINES_DOWNLOAD, DB_CLOSED
}
/**
* Request the server the version of the database
* @return the version of the DB, or an error code
*/
public static int getNewVersion(){
AtomicReference gres = new AtomicReference<>();
String networkRequest = FiveTAPIFetcher.performAPIRequest(FiveTAPIFetcher.QueryType.STOPS_VERSION,null,gres);
if(networkRequest == null){
return VERSION_UNAVAILABLE;
}
try {
JSONObject resp = new JSONObject(networkRequest);
return resp.getInt("id");
} catch (JSONException e) {
e.printStackTrace();
Log.e(DEBUG_TAG,"Error: wrong JSON response\nResponse:\t"+networkRequest);
return JSON_PARSING_ERROR;
}
}
private static boolean updateGTFSAgencies(Context con, AtomicReference res){
final GtfsDBDao dao = GtfsDatabase.Companion.getGtfsDatabase(con).gtfsDao();
final Pair, ArrayList> respair = MatoAPIFetcher.Companion.getFeedsAndAgencies(
con, res
);
dao.insertAgenciesWithFeeds(respair.getFirst(), respair.getSecond());
return true;
}
private static HashMap> updateGTFSRoutes(Context con, AtomicReference res){
final GtfsDBDao dao = GtfsDatabase.Companion.getGtfsDatabase(con).gtfsDao();
final List routes= MatoAPIFetcher.Companion.getRoutes(con, res);
final HashMap> routesStoppingInStop = new HashMap<>();
dao.insertRoutes(routes);
if(res.get()!= Fetcher.Result.OK){
return routesStoppingInStop;
}
final ArrayList gtfsRoutesIDs = new ArrayList<>(routes.size());
final HashMap routesMap = new HashMap<>(routes.size());
for(GtfsRoute r: routes){
gtfsRoutesIDs.add(r.getGtfsId());
routesMap.put(r.getGtfsId(),r);
}
long t0 = System.currentTimeMillis();
final ArrayList patterns = MatoAPIFetcher.Companion.getPatternsWithStops(con,gtfsRoutesIDs,res);
long tend = System.currentTimeMillis() - t0;
Log.d(DEBUG_TAG, "Downloaded patterns in "+tend+" ms");
if(res.get()!=Fetcher.Result.OK){
Log.e(DEBUG_TAG, "Something went wrong downloading patterns");
return routesStoppingInStop;
}
//match patterns with routes
- final ArrayList patternStops = new ArrayList<>(patterns.size());
+ final ArrayList patternStops = makeStopsForPatterns(patterns);
+ final List allPatternsCodeInDB = dao.getPatternsCodes();
+ final HashSet patternsCodesToDelete = new HashSet<>(allPatternsCodeInDB);
for(MatoPattern p: patterns){
//scan patterns
final ArrayList stopsIDs = p.getStopsGtfsIDs();
final GtfsRoute mRoute = routesMap.get(p.getRouteGtfsId());
if (mRoute == null) {
Log.e(DEBUG_TAG, "Error in parsing the route: " + p.getRouteGtfsId() + " , cannot find the IDs in the map");
}
- for (int i=0; i());
+ if (!routesStoppingInStop.containsKey(sID)) {
+ routesStoppingInStop.put(sID, new HashSet<>());
}
- Set mset= routesStoppingInStop.get(ID);
+ Set mset = routesStoppingInStop.get(sID);
assert mset != null;
mset.add(mRoute.getShortName());
}
+ //finally, remove from deletion list
+ patternsCodesToDelete.remove(p.getCode());
}
+ // final time for insert
dao.insertPatterns(patterns);
+ // clear patterns that are unused
+ Log.d(DEBUG_TAG, "Have to remove "+patternsCodesToDelete.size()+ " patterns from the DB");
+ dao.deletePatternsWithCodes(new ArrayList<>(patternsCodesToDelete));
dao.insertPatternStops(patternStops);
return routesStoppingInStop;
}
+ /**
+ * Make the list of stops that each pattern does, to be inserted into the DB
+ * @param patterns the MatoPattern
+ * @return a list of PatternStop
+ */
+ public static ArrayList makeStopsForPatterns(List patterns){
+ final ArrayList patternStops = new ArrayList<>(patterns.size());
+ for (MatoPattern p: patterns){
+ final ArrayList stopsIDs = p.getStopsGtfsIDs();
+ for (int i=0; i gres) {
// GTFS data fetching
AtomicReference gtfsRes = new AtomicReference<>(Fetcher.Result.OK);
updateGTFSAgencies(con, gtfsRes);
if (gtfsRes.get()!= Fetcher.Result.OK){
Log.w(DEBUG_TAG, "Could not insert the feeds and agencies stuff");
} else{
Log.d(DEBUG_TAG, "Done downloading agencies");
}
gtfsRes.set(Fetcher.Result.OK);
final HashMap> routesStoppingByStop = updateGTFSRoutes(con,gtfsRes);
if (gtfsRes.get()!= Fetcher.Result.OK){
Log.w(DEBUG_TAG, "Could not insert the routes into DB");
} else{
Log.d(DEBUG_TAG, "Done downloading routes from MaTO");
}
/*db.beginTransaction();
startTime = System.currentTimeMillis();
int countStop = NextGenDB.writeLinesStoppingHere(db, routesStoppingByStop);
if(countStop!= routesStoppingByStop.size()){
Log.w(DEBUG_TAG, "Something went wrong in updating the linesStoppingBy, have "+countStop+" lines updated, with "
+routesStoppingByStop.size()+" stops to update");
}
db.setTransactionSuccessful();
db.endTransaction();
endTime = System.currentTimeMillis();
Log.d(DEBUG_TAG, "Updating lines took "+(endTime-startTime)+" ms");
*/
// Stops insertion
final List palinasMatoAPI = MatoAPIFetcher.Companion.getAllStopsGTT(con, gres);
if (gres.get() != Fetcher.Result.OK) {
Log.w(DEBUG_TAG, "Something went wrong downloading stops");
return DatabaseUpdate.Result.ERROR_STOPS_DOWNLOAD;
}
final NextGenDB dbHelp = NextGenDB.getInstance(con.getApplicationContext());
final SQLiteDatabase db = dbHelp.getWritableDatabase();
if(!db.isOpen()){
//catch errors like: java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteDatabase
//we have to abort the work and restart it
return Result.DB_CLOSED;
}
//TODO: Get the type of stop from the lines
//Empty the needed tables
db.beginTransaction();
//db.execSQL("DELETE FROM "+StopsTable.TABLE_NAME);
//db.delete(LinesTable.TABLE_NAME,null,null);
//put new data
long startTime = System.currentTimeMillis();
Log.d(DEBUG_TAG, "Inserting " + palinasMatoAPI.size() + " stops");
String routesStoppingString="";
int patternsStopsHits = 0;
for (final Palina p : palinasMatoAPI) {
final ContentValues cv = new ContentValues();
cv.put(NextGenDB.Contract.StopsTable.COL_ID, p.ID);
cv.put(NextGenDB.Contract.StopsTable.COL_NAME, p.getStopDefaultName());
if (p.location != null)
cv.put(NextGenDB.Contract.StopsTable.COL_LOCATION, p.location);
cv.put(NextGenDB.Contract.StopsTable.COL_LAT, p.getLatitude());
cv.put(NextGenDB.Contract.StopsTable.COL_LONG, p.getLongitude());
if (p.getAbsurdGTTPlaceName() != null) cv.put(NextGenDB.Contract.StopsTable.COL_PLACE, p.getAbsurdGTTPlaceName());
if(p.gtfsID!= null && routesStoppingByStop.containsKey(p.gtfsID)){
final ArrayList routesSs= new ArrayList<>(routesStoppingByStop.get(p.gtfsID));
routesStoppingString = Palina.buildRoutesStringFromNames(routesSs);
patternsStopsHits++;
} else{
routesStoppingString = p.routesThatStopHereToString();
}
cv.put(NextGenDB.Contract.StopsTable.COL_LINES_STOPPING, routesStoppingString);
if (p.type != null) cv.put(NextGenDB.Contract.StopsTable.COL_TYPE, p.type.getCode());
if (p.gtfsID != null) cv.put(NextGenDB.Contract.StopsTable.COL_GTFS_ID, p.gtfsID);
//Log.d(DEBUG_TAG,cv.toString());
//cpOp.add(ContentProviderOperation.newInsert(uritobeused).withValues(cv).build());
//valuesArr[i] = cv;
db.replace(NextGenDB.Contract.StopsTable.TABLE_NAME, null, cv);
}
db.setTransactionSuccessful();
db.endTransaction();
long endTime = System.currentTimeMillis();
Log.d(DEBUG_TAG, "Inserting stops took: " + ((double) (endTime - startTime) / 1000) + " s");
Log.d(DEBUG_TAG, "\t"+patternsStopsHits+" routes string were built from the patterns");
db.close();
dbHelp.close();
return DatabaseUpdate.Result.DONE;
}
public static boolean setDBUpdatingFlag(Context con, boolean value){
final SharedPreferences shPr = con.getSharedPreferences(con.getString(R.string.mainSharedPreferences),MODE_PRIVATE);
return setDBUpdatingFlag(con, shPr, value);
}
static boolean setDBUpdatingFlag(Context con, SharedPreferences shPr,boolean value){
final SharedPreferences.Editor editor = shPr.edit();
editor.putBoolean(con.getString(R.string.databaseUpdatingPref),value);
return editor.commit();
}
/**
* Request update using workmanager framework
* @param con the context to use
* @param forced if you want to force the request to go now
*/
public static void requestDBUpdateWithWork(Context con,boolean restart, boolean forced){
final SharedPreferences theShPr = PreferencesHolder.getMainSharedPreferences(con);
final WorkManager workManager = WorkManager.getInstance(con);
final Data reqData = new Data.Builder()
.putBoolean(DBUpdateWorker.FORCED_UPDATE, forced).build();
PeriodicWorkRequest wr = new PeriodicWorkRequest.Builder(DBUpdateWorker.class, 7, TimeUnit.DAYS)
.setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.MINUTES)
.setConstraints(new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED)
.build())
.setInputData(reqData)
.build();
final int version = theShPr.getInt(DatabaseUpdate.DB_VERSION_KEY, -10);
final long lastDBUpdateTime = theShPr.getLong(DatabaseUpdate.DB_LAST_UPDATE_KEY, -10);
if ((version >= 0 || lastDBUpdateTime >=0) && !restart)
workManager.enqueueUniquePeriodicWork(DBUpdateWorker.DEBUG_TAG,
ExistingPeriodicWorkPolicy.KEEP, wr);
else workManager.enqueueUniquePeriodicWork(DBUpdateWorker.DEBUG_TAG,
ExistingPeriodicWorkPolicy.REPLACE, wr);
}
/*
public static boolean isDBUpdating(){
return false;
TODO
}
*/
public static void watchUpdateWorkStatus(Context context, @NonNull LifecycleOwner lifecycleOwner,
@NonNull Observer super List> observer) {
WorkManager workManager = WorkManager.getInstance(context);
workManager.getWorkInfosForUniqueWorkLiveData(DBUpdateWorker.DEBUG_TAG).observe(
lifecycleOwner, observer
);
}
}
diff --git a/app/src/main/java/it/reyboz/bustorino/data/MatoPatternsDownloadWorker.kt b/app/src/main/java/it/reyboz/bustorino/data/MatoPatternsDownloadWorker.kt
new file mode 100644
index 0000000..e263464
--- /dev/null
+++ b/app/src/main/java/it/reyboz/bustorino/data/MatoPatternsDownloadWorker.kt
@@ -0,0 +1,101 @@
+/*
+ BusTO - Data 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.data
+
+import android.app.NotificationManager
+import android.content.Context
+import android.util.Log
+import androidx.work.*
+import it.reyboz.bustorino.backend.Fetcher
+import it.reyboz.bustorino.backend.Notifications
+import it.reyboz.bustorino.backend.mato.MatoAPIFetcher
+import java.util.concurrent.atomic.AtomicReference
+
+class MatoPatternsDownloadWorker(appContext: Context, workerParams: WorkerParameters)
+ : CoroutineWorker(appContext, workerParams) {
+
+
+ override suspend fun doWork(): Result {
+ val routesList = inputData.getStringArray(ROUTES_KEYS)
+
+ if (routesList== null){
+ Log.e(DEBUG_TAG,"routes list given is null")
+ return Result.failure()
+ }
+
+ val res = AtomicReference(Fetcher.Result.OK)
+
+ val gtfsRepository = GtfsRepository(applicationContext)
+ val patterns = MatoAPIFetcher.getPatternsWithStops(applicationContext, routesList.asList().toMutableList(), res)
+ if (res.get() != Fetcher.Result.OK) {
+ Log.e(DatabaseUpdate.DEBUG_TAG, "Something went wrong downloading patterns")
+ return Result.failure()
+ }
+
+ gtfsRepository.gtfsDao.insertPatterns(patterns)
+ //Insert the PatternStops
+ gtfsRepository.gtfsDao.insertPatternStops(DatabaseUpdate.makeStopsForPatterns(patterns))
+ return Result.success()
+ }
+
+
+ override suspend fun getForegroundInfo(): ForegroundInfo {
+ val notificationManager =
+ applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ val context = applicationContext
+ Notifications.createDBNotificationChannel(context)
+
+ return ForegroundInfo(NOTIFICATION_ID, Notifications.makeMatoDownloadNotification(context))
+ }
+
+
+ companion object{
+ const val ROUTES_KEYS = "routesToDownload"
+ const val DEBUG_TAG="BusTO:MatoPattrnDownWRK"
+ const val NOTIFICATION_ID=21983102
+
+ const val TAG_PATTERNS ="matoPatternsDownload"
+
+
+
+ fun downloadPatternsForRoutes(routesIds: List, context: Context): Boolean{
+ if(routesIds.isEmpty()) return false;
+
+ val workManager = WorkManager.getInstance(context);
+ val info = workManager.getWorkInfosForUniqueWork(TAG_PATTERNS).get()
+
+ val runNewWork = if(info.isEmpty()) true
+ else info[0].state!= WorkInfo.State.RUNNING && info[0].state!= WorkInfo.State.ENQUEUED
+ val addDat = if(info.isEmpty())
+ null else info[0].state
+
+ Log.d(DEBUG_TAG, "Request to download and insert patterns for ${routesIds.size} routes, proceed: $runNewWork, workstate: $addDat")
+ if(runNewWork){
+ val routeIdsArray = routesIds.toTypedArray()
+ val dataBuilder = Data.Builder().putStringArray(ROUTES_KEYS,routeIdsArray)
+
+ val requ = OneTimeWorkRequest.Builder(MatoPatternsDownloadWorker::class.java)
+ .setInputData(dataBuilder.build()).setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
+ .addTag(TAG_PATTERNS)
+ .build()
+ workManager.enqueueUniqueWork(TAG_PATTERNS, ExistingWorkPolicy.KEEP, requ)
+ }
+ return true
+ }
+ }
+}
diff --git a/app/src/main/java/it/reyboz/bustorino/data/MatoDownloadTripsWorker.kt b/app/src/main/java/it/reyboz/bustorino/data/MatoTripsDownloadWorker.kt
similarity index 65%
rename from app/src/main/java/it/reyboz/bustorino/data/MatoDownloadTripsWorker.kt
rename to app/src/main/java/it/reyboz/bustorino/data/MatoTripsDownloadWorker.kt
index ac3c597..0a9502e 100644
--- a/app/src/main/java/it/reyboz/bustorino/data/MatoDownloadTripsWorker.kt
+++ b/app/src/main/java/it/reyboz/bustorino/data/MatoTripsDownloadWorker.kt
@@ -1,136 +1,135 @@
/*
BusTO - Data 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.data
-import android.app.NotificationChannel
import android.app.NotificationManager
-import android.app.PendingIntent
import android.content.Context
-import android.content.Intent
-import android.os.Build
import android.util.Log
-import androidx.core.app.NotificationCompat
-import androidx.lifecycle.viewModelScope
-import androidx.work.CoroutineWorker
-import androidx.work.ForegroundInfo
-import androidx.work.Worker
-import androidx.work.WorkerParameters
-import com.android.volley.Response
-import it.reyboz.bustorino.R
+import androidx.work.*
import it.reyboz.bustorino.backend.Notifications
import it.reyboz.bustorino.data.gtfs.GtfsTrip
-import kotlinx.coroutines.launch
import java.util.concurrent.CountDownLatch
-class MatoDownloadTripsWorker(appContext: Context, workerParams: WorkerParameters)
+class MatoTripsDownloadWorker(appContext: Context, workerParams: WorkerParameters)
: CoroutineWorker(appContext, workerParams) {
+
override suspend fun doWork(): Result {
- //val imageUriInput =
- // inputData.("IMAGE_URI") ?: return Result.failure()
+ return downloadGtfsTrips()
+ }
+
+ /**
+ * Download GTFS Trips from Mato
+ */
+ private fun downloadGtfsTrips():Result{
val tripsList = inputData.getStringArray(TRIPS_KEYS)
if (tripsList== null){
Log.e(DEBUG_TAG,"trips list given is null")
return Result.failure()
}
val gtfsRepository = GtfsRepository(applicationContext)
val matoRepository = MatoRepository(applicationContext)
//clear the matoTrips
val queriedMatoTrips = HashSet()
val downloadedMatoTrips = ArrayList()
val failedMatoTripsDownload = HashSet()
Log.i(DEBUG_TAG, "Requesting download for the trips")
val requestCountDown = CountDownLatch(tripsList.size);
for(trip in tripsList){
queriedMatoTrips.add(trip)
matoRepository.requestTripUpdate(trip,{error->
Log.e(DEBUG_TAG, "Cannot download Gtfs Trip $trip")
val stacktrace = error.stackTrace.take(5)
Log.w(DEBUG_TAG, "Stacktrace:\n$stacktrace")
failedMatoTripsDownload.add(trip)
requestCountDown.countDown()
}){
if(it.isSuccess){
if (it.result == null){
Log.e(DEBUG_TAG, "Got null result");
}
downloadedMatoTrips.add(it.result!!)
} else{
failedMatoTripsDownload.add(trip)
}
Log.i(
DEBUG_TAG,"Result download, so far, trips: ${queriedMatoTrips.size}, failed: ${failedMatoTripsDownload.size}," +
- " succeded: ${downloadedMatoTrips.size}")
+ " succeded: ${downloadedMatoTrips.size}")
//check if we can insert the trips
requestCountDown.countDown()
}
}
requestCountDown.await()
val tripsIDsCompleted = downloadedMatoTrips.map { trip-> trip.tripID }
val doInsert = (queriedMatoTrips subtract failedMatoTripsDownload).containsAll(tripsIDsCompleted)
Log.i(DEBUG_TAG, "Inserting missing GtfsTrips in the database, should insert $doInsert")
if(doInsert){
gtfsRepository.gtfsDao.insertTrips(downloadedMatoTrips)
}
return Result.success()
}
override suspend fun getForegroundInfo(): ForegroundInfo {
val notificationManager =
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val context = applicationContext
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- val channel = NotificationChannel(
- Notifications.DB_UPDATE_CHANNELS_ID,
- context.getString(R.string.database_notification_channel),
- NotificationManager.IMPORTANCE_MIN
- )
- notificationManager.createNotificationChannel(channel)
- }
+ Notifications.createDBNotificationChannel(context)
- val notification = NotificationCompat.Builder(context, Notifications.DB_UPDATE_CHANNELS_ID)
- //.setContentIntent(PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java), Constants.PENDING_INTENT_FLAG_IMMUTABLE))
- .setSmallIcon(R.drawable.bus)
- .setOngoing(true)
- .setAutoCancel(true)
- .setOnlyAlertOnce(true)
- .setPriority(NotificationCompat.PRIORITY_MIN)
- .setContentTitle(context.getString(R.string.app_name))
- .setLocalOnly(true)
- .setVisibility(NotificationCompat.VISIBILITY_SECRET)
- .setContentText("Downloading data")
- .build()
- return ForegroundInfo(NOTIFICATION_ID, notification)
+ return ForegroundInfo(NOTIFICATION_ID, Notifications.makeMatoDownloadNotification(context))
}
companion object{
const val TRIPS_KEYS = "tripsToDownload"
- const val WORK_TAG = "tripsDownloaderAndInserter"
const val DEBUG_TAG="BusTO:MatoTripDownWRK"
- const val NOTIFICATION_ID=424242
+ const val NOTIFICATION_ID=42424221
+
+ const val TAG_TRIPS ="gtfsTripsDownload"
+
+ fun downloadTripsFromMato(trips: List, context: Context, debugTag: String): Boolean{
+ if (trips.isEmpty()) return false
+ val workManager = WorkManager.getInstance(context)
+ val info = workManager.getWorkInfosForUniqueWork(TAG_TRIPS).get()
+
+ val runNewWork = if(info.isEmpty()) true
+ else info[0].state!= WorkInfo.State.RUNNING && info[0].state!= WorkInfo.State.ENQUEUED
+ val addDat = if(info.isEmpty())
+ null else info[0].state
+ Log.d(debugTag, "Request to download and insert ${trips.size} trips, proceed: $runNewWork, workstate: $addDat")
+ if(runNewWork) {
+ val tripsArr = trips.toTypedArray()
+ val dataBuilder = Data.Builder().putStringArray(TRIPS_KEYS, tripsArr)
+ //build()
+ val requ = OneTimeWorkRequest.Builder(MatoTripsDownloadWorker::class.java)
+ .setInputData(dataBuilder.build()).setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
+ .addTag(TAG_TRIPS)
+ .build()
+ workManager.enqueueUniqueWork(TAG_TRIPS, ExistingWorkPolicy.KEEP, requ)
+ }
+ return true
+ }
}
}
diff --git a/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsDBDao.kt b/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsDBDao.kt
index 81d991d..b1c02c6 100644
--- a/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsDBDao.kt
+++ b/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsDBDao.kt
@@ -1,144 +1,156 @@
/*
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.gtfs
import androidx.lifecycle.LiveData
import androidx.room.*
@Dao
interface GtfsDBDao {
// get queries
@Query("SELECT * FROM "+GtfsRoute.DB_TABLE)
fun getAllRoutes() : LiveData>
@Query("SELECT * FROM ${GtfsRoute.DB_TABLE} WHERE ${GtfsRoute.COL_ROUTE_ID} IN (:routeGtfsIds)")
fun getRoutesByIDs(routeGtfsIds: List): LiveData>
@Query("SELECT "+GtfsTrip.COL_TRIP_ID+" FROM "+GtfsTrip.DB_TABLE)
fun getAllTripsIDs() : List
@Query("SELECT "+GtfsStop.COL_STOP_ID+" FROM "+GtfsStop.DB_TABLE)
fun getAllStopsIDs() : List
@Query("SELECT * FROM "+GtfsStop.DB_TABLE+" WHERE "+GtfsStop.COL_STOP_CODE+" LIKE :queryID")
fun getStopByStopID(queryID: String): LiveData>
@Query("SELECT * FROM "+GtfsShape.DB_TABLE+
" WHERE "+GtfsShape.COL_SHAPE_ID+" LIKE :shapeID"+
" ORDER BY "+GtfsShape.COL_POINT_SEQ+ " ASC"
)
fun getShapeByID(shapeID: String) : LiveData>
@Query("SELECT * FROM ${GtfsRoute.DB_TABLE} WHERE ${GtfsRoute.COL_AGENCY_ID} LIKE :agencyID")
fun getRoutesByAgency(agencyID:String) : LiveData>
+ @Query("SELECT ${MatoPattern.COL_CODE} FROM ${MatoPattern.TABLE_NAME} ")
+ fun getPatternsCodes(): List
@Query("SELECT * FROM ${MatoPattern.TABLE_NAME} WHERE ${MatoPattern.COL_ROUTE_ID} LIKE :routeID")
fun getPatternsLiveDataByRouteID(routeID: String): LiveData>
@Query("SELECT * FROM "+MatoPattern.TABLE_NAME+" WHERE ${MatoPattern.COL_ROUTE_ID} LIKE :routeGtfsId")
fun getPatternsForRouteID(routeGtfsId: String) : List
@Query("SELECT * FROM ${PatternStop.TABLE_NAME} WHERE ${PatternStop.COL_PATTERN_ID} LIKE :patternGtfsID")
fun getStopsByPatternID(patternGtfsID: String): LiveData>
@Transaction
@Query("SELECT * FROM ${MatoPattern.TABLE_NAME} WHERE ${MatoPattern.COL_ROUTE_ID} LIKE :routeID")
fun getPatternsWithStopsByRouteID(routeID: String): LiveData>
@Transaction
@Query("SELECT * FROM ${MatoPattern.TABLE_NAME} WHERE ${MatoPattern.COL_CODE} IN (:patternGtfsIDs)")
fun getPatternsWithStopsFromIDs(patternGtfsIDs: List) : LiveData>
+ @Query("SELECT ${MatoPattern.COL_CODE} FROM ${MatoPattern.TABLE_NAME} WHERE ${MatoPattern.COL_CODE} IN (:codes)")
+ fun getPatternsCodesInTheDB(codes: List): List
+
@Transaction
@Query("SELECT * FROM ${GtfsTrip.DB_TABLE} WHERE ${GtfsTrip.COL_TRIP_ID} IN (:tripsIds)")
fun getTripsFromIDs(tripsIds: List) : List
@Transaction
@Query("SELECT * FROM ${GtfsTrip.DB_TABLE} WHERE ${GtfsTrip.COL_TRIP_ID} IN (:trips)")
fun getTripPatternStops(trips: List): LiveData>
+
+
fun getRoutesForFeed(feed:String): LiveData>{
val agencyID = "${feed}:%"
return getRoutesByAgency(agencyID)
}
+
+
@Transaction
fun clearAndInsertRoutes(routes: List){
deleteAllRoutes()
insertRoutes(routes)
}
@Transaction
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertRoutes(routes: List)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertStops(stops: List)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertCalendarServices(services: List)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertShapes(shapes: List)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertDates(dates: List)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertServices(services: List)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertTrips(trips: List)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertStopTimes(stopTimes: List)
@Query("DELETE FROM "+GtfsRoute.DB_TABLE)
fun deleteAllRoutes()
@Query("DELETE FROM "+GtfsStop.DB_TABLE)
fun deleteAllStops()
@Query("DELETE FROM "+GtfsTrip.DB_TABLE)
fun deleteAllTrips()
+
+ @Query("DELETE FROM ${MatoPattern.TABLE_NAME} WHERE ${MatoPattern.COL_CODE} IN (:codes)")
+ fun deletePatternsWithCodes(codes: List): Int
@Update(onConflict = OnConflictStrategy.REPLACE)
fun updateShapes(shapes: List) : Int
@Transaction
fun updateAllStops(stops: List){
deleteAllStops()
insertStops(stops)
}
@Query("DELETE FROM "+GtfsStopTime.DB_TABLE)
fun deleteAllStopTimes()
@Query("DELETE FROM "+GtfsService.DB_TABLE)
fun deleteAllServices()
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertFeeds(feeds: List)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAgencies(agencies: List)
@Transaction
fun insertAgenciesWithFeeds(feeds: List, agencies: List){
insertFeeds(feeds)
insertAgencies(agencies)
}
//patterns
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertPatterns(patterns: List)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertPatternStops(patternStops: List)
}
\ No newline at end of file
diff --git a/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsTrip.kt b/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsTrip.kt
index 4c2b51c..4bb78ae 100644
--- a/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsTrip.kt
+++ b/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsTrip.kt
@@ -1,136 +1,136 @@
/*
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.gtfs
import androidx.room.*
@Entity(tableName = GtfsTrip.DB_TABLE,
foreignKeys=[
ForeignKey(entity = GtfsRoute::class,
parentColumns = [GtfsRoute.COL_ROUTE_ID],
childColumns = [GtfsTrip.COL_ROUTE_ID],
onDelete = ForeignKey.CASCADE),
// The service_id: ID referencing calendar.service_id or calendar_dates.service_id
/*
ForeignKey(entity = GtfsService::class,
parentColumns = [GtfsService.COL_SERVICE_ID],
childColumns = [GtfsTrips.COL_SERVICE_ID],
onDelete = GtfsDatabase.FOREIGNKEY_ONDELETE),
*/
],
indices = [Index(GtfsTrip.COL_ROUTE_ID), Index(GtfsTrip.COL_TRIP_ID)]
)
data class GtfsTrip(
@ColumnInfo(name = COL_ROUTE_ID )
val routeID: String,
@ColumnInfo(name = COL_SERVICE_ID)
val serviceID: String,
@PrimaryKey
@ColumnInfo(name = COL_TRIP_ID)
val tripID: String,
@ColumnInfo(name = COL_HEADSIGN)
val tripHeadsign: String,
@ColumnInfo(name = COL_DIRECTION_ID)
val directionID: Int,
@ColumnInfo(name = COL_BLOCK_ID)
val blockID: String,
@ColumnInfo(name = COL_SHAPE_ID)
val shapeID: String,
@ColumnInfo(name = COL_WHEELCHAIR)
val isWheelchairAccess: WheelchairAccess,
@ColumnInfo(name = COL_LIMITED_R)
val isLimitedRoute: Boolean,
@ColumnInfo(name= COL_PATTERN_ID, defaultValue = "")
val patternId: String,
@ColumnInfo(name = COL_SEM_HASH)
val semanticHash: String?,
): GtfsTable {
constructor(valuesByColumn: Map) : this(
valuesByColumn[COL_ROUTE_ID]!!,
valuesByColumn[COL_SERVICE_ID]!!,
valuesByColumn[COL_TRIP_ID]!!,
valuesByColumn[COL_HEADSIGN]!!,
valuesByColumn[COL_DIRECTION_ID]?.toIntOrNull()?: 0,
valuesByColumn[COL_BLOCK_ID]!!,
valuesByColumn[COL_SHAPE_ID]!!,
Converters.wheelchairFromString(valuesByColumn[COL_WHEELCHAIR]),
Converters.fromStringNum(valuesByColumn[COL_LIMITED_R], false),
valuesByColumn[COL_PATTERN_ID]?:"",
valuesByColumn[COL_SEM_HASH],
)
companion object{
const val DB_TABLE="gtfs_trips"
const val COL_ROUTE_ID="route_id"
const val COL_SERVICE_ID="service_id"
const val COL_TRIP_ID = "trip_id"
const val COL_HEADSIGN="trip_headsign"
//const val COL_SHORT_NAME="trip_short_name",
const val COL_DIRECTION_ID="direction_id"
const val COL_BLOCK_ID="block_id"
const val COL_SHAPE_ID = "shape_id"
const val COL_WHEELCHAIR="wheelchair_accessible"
const val COL_LIMITED_R="limited_route"
const val COL_PATTERN_ID="pattern_code"
const val COL_SEM_HASH="semantic_hash"
val COLUMNS= arrayOf(
COL_ROUTE_ID,
COL_SERVICE_ID,
COL_TRIP_ID,
COL_HEADSIGN,
COL_DIRECTION_ID,
COL_BLOCK_ID,
COL_SHAPE_ID,
COL_WHEELCHAIR,
COL_LIMITED_R
)
/*
open fun fromContentValues(values: ContentValues) {
val tripItem = GtfsTrips();
}
*/
}
override fun getColumns(): Array {
return COLUMNS
}
}
data class TripAndPatternWithStops(
@Embedded val trip: GtfsTrip,
@Relation(
parentColumn = GtfsTrip.COL_PATTERN_ID,
entityColumn = MatoPattern.COL_CODE
)
- val pattern: MatoPattern,
+ val pattern: MatoPattern?,
@Relation(
parentColumn = MatoPattern.COL_CODE,
entityColumn = PatternStop.COL_PATTERN_ID,
)
var stopsIndices: List){
init {
stopsIndices = stopsIndices.sortedBy { p-> p.order }
}
}
\ 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 bea3d02..3d38da1 100644
--- a/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java
@@ -1,824 +1,825 @@
/*
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.utils;
+import it.reyboz.bustorino.data.gtfs.MatoPattern;
import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops;
import it.reyboz.bustorino.map.*;
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;
//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 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);
if (context instanceof FragmentListenerMain) {
listenerMain = (FragmentListenerMain) context;
} else {
throw new RuntimeException(context.toString()
+ " must implement FragmentListenerMain");
}
}
@Override
public void onDetach() {
super.onDetach();
listenerMain = null;
//stop animations
// setupOnAttached = true;
Log.w(DEBUG_TAG, "Fragment detached");
}
@Override
public void onPause() {
super.onPause();
Log.w(DEBUG_TAG, "On pause called mapfrag");
saveMapState();
for (ObjectAnimator animator : tripMarkersAnimators.values()) {
if(animator!=null && animator.isRunning()){
animator.cancel();
}
}
tripMarkersAnimators.clear();
if (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();
//mapViewModel.testCascade();
mapViewModel.getTripsGtfsIDsToQuery().observe(this, dat -> {
Log.i(DEBUG_TAG, "Have these trips IDs missing from the DB, to be queried: "+dat);
- mapViewModel.downloadandInsertTripsInDB(dat);
+ mapViewModel.downloadTripsFromMato(dat);
});
}
}
@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);
}
}
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){
//should always be the case
mapViewModel.getUpdatesWithTripAndPatterns().observe(this, data->{
Log.d(DEBUG_TAG, "Have "+data.size()+" trip updates, has Map start finished: "+hasMapStartFinished);
if (hasMapStartFinished) updateBusPositionsInMap(data);
if(!isDetached())
mapViewModel.requestDelayedUpdates(4000);
});
}
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){
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();
}
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));
}
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 GtfsPositionUpdate 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);
/*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);
}
- marker.setInfoWindow(new BusInfoWindow(map, update, tripWithPatternStops != null ? tripWithPatternStops.getPattern() : null, new BusInfoWindow.onTouchUp() {
- @Override
- public void onActionUp() {
+ MatoPattern markerPattern = null;
+ if(tripWithPatternStops != null && tripWithPatternStops.getPattern()!=null)
+ markerPattern = tripWithPatternStops.getPattern();
+ marker.setInfoWindow(new BusInfoWindow(map, update, markerPattern , () -> {
- }
}));
updateBusMarker(marker, update, 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);
}
}
/**
* 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);
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);
// 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/MapViewModel.kt b/app/src/main/java/it/reyboz/bustorino/fragments/MapViewModel.kt
index 9d0d183..bdff94f 100644
--- a/app/src/main/java/it/reyboz/bustorino/fragments/MapViewModel.kt
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/MapViewModel.kt
@@ -1,237 +1,209 @@
/*
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 androidx.work.*
import com.android.volley.Response
-import com.google.transit.realtime.GtfsRealtime.VehiclePosition
import it.reyboz.bustorino.backend.NetworkVolleyManager
-import it.reyboz.bustorino.backend.Result
import it.reyboz.bustorino.backend.gtfs.GtfsPositionUpdate
import it.reyboz.bustorino.backend.gtfs.GtfsRtPositionsRequest
import it.reyboz.bustorino.data.*
-import it.reyboz.bustorino.data.gtfs.GtfsTrip
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) {
private val gtfsRepo = GtfsRepository(application)
- private val executor = Executors.newFixedThreadPool(2)
- private val oldRepo= OldDataRepository(executor, NextGenDB.getInstance(application))
- private val matoRepository = MatoRepository(application)
private val netVolleyManager = NetworkVolleyManager.getInstance(application)
val positionsLiveData = MutableLiveData>()
private val positionsRequestRunning = MutableLiveData()
private val positionRequestListener = object: GtfsRtPositionsRequest.Companion.RequestListener{
override fun onResponse(response: ArrayList?) {
Log.i(DEBUG_TI,"Got response from the GTFS RT server")
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 }
}
- // trips that are in the DB
- val gtfsTripsInDB = tripsIDsInUpdates.switchMap {
+ //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> = gtfsTripsInDB.map { tripswithPatterns ->
+ 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) }
else {
Log.e(DEBUG_TI,"Got results for gtfsTripsInDB but not tripsIDsInUpdates??")
return@map ArrayList()
}
}
- val updatesWithTripAndPatterns = gtfsTripsInDB.map { tripPatterns->
+ val updatesWithTripAndPatterns = gtfsTripsPatternsInDB.map { tripPatterns->
Log.i(DEBUG_TI, "Mapping trips and patterns")
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 not change very often, so they might only need to be inserted once every two months
- TODO: find a way to avoid trips getting scrubbed (check if they are really scrubbed)
+ 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{
+ return MatoPatternsDownloadWorker.downloadPatternsForRoutes(routeIds, getApplication())
+ }
init {
- /*
- //what happens when the trips to query with Mato are determined
- tripsIDsQueried.addSource(tripsGtfsIDsToQuery) { tripList ->
- // avoid infinite loop of querying to Mato, insert in DB and
- // triggering another query update
-
- val tripsToQuery =
- Log.d(DEBUG_TI, "Querying trips IDs to Mato: $tripsToQuery")
- for (t in tripsToQuery){
- matoRepository.requestTripUpdate(t,matoTripReqErrorList, matoTripCallback)
- }
- tripsIDsQueried.value = tripsToQuery
- }
- tripsToInsert.addSource(tripsFromMato) { matoTrips ->
- if (tripsIDsQueried.value == null) return@addSource
- val tripsIdsToInsert = matoTrips.map { trip -> trip.tripID }
-
- //val setTripsToInsert = HashSet(tripsIdsToInsert)
- val tripsRequested = HashSet(tripsIDsQueried.value!!)
- val insertInDB = tripsRequested.containsAll(tripsIdsToInsert)
- if(insertInDB){
- gtfsRepo.gtfsDao.insertTrips(matoTrips)
- }
- Log.d(DEBUG_TI, "MatoTrips: ${matoTrips.size}, total trips req: ${tripsRequested.size}, inserting: $insertInDB")
- }
- */
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
))
positionsLiveData.value = n
}
- private val queriedMatoTrips = HashSet()
- private val downloadedMatoTrips = ArrayList()
- private val failedMatoTripsDownload = HashSet()
/**
* Start downloading missing GtfsTrips and Insert them in the DB
*/
- fun downloadandInsertTripsInDB(trips: List): Boolean{
- if (trips.isEmpty()) return false
- val workManager = WorkManager.getInstance(getApplication())
- val info = workManager.getWorkInfosForUniqueWork(MatoDownloadTripsWorker.WORK_TAG).get()
-
- val runNewWork = if(info.isEmpty()){
- true
- } else info[0].state!=WorkInfo.State.RUNNING && info[0].state!=WorkInfo.State.ENQUEUED
- val addDat = if(info.isEmpty())
- null else info[0].state
- Log.d(DEBUG_TI, "Request to download and insert ${trips.size} trips, proceed: $runNewWork, workstate: $addDat")
- if(runNewWork) {
- val tripsArr = trips.toTypedArray()
- val data = Data.Builder().putStringArray(MatoDownloadTripsWorker.TRIPS_KEYS, tripsArr).build()
- val requ = OneTimeWorkRequest.Builder(MatoDownloadTripsWorker::class.java)
- .setInputData(data).setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
- .addTag(MatoDownloadTripsWorker.WORK_TAG)
- .build()
- workManager.enqueueUniqueWork(MatoDownloadTripsWorker.WORK_TAG, ExistingWorkPolicy.KEEP, requ)
- }
- return true
- }
+
companion object{
const val DEBUG_TI="BusTO-MapViewModel"
const val DEFAULT_DELAY_REQUESTS: Long=4000
}
}
\ No newline at end of file
diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/SettingsFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/SettingsFragment.java
index ea04c73..ac9dc4a 100644
--- a/app/src/main/java/it/reyboz/bustorino/fragments/SettingsFragment.java
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/SettingsFragment.java
@@ -1,222 +1,220 @@
/*
BusTO - Fragments components
Copyright (C) 2020 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.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.Observer;
import androidx.preference.*;
import androidx.work.OneTimeWorkRequest;
-import androidx.work.OutOfQuotaPolicy;
import androidx.work.WorkInfo;
import androidx.work.WorkManager;
import it.reyboz.bustorino.R;
import it.reyboz.bustorino.data.DatabaseUpdate;
import it.reyboz.bustorino.data.GtfsMaintenanceWorker;
-import it.reyboz.bustorino.data.MatoDownloadTripsWorker;
import org.jetbrains.annotations.NotNull;
import java.lang.ref.WeakReference;
import java.util.HashSet;
import java.util.List;
public class SettingsFragment extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final String TAG = SettingsFragment.class.getName();
private static final String DIALOG_FRAGMENT_TAG =
"androidx.preference.PreferenceFragment.DIALOG";
//private static final
Handler mHandler;
public final static String PREF_KEY_STARTUP_SCREEN="startup_screen_to_show";
public final static String KEY_ARRIVALS_FETCHERS_USE = "arrivals_fetchers_use_setting";
private boolean setSummaryStartupPref = false;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
mHandler = new Handler();
return super.onCreateView(inflater, container, savedInstanceState);
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
//getPreferenceManager().setSharedPreferencesName(getString(R.string.mainSharedPreferences));
convertStringPrefToIntIfNeeded(getString(R.string.pref_key_num_recents), getContext());
getPreferenceManager().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
setPreferencesFromResource(R.xml.preferences,rootKey);
/*EditTextPreference editPref = findPreference(getString(R.string.pref_key_num_recents));
editPref.setOnBindEditTextListener(editText -> {
editText.setInputType(InputType.TYPE_CLASS_NUMBER);
editText.setSelection(0,editText.getText().length());
});
*/
ListPreference startupScreenPref = findPreference(PREF_KEY_STARTUP_SCREEN);
if(startupScreenPref !=null){
if (startupScreenPref.getValue()==null){
startupScreenPref.setSummary(getString(R.string.nav_arrivals_text));
setSummaryStartupPref = true;
}
}
//Log.d("BusTO-PrefFrag","startup screen pref is "+startupScreenPref.getValue());
Preference dbUpdateNow = findPreference("pref_db_update_now");
if (dbUpdateNow!=null)
dbUpdateNow.setOnPreferenceClickListener(
new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(@NonNull Preference preference) {
//trigger update
if(getContext()!=null) {
DatabaseUpdate.requestDBUpdateWithWork(getContext().getApplicationContext(), true, true);
Toast.makeText(getContext(),R.string.requesting_db_update,Toast.LENGTH_SHORT).show();
return true;
}
return false;
}
}
);
else {
Log.e("BusTO-Preferences", "Cannot find db update preference");
}
Preference clearGtfsTrips = findPreference("pref_clear_gtfs_trips");
if (clearGtfsTrips != null) {
clearGtfsTrips.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(@NonNull @NotNull Preference preference) {
if (getContext() != null) {
OneTimeWorkRequest requ = GtfsMaintenanceWorker.Companion.makeOneTimeRequest(GtfsMaintenanceWorker.CLEAR_GTFS_TRIPS);
WorkManager.getInstance(getContext()).enqueue(requ);
WorkManager.getInstance(getContext()).getWorkInfosByTagLiveData(GtfsMaintenanceWorker.CLEAR_GTFS_TRIPS).observe(getViewLifecycleOwner(),
(Observer>) workInfos -> {
if(workInfos.isEmpty())
return;
if(workInfos.get(0).getState()==(WorkInfo.State.SUCCEEDED)){
Toast.makeText(
getContext(), R.string.all_trips_removed, Toast.LENGTH_SHORT
).show();
}
});
return true;
}
return false;
}
});
}
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
Preference pref = findPreference(key);
Log.d(TAG,"Preference key "+key+" changed");
if (key.equals(SettingsFragment.KEY_ARRIVALS_FETCHERS_USE)){
Log.d(TAG, "New value is: "+sharedPreferences.getStringSet(key, new HashSet<>()));
}
//sometimes this happens
if(getContext()==null) return;
if(key.equals(PREF_KEY_STARTUP_SCREEN) && setSummaryStartupPref && pref !=null){
ListPreference listPref = (ListPreference) pref;
pref.setSummary(listPref.getEntry());
}
/*
THIS CODE STAYS COMMENTED FOR FUTURE REFERENCES
if (key.equals(getString(R.string.pref_key_num_recents))){
//check that is it an int
String value = sharedPreferences.getString(key,"");
boolean valid = value.length() != 0;
try{
Integer intValue = Integer.parseInt(value);
} catch (NumberFormatException ex){
valid = false;
}
if (!valid){
Toast.makeText(getContext(), R.string.invalid_number, Toast.LENGTH_SHORT).show();
if(pref instanceof EditTextPreference){
EditTextPreference prefEdit = (EditTextPreference) pref;
//Intent intent = prefEdit.getIntent();
Log.d(TAG, "opening preference, dialog showing "+
(getParentFragmentManager().findFragmentByTag(DIALOG_FRAGMENT_TAG)!=null) );
//getPreferenceManager().showDialog(pref);
//onDisplayPreferenceDialog(prefEdit);
mHandler.postDelayed(new DelayedDisplay(prefEdit), 500);
}
}
}
*/
Log.d("BusTO Settings", "changed "+key+"\n "+sharedPreferences.getAll());
}
private void convertStringPrefToIntIfNeeded(String preferenceKey, Context con){
if (con == null) return;
SharedPreferences defaultSharedPref = PreferenceManager.getDefaultSharedPreferences(con);
try{
Integer val = defaultSharedPref.getInt(preferenceKey, 0);
} catch (NumberFormatException | ClassCastException ex){
//convert the preference
//final String preferenceNumRecents = getString(R.string.pref_key_num_recents);
Log.d("Preference - BusTO", "Converting to integer the string preference "+preferenceKey);
String currentValue = defaultSharedPref.getString(preferenceKey, "10");
int newValue;
try{
newValue = Integer.parseInt(currentValue);
} catch (NumberFormatException e){
newValue = 10;
}
final SharedPreferences.Editor editor = defaultSharedPref.edit();
editor.remove(preferenceKey);
editor.putInt(preferenceKey, newValue);
editor.apply();
}
}
class DelayedDisplay implements Runnable{
private final WeakReference preferenceWeakReference;
public DelayedDisplay(DialogPreference preference) {
this.preferenceWeakReference = new WeakReference<>(preference);
}
@Override
public void run() {
if(preferenceWeakReference.get()==null)
return;
getPreferenceManager().showDialog(preferenceWeakReference.get());
}
}
}