Page Menu
Home
GitPull.it
Search
Configure Global Search
Log In
Files
F11420584
D126.1773471420.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
170 KB
Referenced Files
None
Subscribers
None
D126.1773471420.diff
View Options
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<GtfsRoute>,
+ click: onItemClick,
+ private val layoutId: Int = R.layout.line_title_header) :
+ RecyclerView.Adapter<RouteAdapter.ViewHolder>()
+{
+ val clickreference: WeakReference<onItemClick>
+ 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<ArrayList<GtfsPositionUpdate>>(Method.GET, URL_POSITION, errorListener) {
- override fun parseNetworkResponse(response: NetworkResponse?): Response<ArrayList<GtfsPositionUpdate>> {
+ Request<ArrayList<LivePositionUpdate>>(Method.GET, URL_POSITION, errorListener) {
+ override fun parseNetworkResponse(response: NetworkResponse?): Response<ArrayList<LivePositionUpdate>> {
if (response == null){
return Response.error(VolleyError("Null response"))
}
@@ -39,13 +38,13 @@
val gtfsreq = GtfsRealtime.FeedMessage.parseFrom(response.data)
- val positionList = ArrayList<GtfsPositionUpdate>()
+ val positionList = ArrayList<LivePositionUpdate>()
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<GtfsPositionUpdate>?) {
+ override fun deliverResponse(response: ArrayList<LivePositionUpdate>?) {
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<GtfsPositionUpdate>?)
+ fun onResponse(response: ArrayList<LivePositionUpdate>?)
}
}
}
\ 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<String, HashMap<String, LivePositionUpdate> >
+
+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<String, ArrayList<WeakReference<MQTTMatoListener>>>()
+
+ 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<WeakReference<MQTTMatoListener>>): 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<MatoPattern>()
- //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<List<GtfsRoute>>{
+ 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<ArrayList<GtfsPositionUpdate>>()
+ val positionsLiveData = MutableLiveData<ArrayList<LivePositionUpdate>>()
private val positionsRequestRunning = MutableLiveData<Boolean>()
private val positionRequestListener = object: GtfsRtPositionsRequest.Companion.RequestListener{
- override fun onResponse(response: ArrayList<GtfsPositionUpdate>?) {
+ override fun onResponse(response: ArrayList<LivePositionUpdate>?) {
Log.i(DEBUG_TI,"Got response from the GTFS RT server")
- response?.let {it:ArrayList<GtfsPositionUpdate> ->
+ response?.let {it:ArrayList<LivePositionUpdate> ->
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<String>()
@@ -129,7 +128,7 @@
val updatesWithTripAndPatterns = gtfsTripsPatternsInDB.map { tripPatterns->
Log.i(DEBUG_TI, "Mapping trips and patterns")
- val mdict = HashMap<String,Pair<GtfsPositionUpdate, TripAndPatternWithStops?>>()
+ val mdict = HashMap<String,Pair<LivePositionUpdate, TripAndPatternWithStops?>>()
//missing patterns
val routesToDownload = HashSet<String>()
if(positionsLiveData.value!=null)
@@ -174,7 +173,7 @@
fun downloadTripsFromMato(trips: List<String>): Boolean{
return MatoTripsDownloadWorker.downloadTripsFromMato(trips,getApplication(), DEBUG_TI)
}
- fun downloadMissingPatterns(routeIds: List<String>): Boolean{
+ private fun downloadMissingPatterns(routeIds: List<String>): Boolean{
return MatoPatternsDownloadWorker.downloadPatternsForRoutes(routeIds, getApplication())
}
@@ -186,11 +185,9 @@
positionsRequestRunning.value = false;
}
fun testCascade(){
- val n = ArrayList<GtfsPositionUpdate>()
- n.add(GtfsPositionUpdate("22920721U","lala","lalal","lol",1000.0f,1000.0f, 9000.0f,
- 378192810192, GtfsPositionUpdate.VehicleInfo("aj","a"),
- null, null
-
+ val n = ArrayList<LivePositionUpdate>()
+ 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 <http://www.gnu.org/licenses/>.
+ */
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<String>? = null
- private var patternsSpinnerState: Parcelable? = null
+ //private var patternsSpinnerState: Parcelable? = null
private lateinit var currentPatterns: List<MatoPatternWithStops>
- private lateinit var gtfsStopsForCurrentPattern: List<PatternStop>
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<GeoPoint>()
+ }
- 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<GeoPoint>()
+
+ 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<String,Marker>()
+ private var busPositionsOverlay = FolderOverlay()
+ private val tripMarkersAnimators = HashMap<String, ObjectAnimator>()
+ 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<TextView>(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<String>())
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<String, Pair<LivePositionUpdate, TripAndPatternWithStops?>>()
+ 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<String,Int>()
+ 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<MatoPatternWithStops>){
@@ -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<GeoPoint>
+ 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<Stop>){
+ 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<Stop>){
+
+ 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<String, Pair<LivePositionUpdate, TripAndPatternWithStops?>>
+ ) {
+ //Log.d(MapFragment.DEBUG_TAG, "Updating positions of the buses")
+ //if(busPositionsOverlay == null) busPositionsOverlay = new FolderOverlay();
+ val noPatternsTrips = ArrayList<String>()
+ 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>): 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<String, ArrayList<GtfsRoute>>()
+ /*hashMapOf(
+ AG_URBAN to ArrayList<GtfsRoute>(),
+ AG_EXTRAURB to ArrayList(),
+ AG_TOUR to ArrayList()
+ )*/
+
+ private lateinit var fragmentListener: CommonFragmentListener
+
+ private val linesNameSorter = LinesNameSorter()
+ private val linesComparator = Comparator<GtfsRoute> { a,b ->
+ return@Comparator linesNameSorter.compare(a.shortName, b.shortName)
+ }
+
+ private val routeClickListener = RouteAdapter.onItemClick {
+ fragmentListener.showLineOnMap(it.gtfsId)
+ }
+ private val arrows = HashMap<String, ImageView>()
+ private val durations = HashMap<String, Long>()
+ 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<List<Stop>>()
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<List<GtfsRoute>> 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<String,Marker> 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<String, Pair<GtfsPositionUpdate, TripAndPatternWithStops>> tripsPatterns){
+ private void updateBusPositionsInMap(HashMap<String, Pair<LivePositionUpdate, TripAndPatternWithStops>> tripsPatterns){
Log.d(DEBUG_TAG, "Updating positions of the buses");
//if(busPositionsOverlay == null) busPositionsOverlay = new FolderOverlay();
final ArrayList<String> noPatternsTrips = new ArrayList<>();
for(String tripID: tripsPatterns.keySet()) {
- final Pair<GtfsPositionUpdate, TripAndPatternWithStops> pair = tripsPatterns.get(tripID);
+ final Pair<LivePositionUpdate, TripAndPatternWithStops> 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<GtfsPositionUpdate>?) {
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<TextView>(R.id.businfo_description)
val subdescrView = mView.findViewById<TextView>(R.id.businfo_subdescription)
- val nameRoute = GtfsUtils.getLineNameFromGtfsID(update.routeID)
- titleView.text = (mView.resources.getString(R.string.line_fill, nameRoute)
+ val iconClose = mView.findViewById<ImageView>(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<String, ObjectAnimator>,
+ 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<GeoPoint> typeEvaluator = new TypeEvaluator<GeoPoint>() {
- @Override
- public GeoPoint evaluate(float fraction, GeoPoint startValue, GeoPoint endValue) {
- return GeoPointInterpolator.interpolate(fraction, startValue, endValue);
- }
- };
- Property<Marker, GeoPoint> 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<GeoPoint> typeEvaluator = (fraction, startValue, endValue) ->
+ interpolator.interpolate(fraction, startValue, endValue);
+ Property<Marker, GeoPoint> 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 <http://www.gnu.org/licenses/>.
+ */
+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<String, LivePositionUpdate>
+
+class MQTTPositionsViewModel(application: Application): AndroidViewModel(application) {
+
+ private val gtfsRepo = GtfsRepository(application)
+
+ //private val updates = UpdatesMap()
+ private val updatesLiveData = MutableLiveData<ArrayList<LivePositionUpdate>>()
+
+ private var mqttClient = MQTTMatoClient.getInstance()
+
+ private var lineListening = ""
+ private var lastTimeReceived: Long = 0
+
+ private val positionListener = MQTTMatoClient.Companion.MQTTMatoListener{
+
+ val mupds = ArrayList<LivePositionUpdate>()
+ 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<List<String>> = 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<String>()
+ }
+ }
+
+ // unify trips with updates
+ val updatesWithTripAndPatterns = gtfsTripsPatternsInDB.map { tripPatterns->
+ Log.i(DEBUG_TI, "Mapping trips and patterns")
+ val mdict = HashMap<String,Pair<LivePositionUpdate, TripAndPatternWithStops?>>()
+ //missing patterns
+ val routesToDownload = HashSet<String>()
+ 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 @@
+<vector android:height="19dp" android:viewportHeight="200"
+ android:viewportWidth="200" android:width="19dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@color/line_drawn_poly" android:fillType="evenOdd"
+ android:pathData="M100,100m-100,0a100,100 0,1 1,200 0a100,100 0,1 1,-200 0"
+ android:strokeLineCap="round" android:strokeLineJoin="bevel" android:strokeWidth="0.264999"/>
+</vector>
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 @@
+<vector android:height="30dp" android:tint="@color/black_900"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="30dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M10,6L8.59,7.41 13.17,12l-4.58,4.59L10,18l6,-6z"/>
+</vector>
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 @@
+<vector android:height="24dp"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@color/red_darker" android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
+</vector>
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 @@
+<vector android:autoMirrored="true" android:height="30dp"
+ android:tint="@color/white" android:viewportHeight="24"
+ android:viewportWidth="24" android:width="30dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M3,13h2v-2L3,11v2zM3,17h2v-2L3,15v2zM3,9h2L5,7L3,7v2zM7,13h14v-2L7,11v2zM7,17h14v-2L7,15v2zM7,7v2h14L21,7L7,7z"/>
+</vector>
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 @@
+<vector android:height="30dp" android:tint="#FFFFFF"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="30dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M20.5,3l-0.16,0.03L15,5.1 9,3 3.36,4.9c-0.21,0.07 -0.36,0.25 -0.36,0.48V20.5c0,0.28 0.22,0.5 0.5,0.5l0.16,-0.03L9,18.9l6,2.1 5.64,-1.9c0.21,-0.07 0.36,-0.25 0.36,-0.48V3.5c0,-0.28 -0.22,-0.5 -0.5,-0.5zM15,19l-6,-2.11V5l6,2.11V19z"/>
+</vector>
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 @@
+<vector android:height="40dp" android:viewportHeight="35.719"
+ android:viewportWidth="35.719" android:width="40dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@color/bus_marker_color"
+ android:strokeColor="@color/black"
+ android:pathData="m17.859,0.505c-2.419,2.799 -6.614,7.924 -8.107,10.349 -1.294,2.102 -2.431,4.162 -2.431,6.824 0,5.83 4.714,10.559 10.538,10.583 5.824,-0.024 10.538,-4.753 10.538,-10.583 0,-2.662 -1.138,-4.722 -2.431,-6.824C24.474,8.429 20.279,3.304 17.859,0.505Z"
+ android:strokeLineJoin="round" android:strokeWidth="0.8"/>
+</vector>
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 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:height="40dp"
- android:width="40dp"
- android:viewportHeight="35.719"
- android:viewportWidth="35.719"
- >
- <path
-
- android:pathData="M17.859,0.505C15.44,3.305 11.245,8.429 9.753,10.854 8.459,12.956 7.321,15.016 7.321,17.678c0,5.83 4.714,10.559 10.538,10.583zM17.859,0.505c2.419,2.799 6.614,7.924 8.107,10.349 1.294,2.102 2.432,4.162 2.432,6.824 0,5.83 -4.714,10.559 -10.538,10.583z"
- android:fillColor="@color/blue_700"
- android:strokeLineJoin="round"
- android:strokeWidth="0.147034"/>
- <!--android:fillColor="#0e00c1 -->
-</vector>
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">
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical"
- android:gravity="center_horizontal"
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent" android:layout_height="match_parent"
+
>
<TextView
android:id="@+id/businfo_title"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
- android:textColor="@color/blue_700"
+ android:textColor="@color/bus_marker_color"
android:textSize="16sp"
- android:maxWidth="150sp"
- app:layout_constraintStart_toStartOf="parent"
+ android:maxWidth="130sp"
android:text="BALABALA"
android:textAlignment="center"
+ android:layout_marginTop="4dp"
+ android:layout_marginStart="6dp"
+ android:layout_marginEnd="6dp"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintRight_toRightOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintBottom_toTopOf="@+id/businfo_description" android:layout_marginRight="5dp"/>
+ <ImageView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" app:srcCompat="@drawable/baseline_close_16"
+ android:id="@+id/closeIcon"
+ app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
- android:layout_marginLeft="8dp"
- android:layout_marginStart="8dp"
- android:layout_marginRight="8dp" android:layout_marginEnd="8dp"
- app:layout_constraintTop_toTopOf="parent"/>
+ 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"
+ />
+
+
<TextView
android:id="@+id/businfo_description"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:gravity="center_horizontal"
- android:textAlignment="center"
android:textSize="15sp"
- android:maxWidth="100sp"
+ android:maxWidth="120sp"
+ android:textColor="@color/grey_600"
app:layout_constraintTop_toBottomOf="@id/businfo_title"
app:layout_constraintLeft_toLeftOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
-
- android:layout_marginLeft="8dp" android:layout_marginRight="8dp" android:layout_marginEnd="8dp"/>
+ 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"/>
<TextView
android:id="@+id/businfo_subdescription"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:paddingLeft="5dp"
- android:paddingRight="5dp"
- android:gravity="center_horizontal"
- android:textSize="12sp"
- android:textAlignment="center"
- android:maxWidth="130sp"
+ android:textSize="13sp"
+ android:text="672881"
+ android:textColor="@color/grey_600"
app:layout_constraintTop_toBottomOf="@id/businfo_description"
app:layout_constraintLeft_toLeftOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- android:layout_marginLeft="8dp" android:layout_marginRight="8dp" android:layout_marginEnd="8dp"/>
-
- </LinearLayout>
+ 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"/>
+ </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
\ 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 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
- <TextView android:layout_width="match_parent" android:layout_height="wrap_content"
+ <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Headline"
android:text="Line 10"
android:id="@+id/titleTextView"
android:textAlignment="center"
+ android:textSize="28sp"
- app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"
- android:layout_marginTop="8dp" android:gravity="center_horizontal|center_vertical"/>
+ 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"/>
<Spinner
android:layout_width="0dp"
android:layout_height="wrap_content"
android:id="@+id/patternsSpinner"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@id/titleTextView"
- android:layout_marginTop="16dp" />
+ 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"/>
<TextView
- android:text="Descr"
- android:layout_width="0dp"
+ android:text="@string/direction_duep"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/routeDescrTextView"
- app:layout_constraintStart_toEndOf="@id/patternsSpinner"
- app:layout_constraintBottom_toBottomOf="@id/patternsSpinner"
- app:layout_constraintEnd_toEndOf="parent"
-
- android:layout_marginTop="8dp"
- android:layout_marginLeft="16dp"
- android:layout_marginStart="16dp"
- android:textAppearance="@style/TextAppearance.AppCompat.Medium"
-
+ app:layout_constraintStart_toStartOf="parent"
+ android:textAppearance="@style/TextAppearance.AppCompat.Body1"
+ android:textColor="@color/grey_600"
android:gravity="center_vertical"
+ android:textSize="18sp"
+ android:layout_marginLeft="10dp"
- android:layout_marginRight="16dp" android:layout_marginEnd="16dp"/>
+ app:layout_constraintTop_toTopOf="@+id/patternsSpinner"
+ app:layout_constraintBottom_toBottomOf="@+id/patternsSpinner"
+ />
<org.osmdroid.views.MapView android:id="@+id/lineMap"
android:layout_width="fill_parent"
android:layout_height="0dp"
- android:layout_marginTop="20dp"
+ android:layout_marginTop="10dp"
app:layout_constraintTop_toBottomOf="@id/patternsSpinner"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
- <ImageButton
+ <!--<ImageButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/icon_center_map"
@@ -77,7 +78,47 @@
android:layout_marginTop="10dp"
android:layout_marginRight="10dp"
android:layout_marginEnd="10dp"
- />
+ />-->
+ <ImageButton
+ android:src="@drawable/ic_list_30"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" android:id="@+id/switchImageButton"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintRight_toRightOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/patternsSpinner"
+ android:layout_margin="6dp"
+ android:backgroundTint="@color/blue_500"
+ />
+
+ <View
+ android:id="@+id/divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="?android:attr/listDivider"
+ app:layout_constraintTop_toBottomOf="@id/patternsSpinner"
+
+ android:layout_marginTop="8dp"/>
+ <androidx.recyclerview.widget.RecyclerView
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:id="@+id/patternStopsRecyclerView"
+ app:layout_constraintTop_toBottomOf="@+id/divider"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintVertical_bias="0.0"
+ android:layout_marginBottom="8dp"
+ android:layout_marginTop="0dp"
+ android:layout_margin="4dp"
+ app:layout_constraintHorizontal_bias="0.0"
+ app:fastScrollEnabled="true"
+ app:fastScrollHorizontalThumbDrawable="@drawable/thumb_drawable"
+ app:fastScrollHorizontalTrackDrawable="@drawable/line_drawable"
+ app:fastScrollVerticalThumbDrawable="@drawable/thumb_drawable"
+ app:fastScrollVerticalTrackDrawable="@drawable/line_drawable"
+ android:visibility="gone"
+ />
+
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>
\ 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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ 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.LinesGridShowingFragment">
+ <!--<androidx.core.widget.NestedScrollView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:id="@+id/linesScrollView"
+ android:layout_weight="12"
+ >-->
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent" android:layout_height="match_parent"
+ android:animateLayoutChanges="true"
+ >
+ <ImageView
+ android:src="@drawable/baseline_chevron_right_24"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" android:id="@+id/arrowUrb"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintBottom_toBottomOf="@id/urbanLinesTitleView"
+ android:layout_margin="4dp"
+ android:layout_marginStart="16dp"
+ android:rotation="0"
+ />
+ <TextView android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/urban_lines"
+ android:textAppearance="@style/TextAppearance.AppCompat.Body2"
+ android:textSize="@dimen/subtitle_size"
+ android:layout_margin="4dp"
+ android:textColor="@color/black_900"
+ android:gravity="center"
+ android:id="@+id/urbanLinesTitleView"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintLeft_toRightOf="@id/arrowUrb"
+ app:layout_constraintBottom_toTopOf="@id/urbanLinesRecyclerView"
+ android:layout_marginLeft="6dp"
+ app:layout_constraintVertical_bias="0.0"
+ app:layout_constraintVertical_chainStyle="spread"/>
+ <androidx.recyclerview.widget.RecyclerView
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:id="@+id/urbanLinesRecyclerView"
+ android:layout_marginLeft="10dp"
+ android:layout_marginRight="10dp"
+ android:visibility="visible"
+ android:layout_below="@id/urbanLinesTitleView"
+ app:layout_constraintTop_toBottomOf="@id/urbanLinesTitleView"
+ app:layout_constraintBottom_toTopOf="@id/touristLinesTitleView"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintVertical_bias="0.0"
+
+ />
+ <ImageView
+ android:src="@drawable/baseline_chevron_right_24"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" android:id="@+id/arrowTourist"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintBottom_toBottomOf="@id/touristLinesTitleView"
+ android:layout_margin="4dp"
+ android:layout_marginStart="16dp"
+ android:rotation="0"
+ />
+ <TextView android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/turist_lines"
+ android:textAppearance="@style/TextAppearance.AppCompat.Body2"
+ android:textSize="@dimen/subtitle_size"
+ android:textColor="@color/black_900"
+ android:layout_margin="4dp"
+ android:layout_marginStart="6dp"
+ android:gravity="center"
+ android:id="@+id/touristLinesTitleView"
+ app:layout_constraintLeft_toRightOf="@id/arrowTourist"
+ app:layout_constraintTop_toBottomOf="@id/urbanLinesRecyclerView"
+ app:layout_constraintBottom_toTopOf="@id/touristLinesRecyclerView"
+ app:layout_constraintVertical_bias="0.0"
+ android:layout_marginLeft="6dp"/>
+ <androidx.recyclerview.widget.RecyclerView
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:id="@+id/touristLinesRecyclerView"
+ android:layout_marginLeft="10dp"
+ android:layout_marginRight="10dp"
+ android:visibility="gone"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/touristLinesTitleView"
+ app:layout_constraintBottom_toTopOf="@id/extraurbanLinesTitleView"
+ app:layout_constraintVertical_bias="0.0"
+
+ />
+ <ImageView
+ android:src="@drawable/baseline_chevron_right_24"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" android:id="@+id/arrowExtraurban"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintBottom_toBottomOf="@id/extraurbanLinesTitleView"
+ android:layout_margin="4dp"
+ android:layout_marginStart="16dp"
+ android:rotation="0"
+ />
+ <TextView android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/extraurban_lines"
+ android:textAppearance="@style/TextAppearance.AppCompat.Body2"
+ android:textSize="@dimen/subtitle_size"
+ android:layout_margin="4dp"
+ android:textColor="@color/black_900"
+ android:gravity="center"
+ android:layout_marginStart="6dp"
+ android:id="@+id/extraurbanLinesTitleView"
+ app:layout_constraintTop_toBottomOf="@id/touristLinesRecyclerView"
+ app:layout_constraintLeft_toRightOf="@id/arrowExtraurban"
+ app:layout_constraintBottom_toTopOf="@id/extraurbanLinesRecyclerView"
+ app:layout_constraintVertical_bias="0.0"
+
+ />
+ <androidx.recyclerview.widget.RecyclerView
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:id="@+id/extraurbanLinesRecyclerView"
+ android:layout_marginLeft="10dp"
+ android:layout_marginRight="10dp"
+ android:visibility="gone"
+ android:layout_below="@id/extraurbanLinesTitleView"
+
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/extraurbanLinesTitleView"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintVertical_bias="0.0"
+
+ />
+ </androidx.constraintlayout.widget.ConstraintLayout>
+ <!--</androidx.core.widget.NestedScrollView>-->
+</FrameLayout>
\ 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 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
- 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">
+ 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">
<!-- TODO: Update blank fragment layout -->
+ <EditText
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:inputType="text"
+ android:text="10"
+ android:ems="10"
+ android:id="@+id/lineEditText" app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toTopOf="@+id/btn_download_data" app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ android:layout_margin="20dp"
+ app:layout_constraintHorizontal_bias="0.5" android:minHeight="48dp"/>
<TextView
android:id="@+id/gtfsMessageTextView"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
android:text="BABLABLA"
- app:layout_constraintTop_toBottomOf="@id/btn_download_data"
+ app:layout_constraintTop_toBottomOf="@+id/btn_download_data"
app:layout_constraintEnd_toEndOf="parent"
- android:layout_margin="20dp" app:layout_constraintStart_toStartOf="parent"/>
+ android:layout_margin="20dp" app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintBottom_toBottomOf="parent"
+ />
<Button
- android:text="Download GTFS update"
+ android:text="Start/stop"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:id="@+id/btn_download_data"
android:layout_margin="10dp"
- app:layout_constraintTop_toTopOf="parent" app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"/>
-
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/lineEditText" app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintBottom_toTopOf="@+id/gtfsMessageTextView"/>
</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/line_title_header.xml b/app/src/main/res/layout/line_title_header.xml
new file mode 100644
--- /dev/null
+++ b/app/src/main/res/layout/line_title_header.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:cardCornerRadius="5dp"
+ android:layout_margin="4dp"
+ >
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ >
+ <androidx.cardview.widget.CardView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/innerCardView"
+ android:background="@color/orange_500"
+ app:cardCornerRadius="54sp"
+ app:cardElevation="0sp"
+ android:layout_gravity="center_vertical"
+ android:layout_margin="5dp"
+ android:padding="3dp"
+ app:cardBackgroundColor="@color/orange_500"
+
+
+ >
+ <RelativeLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:minHeight="54sp"
+ android:minWidth="54sp"
+ >
+ <TextView
+ android:id="@+id/lineShortNameTextView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:textColor="@color/grey_100"
+ android:textSize="21sp"
+ android:text="231"
+ android:paddingStart="4sp"
+ android:paddingLeft="4sp"
+ android:paddingRight="4sp"
+ android:paddingEnd="4sp"
+ >
+ </TextView>
+ </RelativeLayout>
+ </androidx.cardview.widget.CardView>
+ <TextView android:layout_width="0dp" android:layout_height="match_parent"
+ android:id="@+id/lineDirectionTextView"
+ android:layout_weight="8"
+ android:textSize="20sp"
+ android:text="@string/route_towards_destination"
+ android:layout_margin="5dp"
+ android:gravity="center_vertical"
+ android:textAppearance="@style/TextAppearance.AppCompat.Medium"
+ android:layout_marginEnd="10dp"
+ />
+ </LinearLayout>
+
+</androidx.cardview.widget.CardView>
\ No newline at end of file
diff --git a/app/src/main/res/layout/linedetail_stop_infowindow.xml b/app/src/main/res/layout/linedetail_stop_infowindow.xml
new file mode 100644
--- /dev/null
+++ b/app/src/main/res/layout/linedetail_stop_infowindow.xml
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools" android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@color/cardview_light_background"
+ app:cardCornerRadius="5dp"
+ app:cardElevation="0dp"
+ android:textAlignment="center"
+ android:padding="0dp"
+ android:gravity="center_horizontal"
+>
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ >
+ <TextView
+ android:id="@+id/bubble_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:textColor="@color/line_drawn_poly"
+ android:textSize="16sp"
+ android:maxWidth="130sp"
+ app:layout_constraintStart_toStartOf="parent"
+ android:text="BALABALA"
+ android:textAlignment="center"
+ android:layout_margin="10dp"
+ app:layout_constraintTop_toTopOf="parent"
+ />
+ <TextView
+ android:id="@+id/bubble_description"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:textAlignment="center"
+ android:textSize="15sp"
+ android:maxWidth="100sp"
+ android:text="8"
+ app:layout_constraintTop_toBottomOf="@id/bubble_title"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ android:layout_marginLeft="8dp" android:layout_marginEnd="8dp"/>
+
+ <TextView
+ android:id="@+id/bubble_subdescription"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:paddingLeft="5dp"
+ android:paddingRight="5dp"
+ android:gravity="center_horizontal"
+ android:textSize="12sp"
+ android:textAlignment="center"
+ android:maxWidth="130sp"
+ android:text="21"
+ app:flow_horizontalBias="0.5"
+ app:layout_constraintTop_toBottomOf="@id/bubble_description"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ android:layout_marginLeft="8dp" android:layout_marginEnd="8dp"
+ android:layout_marginBottom="10dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ />
+ <ImageView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" app:srcCompat="@drawable/baseline_close_16"
+ android:id="@+id/closeIcon"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ android:layout_marginLeft="5dp"
+ android:layout_marginBottom="10dp"
+ android:layout_marginRight="1dp"
+ android:layout_marginTop="1dp"
+
+ app:layout_constraintStart_toEndOf="@+id/bubble_title"/>
+ </androidx.constraintlayout.widget.ConstraintLayout>
+</androidx.cardview.widget.CardView>
\ No newline at end of file
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -20,6 +20,11 @@
<string name="passages">Fermata: %1$s</string>
<string name="line">Linea</string>
<string name="lines">Linee</string>
+ <string name="urban_lines">Linee urbane</string>
+ <string name="extraurban_lines">Linee extraurbane</string>
+ <string name="turist_lines">Linee turistiche</string>
+ <string name="direction_duep">Direzione:</string>
+
<string name="line_fill">Linea: %1$s</string>
<string name="lines_fill">Linee: %1$s</string>
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -7,13 +7,18 @@
<color name="orange_700_30light">#994d00</color>
<color name="blue_500">#2196F3</color>
<color name="blue_620">#2a65e8</color>
- <color name="blue_700">#2060dd</color> <!-- #1976D2 -->
+ <color name="blue_700">#2060dd</color>
+ <color name="brown_vd">#8A4247</color><!-- #1976D2 -->
<color name="blue_mid_2">#2378e8</color>
<color name="blue_c_or_700">#0079f5</color>
<color name="teal_dark">#2a968b</color>
<color name="blue_comp_500">#0067ff</color>
+ <color name="blue_extra">#2F59CC</color>
+ <color name="brown_mattone">#CC5E43</color>
+ <color name="green_dark">#548017</color>
+
<color name="teal_500">#009688</color>
<color name="teal_300">#4DB6AC</color>
<color name="teal_200">#80cbc4</color>
@@ -21,14 +26,22 @@
<color name="grey_200">#dddddd</color>
<color name="grey_050">#f8f8f8</color>
<color name="grey_600">#757575</color>
+
<!--<color name="white">#FFFFFF</color>
<color name="accent">#009688</color>-->
<color name="metro_red">#DE0908</color>
<color name="red_darker">#b30000</color>
+ <color name="red_orange">#dd441f</color>
+ <color name="red_dark">#b30d0d</color>
<color name="blue_extraurbano">#2060DD</color>
<color name="white">#FFFFFF</color>
<color name="black">#000000</color>
<color name="black_900">#1c1c1c</color>
+
<color name="line_pattern_color">@color/blue_mid_2</color><!-- 2e8df0-->
+ <color name="line_drawn_poly">@color/red_dark</color>
+
+ <color name="bus_marker_color">@color/blue_extra</color>
+
</resources>
\ No newline at end of file
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -10,5 +10,6 @@
<dimen name="default_textView_margin">6dp</dimen>
<dimen name="margin_arr">5dp</dimen>
+ <dimen name="subtitle_size">28sp</dimen>
</resources>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -32,6 +32,11 @@
<string name="results">Choose the bus stop…</string>
<string name="line">Line</string>
<string name="lines">Lines</string>
+ <string name="urban_lines">Urban lines</string>
+ <string name="extraurban_lines">Extra urban lines</string>
+ <string name="turist_lines">Tourist lines</string>
+
+ <string name="direction_duep">Heading to:</string>
<string name="lines_fill">Lines: %1$s</string>
<string name="line_fill">Line: %1$s</string>
<string name="no_passages">No timetable found</string>
diff --git a/app/src/test/java/it/reyboz/bustorino/util/DistanceTest.java b/app/src/test/java/it/reyboz/bustorino/util/DistanceTest.java
new file mode 100644
--- /dev/null
+++ b/app/src/test/java/it/reyboz/bustorino/util/DistanceTest.java
@@ -0,0 +1,13 @@
+package it.reyboz.bustorino.util;
+
+import it.reyboz.bustorino.backend.utils;
+import org.junit.Test;
+import static org.junit.Assert.*;
+public class DistanceTest {
+
+ @Test
+ public void testDistance(){
+ double dist = utils.measuredistanceBetween(44.161957,8.302445, 44.645321, 7.656055);
+ assertEquals(dist,74333.9, 0.05);
+ }
+}
diff --git a/build.gradle b/build.gradle
--- a/build.gradle
+++ b/build.gradle
@@ -5,34 +5,36 @@
mavenCentral()
maven { url 'https://maven.google.com' }
google()
+ maven { url 'https://jitpack.io' }
+
}
dependencies {
classpath 'com.android.tools.build:gradle:7.3.1'
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.21"
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.20"
}
}
ext {
- androidXTestVersion = "1.4.0"
+ androidXTestVersion = "1.5.0"
//multidex
multidex_version = "2.0.1"
//libraries versions
- fragment_version = "1.4.1"
- activity_version = "1.4.0"
- appcompat_version = "1.4.1"
- preference_version = "1.2.0"
- work_version = "2.7.1"
+ fragment_version = "1.6.1"
+ activity_version = "1.7.2"
+ appcompat_version = "1.6.1"
+ preference_version = "1.2.1"
+ work_version = "2.8.1"
acra_version = "5.7.0"
lifecycle_version = "2.4.1"
arch_version = "2.1.0"
- room_version = "2.4.1"
+ room_version = "2.5.2"
//kotlin
- kotlin_version = '1.6.0'
- coroutines_version = "1.5.0"
+ kotlin_version = '1.8.22'
+ coroutines_version = "1.7.0"
}
@@ -41,7 +43,7 @@
maven { url 'https://maven.google.com' }
google()
mavenCentral()
- //maven { url "https://jitpack.io" }
+ maven { url "https://jitpack.io" }
}
}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sat, Mar 14, 07:57 (4 h, 6 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1744744
Default Alt Text
D126.1773471420.diff (170 KB)
Attached To
Mode
D126: Make new lines fragment showing each line on the map, use MQTT positions
Attached
Detach File
Event Timeline
Log In to Comment