diff --git a/app/build.gradle b/app/build.gradle --- a/app/build.gradle +++ b/app/build.gradle @@ -9,7 +9,7 @@ defaultConfig { applicationId "it.reyboz.bustorino" - minSdkVersion 16 + minSdkVersion 21 targetSdkVersion 33 versionCode 48 versionName "1.19.1" @@ -68,15 +68,18 @@ implementation "androidx.fragment:fragment-ktx:$fragment_version" implementation "androidx.activity:activity:$activity_version" - implementation "androidx.annotation:annotation:1.3.0" + implementation "androidx.annotation:annotation:1.6.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation "androidx.appcompat:appcompat:$appcompat_version" implementation "androidx.appcompat:appcompat-resources:$appcompat_version" implementation "androidx.preference:preference:$preference_version" + implementation "androidx.work:work-runtime:$work_version" + implementation "androidx.work:work-runtime-ktx:$work_version" + - implementation "com.google.android.material:material:1.5.0" - implementation 'androidx.constraintlayout:constraintlayout:2.1.3' + implementation "com.google.android.material:material:1.9.0" + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0" @@ -91,7 +94,11 @@ implementation "ch.acra:acra-mail:$acra_version" implementation "ch.acra:acra-dialog:$acra_version" // google transit realtime - implementation 'com.google.protobuf:protobuf-java:3.14.0' + implementation 'com.google.protobuf:protobuf-java:3.17.2' + // mqtt library + implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5' + implementation 'com.github.hannesa2:paho.mqtt.android:3.5.3' + // ViewModel implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" @@ -103,8 +110,8 @@ implementation 'androidx.legacy:legacy-support-v4:1.0.0' // Room components + implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-ktx:$room_version" - implementation "androidx.work:work-runtime-ktx:$work_version" kapt "androidx.room:room-compiler:$room_version" //multidex - we need this to build the app implementation "androidx.multidex:multidex:$multidex_version" @@ -114,12 +121,12 @@ testImplementation 'junit:junit:4.12' implementation 'junit:junit:4.12' - implementation "androidx.test.ext:junit:1.1.3" + implementation "androidx.test.ext:junit:1.1.5" implementation "androidx.test:core:$androidXTestVersion" implementation "androidx.test:runner:$androidXTestVersion" implementation "androidx.room:room-testing:$room_version" - androidTestImplementation "androidx.test.ext:junit:1.1.3" + androidTestImplementation "androidx.test.ext:junit:1.1.5" androidTestImplementation "androidx.test:core:$androidXTestVersion" androidTestImplementation "androidx.test:runner:$androidXTestVersion" androidTestImplementation "androidx.test:rules:$androidXTestVersion" diff --git a/app/src/androidTest/java/it/reyboz/bustorino/data/gtfs/GtfsDBMigrationsTest.java b/app/src/androidTest/java/it/reyboz/bustorino/data/gtfs/GtfsDBMigrationsTest.java --- a/app/src/androidTest/java/it/reyboz/bustorino/data/gtfs/GtfsDBMigrationsTest.java +++ b/app/src/androidTest/java/it/reyboz/bustorino/data/gtfs/GtfsDBMigrationsTest.java @@ -25,8 +25,7 @@ public GtfsDBMigrationsTest() { helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(), - GtfsDatabase.class.getCanonicalName(), - new FrameworkSQLiteOpenHelperFactory()); + GtfsDatabase.class.getCanonicalName()); } @Test diff --git a/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java b/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java --- a/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java +++ b/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java @@ -18,12 +18,14 @@ package it.reyboz.bustorino; import android.os.Bundle; +import android.util.Log; import androidx.appcompat.app.ActionBar; -import it.reyboz.bustorino.fragments.LinesDetailFragment; -import it.reyboz.bustorino.fragments.TestRealtimeGtfsFragment; +import androidx.fragment.app.FragmentTransaction; +import it.reyboz.bustorino.backend.Stop; +import it.reyboz.bustorino.fragments.*; import it.reyboz.bustorino.middleware.GeneralActivity; -public class ActivityExperiments extends GeneralActivity { +public class ActivityExperiments extends GeneralActivity implements CommonFragmentListener { final static String DEBUG_TAG = "ExperimentsGTFS"; @@ -40,15 +42,51 @@ if (savedInstanceState==null) { getSupportFragmentManager().beginTransaction() .setReorderingAllowed(true) - /* - .add(R.id.fragment_container_view, LinesDetailFragment.class, - LinesDetailFragment.Companion.makeArgs("gtt:56U")) - .commit(); - */ - .add(R.id.fragment_container_view, LinesDetailFragment.class, - LinesDetailFragment.Companion.makeArgs("gtt:10U")) + /* .add(R.id.fragment_container_view, LinesDetailFragment.class, + + LinesDetailFragment.Companion.makeArgs("gtt:4U")) + + */ + .add(R.id.fragment_container_view, LinesGridShowingFragment.class, null) .commit(); + + //.add(R.id.fragment_container_view, LinesDetailFragment.class, + // LinesDetailFragment.Companion.makeArgs("gtt:4U")) + //.add(R.id.fragment_container_view, TestRealtimeGtfsFragment.class, null) + //.commit(); } } + + @Override + public void showFloatingActionButton(boolean yes) { + Log.d(DEBUG_TAG, "Asked to show the action button"); + } + + @Override + public void readyGUIfor(FragmentKind fragmentType) { + Log.d(DEBUG_TAG, "Asked to prepare the GUI for fragmentType "+fragmentType); + } + + @Override + public void requestArrivalsForStopID(String ID) { + + } + + @Override + public void showMapCenteredOnStop(Stop stop) { + + } + @Override + public void showLineOnMap(String routeGtfsId){ + + readyGUIfor(FragmentKind.LINES); + FragmentTransaction tr = getSupportFragmentManager().beginTransaction(); + tr.replace(R.id.fragment_container_view, LinesDetailFragment.class, + LinesDetailFragment.Companion.makeArgs(routeGtfsId)); + tr.addToBackStack("LineonMap-"+routeGtfsId); + tr.commit(); + + + } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java b/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java --- a/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java +++ b/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java @@ -521,15 +521,16 @@ private static void showLinesFragment(@NonNull FragmentManager fragmentManager, boolean addToBackStack, @Nullable Bundle fragArgs){ FragmentTransaction ft = fragmentManager.beginTransaction(); - Fragment f = fragmentManager.findFragmentByTag(LinesFragment.FRAGMENT_TAG); + Fragment f = fragmentManager.findFragmentByTag(LinesGridShowingFragment.FRAGMENT_TAG); if(f!=null){ - ft.replace(R.id.mainActContentFrame, f, LinesFragment.FRAGMENT_TAG); + ft.replace(R.id.mainActContentFrame, f, LinesGridShowingFragment.FRAGMENT_TAG); }else{ //use new method - ft.replace(R.id.mainActContentFrame,LinesFragment.class,fragArgs,LinesFragment.FRAGMENT_TAG); + ft.replace(R.id.mainActContentFrame,LinesGridShowingFragment.class,fragArgs, + LinesGridShowingFragment.FRAGMENT_TAG); } if (addToBackStack) - ft.addToBackStack("lines"); + ft.addToBackStack("linesGrid"); ft.setReorderingAllowed(true) .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) .commit(); @@ -655,6 +656,19 @@ mNavView.setCheckedItem(R.id.nav_arrivals); } + @Override + public void showLineOnMap(String routeGtfsId){ + + readyGUIfor(FragmentKind.LINES); + + FragmentTransaction tr = getSupportFragmentManager().beginTransaction(); + tr.replace(R.id.mainActContentFrame, LinesDetailFragment.class, + LinesDetailFragment.Companion.makeArgs(routeGtfsId)); + tr.addToBackStack("LineonMap-"+routeGtfsId); + tr.commit(); + + + } @Override public void toggleSpinner(boolean state) { diff --git a/app/src/main/java/it/reyboz/bustorino/adapters/RouteAdapter.kt b/app/src/main/java/it/reyboz/bustorino/adapters/RouteAdapter.kt new file mode 100644 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/adapters/RouteAdapter.kt @@ -0,0 +1,59 @@ +package it.reyboz.bustorino.adapters + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.cardview.widget.CardView +import androidx.recyclerview.widget.RecyclerView +import it.reyboz.bustorino.R +import it.reyboz.bustorino.data.gtfs.GtfsRoute +import java.lang.ref.WeakReference + +class RouteAdapter(val routes: List, + click: onItemClick, + private val layoutId: Int = R.layout.line_title_header) : + RecyclerView.Adapter() +{ + val clickreference: WeakReference + init { + clickreference = WeakReference(click) + } + + class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { + val descrptionTextView: TextView + val nameTextView : TextView + val innerCardView : CardView? + init { + // Define click listener for the ViewHolder's View + nameTextView = view.findViewById(R.id.lineShortNameTextView) + descrptionTextView = view.findViewById(R.id.lineDirectionTextView) + innerCardView = view.findViewById(R.id.innerCardView) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(layoutId, parent, false) + + return ViewHolder(view) + } + + override fun getItemCount() = routes.size + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + // Get element from your dataset at this position and replace the + // contents of the view with that element + val route = routes[position] + holder.nameTextView.text = route.shortName + holder.descrptionTextView.text = route.longName + + holder.itemView.setOnClickListener{ + clickreference.get()?.onRouteItemClicked(route) + } + } + + fun interface onItemClick{ + fun onRouteItemClicked(gtfsRoute: GtfsRoute) + } +} \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/adapters/SquareStopAdapter.java b/app/src/main/java/it/reyboz/bustorino/adapters/SquareStopAdapter.java --- a/app/src/main/java/it/reyboz/bustorino/adapters/SquareStopAdapter.java +++ b/app/src/main/java/it/reyboz/bustorino/adapters/SquareStopAdapter.java @@ -53,7 +53,7 @@ final View view = LayoutInflater.from(parent.getContext()).inflate(layoutRes, parent, false); //sort the stops by distance if(stops != null && stops.size() > 0) - Collections.sort(stops,new StopSorterByDistance(userPosition)); + Collections.sort(stops,new StopSorterByDistance(userPosition)); return new SquareViewHolder(view); } diff --git a/app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsRtPositionsRequest.kt b/app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsRtPositionsRequest.kt --- a/app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsRtPositionsRequest.kt +++ b/app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsRtPositionsRequest.kt @@ -23,13 +23,12 @@ import com.android.volley.VolleyError import com.android.volley.toolbox.HttpHeaderParser import com.google.transit.realtime.GtfsRealtime -import com.google.transit.realtime.GtfsRealtime.VehiclePosition class GtfsRtPositionsRequest( errorListener: Response.ErrorListener?, val listener: RequestListener) : - Request>(Method.GET, URL_POSITION, errorListener) { - override fun parseNetworkResponse(response: NetworkResponse?): Response> { + Request>(Method.GET, URL_POSITION, errorListener) { + override fun parseNetworkResponse(response: NetworkResponse?): Response> { if (response == null){ return Response.error(VolleyError("Null response")) } @@ -39,13 +38,13 @@ val gtfsreq = GtfsRealtime.FeedMessage.parseFrom(response.data) - val positionList = ArrayList() + val positionList = ArrayList() if (gtfsreq.hasHeader() && gtfsreq.entityCount>0){ for (i in 0 until gtfsreq.entityCount){ val entity = gtfsreq.getEntity(i) if (entity.hasVehicle()){ - positionList.add(GtfsPositionUpdate(entity.vehicle)) + positionList.add(LivePositionUpdate(entity.vehicle)) } } } @@ -53,7 +52,7 @@ return Response.success(positionList, HttpHeaderParser.parseCacheHeaders(response)) } - override fun deliverResponse(response: ArrayList?) { + override fun deliverResponse(response: ArrayList?) { listener.onResponse(response) } @@ -64,9 +63,9 @@ const val URL_ALERTS = "http://percorsieorari.gtt.to.it/das_gtfsrt/alerts.aspx" public interface RequestListener{ - fun onResponse(response: ArrayList?) + fun onResponse(response: ArrayList?) } } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsPositionUpdate.kt b/app/src/main/java/it/reyboz/bustorino/backend/gtfs/LivePositionUpdate.kt rename from app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsPositionUpdate.kt rename to app/src/main/java/it/reyboz/bustorino/backend/gtfs/LivePositionUpdate.kt --- a/app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsPositionUpdate.kt +++ b/app/src/main/java/it/reyboz/bustorino/backend/gtfs/LivePositionUpdate.kt @@ -21,39 +21,45 @@ import com.google.transit.realtime.GtfsRealtime.VehiclePosition import com.google.transit.realtime.GtfsRealtime.VehiclePosition.OccupancyStatus -data class GtfsPositionUpdate( - val tripID: String, - val startTime: String, - val startDate: String, +data class LivePositionUpdate( + val tripID: String, //tripID WITHOUT THE "gtt:" prefix + val startTime: String?, + val startDate: String?, val routeID: String, + val vehicle: String, - val latitude: Float, - val longitude: Float, - val bearing: Float, + val latitude: Double, + val longitude: Double, + val bearing: Float?, val timestamp: Long, - val vehicleInfo: VehicleInfo, + val nextStop: String?, + + /*val vehicleInfo: VehicleInfo, val occupancyStatus: OccupancyStatus?, val scheduleRelationship: ScheduleRelationship? + */ ){ constructor(position: VehiclePosition) : this( position.trip.tripId, position.trip.startTime, position.trip.startDate, position.trip.routeId, - position.position.latitude, - position.position.longitude, + position.vehicle.label, + + position.position.latitude.toDouble(), + position.position.longitude.toDouble(), position.position.bearing, position.timestamp, - VehicleInfo(position.vehicle.id, position.vehicle.label), - position.occupancyStatus, null ) - data class VehicleInfo( + /*data class VehicleInfo( val id: String, val label:String ) + + */ } diff --git a/app/src/main/java/it/reyboz/bustorino/backend/mato/MQTTMatoClient.kt b/app/src/main/java/it/reyboz/bustorino/backend/mato/MQTTMatoClient.kt new file mode 100644 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/backend/mato/MQTTMatoClient.kt @@ -0,0 +1,323 @@ +package it.reyboz.bustorino.backend.mato + +import android.content.Context +import android.util.Log +import androidx.lifecycle.LifecycleOwner +import info.mqtt.android.service.Ack +import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import org.eclipse.paho.client.mqttv3.* +import info.mqtt.android.service.MqttAndroidClient +import info.mqtt.android.service.QoS + +import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence +import org.json.JSONArray +import org.json.JSONException +import java.lang.ref.WeakReference +import java.util.ArrayList +import java.util.Properties + +typealias PositionsMap = HashMap > + +class MQTTMatoClient private constructor(): MqttCallbackExtended{ + + private var isStarted = false + private var subscribedToAll = false + + private lateinit var client: MqttAndroidClient + //private var clientID = "" + + private val respondersMap = HashMap>>() + + private val currentPositions = PositionsMap() + + private lateinit var lifecycle: LifecycleOwner + private var context: Context?= null + + private fun connect(context: Context, iMqttActionListener: IMqttActionListener?){ + + val clientID = "mqttjs_${getRandomString(8)}" + + client = MqttAndroidClient(context,SERVER_ADDR,clientID,Ack.AUTO_ACK) + + val options = MqttConnectOptions() + //options.sslProperties = + options.isCleanSession = true + val headersPars = Properties() + headersPars.setProperty("Origin","https://mato.muoversiatorino.it") + headersPars.setProperty("Host","mapi.5t.torino.it") + options.customWebSocketHeaders = headersPars + + //actually connect + client.connect(options,null, iMqttActionListener) + isStarted = true + client.setCallback(this) + + if (this.context ==null) + this.context = context.applicationContext + } + + + override fun connectComplete(reconnect: Boolean, serverURI: String?) { + Log.d(DEBUG_TAG, "Connected to server, reconnect: $reconnect") + Log.d(DEBUG_TAG, "Have listeners: $respondersMap") + } + + fun startAndSubscribe(lineId: String, responder: MQTTMatoListener, context: Context): Boolean{ + //start the client, and then subscribe to the topic + val topic = mapTopic(lineId) + synchronized(this) { + if(!isStarted){ + connect(context, object : IMqttActionListener{ + override fun onSuccess(asyncActionToken: IMqttToken?) { + val disconnectedBufferOptions = DisconnectedBufferOptions() + disconnectedBufferOptions.isBufferEnabled = true + disconnectedBufferOptions.bufferSize = 100 + disconnectedBufferOptions.isPersistBuffer = false + disconnectedBufferOptions.isDeleteOldestMessages = false + client.setBufferOpts(disconnectedBufferOptions) + client.subscribe(topic, QoS.AtMostOnce.value) + } + + override fun onFailure(asyncActionToken: IMqttToken?, exception: Throwable?) { + Log.e(DEBUG_TAG, "FAILED To connect to the server") + } + + }) + //wait for connection + } else { + client.subscribe(topic, QoS.AtMostOnce.value) + } + } + + + + synchronized(this){ + if (!respondersMap.contains(lineId)) + respondersMap[lineId] = ArrayList() + respondersMap[lineId]!!.add(WeakReference(responder)) + Log.d(DEBUG_TAG, "Add MQTT Listener for line $lineId, topic $topic") + } + + return true + } + + fun desubscribe(responder: MQTTMatoListener){ + var removed = false + for ((line,v)in respondersMap.entries){ + var done = false + for (el in v){ + if (el.get()==null){ + v.remove(el) + } else if(el.get() == responder){ + v.remove(el) + done = true + } + if (done) + break + } + if(done) Log.d(DEBUG_TAG, "Removed one listener for line $line, listeners: $v") + //if (done) break + if (v.isEmpty()){ + //actually unsubscribe + client.unsubscribe( mapTopic(line)) + } + removed = done || removed + } + Log.d(DEBUG_TAG, "Removed: $removed, respondersMap: $respondersMap") + } + fun getPositions(): PositionsMap{ + return currentPositions + } + + fun sendUpdateToResponders(responders: ArrayList>): Boolean{ + var sent = false + for (wrD in responders) + if (wrD.get() == null) + responders.remove(wrD) + else { + wrD.get()!!.onUpdateReceived(currentPositions) + sent = true + } + return sent + } + + override fun connectionLost(cause: Throwable?) { + Log.w(DEBUG_TAG, "Lost connection in MQTT Mato Client") + + + synchronized(this){ + // isStarted = false + //var i = 0 + // while(i < 20 && !isStarted) { + connect(context!!, object: IMqttActionListener{ + override fun onSuccess(asyncActionToken: IMqttToken?) { + //relisten to messages + for ((line,elms) in respondersMap.entries){ + val topic = mapTopic(line) + if(elms.isEmpty()) + respondersMap.remove(line) + else + client.subscribe(topic, QoS.AtMostOnce.value, null, null) + } + Log.d(DEBUG_TAG, "Reconnected to MQTT Mato Client") + + } + + override fun onFailure(asyncActionToken: IMqttToken?, exception: Throwable?) { + Log.w(DEBUG_TAG, "Failed to reconnect to MQTT server") + } + }) + + } + + + } + + override fun messageArrived(topic: String?, message: MqttMessage?) { + if (topic==null || message==null) return + + parseMessageAndAddToList(topic, message) + //GlobalScope.launch { } + + } + + private fun parseMessageAndAddToList(topic: String, message: MqttMessage){ + + val vals = topic.split("/") + val lineId = vals[1] + val vehicleId = vals[2] + val timestamp = (System.currentTimeMillis() / 1000 ) as Long + + val messString = String(message.payload) + + + try { + val jsonList = JSONArray(messString) + //val full = if(jsonList.length()>7) { + // if (jsonList.get(7).equals(null)) null else jsonList.getInt(7) + //}else null + /*val posUpdate = MQTTPositionUpdate(lineId+"U", vehicleId, + jsonList.getDouble(0), + jsonList.getDouble(1), + if(jsonList.get(2).equals(null)) null else jsonList.getInt(2), + if(jsonList.get(3).equals(null)) null else jsonList.getInt(3), + if(jsonList.get(4).equals(null)) null else jsonList.getString(4)+"U", + if(jsonList.get(5).equals(null)) null else jsonList.getInt(5), + if(jsonList.get(6).equals(null)) null else jsonList.getInt(6), + //full + ) + + */ + if(jsonList.get(4)==null){ + Log.d(DEBUG_TAG, "We have null tripId: line $lineId veh $vehicleId: $jsonList") + return + } + val posUpdate = LivePositionUpdate( + jsonList.getString(4)+"U", + null, + null, + lineId+"U", + vehicleId, + jsonList.getDouble(0), //latitude + jsonList.getDouble(1), //longitude + if(jsonList.get(2).equals(null)) null else jsonList.getInt(2).toFloat(), //"heading" (same as bearing?) + timestamp, + if(jsonList.get(6).equals(null)) null else jsonList.getInt(6).toString() //nextStop + ) + + //add update + var valid = false + if(!currentPositions.contains(lineId)) + currentPositions[lineId] = HashMap() + currentPositions[lineId]?.let{ + it[vehicleId] = posUpdate + valid = true + } + var sent = false + if (LINES_ALL in respondersMap.keys) { + sent = sendUpdateToResponders(respondersMap[LINES_ALL]!!) + + + } + if(lineId in respondersMap.keys){ + sent = sendUpdateToResponders(respondersMap[lineId]!!) or sent + + } + if(!sent){ + Log.w(DEBUG_TAG, "We have received an update but apparently there is no one to send it") + var emptyResp = true + for(en in respondersMap.values){ + if(!en.isEmpty()){ + emptyResp=false + break + } + } + //try unsubscribing to all + if(emptyResp) { + Log.d(DEBUG_TAG, "Unsubscribe all") + client.unsubscribe(LINES_ALL) + } + } + //Log.d(DEBUG_TAG, "We have update on line $lineId, vehicle $vehicleId") + } catch (e: JSONException){ + Log.e(DEBUG_TAG,"Cannot decipher message on topic $topic, line $lineId, veh $vehicleId") + e.printStackTrace() + } + } + + + override fun deliveryComplete(token: IMqttDeliveryToken?) { + //NOT USED (we're not sending any messages) + } + + + companion object{ + + const val SERVER_ADDR="wss://mapi.5t.torino.it:443/scre" + const val LINES_ALL="ALL" + private const val DEBUG_TAG="BusTO-MatoMQTT" + @Volatile + private var instance: MQTTMatoClient? = null + + fun getInstance() = instance?: synchronized(this){ + instance?: MQTTMatoClient().also { instance= it } + } + + @JvmStatic + fun mapTopic(lineId: String): String{ + return if(lineId== LINES_ALL || lineId == "#") + "#" + else{ + "/${lineId}/#" + } + } + + fun getRandomString(length: Int) : String { + val allowedChars = ('a'..'f') + ('0'..'9') + return (1..length) + .map { allowedChars.random() } + .joinToString("") + } + + + fun interface MQTTMatoListener{ + //positionsMap is a dict with line -> vehicle -> Update + fun onUpdateReceived(posUpdates: PositionsMap) + } + } +} + +data class MQTTPositionUpdate( + val lineId: String, + val vehicleId: String, + val latitude: Double, + val longitude: Double, + val heading: Int?, + val speed: Int?, + val tripId: String?, + val direct: Int?, + val nextStop: Int?, + //val full: Int? +) \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt b/app/src/main/java/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt --- a/app/src/main/java/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt +++ b/app/src/main/java/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt @@ -383,9 +383,9 @@ requestQueue.add(request) val patterns = ArrayList() - //var outObj = "" + var resObj = JSONObject() try { - val resObj = future.get(60,TimeUnit.SECONDS) + resObj = future.get(60,TimeUnit.SECONDS) //outObj = resObj.toString(1) val routesJSON = resObj.getJSONArray("routes") for (i in 0 until routesJSON.length()){ @@ -406,7 +406,7 @@ } catch (e: JSONException){ e.printStackTrace() res?.set(Fetcher.Result.PARSER_ERROR) - //Log.e(DEBUG_TAG, "Downloading feeds: $outObj") + Log.e(DEBUG_TAG, "Got result: $resObj") } diff --git a/app/src/main/java/it/reyboz/bustorino/backend/mato/ResponseParsing.kt b/app/src/main/java/it/reyboz/bustorino/backend/mato/ResponseParsing.kt --- a/app/src/main/java/it/reyboz/bustorino/backend/mato/ResponseParsing.kt +++ b/app/src/main/java/it/reyboz/bustorino/backend/mato/ResponseParsing.kt @@ -117,7 +117,9 @@ MatoPattern( mPatternJSON.getString("name"), mPatternJSON.getString("code"), mPatternJSON.getString("semanticHash"), mPatternJSON.getInt("directionId"), - routeGtfsId, mPatternJSON.getString("headsign"), polyline, numGeo, stopsCodes + routeGtfsId, + sanitize( mPatternJSON.getString("headsign")), + polyline, numGeo, stopsCodes ) ) } @@ -135,12 +137,26 @@ // still have "activeDates" which are the days in which the pattern is active //Log.d("BusTO:RequestParsing", "Making GTFS trip for: $jsonData") val trip = GtfsTrip( - routeId, jsonTrip.getString("serviceId"), jsonTrip.getString("gtfsId"), - jsonTrip.getString("tripHeadsign"), -1, "", "", + routeId, jsonTrip.getString("serviceId"), + jsonTrip.getString("gtfsId"), + sanitize(jsonTrip.getString("tripHeadsign")), + -1, "", "", Converters.wheelchairFromString(jsonTrip.getString("wheelchairAccessible")), false, patternId, jsonTrip.getString("semanticHash") ) return trip } + + @JvmStatic + fun sanitize(dir: String): String{ + var str = dir.trim() + val lastChar = str[str.length-1] + if(lastChar==','|| lastChar==';') { + Log.d(DEBUG_TAG, "Sanitization: removing last char from $str") + str = str.dropLast(1) + } + + return str + } } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/backend/utils.java b/app/src/main/java/it/reyboz/bustorino/backend/utils.java --- a/app/src/main/java/it/reyboz/bustorino/backend/utils.java +++ b/app/src/main/java/it/reyboz/bustorino/backend/utils.java @@ -64,16 +64,17 @@ public static Double angleRawDifferenceFromMeters(double distanceInMeters){ return Math.toDegrees(distanceInMeters/EarthRadius); } - /* - public static int convertDipToPixels(Context con,float dips) + + public static int convertDipToPixelsInt(Context con,double dips) { return (int) (dips * con.getResources().getDisplayMetrics().density + 0.5f); } - */ + public static float convertDipToPixels(Context con, float dp){ return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,con.getResources().getDisplayMetrics()); } + /* public static int calculateNumColumnsFromSize(View containerView, int pixelsize){ int width = containerView.getWidth(); diff --git a/app/src/main/java/it/reyboz/bustorino/data/GtfsRepository.kt b/app/src/main/java/it/reyboz/bustorino/data/GtfsRepository.kt --- a/app/src/main/java/it/reyboz/bustorino/data/GtfsRepository.kt +++ b/app/src/main/java/it/reyboz/bustorino/data/GtfsRepository.kt @@ -31,4 +31,8 @@ else MutableLiveData(listOf()) } + + fun getAllRoutes(): LiveData>{ + return gtfsDao.getAllRoutes() + } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/CommonFragmentListener.java b/app/src/main/java/it/reyboz/bustorino/fragments/CommonFragmentListener.java --- a/app/src/main/java/it/reyboz/bustorino/fragments/CommonFragmentListener.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/CommonFragmentListener.java @@ -37,4 +37,9 @@ */ void showMapCenteredOnStop(Stop stop); + /** + * We want to show the line in detail for route + * @param routeGtfsId the route gtfsID (eg, "gtt:10U") + */ + void showLineOnMap(String routeGtfsId); } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/MapViewModel.kt b/app/src/main/java/it/reyboz/bustorino/fragments/GTFSPositionsViewModel.kt rename from app/src/main/java/it/reyboz/bustorino/fragments/MapViewModel.kt rename to app/src/main/java/it/reyboz/bustorino/fragments/GTFSPositionsViewModel.kt --- a/app/src/main/java/it/reyboz/bustorino/fragments/MapViewModel.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/GTFSPositionsViewModel.kt @@ -22,31 +22,30 @@ import androidx.lifecycle.* import com.android.volley.Response import it.reyboz.bustorino.backend.NetworkVolleyManager -import it.reyboz.bustorino.backend.gtfs.GtfsPositionUpdate +import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate import it.reyboz.bustorino.backend.gtfs.GtfsRtPositionsRequest import it.reyboz.bustorino.data.* import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import java.util.concurrent.Executors /** * View Model for the map. For containing the stops, the trips and whatever */ -class MapViewModel(application: Application): AndroidViewModel(application) { +class GTFSPositionsViewModel(application: Application): AndroidViewModel(application) { private val gtfsRepo = GtfsRepository(application) private val netVolleyManager = NetworkVolleyManager.getInstance(application) - val positionsLiveData = MutableLiveData>() + val positionsLiveData = MutableLiveData>() private val positionsRequestRunning = MutableLiveData() private val positionRequestListener = object: GtfsRtPositionsRequest.Companion.RequestListener{ - override fun onResponse(response: ArrayList?) { + override fun onResponse(response: ArrayList?) { Log.i(DEBUG_TI,"Got response from the GTFS RT server") - response?.let {it:ArrayList -> + response?.let {it:ArrayList -> if (it.size == 0) { Log.w(DEBUG_TI,"No position updates from the server") return @@ -120,7 +119,7 @@ val tripNames=tripswithPatterns.map { twp-> twp.trip.tripID } Log.i(DEBUG_TI, "Have ${tripswithPatterns.size} trips in the DB") if (tripsIDsInUpdates.value!=null) - return@map tripsIDsInUpdates.value!!.filter { !tripNames.contains(it) } + return@map tripsIDsInUpdates.value!!.filter { !tripNames.contains(it) } else { Log.e(DEBUG_TI,"Got results for gtfsTripsInDB but not tripsIDsInUpdates??") return@map ArrayList() @@ -129,7 +128,7 @@ val updatesWithTripAndPatterns = gtfsTripsPatternsInDB.map { tripPatterns-> Log.i(DEBUG_TI, "Mapping trips and patterns") - val mdict = HashMap>() + val mdict = HashMap>() //missing patterns val routesToDownload = HashSet() if(positionsLiveData.value!=null) @@ -174,7 +173,7 @@ fun downloadTripsFromMato(trips: List): Boolean{ return MatoTripsDownloadWorker.downloadTripsFromMato(trips,getApplication(), DEBUG_TI) } - fun downloadMissingPatterns(routeIds: List): Boolean{ + private fun downloadMissingPatterns(routeIds: List): Boolean{ return MatoPatternsDownloadWorker.downloadPatternsForRoutes(routeIds, getApplication()) } @@ -186,11 +185,9 @@ positionsRequestRunning.value = false; } fun testCascade(){ - val n = ArrayList() - n.add(GtfsPositionUpdate("22920721U","lala","lalal","lol",1000.0f,1000.0f, 9000.0f, - 378192810192, GtfsPositionUpdate.VehicleInfo("aj","a"), - null, null - + val n = ArrayList() + n.add(LivePositionUpdate("22920721U","lala","lalal","lol","ASD", + 1000.0,1000.0, 9000.0f, 21838191, null )) positionsLiveData.value = n } @@ -202,8 +199,8 @@ companion object{ - const val DEBUG_TI="BusTO-MapViewModel" + private const val DEBUG_TI="BusTO-GTFSRTViewModel" const val DEFAULT_DELAY_REQUESTS: Long=4000 } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt --- a/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt @@ -1,145 +1,198 @@ +/* + BusTO - Fragments components + Copyright (C) 2023 Fabio Mazza + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ package it.reyboz.bustorino.fragments +import android.animation.ObjectAnimator +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Paint import android.os.Bundle -import android.os.Parcelable import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.AdapterView -import android.widget.ArrayAdapter -import android.widget.Spinner -import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModelProvider +import android.widget.* +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import it.reyboz.bustorino.R +import it.reyboz.bustorino.adapters.NameCapitalize +import it.reyboz.bustorino.adapters.StopAdapterListener +import it.reyboz.bustorino.adapters.StopRecyclerAdapter +import it.reyboz.bustorino.backend.Stop +import it.reyboz.bustorino.backend.gtfs.GtfsUtils +import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate import it.reyboz.bustorino.backend.gtfs.PolylineParser +import it.reyboz.bustorino.backend.utils +import it.reyboz.bustorino.data.MatoTripsDownloadWorker +import it.reyboz.bustorino.data.gtfs.MatoPattern import it.reyboz.bustorino.data.gtfs.MatoPatternWithStops -import it.reyboz.bustorino.data.gtfs.PatternStop -import org.osmdroid.tileprovider.tilesource.OnlineTileSourceBase +import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops +import it.reyboz.bustorino.map.BusInfoWindow +import it.reyboz.bustorino.map.BusPositionUtils +import it.reyboz.bustorino.map.CustomInfoWindow.TouchResponder +import it.reyboz.bustorino.map.MapViewModel +import it.reyboz.bustorino.map.MarkerUtils +import it.reyboz.bustorino.viewmodels.MQTTPositionsViewModel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.osmdroid.config.Configuration import org.osmdroid.tileprovider.tilesource.TileSourceFactory +import org.osmdroid.util.BoundingBox import org.osmdroid.util.GeoPoint -import org.osmdroid.util.MapTileIndex import org.osmdroid.views.MapView +import org.osmdroid.views.overlay.FolderOverlay +import org.osmdroid.views.overlay.Marker import org.osmdroid.views.overlay.Polyline +import org.osmdroid.views.overlay.advancedpolyline.MonochromaticPaintList -class LinesDetailFragment() : Fragment() { - - private lateinit var lineID: String +class LinesDetailFragment() : ScreenBaseFragment() { + private lateinit var lineID: String private lateinit var patternsSpinner: Spinner private var patternsAdapter: ArrayAdapter? = null - private var patternsSpinnerState: Parcelable? = null + //private var patternsSpinnerState: Parcelable? = null private lateinit var currentPatterns: List - private lateinit var gtfsStopsForCurrentPattern: List private lateinit var map: MapView - private lateinit var viewingPattern: MatoPatternWithStops + private var viewingPattern: MatoPatternWithStops? = null + + private val viewModel: LinesViewModel by viewModels() + private val mapViewModel: MapViewModel by viewModels() + private var firstInit = true + private var pausedFragment = false + private lateinit var switchButton: ImageButton + private lateinit var stopsRecyclerView: RecyclerView + //adapter for recyclerView + private val stopAdapterListener= object : StopAdapterListener { + override fun onTappedStop(stop: Stop?) { + + if(viewModel.shouldShowMessage) { + Toast.makeText(context, R.string.long_press_stop_4_options, Toast.LENGTH_SHORT).show() + viewModel.shouldShowMessage=false + } + stop?.let { + fragmentListener?.requestArrivalsForStopID(it.ID) + } + if(stop == null){ + Log.e(DEBUG_TAG,"Passed wrong stop") + } + if(fragmentListener == null){ + Log.e(DEBUG_TAG, "Fragment listener is null") + } + } - private lateinit var viewModel: LinesViewModel + override fun onLongPressOnStop(stop: Stop?): Boolean { + TODO("Not yet implemented") + } - private var polyline = Polyline(); - private var stopPosList = ArrayList() + } - companion object { - private const val LINEID_KEY="lineID" - fun newInstance() = LinesDetailFragment() - const val DEBUG_TAG="LinesDetailFragment" - fun makeArgs(lineID: String): Bundle{ - val b = Bundle() - b.putString(LINEID_KEY, lineID) - return b - } - private const val DEFAULT_CENTER_LAT = 45.0708 - private const val DEFAULT_CENTER_LON = 7.6858 + private var polyline: Polyline? = null + //private var stopPosList = ArrayList() + + private lateinit var stopsOverlay: FolderOverlay + //fragment actions + private lateinit var fragmentListener: CommonFragmentListener + + private val stopTouchResponder = TouchResponder { stopID, stopName -> + Log.d(DEBUG_TAG, "Asked to show arrivals for stop ID: $stopID") + fragmentListener.requestArrivalsForStopID(stopID) } + private var showOnTopOfLine = true + private var recyclerInitDone = false + + //position of live markers + private val busPositionMarkersByTrip = HashMap() + private var busPositionsOverlay = FolderOverlay() + private val tripMarkersAnimators = HashMap() + private val liveBusViewModel: MQTTPositionsViewModel by viewModels() + @SuppressLint("SetTextI18n") override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { val rootView = inflater.inflate(R.layout.fragment_lines_detail, container, false) lineID = requireArguments().getString(LINEID_KEY, "") + switchButton = rootView.findViewById(R.id.switchImageButton) + stopsRecyclerView = rootView.findViewById(R.id.patternStopsRecyclerView) + + val titleTextView = rootView.findViewById(R.id.titleTextView) + + titleTextView.text = getString(R.string.line)+" "+GtfsUtils.getLineNameFromGtfsID(lineID) patternsSpinner = rootView.findViewById(R.id.patternsSpinner) patternsAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, ArrayList()) patternsSpinner.adapter = patternsAdapter - map = rootView.findViewById(R.id.lineMap) - val USGS_SAT: OnlineTileSourceBase = object : OnlineTileSourceBase( - "USGS National Map Sat", - 0, - 15, - 256, - "", - arrayOf("https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryTopo/MapServer/tile/"), - "USGS" - ) { - override fun getTileURLString(pMapTileIndex: Long): String { - return baseUrl + MapTileIndex.getZoom(pMapTileIndex) + "/" + MapTileIndex.getY(pMapTileIndex) + "/" + MapTileIndex.getX( - pMapTileIndex - ) - } - } - map.setTileSource(TileSourceFactory.MAPNIK) - /* - object : OnlineTileSourceBase("USGS Topo", 0, 18, 256, "", - arrayOf("https://basemap.nationalmap.gov/ArcGIS/rest/services/USGSTopo/MapServer/tile/" )) { - override fun getTileURLString(pMapTileIndex: Long) : String{ - return baseUrl + - MapTileIndex.getZoom(pMapTileIndex)+"/" + MapTileIndex.getY(pMapTileIndex) + - "/" + MapTileIndex.getX(pMapTileIndex)+ mImageFilenameEnding; - } - } - */ - //map.setTilesScaledToDpi(true); - //map.setTilesScaledToDpi(true); - map.setFlingEnabled(true) - map.setUseDataConnection(true) + initializeMap(rootView) + + initializeRecyclerView() - // add ability to zoom with 2 fingers - map.setMultiTouchControls(true) - map.minZoomLevel = 10.0 + switchButton.setOnClickListener{ + if(map.visibility == View.VISIBLE){ + map.visibility = View.GONE + stopsRecyclerView.visibility = View.VISIBLE - //map controller setup - val mapController = map.controller - mapController.setZoom(12.0) - mapController.setCenter(GeoPoint(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON)) - map.invalidate() + viewModel.setMapShowing(false) + liveBusViewModel.stopPositionsListening() + switchButton.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.ic_map_white_30)) + } else{ + stopsRecyclerView.visibility = View.GONE + map.visibility = View.VISIBLE + viewModel.setMapShowing(true) + liveBusViewModel.requestPosUpdates(lineID) + switchButton.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.ic_list_30)) + } + } + viewModel.setRouteIDQuery(lineID) viewModel.patternsWithStopsByRouteLiveData.observe(viewLifecycleOwner){ patterns -> savePatternsToShow(patterns) } - - /* We have the pattern and the stops here, time to display them */ viewModel.stopsForPatternLiveData.observe(viewLifecycleOwner) { stops -> - Log.d(DEBUG_TAG, "Got the stops: ${stops.map { s->s.gtfsID }}}") - - val pattern = viewingPattern.pattern - - val pointsList = PolylineParser.decodePolyline(pattern.patternGeometryPoly, pattern.patternGeometryLength) - //val polyLine=Polyline(map) - //polyLine.setPoints(pointsList) - //save points - if(map.overlayManager.contains(polyline)){ - map.overlayManager.remove(polyline) + if(map.visibility ==View.VISIBLE) + showPatternWithStopsOnMap(stops) + else{ + if(stopsRecyclerView.visibility==View.VISIBLE) + showStopsAsList(stops) } - polyline = Polyline(map) - polyline.setPoints(pointsList) - - map.overlayManager.add(polyline) - map.controller.animateTo(pointsList[0]) - map.invalidate() } - - viewModel.setRouteIDQuery(lineID) + if(pausedFragment && viewModel.selectedPatternLiveData.value!=null){ + val patt = viewModel.selectedPatternLiveData.value!! + Log.d(DEBUG_TAG, "Recreating views on resume, setting pattern: ${patt.pattern.code}") + showPattern(patt) + pausedFragment = false + } Log.d(DEBUG_TAG,"Data ${viewModel.stopsForPatternLiveData.value}") @@ -147,7 +200,19 @@ patternsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected(p0: AdapterView<*>?, p1: View?, position: Int, p3: Long) { val patternWithStops = currentPatterns.get(position) + //viewModel.setPatternToDisplay(patternWithStops) setPatternAndReqStops(patternWithStops) + + Log.d(DEBUG_TAG, "item Selected, cleaning bus markers") + if(map?.visibility == View.VISIBLE) { + busPositionsOverlay.closeAllInfoWindows() + busPositionsOverlay.items.clear() + busPositionMarkersByTrip.clear() + + stopAnimations() + tripMarkersAnimators.clear() + liveBusViewModel.retriggerPositionUpdate() + } } override fun onNothingSelected(p0: AdapterView<*>?) { @@ -155,12 +220,127 @@ } + //live bus positions + liveBusViewModel.updatesWithTripAndPatterns.observe(viewLifecycleOwner){ + if(map.visibility == View.GONE || viewingPattern ==null){ + //DO NOTHING + return@observe + } + //filter buses with direction, show those only with the same direction + val outmap = HashMap>() + val currentPattern = viewingPattern!!.pattern + val numUpds = it.entries.size + Log.d(DEBUG_TAG, "Got $numUpds updates, current pattern is: ${currentPattern.name}, directionID: ${currentPattern.directionId}") + val patternsDirections = HashMap() + for((tripId, pair) in it.entries){ + + if(pair.second!=null && pair.second?.pattern !=null){ + val dir = pair.second?.pattern?.directionId + if(dir !=null && dir == currentPattern.directionId){ + outmap.set(tripId, pair) + } + patternsDirections.set(tripId,if (dir!=null) dir else -10) + } else{ + outmap[tripId] = pair + //Log.d(DEBUG_TAG, "No pattern for tripID: $tripId") + patternsDirections.set(tripId, -10) + } + } + Log.d(DEBUG_TAG, " Filtered updates are ${outmap.keys.size}") // Original updates directs: $patternsDirections\n + updateBusPositionsInMap(outmap) + } + + //download missing tripIDs + liveBusViewModel.tripsGtfsIDsToQuery.observe(viewLifecycleOwner){ + //gtfsPosViewModel.downloadTripsFromMato(dat); + MatoTripsDownloadWorker.downloadTripsFromMato( + it, requireContext().applicationContext, + "BusTO-MatoTripDownload" + ) + } + + return rootView } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - viewModel = ViewModelProvider(this).get(LinesViewModel::class.java) + private fun initializeMap(rootView : View){ + val ctx = requireContext().applicationContext + Configuration.getInstance().load(ctx, PreferenceManager.getDefaultSharedPreferences(ctx)) + + map = rootView.findViewById(R.id.lineMap) + map.let { + it.setTileSource(TileSourceFactory.MAPNIK) + /* + object : OnlineTileSourceBase("USGS Topo", 0, 18, 256, "", + arrayOf("https://basemap.nationalmap.gov/ArcG IS/rest/services/USGSTopo/MapServer/tile/" )) { + override fun getTileURLString(pMapTileIndex: Long) : String{ + return baseUrl + + MapTileIndex.getZoom(pMapTileIndex)+"/" + MapTileIndex.getY(pMapTileIndex) + + "/" + MapTileIndex.getX(pMapTileIndex)+ mImageFilenameEnding; + } + } + */ + stopsOverlay = FolderOverlay() + busPositionsOverlay = FolderOverlay() + //map.setTilesScaledToDpi(true); + //map.setTilesScaledToDpi(true); + it.setFlingEnabled(true) + it.setUseDataConnection(true) + + // add ability to zoom with 2 fingers + it.setMultiTouchControls(true) + it.minZoomLevel = 11.0 + + //map controller setup + val mapController = it.controller + var zoom = 12.0 + var centerMap = GeoPoint(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON) + if(mapViewModel.currentLat.value!=MapViewModel.INVALID) { + Log.d(DEBUG_TAG, "mapViewModel posi: ${mapViewModel.currentLat.value}, ${mapViewModel.currentLong.value}"+ + " zoom ${mapViewModel.currentZoom.value}") + zoom = mapViewModel.currentZoom.value!! + centerMap = GeoPoint(mapViewModel.currentLat.value!!, mapViewModel.currentLong.value!!) + /*viewLifecycleOwner.lifecycleScope.launch { + delay(100) + Log.d(DEBUG_TAG, "zooming back to point") + controller.animateTo(GeoPoint(mapViewModel.currentLat.value!!, mapViewModel.currentLong.value!!), + mapViewModel.currentZoom.value!!,null,null) + //controller.setCenter(GeoPoint(mapViewModel.currentLat.value!!, mapViewModel.currentLong.value!!)) + //controller.setZoom(mapViewModel.currentZoom.value!!) + + */ + + } + mapController.setZoom(zoom) + mapController.setCenter(centerMap) + Log.d(DEBUG_TAG, "Initializing map, first init $firstInit") + //map.invalidate() + + it.overlayManager.add(stopsOverlay) + it.overlayManager.add(busPositionsOverlay) + + zoomToCurrentPattern() + firstInit = false + + } + + + } + + override fun onAttach(context: Context) { + super.onAttach(context) + if(context is CommonFragmentListener){ + fragmentListener = context + } else throw RuntimeException("$context must implement CommonFragmentListener") + + fragmentListener.readyGUIfor(FragmentKind.LINES) + } + + + private fun stopAnimations(){ + for(anim in tripMarkersAnimators.values){ + anim.cancel() + } } private fun savePatternsToShow(patterns: List){ @@ -171,25 +351,375 @@ it.addAll(currentPatterns.map { p->"${p.pattern.directionId} - ${p.pattern.headsign}" }) it.notifyDataSetChanged() } - - val pos = patternsSpinner.selectedItemPosition - //might be possible that the selectedItem is different (larger than list size) - if(pos!= AdapterView.INVALID_POSITION && pos >= 0 && (pos < currentPatterns.size)){ - val p = currentPatterns[pos] - Log.d(LinesFragment.DEBUG_TAG, "Setting patterns with pos $pos and p gtfsID ${p.pattern.code}") - setPatternAndReqStops(currentPatterns[pos]) + viewingPattern?.let { + showPattern(it) } - Log.d(DEBUG_TAG, "Patterns changed") } + /** + * Called when the position of the spinner is updated + */ private fun setPatternAndReqStops(patternWithStops: MatoPatternWithStops){ Log.d(DEBUG_TAG, "Requesting stops for pattern ${patternWithStops.pattern.code}") - gtfsStopsForCurrentPattern = patternWithStops.stopsIndices.sortedBy { i-> i.order } + viewModel.selectedPatternLiveData.value = patternWithStops + viewModel.currentPatternStops.value = patternWithStops.stopsIndices.sortedBy { i-> i.order } viewingPattern = patternWithStops viewModel.requestStopsForPatternWithStops(patternWithStops) } + private fun showPattern(patternWs: MatoPatternWithStops){ + Log.d(DEBUG_TAG, "Finding pattern to show: ${patternWs.pattern.code}") + var pos = -2 + val code = patternWs.pattern.code.trim() + for(k in currentPatterns.indices){ + if(currentPatterns[k].pattern.code.trim() == code){ + pos = k + break + } + } + Log.d(DEBUG_TAG, "Found pattern $code in position: $pos") + if(pos>=0) + patternsSpinner.setSelection(pos) + //set pattern + setPatternAndReqStops(patternWs) + } + + private fun zoomToCurrentPattern(){ + var pointsList: List + if(viewingPattern==null) { + Log.e(DEBUG_TAG, "asked to zoom to pattern but current viewing pattern is null") + if(polyline!=null) + pointsList = polyline!!.actualPoints + else { + Log.d(DEBUG_TAG, "The polyline is null") + return + } + }else{ + val pattern = viewingPattern!!.pattern + + pointsList = PolylineParser.decodePolyline(pattern.patternGeometryPoly, pattern.patternGeometryLength) + } + + var maxLat = -4000.0 + var minLat = -4000.0 + var minLong = -4000.0 + var maxLong = -4000.0 + for (p in pointsList){ + // get max latitude + if(maxLat == -4000.0) + maxLat = p.latitude + else if (maxLat < p.latitude) maxLat = p.latitude + // find min latitude + if (minLat == -4000.0) + minLat = p.latitude + else if (minLat > p.latitude) minLat = p.latitude + if(maxLong == -4000.0 || maxLong < p.longitude ) + maxLong = p.longitude + if (minLong == -4000.0 || minLong > p.longitude) + minLong = p.longitude + } + + val del = 0.008 + //map.controller.c + Log.d(DEBUG_TAG, "Setting limits of bounding box of line: $minLat -> $maxLat, $minLong -> $maxLong") + map.zoomToBoundingBox(BoundingBox(maxLat+del, maxLong+del, minLat-del, minLong-del), false) + } + + private fun showPatternWithStopsOnMap(stops: List){ + Log.d(DEBUG_TAG, "Got the stops: ${stops.map { s->s.gtfsID }}}") + if(viewingPattern==null || map == null) return + + val pattern = viewingPattern!!.pattern + + val pointsList = PolylineParser.decodePolyline(pattern.patternGeometryPoly, pattern.patternGeometryLength) + + var maxLat = -4000.0 + var minLat = -4000.0 + var minLong = -4000.0 + var maxLong = -4000.0 + for (p in pointsList){ + // get max latitude + if(maxLat == -4000.0) + maxLat = p.latitude + else if (maxLat < p.latitude) maxLat = p.latitude + // find min latitude + if (minLat == -4000.0) + minLat = p.latitude + else if (minLat > p.latitude) minLat = p.latitude + if(maxLong == -4000.0 || maxLong < p.longitude ) + maxLong = p.longitude + if (minLong == -4000.0 || minLong > p.longitude) + minLong = p.longitude + } + //val polyLine=Polyline(map) + //polyLine.setPoints(pointsList) + //save points + if(map.overlayManager.contains(polyline)){ + map.overlayManager.remove(polyline) + } + polyline = Polyline(map, false) + polyline!!.setPoints(pointsList) + //polyline.color = ContextCompat.getColor(context!!,R.color.brown_vd) + polyline!!.infoWindow = null + val paint = Paint() + paint.color = ContextCompat.getColor(requireContext(),R.color.line_drawn_poly) + paint.isAntiAlias = true + paint.strokeWidth = 16f + paint.style = Paint.Style.FILL_AND_STROKE + paint.strokeJoin = Paint.Join.ROUND + paint.strokeCap = Paint.Cap.ROUND + polyline!!.outlinePaintLists.add(MonochromaticPaintList(paint)) + + map.overlayManager.add(0,polyline!!) + + stopsOverlay.closeAllInfoWindows() + stopsOverlay.items.clear() + val stopIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ball) + + for(s in stops){ + val gp = if (showOnTopOfLine) + findOptimalPosition(s,pointsList) + else GeoPoint(s.latitude!!,s.longitude!!) + + val marker = MarkerUtils.makeMarker( + gp, s.ID, s.stopDefaultName, + s.routesThatStopHereToString(), + map,stopTouchResponder, stopIcon, + R.layout.linedetail_stop_infowindow, + R.color.line_drawn_poly + ) + marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER) + stopsOverlay.add(marker) + } + //POINTS LIST IS NOT IN ORDER ANY MORE + //if(!map.overlayManager.contains(stopsOverlay)){ + // map.overlayManager.add(stopsOverlay) + //} + polyline!!.setOnClickListener(Polyline.OnClickListener { polyline, mapView, eventPos -> + Log.d(DEBUG_TAG, "clicked") + true + }) + + //map.controller.zoomToB//#animateTo(pointsList[0]) + val del = 0.008 + map.zoomToBoundingBox(BoundingBox(maxLat+del, maxLong+del, minLat-del, minLong-del), true) + //map.invalidate() + } + + private fun initializeRecyclerView(){ + val llManager = LinearLayoutManager(context) + llManager.orientation = LinearLayoutManager.VERTICAL + + stopsRecyclerView.layoutManager = llManager + } + private fun showStopsAsList(stops: List){ + + Log.d(DEBUG_TAG, "Setting stops from: "+viewModel.currentPatternStops.value) + val orderBy = viewModel.currentPatternStops.value!!.withIndex().associate{it.value.stopGtfsId to it.index} + val stopsSorted = stops.sortedBy { s -> orderBy[s.gtfsID] } + val numStops = stopsSorted.size + Log.d(DEBUG_TAG, "RecyclerView adapter is: ${stopsRecyclerView.adapter}") + + val setNewAdapter = true + if(setNewAdapter){ + stopsRecyclerView.adapter = StopRecyclerAdapter( + stopsSorted, stopAdapterListener, StopRecyclerAdapter.Use.LINES, + NameCapitalize.FIRST + ) + + } + + + + } + + + /** + * Remove bus marker from overlay associated with tripID + */ + private fun removeBusMarker(tripID: String){ + if(!busPositionMarkersByTrip.containsKey(tripID)){ + Log.e(DEBUG_TAG, "Asked to remove veh with tripID $tripID but it's supposedly not shown") + return + } + val marker = busPositionMarkersByTrip[tripID] + busPositionsOverlay.remove(marker) + busPositionMarkersByTrip.remove(tripID) + + val animator = tripMarkersAnimators[tripID] + animator?.let{ + it.cancel() + tripMarkersAnimators.remove(tripID) + } + + } + + private fun showPatternWithStop(patternId: String){ + //var index = 0 + Log.d(DEBUG_TAG, "Showing pattern with code $patternId ") + for (i in currentPatterns.indices){ + val pattStop = currentPatterns[i] + if(pattStop.pattern.code == patternId){ + Log.d(DEBUG_TAG, "Pattern found in position $i") + //setPatternAndReqStops(pattStop) + patternsSpinner.setSelection(i) + break + } + } + } + /** + * draw the position of the buses in the map. Copied from MapFragment + */ + private fun updateBusPositionsInMap(tripsPatterns: java.util.HashMap> + ) { + //Log.d(MapFragment.DEBUG_TAG, "Updating positions of the buses") + //if(busPositionsOverlay == null) busPositionsOverlay = new FolderOverlay(); + val noPatternsTrips = ArrayList() + for (tripID in tripsPatterns.keys) { + val (update, tripWithPatternStops) = tripsPatterns[tripID] ?: continue + + var marker: Marker? = null + //check if Marker is already created + if (busPositionMarkersByTrip.containsKey(tripID)) { + + //check if the trip direction ID is the same, if not remove + if(tripWithPatternStops?.pattern != null && + tripWithPatternStops.pattern.directionId != viewingPattern?.pattern?.directionId){ + removeBusMarker(tripID) + + } else { + //need to change the position of the marker + marker = busPositionMarkersByTrip.get(tripID)!! + BusPositionUtils.updateBusPositionMarker(map, marker, update, tripMarkersAnimators, false) + // Set the pattern to add the info + if (marker.infoWindow != null && marker.infoWindow is BusInfoWindow) { + val window = marker.infoWindow as BusInfoWindow + if (window.pattern == null && tripWithPatternStops != null) { + //Log.d(DEBUG_TAG, "Update pattern for trip: "+tripID); + window.setPatternAndDraw(tripWithPatternStops.pattern) + } + } + } + } else { + //marker is not there, need to make it + //if (mapView == null) Log.e(MapFragment.DEBUG_TAG, "Creating marker with null map, things will explode") + marker = Marker(map) + + //String route = GtfsUtils.getLineNameFromGtfsID(update.getRouteID()); + val mdraw = ResourcesCompat.getDrawable(getResources(), R.drawable.map_bus_position_icon, null)!! + //mdraw.setBounds(0,0,28,28); + + marker.icon = mdraw + var markerPattern: MatoPattern? = null + if (tripWithPatternStops != null) { + if (tripWithPatternStops.pattern != null) + markerPattern = tripWithPatternStops.pattern + } + marker.infoWindow = BusInfoWindow(map, update, markerPattern, true) { + // set pattern to show + if(it!=null) + showPatternWithStop(it.code) + } + //marker.infoWindow as BusInfoWindow + marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER) + BusPositionUtils.updateBusPositionMarker(map,marker, update, tripMarkersAnimators,true) + // the overlay is null when it's not attached yet? + // cannot recreate it because it becomes null very soon + // if(busPositionsOverlay == null) busPositionsOverlay = new FolderOverlay(); + //save the marker + if (busPositionsOverlay != null) { + busPositionsOverlay.add(marker) + busPositionMarkersByTrip.put(tripID, marker) + } + } + } + if (noPatternsTrips.size > 0) { + Log.i(DEBUG_TAG, "These trips have no matching pattern: $noPatternsTrips") + } + } + + override fun onResume() { + super.onResume() + Log.d(DEBUG_TAG, "Resetting paused from onResume") + pausedFragment = false + + liveBusViewModel.requestPosUpdates(GtfsUtils.getLineNameFromGtfsID(lineID)) + + if(mapViewModel.currentLat.value!=MapViewModel.INVALID) { + Log.d(DEBUG_TAG, "mapViewModel posi: ${mapViewModel.currentLat.value}, ${mapViewModel.currentLong.value}"+ + " zoom ${mapViewModel.currentZoom.value}") + val controller = map.controller + viewLifecycleOwner.lifecycleScope.launch { + delay(100) + Log.d(DEBUG_TAG, "zooming back to point") + controller.animateTo(GeoPoint(mapViewModel.currentLat.value!!, mapViewModel.currentLong.value!!), + mapViewModel.currentZoom.value!!,null,null) + //controller.setCenter(GeoPoint(mapViewModel.currentLat.value!!, mapViewModel.currentLong.value!!)) + //controller.setZoom(mapViewModel.currentZoom.value!!) + } + //controller.setZoom() + } + } + + override fun onPause() { + super.onPause() + liveBusViewModel.stopPositionsListening() + pausedFragment = true + //save map + val center = map.mapCenter + mapViewModel.currentLat.value = center.latitude + mapViewModel.currentLong.value = center.longitude + mapViewModel.currentZoom.value = map.zoomLevel.toDouble() + } + + override fun getBaseViewForSnackBar(): View? { + return null + } + + companion object { + private const val LINEID_KEY="lineID" + fun newInstance() = LinesDetailFragment() + const val DEBUG_TAG="LinesDetailFragment" + + fun makeArgs(lineID: String): Bundle{ + val b = Bundle() + b.putString(LINEID_KEY, lineID) + return b + } + @JvmStatic + private fun findOptimalPosition(stop: Stop, pointsList: MutableList): GeoPoint{ + if(stop.latitude==null || stop.longitude ==null|| pointsList.isEmpty()) + throw IllegalArgumentException() + val sLat = stop.latitude!! + val sLong = stop.longitude!! + if(pointsList.size < 2) + return pointsList[0] + pointsList.sortBy { utils.measuredistanceBetween(sLat, sLong, it.latitude, it.longitude) } + + val p1 = pointsList[0] + val p2 = pointsList[1] + if (p1.longitude == p2.longitude){ + //Log.e(DEBUG_TAG, "Same longitude") + return GeoPoint(sLat, p1.longitude) + } else if (p1.latitude == p2.latitude){ + //Log.d(DEBUG_TAG, "Same latitude") + return GeoPoint(p2.latitude,sLong) + } + + val m = (p1.latitude - p2.latitude) / (p1.longitude - p2.longitude) + val minv = (p1.longitude-p2.longitude)/(p1.latitude - p2.latitude) + val cR = p1.latitude - p1.longitude * m + + val longNew = (minv * sLong + sLat -cR ) / (m+minv) + val latNew = (m*longNew + cR) + //Log.d(DEBUG_TAG,"Stop ${stop.ID} old pos: ($sLat, $sLong), new pos ($latNew,$longNew)") + return GeoPoint(latNew,longNew) + } + + private const val DEFAULT_CENTER_LAT = 45.12 + private const val DEFAULT_CENTER_LON = 7.6858 + } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/LinesFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/LinesFragment.kt --- a/app/src/main/java/it/reyboz/bustorino/fragments/LinesFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/LinesFragment.kt @@ -45,7 +45,7 @@ fun newInstance(){ LinesFragment() } - const val DEBUG_TAG="BusTO-LinesFragment" + private const val DEBUG_TAG="BusTO-LinesFragment" const val FRAGMENT_TAG="LinesFragment" val patternStopsComparator = PatternWithStopsSorter() diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/LinesGridShowingFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/LinesGridShowingFragment.kt new file mode 100644 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/fragments/LinesGridShowingFragment.kt @@ -0,0 +1,277 @@ +package it.reyboz.bustorino.fragments + +import android.content.Context +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.animation.Animation +import android.view.animation.LinearInterpolator +import android.view.animation.RotateAnimation +import android.widget.ImageView +import android.widget.TextView +import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.RecyclerView +import it.reyboz.bustorino.R +import it.reyboz.bustorino.adapters.RouteAdapter +import it.reyboz.bustorino.backend.utils +import it.reyboz.bustorino.data.gtfs.GtfsRoute +import it.reyboz.bustorino.middleware.AutoFitGridLayoutManager +import it.reyboz.bustorino.util.LinesNameSorter +import it.reyboz.bustorino.util.ViewUtils +import it.reyboz.bustorino.viewmodels.LinesGridShowingViewModel + + +class LinesGridShowingFragment : ScreenBaseFragment() { + + + + private val viewModel: LinesGridShowingViewModel by viewModels() + //private lateinit var gridLayoutManager: AutoFitGridLayoutManager + + private lateinit var urbanRecyclerView: RecyclerView + private lateinit var extraurbanRecyclerView: RecyclerView + private lateinit var touristRecyclerView: RecyclerView + + private lateinit var urbanLinesTitle: TextView + private lateinit var extrurbanLinesTitle: TextView + private lateinit var touristLinesTitle: TextView + + + private var routesByAgency = HashMap>() + /*hashMapOf( + AG_URBAN to ArrayList(), + AG_EXTRAURB to ArrayList(), + AG_TOUR to ArrayList() + )*/ + + private lateinit var fragmentListener: CommonFragmentListener + + private val linesNameSorter = LinesNameSorter() + private val linesComparator = Comparator { a,b -> + return@Comparator linesNameSorter.compare(a.shortName, b.shortName) + } + + private val routeClickListener = RouteAdapter.onItemClick { + fragmentListener.showLineOnMap(it.gtfsId) + } + private val arrows = HashMap() + private val durations = HashMap() + private var openRecyclerView = "AG_URBAN" + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val rootView = inflater.inflate(R.layout.fragment_lines_grid, container, false) + + urbanRecyclerView = rootView.findViewById(R.id.urbanLinesRecyclerView) + extraurbanRecyclerView = rootView.findViewById(R.id.extraurbanLinesRecyclerView) + touristRecyclerView = rootView.findViewById(R.id.touristLinesRecyclerView) + + urbanLinesTitle = rootView.findViewById(R.id.urbanLinesTitleView) + extrurbanLinesTitle = rootView.findViewById(R.id.extraurbanLinesTitleView) + touristLinesTitle = rootView.findViewById(R.id.touristLinesTitleView) + + arrows[AG_URBAN] = rootView.findViewById(R.id.arrowUrb) + arrows[AG_TOUR] = rootView.findViewById(R.id.arrowTourist) + arrows[AG_EXTRAURB] = rootView.findViewById(R.id.arrowExtraurban) + //show urban expanded by default + + val recViews = listOf(urbanRecyclerView, extraurbanRecyclerView, touristRecyclerView) + for (recyView in recViews) { + val gridLayoutManager = AutoFitGridLayoutManager( + requireContext().applicationContext, + (utils.convertDipToPixels(context, COLUMN_WIDTH_DP.toFloat())).toInt() + ) + recyView.layoutManager = gridLayoutManager + } + + viewModel.routesLiveData.observe(viewLifecycleOwner){ + //routesList = ArrayList(it) + //routesList.sortWith(linesComparator) + routesByAgency.clear() + + for(route in it){ + val agency = route.agencyID + if(!routesByAgency.containsKey(agency)){ + routesByAgency[agency] = ArrayList() + } + routesByAgency[agency]?.add(route) + + } + + + //val adapter = RouteOnlyLineAdapter(routesByAgency.map { route-> route.shortName }) + //zip agencies and recyclerviews + Companion.AGENCIES.zip(recViews) { ag, recView -> + routesByAgency[ag]?.let { routeList -> + routeList.sortWith(linesComparator) + //val adapter = RouteOnlyLineAdapter(it.map { rt -> rt.shortName }) + val adapter = RouteAdapter(routeList,routeClickListener) + recView.adapter = adapter + durations[ag] = if(routeList.size < 20) ViewUtils.DEF_DURATION else 1000 + } + } + + } + + //onClicks + urbanLinesTitle.setOnClickListener { + if(openRecyclerView!=""&& openRecyclerView!= AG_URBAN){ + openCloseRecyclerView(openRecyclerView) + openCloseRecyclerView(AG_URBAN) + } + } + extrurbanLinesTitle.setOnClickListener { + if(openRecyclerView!=""&& openRecyclerView!= AG_EXTRAURB){ + openCloseRecyclerView(openRecyclerView) + openCloseRecyclerView(AG_EXTRAURB) + + } + } + touristLinesTitle.setOnClickListener { + if(openRecyclerView!="" && openRecyclerView!= AG_TOUR) { + openCloseRecyclerView(openRecyclerView) + openCloseRecyclerView(AG_TOUR) + } + } + + return rootView + } + + private fun openCloseRecyclerView(agency: String){ + val recyclerView = when(agency){ + AG_TOUR -> touristRecyclerView + AG_EXTRAURB -> extraurbanRecyclerView + AG_URBAN -> urbanRecyclerView + else -> throw IllegalArgumentException("$DEBUG_TAG: Agency Invalid") + } + val expandedLiveData = when(agency){ + AG_TOUR -> viewModel.isTouristExpanded + AG_URBAN -> viewModel.isUrbanExpanded + AG_EXTRAURB -> viewModel.isExtraUrbanExpanded + else -> throw IllegalArgumentException("$DEBUG_TAG: Agency Invalid") + } + val duration = durations[agency] + val arrow = arrows[agency] + val durArrow = if(duration == null || duration==ViewUtils.DEF_DURATION) 500 else duration + if(duration!=null&&arrow!=null) + when (recyclerView.visibility){ + View.GONE -> { + Log.d(DEBUG_TAG, "Open recyclerview $agency") + //val a =ViewUtils.expand(recyclerView, duration, 0) + recyclerView.visibility = View.VISIBLE + expandedLiveData.value = true + Log.d(DEBUG_TAG, "Arrow for $agency has rotation: ${arrow.rotation}") + + setOpen(arrow, true) + //arrow.startAnimation(rotateArrow(true,durArrow)) + openRecyclerView = agency + + } + View.VISIBLE -> { + Log.d(DEBUG_TAG, "Close recyclerview $agency") + //ViewUtils.collapse(recyclerView, duration) + recyclerView.visibility = View.GONE + expandedLiveData.value = false + //arrow.rotation = 90f + Log.d(DEBUG_TAG, "Arrow for $agency has rotation ${arrow.rotation} pre-rotate") + setOpen(arrow, false) + //arrow.startAnimation(rotateArrow(false,durArrow)) + openRecyclerView = "" + } + View.INVISIBLE -> { + TODO() + } + } + } + + override fun onAttach(context: Context) { + super.onAttach(context) + if(context is CommonFragmentListener){ + fragmentListener = context + } else throw RuntimeException("$context must implement CommonFragmentListener") + + fragmentListener.readyGUIfor(FragmentKind.LINES) + } + + override fun getBaseViewForSnackBar(): View? { + return null + } + + override fun onResume() { + super.onResume() + viewModel.isUrbanExpanded.value?.let { + if(it) { + urbanRecyclerView.visibility = View.VISIBLE + arrows[AG_URBAN]?.rotation= 90f + openRecyclerView = AG_URBAN + Log.d(DEBUG_TAG, "RecyclerView gtt:U is expanded") + } + else { + urbanRecyclerView.visibility = View.GONE + arrows[AG_URBAN]?.rotation= 0f + } + } + viewModel.isTouristExpanded.value?.let { + val recview = touristRecyclerView + if(it) { + recview.visibility = View.VISIBLE + arrows[AG_TOUR]?.rotation=90f + openRecyclerView = AG_TOUR + } else { + recview.visibility = View.GONE + arrows[AG_TOUR]?.rotation= 0f + } + } + viewModel.isExtraUrbanExpanded.value?.let { + val recview = extraurbanRecyclerView + if(it) { + openRecyclerView = AG_EXTRAURB + recview.visibility = View.VISIBLE + arrows[AG_EXTRAURB]?.rotation=90f + } else { + recview.visibility = View.GONE + arrows[AG_EXTRAURB]?.rotation=0f + } + } + } + + + companion object { + private const val COLUMN_WIDTH_DP=200 + private const val AG_URBAN = "gtt:U" + private const val AG_EXTRAURB ="gtt:E" + private const val AG_TOUR ="gtt:T" + private const val DEBUG_TAG ="BusTO-LinesGridFragment" + + const val FRAGMENT_TAG = "LinesGridShowingFragment" + + private val AGENCIES = listOf(AG_URBAN, AG_EXTRAURB, AG_TOUR) + fun newInstance() = LinesGridShowingFragment() + + @JvmStatic + fun setOpen(imageView: ImageView, value: Boolean){ + if(value) + imageView.rotation = 90f + else + imageView.rotation = 0f + } + @JvmStatic + fun rotateArrow(toOpen: Boolean, duration: Long): RotateAnimation{ + val start = if (toOpen) 0f else 90f + val stop = if(toOpen) 90f else 0f + Log.d(DEBUG_TAG, "Rotate arrow from $start to $stop") + val rotate = RotateAnimation(start, stop, Animation.RELATIVE_TO_SELF, + 0.5f, Animation.RELATIVE_TO_SELF, 0.5f) + rotate.duration = duration + rotate.interpolator = LinearInterpolator() + //rotate.fillAfter = true + rotate.fillBefore = false + return rotate + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/LinesViewModel.kt b/app/src/main/java/it/reyboz/bustorino/fragments/LinesViewModel.kt --- a/app/src/main/java/it/reyboz/bustorino/fragments/LinesViewModel.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/LinesViewModel.kt @@ -29,6 +29,12 @@ val stopsForPatternLiveData = MutableLiveData>() private val executor = Executors.newFixedThreadPool(2) + val mapShowing = MutableLiveData(true) + fun setMapShowing(yes: Boolean){ + mapShowing.value = yes + //retrigger redraw + stopsForPatternLiveData.postValue(stopsForPatternLiveData.value) + } init { val gtfsDao = GtfsDatabase.getGtfsDatabase(application).gtfsDao() gtfsRepo = GtfsRepository(gtfsDao) @@ -37,6 +43,7 @@ } + val routesGTTLiveData: LiveData> by lazy{ gtfsRepo.getLinesLiveDataForFeed("gtt") } @@ -45,6 +52,7 @@ } + fun setRouteIDQuery(routeID: String){ routeIDToSearch.value = routeID } @@ -54,6 +62,10 @@ } var shouldShowMessage = true + fun setPatternToDisplay(patternStops: MatoPatternWithStops){ + + selectedPatternLiveData.value = patternStops + } /** * Find the */ diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java --- a/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java @@ -148,7 +148,7 @@ } else { //Toast.makeText(MyActivity.this, "Scanned: " + result.getContents(), Toast.LENGTH_LONG).show(); if (getContext()!=null) - Toast.makeText(getContext().getApplicationContext(), + Toast.makeText(getContext().getApplicationContext(), R.string.no_qrcode, Toast.LENGTH_SHORT).show(); @@ -729,6 +729,12 @@ } + @Override + public void showLineOnMap(String routeGtfsId) { + //pass to activity + mListener.showLineOnMap(routeGtfsId); + } + @Override public void showMapCenteredOnStop(Stop stop) { if(mListener!=null) mListener.showMapCenteredOnStop(stop); diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java --- a/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java @@ -27,7 +27,6 @@ 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; @@ -44,12 +43,14 @@ import androidx.lifecycle.ViewModelProvider; import androidx.preference.PreferenceManager; -import it.reyboz.bustorino.backend.gtfs.GtfsPositionUpdate; -import it.reyboz.bustorino.backend.gtfs.GtfsUtils; +import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate; +import it.reyboz.bustorino.backend.mato.MQTTMatoClient; import it.reyboz.bustorino.backend.utils; +import it.reyboz.bustorino.data.MatoTripsDownloadWorker; import it.reyboz.bustorino.data.gtfs.MatoPattern; import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops; import it.reyboz.bustorino.map.*; +import it.reyboz.bustorino.viewmodels.MQTTPositionsViewModel; import org.osmdroid.api.IGeoPoint; import org.osmdroid.api.IMapController; import org.osmdroid.config.Configuration; @@ -78,7 +79,7 @@ public class MapFragment extends ScreenBaseFragment { - private static final String TAG = "Busto-MapActivity"; + //private static final String TAG = "Busto-MapActivity"; private static final String MAP_CURRENT_ZOOM_KEY = "map-current-zoom"; private static final String MAP_CENTER_LAT_KEY = "map-center-lat"; private static final String MAP_CENTER_LON_KEY = "map-center-lon"; @@ -117,7 +118,8 @@ private boolean hasMapStartFinished = false; private boolean followingLocation = false; - private MapViewModel mapViewModel ; //= new ViewModelProvider(this).get(MapViewModel.class); + //private GTFSPositionsViewModel gtfsPosViewModel; //= new ViewModelProvider(this).get(MapViewModel.class); + private MQTTPositionsViewModel positionsViewModel; private final HashMap busPositionMarkersByTrip = new HashMap<>(); private FolderOverlay busPositionsOverlay = null; @@ -270,6 +272,7 @@ .show(); }); + return root; } @@ -277,7 +280,9 @@ public void onAttach(@NonNull Context context) { super.onAttach(context); - mapViewModel = new ViewModelProvider(this).get(MapViewModel.class); + //gtfsPosViewModel = new ViewModelProvider(this).get(GTFSPositionsViewModel.class); + //viewModel + positionsViewModel = new ViewModelProvider(this).get(MQTTPositionsViewModel.class); if (context instanceof FragmentListenerMain) { listenerMain = (FragmentListenerMain) context; } else { @@ -306,6 +311,7 @@ } } tripMarkersAnimators.clear(); + positionsViewModel.stopPositionsListening(); if (stopFetcher!= null) stopFetcher.cancel(true); @@ -342,12 +348,15 @@ public void onResume() { super.onResume(); if(listenerMain!=null) listenerMain.readyGUIfor(FragmentKind.MAP); - if(mapViewModel!=null) { - mapViewModel.requestUpdates(); + if(positionsViewModel !=null) { + //gtfsPosViewModel.requestUpdates(); + positionsViewModel.requestPosUpdates(MQTTMatoClient.LINES_ALL); //mapViewModel.testCascade(); - mapViewModel.getTripsGtfsIDsToQuery().observe(this, dat -> { + positionsViewModel.getTripsGtfsIDsToQuery().observe(this, dat -> { Log.i(DEBUG_TAG, "Have these trips IDs missing from the DB, to be queried: "+dat); - mapViewModel.downloadTripsFromMato(dat); + //gtfsPosViewModel.downloadTripsFromMato(dat); + MatoTripsDownloadWorker.Companion.downloadTripsFromMato(dat,getContext().getApplicationContext(), + "BusTO-MatoTripDownload"); }); } } @@ -484,11 +493,16 @@ @SuppressLint("MissingPermission") Location userLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); + if (userLocation != null) { - mapController.setZoom(POSITION_FOUND_ZOOM); - startPoint = new GeoPoint(userLocation); - found = true; - setLocationFollowing(true); + double distan = utils.measuredistanceBetween(userLocation.getLatitude(), userLocation.getLongitude(), + DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON); + if (distan < 100_000.0) { + mapController.setZoom(POSITION_FOUND_ZOOM); + startPoint = new GeoPoint(userLocation); + found = true; + setLocationFollowing(true); + } } } if(!found){ @@ -528,14 +542,16 @@ } - if(mapViewModel!=null){ + if(positionsViewModel !=null){ //should always be the case - mapViewModel.getUpdatesWithTripAndPatterns().observe(this, data->{ + positionsViewModel.getUpdatesWithTripAndPatterns().observe(getViewLifecycleOwner(), data->{ Log.d(DEBUG_TAG, "Have "+data.size()+" trip updates, has Map start finished: "+hasMapStartFinished); if (hasMapStartFinished) updateBusPositionsInMap(data); - if(!isDetached()) - mapViewModel.requestDelayedUpdates(4000); + //if(!isDetached()) + // gtfsPosViewModel.requestDelayedUpdates(4000); }); + } else { + Log.e(DEBUG_TAG, "PositionsViewModel is null"); } map.getOverlays().add(this.busPositionsOverlay); //set map as started @@ -560,21 +576,16 @@ new AsyncStopFetcher.BoundingBoxLimit(lngFrom,lngTo,latFrom, latTo)); } - private void updateBusMarker(final Marker marker,final GtfsPositionUpdate posUpdate,@Nullable boolean justCreated){ + private void updateBusMarker(final Marker marker, final LivePositionUpdate posUpdate, @Nullable boolean justCreated){ GeoPoint position; final String updateID = posUpdate.getTripID(); if(!justCreated){ position = marker.getPosition(); if(posUpdate.getLatitude()!=position.getLatitude() || posUpdate.getLongitude()!=position.getLongitude()){ GeoPoint newpos = new GeoPoint(posUpdate.getLatitude(), posUpdate.getLongitude()); - ObjectAnimator valueAnimator = MarkerAnimation.makeMarkerAnimator(map, marker, newpos, new GeoPointInterpolator.LinearFixed(), 2500); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { - valueAnimator.setAutoCancel(true); - } else if(tripMarkersAnimators.containsKey(updateID)) { - ObjectAnimator otherAnim = tripMarkersAnimators.get(updateID); - assert otherAnim != null; - otherAnim.cancel(); - } + ObjectAnimator valueAnimator = MarkerUtils.makeMarkerAnimator( + map, marker, newpos, MarkerUtils.LINEAR_ANIMATION, 1200); + valueAnimator.setAutoCancel(true); tripMarkersAnimators.put(updateID,valueAnimator); valueAnimator.start(); } @@ -585,17 +596,18 @@ marker.setPosition(position); } - marker.setRotation(posUpdate.getBearing()*(-1.f)); + if(posUpdate.getBearing()!=null) + marker.setRotation(posUpdate.getBearing()*(-1.f)); } - private void updateBusPositionsInMap(HashMap> tripsPatterns){ + private void updateBusPositionsInMap(HashMap> tripsPatterns){ Log.d(DEBUG_TAG, "Updating positions of the buses"); //if(busPositionsOverlay == null) busPositionsOverlay = new FolderOverlay(); final ArrayList noPatternsTrips = new ArrayList<>(); for(String tripID: tripsPatterns.keySet()) { - final Pair pair = tripsPatterns.get(tripID); + final Pair pair = tripsPatterns.get(tripID); if (pair == null) continue; - final GtfsPositionUpdate update = pair.getFirst(); + final LivePositionUpdate update = pair.getFirst(); final TripAndPatternWithStops tripWithPatternStops = pair.getSecond(); @@ -624,8 +636,8 @@ R.dimen.map_icons_size, R.dimen.map_icons_size); */ - String route = GtfsUtils.getLineNameFromGtfsID(update.getRouteID()); - final Drawable mdraw = ResourcesCompat.getDrawable(getResources(),R.drawable.point_heading_icon, null); + //String route = GtfsUtils.getLineNameFromGtfsID(update.getRouteID()); + final Drawable mdraw = ResourcesCompat.getDrawable(getResources(),R.drawable.map_bus_position_icon, null); /*final Drawable mdraw = DrawableUtils.Companion.writeOnDrawable(getResources(), R.drawable.point_heading_icon, R.color.white, @@ -641,12 +653,11 @@ MatoPattern markerPattern = null; if(tripWithPatternStops != null && tripWithPatternStops.getPattern()!=null) markerPattern = tripWithPatternStops.getPattern(); - marker.setInfoWindow(new BusInfoWindow(map, update, markerPattern , () -> { - - })); + marker.setInfoWindow(new BusInfoWindow(map, update, markerPattern , false, (pattern) -> { })); + marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER); updateBusMarker(marker, update, true); - // the overlay is null when it's not attached yet? + // the overlay is null when it's not attached yet?5 // cannot recreate it because it becomes null very soon // if(busPositionsOverlay == null) busPositionsOverlay = new FolderOverlay(); //save the marker @@ -717,7 +728,7 @@ // set custom info window as info window CustomInfoWindow popup = new CustomInfoWindow(map, stopID, stopName, routesStopping, - responder); + responder, R.layout.linedetail_stop_infowindow, R.color.red_darker); marker.setInfoWindow(popup); // make the marker clickable @@ -741,7 +752,7 @@ // set its position marker.setPosition(geoPoint); - marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM); + marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER); // add to it an icon //marker.setIcon(getResources().getDrawable(R.drawable.bus_marker)); diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.java --- a/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.java @@ -59,6 +59,7 @@ import it.reyboz.bustorino.data.AppDataProvider; import it.reyboz.bustorino.data.NextGenDB.Contract.*; import it.reyboz.bustorino.adapters.SquareStopAdapter; +import it.reyboz.bustorino.middleware.AutoFitGridLayoutManager; import it.reyboz.bustorino.util.LocationCriteria; import it.reyboz.bustorino.util.StopSorterByDistance; @@ -632,43 +633,4 @@ } } - - /** - * Simple trick to get an automatic number of columns (from https://www.journaldev.com/13792/android-gridlayoutmanager-example) - * - */ - class AutoFitGridLayoutManager extends GridLayoutManager { - - private int columnWidth; - private boolean columnWidthChanged = true; - - public AutoFitGridLayoutManager(Context context, int columnWidth) { - super(context, 1); - - setColumnWidth(columnWidth); - } - - public void setColumnWidth(int newColumnWidth) { - if (newColumnWidth > 0 && newColumnWidth != columnWidth) { - columnWidth = newColumnWidth; - columnWidthChanged = true; - } - } - - @Override - public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { - if (columnWidthChanged && columnWidth > 0) { - int totalSpace; - if (getOrientation() == VERTICAL) { - totalSpace = getWidth() - getPaddingRight() - getPaddingLeft(); - } else { - totalSpace = getHeight() - getPaddingTop() - getPaddingBottom(); - } - int spanCount = Math.max(1, totalSpace / columnWidth); - setSpanCount(spanCount); - columnWidthChanged = false; - } - super.onLayoutChildren(recycler, state); - } - } } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/TestRealtimeGtfsFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/TestRealtimeGtfsFragment.kt --- a/app/src/main/java/it/reyboz/bustorino/fragments/TestRealtimeGtfsFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/TestRealtimeGtfsFragment.kt @@ -1,24 +1,19 @@ package it.reyboz.bustorino.fragments import android.os.Bundle +import android.util.Log import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Button +import android.widget.EditText import android.widget.TextView -import android.widget.Toast -import com.android.volley.Response -import com.google.transit.realtime.GtfsRealtime +import androidx.fragment.app.viewModels import it.reyboz.bustorino.R -import it.reyboz.bustorino.backend.NetworkVolleyManager -import it.reyboz.bustorino.backend.gtfs.GtfsPositionUpdate -import it.reyboz.bustorino.backend.gtfs.GtfsRtPositionsRequest +import it.reyboz.bustorino.backend.mato.MQTTMatoClient +import it.reyboz.bustorino.viewmodels.MQTTPositionsViewModel -// TODO: Rename parameter arguments, choose names that match -// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER -private const val ARG_PARAM1 = "param1" -private const val ARG_PARAM2 = "param2" /** * A simple [Fragment] subclass. @@ -30,7 +25,14 @@ private lateinit var buttonLaunch: Button private lateinit var messageTextView: TextView - private val requestListener = object: GtfsRtPositionsRequest.Companion.RequestListener{ + private var subscribed = false + private lateinit var mqttMatoClient: MQTTMatoClient + + private lateinit var lineEditText: EditText + + private val mqttViewModel: MQTTPositionsViewModel by viewModels() + + /*private val requestListener = object: GtfsRtPositionsRequest.Companion.RequestListener{ override fun onResponse(response: ArrayList?) { if (response == null) return @@ -43,6 +45,14 @@ messageTextView.text = "Entity message 0: ${position}" } + + } + */ + + private val listener = MQTTMatoClient.Companion.MQTTMatoListener{ + + messageTextView.text = "Update: ${it}" + Log.d("BUSTO-TestMQTT", "Received update $it") } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -60,16 +70,37 @@ val rootView= inflater.inflate(R.layout.fragment_test_realtime_gtfs, container, false) buttonLaunch = rootView.findViewById(R.id.btn_download_data) + buttonLaunch.text="Start" messageTextView = rootView.findViewById(R.id.gtfsMessageTextView) + lineEditText = rootView.findViewById(R.id.lineEditText) + + mqttViewModel.updatesWithTripAndPatterns.observe(viewLifecycleOwner){ + val upds = it.entries.map { it.value.first } + messageTextView.text = "$upds" + } buttonLaunch.setOnClickListener { context?.let {cont-> - val req = GtfsRtPositionsRequest( + /*val req = GtfsRtPositionsRequest( Response.ErrorListener { Toast.makeText(cont, "Error: ${it.message}",Toast.LENGTH_SHORT) }, requestListener ) NetworkVolleyManager.getInstance(cont).addToRequestQueue(req) + + */ + subscribed = if(subscribed){ + //mqttMatoClient.desubscribe(listener) + mqttViewModel.stopPositionsListening() + buttonLaunch.text="Start" + false + } else{ + //mqttMatoClient.startAndSubscribe(lineEditText.text.trim().toString(), listener) + mqttViewModel.requestPosUpdates(lineEditText.text.trim().toString()) + buttonLaunch.text="Stop" + true + } + } diff --git a/app/src/main/java/it/reyboz/bustorino/map/BusInfoWindow.kt b/app/src/main/java/it/reyboz/bustorino/map/BusInfoWindow.kt --- a/app/src/main/java/it/reyboz/bustorino/map/BusInfoWindow.kt +++ b/app/src/main/java/it/reyboz/bustorino/map/BusInfoWindow.kt @@ -18,34 +18,47 @@ package it.reyboz.bustorino.map import android.annotation.SuppressLint -import android.view.MotionEvent -import android.view.View import android.view.View.* +import android.widget.ImageView import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.marginEnd import it.reyboz.bustorino.R -import it.reyboz.bustorino.backend.gtfs.GtfsPositionUpdate +import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate import it.reyboz.bustorino.backend.gtfs.GtfsUtils -import it.reyboz.bustorino.data.gtfs.GtfsTrip +import it.reyboz.bustorino.backend.utils import it.reyboz.bustorino.data.gtfs.MatoPattern import org.osmdroid.views.MapView import org.osmdroid.views.overlay.infowindow.BasicInfoWindow @SuppressLint("ClickableViewAccessibility") class BusInfoWindow(map: MapView, - val update: GtfsPositionUpdate, + private val routeName: String, + private val vehicleLabel: String, var pattern: MatoPattern?, - private val touchUp: onTouchUp): + val showClose: Boolean, + private val touchUp: onTouchUp + ): BasicInfoWindow(R.layout.bus_info_window,map) { init { mView.setOnTouchListener { view, motionEvent -> - touchUp.onActionUp() + touchUp.onActionUp(pattern) close() //mView.performClick() true } } + constructor(map: MapView, update: LivePositionUpdate, pattern: MatoPattern?, showClose: Boolean, touchUp: onTouchUp, ): + this(map, + GtfsUtils.getLineNameFromGtfsID(update.routeID), + update.vehicle, + pattern, + showClose, + touchUp + ) + override fun onOpen(item: Any?) { // super.onOpen(item) @@ -53,10 +66,13 @@ val descrView = mView.findViewById(R.id.businfo_description) val subdescrView = mView.findViewById(R.id.businfo_subdescription) - val nameRoute = GtfsUtils.getLineNameFromGtfsID(update.routeID) - titleView.text = (mView.resources.getString(R.string.line_fill, nameRoute) + val iconClose = mView.findViewById(R.id.closeIcon) + + //val nameRoute = GtfsUtils.getLineNameFromGtfsID(update.lineGtfsId) + + titleView.text = (mView.resources.getString(R.string.line_fill, routeName) ) - subdescrView.text = update.vehicleInfo.label + subdescrView.text = vehicleLabel if(pattern!=null){ @@ -65,7 +81,19 @@ } else{ descrView.visibility = GONE } + if(!showClose){ + iconClose.visibility = GONE + val ctx = titleView.context + val layPars = (titleView.layoutParams as ConstraintLayout.LayoutParams).apply { + marginStart= 0 //utils.convertDipToPixelsInt(ctx, 8.0)//8.dpToPixels() + topMargin=utils.convertDipToPixelsInt(ctx, 4.0) + marginEnd=0 + bottomMargin=0 + } + //titleView.layoutParams = layPars + } } + fun setPatternAndDraw(pattern: MatoPattern?){ if(pattern==null){ return @@ -77,6 +105,6 @@ } fun interface onTouchUp{ - fun onActionUp() + fun onActionUp(pattern: MatoPattern?) } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/map/BusPositionUtils.kt b/app/src/main/java/it/reyboz/bustorino/map/BusPositionUtils.kt new file mode 100644 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/map/BusPositionUtils.kt @@ -0,0 +1,41 @@ +package it.reyboz.bustorino.map + +import android.animation.ObjectAnimator +import android.util.Log +import androidx.core.content.res.ResourcesCompat +import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate +import it.reyboz.bustorino.data.gtfs.MatoPattern +import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops +import it.reyboz.bustorino.fragments.MapFragment +import org.osmdroid.util.GeoPoint +import org.osmdroid.views.MapView +import org.osmdroid.views.overlay.Marker + +class BusPositionUtils { + companion object{ + @JvmStatic + public fun updateBusPositionMarker(map: MapView, marker: Marker?, posUpdate: LivePositionUpdate, + tripMarkersAnimators: HashMap, + justCreated: Boolean) { + val position: GeoPoint + val updateID = posUpdate.tripID + if (!justCreated) { + position = marker!!.position + if (posUpdate.latitude != position.latitude || posUpdate.longitude != position.longitude) { + val newpos = GeoPoint(posUpdate.latitude, posUpdate.longitude) + val valueAnimator = MarkerUtils.makeMarkerAnimator( + map, marker, newpos, MarkerUtils.LINEAR_ANIMATION, 1200 + ) + valueAnimator.setAutoCancel(true) + tripMarkersAnimators.put(updateID, valueAnimator) + valueAnimator.start() + } + //marker.setPosition(new GeoPoint(posUpdate.getLatitude(), posUpdate.getLongitude())); + } else { + position = GeoPoint(posUpdate.latitude, posUpdate.longitude) + marker!!.position = position + } + if (posUpdate.bearing != null) marker.rotation = posUpdate.bearing * -1f + } + } +} \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/map/CustomInfoWindow.java b/app/src/main/java/it/reyboz/bustorino/map/CustomInfoWindow.java --- a/app/src/main/java/it/reyboz/bustorino/map/CustomInfoWindow.java +++ b/app/src/main/java/it/reyboz/bustorino/map/CustomInfoWindow.java @@ -23,11 +23,13 @@ import android.view.MotionEvent; import android.view.View; +import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; import org.osmdroid.views.MapView; import org.osmdroid.views.overlay.infowindow.BasicInfoWindow; @@ -37,6 +39,8 @@ //TODO: Make the action on the Click customizable private final TouchResponder touchResponder; private final String stopID, name, routesStopping; + + private final int colorResID; //final DisplayMetrics metrics; @Override @@ -45,6 +49,7 @@ TextView descr_textView = mView.findViewById(R.id.bubble_description); CharSequence text = descr_textView.getText(); TextView titleTV = mView.findViewById(R.id.bubble_title); + titleTV.setTextColor(ContextCompat.getColor(mView.getContext(),colorResID)); //Log.d("BusTO-MapInfoWindow", "Descrip: "+text+", title "+(titleTV==null? "null": titleTV.getText())); if (text==null || !text.toString().isEmpty()){ @@ -61,16 +66,34 @@ subDescriptTextView.setVisibility(View.VISIBLE); } + //check if there is a close image + ImageView image = mView.findViewById(R.id.closeIcon); + if (image != null) { + image.setOnClickListener( view -> close()); + } + + } + public CustomInfoWindow(MapView mapView, String stopID, String name, String routesStopping, + TouchResponder responder){ + + this(mapView, stopID, name, routesStopping, responder,R.layout.map_popup, R.color.red_darker); } @SuppressLint("ClickableViewAccessibility") - public CustomInfoWindow(MapView mapView, String stopID, String name, String routesStopping, TouchResponder responder) { + public CustomInfoWindow(MapView mapView, + String stopID, + String name, + String routesStopping, + TouchResponder responder, + int layoutId, + int colorResId) { // get the personalized layout - super(R.layout.map_popup, mapView); + super(layoutId, mapView); touchResponder =responder; this.stopID = stopID; this.name = name; this.routesStopping = routesStopping; + colorResID = colorResId; //metrics = Resources.getSystem().getDisplayMetrics(); @@ -82,6 +105,8 @@ } return true; }); + + } public interface TouchResponder{ diff --git a/app/src/main/java/it/reyboz/bustorino/map/MapViewModel.kt b/app/src/main/java/it/reyboz/bustorino/map/MapViewModel.kt new file mode 100644 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/map/MapViewModel.kt @@ -0,0 +1,15 @@ +package it.reyboz.bustorino.map + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel + +class MapViewModel : ViewModel() { + + val currentLat = MutableLiveData(INVALID) + val currentLong = MutableLiveData(INVALID) + val currentZoom = MutableLiveData(-10.0) + + companion object{ + const val INVALID = -1000.0 + } +} \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/map/MarkerAnimation.java b/app/src/main/java/it/reyboz/bustorino/map/MarkerAnimation.java deleted file mode 100644 --- a/app/src/main/java/it/reyboz/bustorino/map/MarkerAnimation.java +++ /dev/null @@ -1,31 +0,0 @@ -package it.reyboz.bustorino.map; - -/* Copyright 2013 Google Inc. - Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0.html */ - - - import android.animation.ObjectAnimator; - import android.animation.TypeEvaluator; - import android.util.Property; - - import org.osmdroid.util.GeoPoint; - import org.osmdroid.views.MapView; - import org.osmdroid.views.overlay.Marker; - -public class MarkerAnimation { - - - public static ObjectAnimator makeMarkerAnimator(final MapView map, Marker marker, GeoPoint finalPosition, final GeoPointInterpolator GeoPointInterpolator, int durationMs) { - TypeEvaluator typeEvaluator = new TypeEvaluator() { - @Override - public GeoPoint evaluate(float fraction, GeoPoint startValue, GeoPoint endValue) { - return GeoPointInterpolator.interpolate(fraction, startValue, endValue); - } - }; - Property property = Property.of(Marker.class, GeoPoint.class, "position"); - ObjectAnimator animator = ObjectAnimator.ofObject(marker, property, typeEvaluator, finalPosition); - animator.setDuration(durationMs); - //animator.start(); - return animator; - } -} diff --git a/app/src/main/java/it/reyboz/bustorino/map/MarkerUtils.java b/app/src/main/java/it/reyboz/bustorino/map/MarkerUtils.java new file mode 100644 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/map/MarkerUtils.java @@ -0,0 +1,102 @@ +package it.reyboz.bustorino.map; + +import android.animation.ObjectAnimator; +import android.animation.TypeEvaluator; +import android.graphics.drawable.Drawable; +import android.util.Log; +import android.util.Property; + + +import android.view.animation.LinearInterpolator; +import it.reyboz.bustorino.R; +import org.osmdroid.util.GeoPoint; +import org.osmdroid.views.MapView; +import org.osmdroid.views.overlay.Marker; +import org.osmdroid.views.overlay.infowindow.InfoWindow; + +public class MarkerUtils { + + public static final int LINEAR_ANIMATION = 1; + + /* Copyright 2013 Google Inc. + Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0.html */ + public static ObjectAnimator makeMarkerAnimator(final MapView map, Marker marker, GeoPoint finalPosition, int animationType, int durationMs) { + + GeoPointInterpolator interpolator; + switch (animationType){ + case LINEAR_ANIMATION: + interpolator = new GeoPointInterpolator.Linear(); + break; + default: + throw new IllegalArgumentException("Value "+animationType+ " for animationType is invalid"); + } + TypeEvaluator typeEvaluator = (fraction, startValue, endValue) -> + interpolator.interpolate(fraction, startValue, endValue); + Property property = Property.of(Marker.class, GeoPoint.class, "position"); + ObjectAnimator animator = ObjectAnimator.ofObject(marker, property, typeEvaluator, finalPosition); + switch (animationType){ + case LINEAR_ANIMATION: + + animator.setInterpolator(new LinearInterpolator()); + default: + } + animator.setDuration(durationMs); + //animator.start(); + return animator; + } + + public static Marker makeMarker(GeoPoint geoPoint, String stopID, String stopName, + String routesStopping, + MapView map, + CustomInfoWindow.TouchResponder responder, + Drawable icon, + int infoWindowLayout, + int titleColorId) { + + // add a marker + final Marker marker = new Marker(map); + + // set custom info window as info window + CustomInfoWindow popup = new CustomInfoWindow(map, stopID, stopName, routesStopping, responder, infoWindowLayout, titleColorId); + marker.setInfoWindow(popup); + + // make the marker clickable + marker.setOnMarkerClickListener((thisMarker, mapView) -> { + if (thisMarker.isInfoWindowOpen()) { + // on second click + Log.w("BusTO-OsmMap", "Pressed on the click marker"); + } else { + // on first click + + // hide all opened info window + InfoWindow.closeAllInfoWindowsOn(map); + // show this particular info window + thisMarker.showInfoWindow(); + // move the map to its position + map.getController().animateTo(thisMarker.getPosition()); + } + + return true; + }); + + // set its position + marker.setPosition(geoPoint); + marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM); + // add to it an icon + //marker.setIcon(getResources().getDrawable(R.drawable.bus_marker)); + + marker.setIcon(icon); + // add to it a title + marker.setTitle(stopName); + // set the description as the ID + marker.setSnippet(stopID); + + // show popup info window of the searched marker + /*if (isStartMarker) { + marker.showInfoWindow(); + //map.getController().animateTo(marker.getPosition()); + }*/ + + return marker; + } +} diff --git a/app/src/main/java/it/reyboz/bustorino/middleware/AutoFitGridLayoutManager.kt b/app/src/main/java/it/reyboz/bustorino/middleware/AutoFitGridLayoutManager.kt new file mode 100644 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/middleware/AutoFitGridLayoutManager.kt @@ -0,0 +1,41 @@ +package it.reyboz.bustorino.middleware + +import android.content.Context +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.Recycler + +/** + * Simple trick to get an automatic number of columns (from https://www.journaldev.com/13792/android-gridlayoutmanager-example) + * + */ +class AutoFitGridLayoutManager(context: Context?, columnWidth: Int): + GridLayoutManager(context, 1) { + private var columnWidth = 0 + private var columnWidthChanged = true + + init { + setColumnWidth(columnWidth) + } + + fun setColumnWidth(newColumnWidth: Int) { + if (newColumnWidth > 0 && newColumnWidth != columnWidth) { + columnWidth = newColumnWidth + columnWidthChanged = true + } + } + + override fun onLayoutChildren(recycler: Recycler, state: RecyclerView.State) { + if (columnWidthChanged && columnWidth > 0) { + val totalSpace: Int = if (orientation == VERTICAL) { + width - paddingRight - paddingLeft + } else { + height - paddingTop - paddingBottom + } + val spanCount = Math.max(1, totalSpace / columnWidth) + setSpanCount(spanCount) + columnWidthChanged = false + } + super.onLayoutChildren(recycler, state) + } +} diff --git a/app/src/main/java/it/reyboz/bustorino/util/ViewUtils.kt b/app/src/main/java/it/reyboz/bustorino/util/ViewUtils.kt --- a/app/src/main/java/it/reyboz/bustorino/util/ViewUtils.kt +++ b/app/src/main/java/it/reyboz/bustorino/util/ViewUtils.kt @@ -2,8 +2,10 @@ import android.graphics.Rect import android.util.Log - import android.view.View +import android.view.WindowManager +import android.view.animation.Animation +import android.view.animation.Transformation import androidx.core.widget.NestedScrollView @@ -29,5 +31,65 @@ return false } } + + //from https://stackoverflow.com/questions/4946295/android-expand-collapse-animation + fun expand(v: View,duration: Long, layoutHeight: Int = WindowManager.LayoutParams.WRAP_CONTENT) { + val matchParentMeasureSpec = + View.MeasureSpec.makeMeasureSpec((v.parent as View).width, View.MeasureSpec.EXACTLY) + val wrapContentMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) + v.measure(matchParentMeasureSpec, wrapContentMeasureSpec) + val targetHeight = v.measuredHeight + + // Older versions of android (pre API 21) cancel animations for views with a height of 0. + v.layoutParams.height = 1 + v.visibility = View.VISIBLE + val a: Animation = object : Animation() { + override fun applyTransformation(interpolatedTime: Float, t: Transformation?) { + v.layoutParams.height = + if (interpolatedTime == 1f) layoutHeight + else (targetHeight * interpolatedTime).toInt() + v.requestLayout() + } + + override fun willChangeBounds(): Boolean { + return true + } + } + + // Expansion speed of 1dp/ms + if(duration == DEF_DURATION) + a.duration = (targetHeight / v.context.resources.displayMetrics.density).toInt().toLong() + else + a.duration = duration + v.startAnimation(a) + } + + fun collapse(v: View, duration: Long): Animation { + val initialHeight = v.measuredHeight + val a: Animation = object : Animation() { + override fun applyTransformation(interpolatedTime: Float, t: Transformation?) { + if (interpolatedTime == 1f) { + v.visibility = View.GONE + } else { + v.layoutParams.height = initialHeight - (initialHeight * interpolatedTime).toInt() + v.requestLayout() + } + } + + override fun willChangeBounds(): Boolean { + return true + } + } + + // Collapse speed of 1dp/ms + if (duration == DEF_DURATION) + a.duration = (initialHeight / v.context.resources.displayMetrics.density).toInt().toLong() + else + a.duration = duration + v.startAnimation(a) + return a + } + + const val DEF_DURATION: Long = -2 } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesGridShowingViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesGridShowingViewModel.kt new file mode 100644 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesGridShowingViewModel.kt @@ -0,0 +1,27 @@ +package it.reyboz.bustorino.viewmodels + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import it.reyboz.bustorino.data.GtfsRepository +import it.reyboz.bustorino.data.NextGenDB +import it.reyboz.bustorino.data.OldDataRepository +import it.reyboz.bustorino.data.gtfs.GtfsDatabase + +class LinesGridShowingViewModel(application: Application) : AndroidViewModel(application) { + + private val gtfsRepo: GtfsRepository + + init { + val gtfsDao = GtfsDatabase.getGtfsDatabase(application).gtfsDao() + gtfsRepo = GtfsRepository(gtfsDao) + + } + + val routesLiveData = gtfsRepo.getAllRoutes() + + val isUrbanExpanded = MutableLiveData(true) + val isExtraUrbanExpanded = MutableLiveData(false) + val isTouristExpanded = MutableLiveData(false) +} \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/MQTTPositionsViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/MQTTPositionsViewModel.kt new file mode 100644 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/MQTTPositionsViewModel.kt @@ -0,0 +1,165 @@ +/* + BusTO - ViewModel components + Copyright (C) 2023 Fabio Mazza + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ +package it.reyboz.bustorino.viewmodels + +import android.app.Application +import android.util.Log +import androidx.lifecycle.* +import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate +import it.reyboz.bustorino.backend.mato.MQTTMatoClient +import it.reyboz.bustorino.data.GtfsRepository +import it.reyboz.bustorino.data.MatoPatternsDownloadWorker +import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops +import it.reyboz.bustorino.fragments.GTFSPositionsViewModel +import kotlinx.coroutines.launch + + +typealias UpdatesMap = HashMap + +class MQTTPositionsViewModel(application: Application): AndroidViewModel(application) { + + private val gtfsRepo = GtfsRepository(application) + + //private val updates = UpdatesMap() + private val updatesLiveData = MutableLiveData>() + + private var mqttClient = MQTTMatoClient.getInstance() + + private var lineListening = "" + private var lastTimeReceived: Long = 0 + + private val positionListener = MQTTMatoClient.Companion.MQTTMatoListener{ + + val mupds = ArrayList() + if(lineListening==MQTTMatoClient.LINES_ALL){ + for(sdic in it.values){ + for(update in sdic.values){ + mupds.add(update) + } + } + } else{ + //we're listening to one + if (it.containsKey(lineListening.trim()) ){ + for(up in it[lineListening]?.values!!){ + mupds.add(up) + } + } + } + val time = System.currentTimeMillis() + if(lastTimeReceived == (0.toLong()) || (time-lastTimeReceived)>500){ + updatesLiveData.value = (mupds) + lastTimeReceived = time + } + + } + + //find the trip IDs in the updates + private val tripsIDsInUpdates = updatesLiveData.map { it -> + //Log.d(DEBUG_TI, "Updates map has keys ${upMap.keys}") + it.map { pos -> "gtt:"+pos.tripID } + + } + // get the trip IDs in the DB + private val gtfsTripsPatternsInDB = tripsIDsInUpdates.switchMap { + Log.i(DEBUG_TI, "tripsIds in updates changed: ${it.size}") + gtfsRepo.gtfsDao.getTripPatternStops(it) + } + //trip IDs to query, which are not present in the DB + //REMEMBER TO OBSERVE THIS IN THE MAP + val tripsGtfsIDsToQuery: LiveData> = gtfsTripsPatternsInDB.map { tripswithPatterns -> + val tripNames=tripswithPatterns.map { twp-> twp.trip.tripID } + Log.i(DEBUG_TI, "Have ${tripswithPatterns.size} trips in the DB") + if (tripsIDsInUpdates.value!=null) + return@map tripsIDsInUpdates.value!!.filter { !(tripNames.contains(it) || it.contains("null"))} + else { + Log.e(DEBUG_TI,"Got results for gtfsTripsInDB but not tripsIDsInUpdates??") + return@map ArrayList() + } + } + + // unify trips with updates + val updatesWithTripAndPatterns = gtfsTripsPatternsInDB.map { tripPatterns-> + Log.i(DEBUG_TI, "Mapping trips and patterns") + val mdict = HashMap>() + //missing patterns + val routesToDownload = HashSet() + if(updatesLiveData.value!=null) + for(update in updatesLiveData.value!!){ + + val trID:String = update.tripID + var found = false + for(trip in tripPatterns){ + if (trip.pattern == null){ + //pattern is null, which means we have to download + // the pattern data from MaTO + routesToDownload.add(trip.trip.routeID) + } + if (trip.trip.tripID == "gtt:$trID"){ + found = true + //insert directly + mdict[trID] = Pair(update,trip) + break + } + } + if (!found){ + //Log.d(DEBUG_TI, "Cannot find pattern ${tr}") + //give the update anyway + mdict[trID] = Pair(update,null) + } + } + //have to request download of missing Patterns + if (routesToDownload.size > 0){ + Log.d(DEBUG_TI, "Have ${routesToDownload.size} missing patterns from the DB: $routesToDownload") + //downloadMissingPatterns (ArrayList(routesToDownload)) + MatoPatternsDownloadWorker.downloadPatternsForRoutes(routesToDownload.toList(), getApplication()) + } + + return@map mdict + } + + + fun requestPosUpdates(line: String){ + lineListening = line + viewModelScope.launch { + mqttClient.startAndSubscribe(line,positionListener, getApplication()) + } + + + //updatePositions(1000) + } + + fun stopPositionsListening(){ + viewModelScope.launch { + val tt = System.currentTimeMillis() + mqttClient.desubscribe(positionListener) + val time = System.currentTimeMillis() -tt + Log.d(DEBUG_TI, "Took $time ms to unsubscribe") + } + + } + + fun retriggerPositionUpdate(){ + if(updatesLiveData.value!=null){ + updatesLiveData.postValue(updatesLiveData.value) + } + } + + companion object{ + private const val DEBUG_TI = "BusTO-MQTTLiveData" + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ball.xml b/app/src/main/res/drawable/ball.xml new file mode 100644 --- /dev/null +++ b/app/src/main/res/drawable/ball.xml @@ -0,0 +1,6 @@ + + + diff --git a/app/src/main/res/drawable/baseline_chevron_right_24.xml b/app/src/main/res/drawable/baseline_chevron_right_24.xml new file mode 100644 --- /dev/null +++ b/app/src/main/res/drawable/baseline_chevron_right_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/baseline_close_16.xml b/app/src/main/res/drawable/baseline_close_16.xml new file mode 100644 --- /dev/null +++ b/app/src/main/res/drawable/baseline_close_16.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_list_30.xml b/app/src/main/res/drawable/ic_list_30.xml new file mode 100644 --- /dev/null +++ b/app/src/main/res/drawable/ic_list_30.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_map_white_30.xml b/app/src/main/res/drawable/ic_map_white_30.xml new file mode 100644 --- /dev/null +++ b/app/src/main/res/drawable/ic_map_white_30.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/map_bus_position_icon.xml b/app/src/main/res/drawable/map_bus_position_icon.xml new file mode 100644 --- /dev/null +++ b/app/src/main/res/drawable/map_bus_position_icon.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/drawable/point_heading_icon.xml b/app/src/main/res/drawable/point_heading_icon.xml deleted file mode 100644 --- a/app/src/main/res/drawable/point_heading_icon.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - diff --git a/app/src/main/res/layout/bus_info_window.xml b/app/src/main/res/layout/bus_info_window.xml --- a/app/src/main/res/layout/bus_info_window.xml +++ b/app/src/main/res/layout/bus_info_window.xml @@ -9,56 +9,78 @@ android:textAlignment="center" android:padding="2dp" android:gravity="center_horizontal"> - + + app:layout_constraintLeft_toRightOf="@id/businfo_title" + + android:layout_alignParentTop="true" + app:layout_constraintHorizontal_bias="0.5" + + app:layout_constraintStart_toEndOf="@+id/businfo_title" + android:layout_marginTop="4dp" + android:layout_marginEnd="2dp" + android:layout_marginStart="6dp" + /> + + + app:layout_constraintRight_toRightOf="parent" + android:layout_below="@id/businfo_title" + android:text="BUCAGLIONE GIANGI" + android:gravity="center" + android:textAlignment="center" + android:layout_marginTop="2dp" + app:layout_constraintBottom_toTopOf="@+id/businfo_subdescription" + android:layout_marginLeft="4dp" android:layout_marginRight="4dp"/> - - + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintHorizontal_bias="0.5" + android:layout_marginStart="8dp" android:layout_marginEnd="8dp" android:layout_marginTop="2dp" + android:layout_marginBottom="3dp" + app:layout_constraintBottom_toBottomOf="parent"/> + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_lines_detail.xml b/app/src/main/res/layout/fragment_lines_detail.xml --- a/app/src/main/res/layout/fragment_lines_detail.xml +++ b/app/src/main/res/layout/fragment_lines_detail.xml @@ -9,47 +9,48 @@ - + app:layout_constraintTop_toTopOf="parent" + android:layout_marginTop="8dp" android:gravity="center_horizontal|center_vertical" + app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" + android:layout_marginStart="8dp" android:layout_marginEnd="8dp"/> + app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/routeDescrTextView" + android:layout_marginTop="8dp" app:layout_constraintTop_toBottomOf="@+id/titleTextView" + android:layout_marginStart="4dp"/> + app:layout_constraintTop_toTopOf="@+id/patternsSpinner" + app:layout_constraintBottom_toBottomOf="@+id/patternsSpinner" + /> - + />--> + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_lines_grid.xml b/app/src/main/res/layout/fragment_lines_grid.xml new file mode 100644 --- /dev/null +++ b/app/src/main/res/layout/fragment_lines_grid.xml @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_test_realtime_gtfs.xml b/app/src/main/res/layout/fragment_test_realtime_gtfs.xml --- a/app/src/main/res/layout/fragment_test_realtime_gtfs.xml +++ b/app/src/main/res/layout/fragment_test_realtime_gtfs.xml @@ -1,27 +1,43 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context=".fragments.TestRealtimeGtfsFragment"> + + android:layout_margin="20dp" app:layout_constraintStart_toStartOf="parent" + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintBottom_toBottomOf="parent" + />