diff --git a/app/build.gradle b/app/build.gradle --- a/app/build.gradle +++ b/app/build.gradle @@ -116,6 +116,8 @@ // Guava implementation for DBUpdateWorker implementation 'com.google.guava:guava:33.5.0-android' + + implementation 'com.google.android.flexbox:flexbox:3.0.0' implementation "androidx.fragment:fragment-ktx:$fragment_version" implementation "androidx.activity:activity-ktx:$activity_version" diff --git a/app/src/main/java/it/reyboz/bustorino/adapters/PalinaAdapter.java b/app/src/main/java/it/reyboz/bustorino/adapters/PalinaAdapter.java --- a/app/src/main/java/it/reyboz/bustorino/adapters/PalinaAdapter.java +++ b/app/src/main/java/it/reyboz/bustorino/adapters/PalinaAdapter.java @@ -25,11 +25,13 @@ import androidx.preference.PreferenceManager; import android.content.SharedPreferences; +import android.view.Gravity; import android.os.Build; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.PopupMenu; import android.widget.TextView; import java.util.*; @@ -87,7 +89,13 @@ final Resources res = con.getResources(); vh.routeIDTextView.setText(route.getDisplayCode()); - vh.routeCard.setOnClickListener(view -> mRouteListener.requestShowingRoute(route)); + //vh.routeCard.setOnClickListener(view -> mRouteListener.requestShowingRoute(route)); + + // Clicking anywhere on the row shows a popup menu + vh.itemView.setOnClickListener(view -> + openPopupMenuDetails(con,view, route) + ); //vh.rowRouteDestination.getVisibility() == View.VISIBLE ? vh.rowRouteDestination : vh.itemView + if(route.destinazione==null || route.destinazione.length() == 0) { vh.rowRouteDestination.setVisibility(View.GONE); // move around the route timetable @@ -114,11 +122,6 @@ } vh.rowRouteDestination.setText(dest); - - //set click listener - vh.itemView.setOnClickListener(view -> { - mRouteListener.showRouteFullDirection(route); - }); } switch (route.type) { @@ -208,6 +211,26 @@ return Capitalize.DO_NOTHING; } + private void openPopupMenuDetails(Context con, View view, Route route){ + PopupMenu popup = new PopupMenu(con, view, Gravity.END); + popup.inflate(R.menu.menu_arrivals_line_item); + if (route.destinazione == null || route.destinazione.isEmpty()) { + popup.getMenu().findItem(R.id.action_show_direction).setVisible(false); + } + popup.setOnMenuItemClickListener(item -> { + int id = item.getItemId(); + if (id == R.id.action_open_line) { + mRouteListener.requestShowingRoute(route); + return true; + } else if (id == R.id.action_show_direction) { + mRouteListener.showRouteFullDirection(route); + return true; + } + return false; + }); + popup.show(); + } + public PalinaAdapter(Context context, Palina p, PalinaClickListener listener, boolean hideEmptyRoutes) { Comparator sorter = null; if (p.getPassaggiSourceIfAny()== Passaggio.Source.GTTJSON){ diff --git a/app/src/main/java/it/reyboz/bustorino/adapters/RouteAdapter.kt b/app/src/main/java/it/reyboz/bustorino/adapters/RouteAdapter.kt --- a/app/src/main/java/it/reyboz/bustorino/adapters/RouteAdapter.kt +++ b/app/src/main/java/it/reyboz/bustorino/adapters/RouteAdapter.kt @@ -12,7 +12,7 @@ class RouteAdapter(val routes: List, click: ItemClicker, - private val layoutId: Int = R.layout.entry_line_num_descr) : + private val layoutId: Int = R.layout.entry_line_name_description) : RecyclerView.Adapter() { val clickreference: WeakReference @@ -45,7 +45,7 @@ // 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.nameTextView.text = route.getShortNameDisplay() holder.descrptionTextView.text = route.longName holder.itemView.setOnClickListener{ diff --git a/app/src/main/java/it/reyboz/bustorino/adapters/RouteOnlyLineAdapter.kt b/app/src/main/java/it/reyboz/bustorino/adapters/RouteOnlyLineAdapter.kt --- a/app/src/main/java/it/reyboz/bustorino/adapters/RouteOnlyLineAdapter.kt +++ b/app/src/main/java/it/reyboz/bustorino/adapters/RouteOnlyLineAdapter.kt @@ -6,6 +6,7 @@ import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import it.reyboz.bustorino.R +import it.reyboz.bustorino.backend.FiveTNormalizer import it.reyboz.bustorino.backend.Palina import java.lang.ref.WeakReference @@ -47,7 +48,8 @@ // Get element from your dataset at this position and replace the // contents of the view with that element - viewHolder.textView.text = routeNames[position] + // SHOW "STAR" as "ST" + viewHolder.textView.text = FiveTNormalizer.filterFullStarName(routeNames[position]) viewHolder.itemView.setOnClickListener{ clickreference?.get()?.onItemClick(position, routeNames[position]) } diff --git a/app/src/main/java/it/reyboz/bustorino/backend/FiveTNormalizer.java b/app/src/main/java/it/reyboz/bustorino/backend/FiveTNormalizer.java --- a/app/src/main/java/it/reyboz/bustorino/backend/FiveTNormalizer.java +++ b/app/src/main/java/it/reyboz/bustorino/backend/FiveTNormalizer.java @@ -182,6 +182,7 @@ if(routeID.length() == 3 && routeID.charAt(2) == 'B') { return routeID.substring(0,2).concat("/"); } + //TODO: Decide what to do about the "+" lines (68+, 13+) switch(routeID) { case "1C": @@ -380,4 +381,13 @@ return sb.toString(); } + + public static String filterFullStarName(String name){ + String outName = name; + if(name.contains("STAR ")){ + //FIX FOR THE MaTO data + outName = outName.replace("STAR ","ST"); + } + return outName; + } } diff --git a/app/src/main/java/it/reyboz/bustorino/backend/Route.java b/app/src/main/java/it/reyboz/bustorino/backend/Route.java --- a/app/src/main/java/it/reyboz/bustorino/backend/Route.java +++ b/app/src/main/java/it/reyboz/bustorino/backend/Route.java @@ -432,6 +432,15 @@ return adjusted; } + public String getRouteLongDisplayName() { + + String routeName = FiveTNormalizer.routeInternalToDisplay(this.name); + if (routeName == null) { + routeName = this.displayCode; + } + return routeName; + } + // ---- Parcelable implem --- protected Route(Parcel in) { diff --git a/app/src/main/java/it/reyboz/bustorino/backend/Stop.java b/app/src/main/java/it/reyboz/bustorino/backend/Stop.java --- a/app/src/main/java/it/reyboz/bustorino/backend/Stop.java +++ b/app/src/main/java/it/reyboz/bustorino/backend/Stop.java @@ -282,6 +282,13 @@ return String.format(Locale.US, "geo:%f,%f", this.lat, this.lon); } + /** + * Check if the stop contains the coordinates + * @return true if both the latitude and the longitude are not null + */ + public final boolean hasCoords(){ + return (this.lat!=null)&&(this.lon!=null); + } public final @Nullable String getGeoURLWithAddress() { String url = getGeoURL(); diff --git a/app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsUtils.java b/app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsUtils.java --- a/app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsUtils.java +++ b/app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsUtils.java @@ -18,6 +18,7 @@ package it.reyboz.bustorino.backend.gtfs; import androidx.core.util.Pair; +import it.reyboz.bustorino.backend.FiveTNormalizer; import it.reyboz.bustorino.backend.ServiceType; abstract public class GtfsUtils { @@ -69,4 +70,13 @@ public static String getLineNameFromGtfsID(String routeID){ return getRouteInfoFromGTFS(routeID).second; } + + public static String lineNameDisplayFromGtfsID(String routeID){ + String name = getRouteInfoFromGTFS(routeID).second; + + String altName = FiveTNormalizer.routeInternalToDisplay(name); + if (altName==null) //WTF WHY DOES IT HAVE TO BE NULL + return name; + else return altName; + } } 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 @@ -199,7 +199,7 @@ for (i in 0 until routesStoppingJSON.length()){ val routeBaseInfo = routesStoppingJSON.getJSONObject(i) val r = Route(routeBaseInfo.getString("shortName"), Route.Type.UNKNOWN,"") - r.setGtfsId(routeBaseInfo.getString("gtfsId").trim()) + r.gtfsId = routeBaseInfo.getString("gtfsId").trim() baseRoutes.add(r) } @@ -233,7 +233,7 @@ //val gtfsRoutes = mutableListOf<>() return palina } - fun parseRouteStoptimesJSON(jsonPatternWithStops: JSONObject): Route{ + private fun parseRouteStoptimesJSON(jsonPatternWithStops: JSONObject): Route{ val patternJSON = jsonPatternWithStops.getJSONObject("pattern") val routeJSON = patternJSON.getJSONObject("route") @@ -258,7 +258,7 @@ "TRAM" -> routeType = Route.Type.TRAM } val route = Route( - routeJSON.getString("shortName"), + FiveTNormalizer.filterFullStarName(routeJSON.getString("shortName")), patternJSON.getString("headsign"), routeType, passages, diff --git a/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsRoute.kt b/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsRoute.kt --- a/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsRoute.kt +++ b/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsRoute.kt @@ -20,6 +20,7 @@ import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey +import it.reyboz.bustorino.backend.FiveTNormalizer @Entity(tableName=GtfsRoute.DB_TABLE) data class GtfsRoute( @@ -80,4 +81,8 @@ override fun getColumns(): Array { return COLUMNS } + + fun getShortNameDisplay(): String { + return FiveTNormalizer.filterFullStarName(shortName) + } } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.kt --- a/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.kt @@ -65,6 +65,7 @@ //Views protected lateinit var addToFavorites: ImageButton + protected lateinit var openInMapButton: ImageButton protected lateinit var timesSourceTextView: TextView protected lateinit var messageTextView: TextView protected lateinit var arrivalsRecyclerView: RecyclerView @@ -95,14 +96,8 @@ private val palinaClickListener: PalinaClickListener = object : PalinaClickListener { override fun showRouteFullDirection(route: Route) { - var routeName: String? + var routeName = route.routeLongDisplayName Log.d(DEBUG_TAG, "Make toast for line " + route.name) - - - routeName = FiveTNormalizer.routeInternalToDisplay(route.name) - if (routeName == null) { - routeName = route.displayCode - } if (context == null) Log.e(DEBUG_TAG, "Touched on a route but Context is null") else if (route.destinazione == null || route.destinazione.length == 0) { Toast.makeText( @@ -169,6 +164,7 @@ val root = inflater.inflate(R.layout.fragment_arrivals, container, false) messageTextView = root.findViewById(R.id.messageTextView) addToFavorites = root.findViewById(R.id.addToFavorites) + openInMapButton = root.findViewById(R.id.openInMapButton) // "How does it work part" howDoesItWorkTextView = root.findViewById(R.id.howDoesItWorkTextView) hideHintButton = root.findViewById(R.id.hideHintButton) @@ -219,7 +215,7 @@ val displayName = requireArguments().getString(STOP_TITLE) if (displayName != null) setTextViewMessage( String.format( - getString(R.string.passages), displayName + getString(R.string.passages_fill), displayName ) ) @@ -459,18 +455,25 @@ val adapter = PalinaAdapter(context, lastUpdatedPalina, palinaClickListener, true) showArrivalsSources(lastUpdatedPalina!!) resetListAdapter(adapter) + lastUpdatedPalina?.let{ pal -> + openInMapButton.setOnClickListener { + if (pal.hasCoords()) + mListener.showMapCenteredOnStop(pal) + } + } + val routesWithNoPassages = lastUpdatedPalina!!.routesNamesWithNoPassages if (routesWithNoPassages.isEmpty()) { //hide the views if there are no empty routes - noArrivalsRecyclerView!!.visibility = View.GONE + noArrivalsRecyclerView.visibility = View.GONE noArrivalsTitleView!!.visibility = View.GONE } else { Collections.sort(routesWithNoPassages, LinesNameSorter()) noArrivalsAdapter = RouteOnlyLineAdapter(routesWithNoPassages, null) - noArrivalsRecyclerView!!.adapter = noArrivalsAdapter + noArrivalsRecyclerView.adapter = noArrivalsAdapter - noArrivalsRecyclerView!!.visibility = View.VISIBLE + noArrivalsRecyclerView.visibility = View.VISIBLE noArrivalsTitleView!!.visibility = View.VISIBLE } @@ -539,7 +542,8 @@ Log.e("ArrivalsFragm$tag", "NO ID FOR THIS FRAGMENT - something went horribly wrong") } if (message.isNotEmpty()) { - setTextViewMessage(getString(R.string.passages, message)) + setTextViewMessage(getString(R.string.passages_fill, message)) + //setTextViewMessage(message) } // whatever is the case, update the star icon diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/GeneralMapLibreFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/GeneralMapLibreFragment.kt --- a/app/src/main/java/it/reyboz/bustorino/fragments/GeneralMapLibreFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/GeneralMapLibreFragment.kt @@ -45,6 +45,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.maplibre.android.MapLibre +import org.maplibre.android.camera.CameraPosition import org.maplibre.android.geometry.LatLng import org.maplibre.android.location.LocationComponent import org.maplibre.android.location.LocationComponentOptions @@ -326,7 +327,7 @@ } // Hide the bottom sheet and remove extra symbol - protected fun hideStopOrBusBottomSheet(){ + protected open fun hideStopOrBusBottomSheet(){ if (stopActiveSymbol!=null){ symbolManager?.delete(stopActiveSymbol) stopActiveSymbol = null @@ -534,7 +535,7 @@ */ protected fun showVehicleTripInBottomSheet( veh: String, - onDirectionsClick: (patternCode: String) -> Unit + onDirectionsClick: (patternCode: String, veh: String) -> Unit ) { val data = updatesByVehDict[veh] ?: run { Log.w(DEBUG_TAG, "Asked to show vehicle $veh, but it's not present in the updates") @@ -542,7 +543,7 @@ } bottomLayout?.let { val lineName = FiveTNormalizer.fixShortNameForDisplay( - GtfsUtils.getLineNameFromGtfsID(data.posUpdate.routeID), true + GtfsUtils.getLineNameFromGtfsID(data.posUpdate.routeID), false ) val pat = data.pattern if (pat != null) { @@ -554,7 +555,7 @@ stopNumberTextView.text = getString(R.string.line_fill, lineName) } directionsCard.setOnClickListener { - onDirectionsClick(pat?.code ?: "") + onDirectionsClick(pat?.code ?: "", veh) } directionsCard.visibility = View.VISIBLE bottomrightImage.setImageDrawable( @@ -700,6 +701,7 @@ val string_show = if (stop.numRoutesStopping==0) "" else requireContext().getString(R.string.lines_fill, stop.routesThatStopHereToString()) linesPassingTextView.text = string_show + linesPassingTextView.visibility = View.VISIBLE //SET ON CLICK LISTENER arrivalsCard.setOnClickListener{ @@ -911,6 +913,13 @@ updatePositionsIcons(forced = false) } + protected fun setCameraPosition(latitude: Double, longitude: Double, zoom: Double) { + map?.cameraPosition = CameraPosition.Builder() + .target(LatLng(latitude, longitude)) + .zoom(zoom) + .build() + } + companion object{ private const val DEBUG_TAG="GeneralMapLibreFragment" 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 @@ -23,7 +23,6 @@ import android.annotation.SuppressLint import android.content.Context import android.content.SharedPreferences -import android.content.res.ColorStateList import android.os.Bundle import android.util.Log import android.view.LayoutInflater @@ -35,18 +34,15 @@ import androidx.appcompat.content.res.AppCompatResources import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat -import androidx.core.view.ViewCompat import androidx.fragment.app.viewModels import androidx.preference.PreferenceManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.gson.JsonObject 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.FiveTNormalizer import it.reyboz.bustorino.backend.Stop import it.reyboz.bustorino.backend.gtfs.GtfsUtils import it.reyboz.bustorino.backend.gtfs.PolylineParser @@ -101,7 +97,7 @@ private var patternShown: MatoPatternWithStops? = null private val viewModel: LinesViewModel by viewModels() - private var firstInit = true + //private var firstInit = true private var pausedFragment = false private lateinit var switchButton: ImageButton @@ -263,8 +259,7 @@ } val titleTextView = rootView.findViewById(R.id.titleTextView) - titleTextView.text = getString(R.string.line)+" "+FiveTNormalizer.fixShortNameForDisplay( - GtfsUtils.getLineNameFromGtfsID(lineID), true) + titleTextView.text = getString(R.string.line)+" "+ GtfsUtils.lineNameDisplayFromGtfsID(lineID) favoritesButton?.isClickable = true favoritesButton?.setOnClickListener { @@ -342,24 +337,27 @@ usingMQTTPositions = PreferenceManager.getDefaultSharedPreferences(requireContext()) .getString(keySourcePositions, "mqtt").contentEquals("mqtt") - viewModel.patternsWithStopsByRouteLiveData.observe(viewLifecycleOwner){ - patterns -> savePatternsToShow(patterns) - } + viewModel.patternsWithStopsByRouteLiveData.observe(viewLifecycleOwner, this::savePatternsToShow) /* */ viewModel.stopsForPatternLiveData.observe(viewLifecycleOwner) { stops -> - if(mapView.visibility ==View.VISIBLE) - patternShown?.let{ - // We have the pattern and the stops here, time to display them - //TODO: Decide if we should follow the camera view given by the previous screen (probably the map fragment) - // use !restoredCameraInMap to do so - displayPatternWithStopsOnMap(it,stops, true) - } ?:{ - Log.w(DEBUG_TAG, "The viewingPattern is null!") - } - else{ - if(stopsRecyclerView.visibility==View.VISIBLE) + val pattern = viewModel.selectedPatternLiveData.value + if (pattern == null) { + Log.w(DEBUG_TAG, "The selectedPattern is null!") + return@observe + } + if(mapView.visibility ==View.VISIBLE) { + // We have the pattern and the stops here, time to display them + //TODO: Decide if we should follow the camera view given by the previous screen (probably the map fragment) + // use !restoredCameraInMap to do so + + // val shouldZoom = (shownStopInBottomSheet == null) //use this if we want to avoid zoom when we're keeping the stop open + displayPatternWithStopsOnMap(pattern, stops, true) + } else { + if(stopsRecyclerView.visibility==View.VISIBLE) { + patternShown = pattern showStopsInRecyclerView(stops) + } } } viewModel.gtfsRoute.observe(viewLifecycleOwner){route-> @@ -395,6 +393,20 @@ updatePositionsIcons(true) livePositionsViewModel.retriggerPositionUpdate() } + if (shownStopInBottomSheet!=null){ + //check if the stop is inside the new pattern + /*val s = shownStopInBottomSheet!! + val newPatternStops = patternWithStops.stopsIndices + val filterPStops = newPatternStops.filter { ps -> ps.stopGtfsId == "gtt:${s.ID}" } + if (filterPStops.isEmpty()){ + hideStopOrBusBottomSheet() + } + */ + // do another thing, just close the stop when the pattern is changed + if (patt.code != patternWithStops.pattern.code){ + hideStopOrBusBottomSheet() + } + } } } livePositionsViewModel.setGtfsLineToFilterPos(lineID, patternWithStops.pattern) @@ -576,16 +588,16 @@ mapReady.addOnMapClickListener { point -> val screenPoint = mapReady.projection.toScreenLocation(point) - val features = mapReady.queryRenderedFeatures(screenPoint, STOPS_LAYER_ID) + val stopsNearby = mapReady.queryRenderedFeatures(screenPoint, STOPS_LAYER_ID) val busNearby = mapReady.queryRenderedFeatures(screenPoint, BUSES_LAYER_ID) - if (features.isNotEmpty()) { - val feature = features[0] + //Log.d(DEBUG_TAG, "onMapClick, stopsNearby: $stopsNearby \nstopShown: $shownStopInBottomSheet \nbusNearby: $busNearby,") + + if (stopsNearby.isNotEmpty()) { + val feature = stopsNearby[0] val id = feature.getStringProperty("id") - val name = feature.getStringProperty("name") - //Toast.makeText(requireContext(), "Clicked on $name ($id)", Toast.LENGTH_SHORT).show() val stop = viewModel.getStopByID(id) stop?.let { - if (isBottomSheetShowing() || vehShowing.isNotEmpty()){ + if (isBottomSheetShowing() || vehShowing.isNotEmpty()) { hideStopOrBusBottomSheet() } openStopInBottomSheet(it) @@ -597,21 +609,7 @@ return@addOnMapClickListener true } else if (busNearby.isNotEmpty()){ val feature = busNearby[0] - val vehid = feature.getStringProperty("veh") - val route = feature.getStringProperty("line") - if(isBottomSheetShowing()) - hideStopOrBusBottomSheet() - //if(context!=null){ - // Toast.makeText(context, "Veh $vehid on route ${route.slice(0..route.length-2)}", Toast.LENGTH_SHORT).show() - //} - showVehicleTripInBottomSheet(vehid) - updatesByVehDict[vehid]?.let { - //if (it.posUpdate.latitude != null && it.longitude != null) - mapReady.animateCamera( - CameraUpdateFactory.newLatLng(LatLng(it.posUpdate.latitude, it.posUpdate.longitude)), - 750 - ) - } + openBusFromMapClick(feature) return@addOnMapClickListener true } @@ -626,7 +624,7 @@ val zoom = 12.0 val latlngTarget = LatLng(MapLibreFragment.DEFAULT_CENTER_LAT, MapLibreFragment.DEFAULT_CENTER_LON) if(!setViewAlready) - mapReady.cameraPosition = savedCameraPosition ?:CameraPosition.Builder().target(latlngTarget).zoom(zoom).build() + mapReady.cameraPosition = savedCameraPosition ?:CameraPosition.Builder().target(latlngTarget).zoom(zoom).build() savedCameraPosition = null @@ -637,9 +635,23 @@ return true } - private fun observeBusPositionUpdates(){ - + /** + * Separate function to find the vehicle associated with a feature and display it + */ + private fun openBusFromMapClick(feature: Feature){ + val vehid = feature.getStringProperty("veh") + if(isBottomSheetShowing()) + hideStopOrBusBottomSheet() + showVehicleTripInBottomSheet(vehid) + updatesByVehDict[vehid]?.let { + map?.animateCamera( + CameraUpdateFactory.newLatLng(LatLng(it.posUpdate.latitude, it.posUpdate.longitude)), + 750 + ) + } + } + private fun observeBusPositionUpdates(){ //live bus positions livePositionsViewModel.filteredLocationUpdates.observe(viewLifecycleOwner){ pair -> //Log.d(DEBUG_TAG, "Received ${updates.size} updates for the positions") @@ -672,12 +684,27 @@ } private fun showVehicleTripInBottomSheet(veh: String) { - super.showVehicleTripInBottomSheet(veh) { patternCode -> + super.showVehicleTripInBottomSheet(veh) { patternCode, veh -> //this is checked in @GeneralMapLibreFragment //val data = updatesByVehDict[veh] ?: return@showVehicleTripInBottomSheet if (patternCode.isEmpty()) return@showVehicleTripInBottomSheet if (patternShown?.pattern?.code == patternCode) { - Toast.makeText(context, R.string.showing_same_direction, Toast.LENGTH_SHORT).show() + //center view on vehicle + updatesByVehDict[veh]?.let { up-> + map?.let{ + /* + val c = it.cameraPosition + it.moveCamera(CameraUpdateFactory.CameraPositionUpdate(c.bearing, + LatLng(up.posUpdate.latitude, up.posUpdate.longitude), + c.tilt,c.zoom, c.padding) + ) + */ + it.animateCamera(CameraUpdateFactory.newLatLng(LatLng(up.posUpdate.latitude, up.posUpdate.longitude))) + } + } ?: { + Toast.makeText(context, R.string.showing_same_direction, Toast.LENGTH_SHORT).show() + } + } else { showPatternWithCode(patternCode) } @@ -749,7 +776,7 @@ var p: MatoPatternWithStops? = null if (patternIdToShow.isNotEmpty()){ - for (patt in currentPatterns) { + for (patt in patterns) { if (patt.pattern.code == patternIdToShow){ p = patt } @@ -763,7 +790,7 @@ else if(stopIDFromToShow.isNotEmpty()) { val stopGtfsID = "gtt:$stopIDFromToShow" var pLength = 0 - for (patt in currentPatterns) { + for (patt in patterns) { for (pstop in patt.stopsIndices) { if (pstop.stopGtfsId == stopGtfsID) { //found @@ -781,9 +808,9 @@ else Log.d(DEBUG_TAG, "Requesting to show pattern from stop $stopIDFromToShow, found pattern ${p.pattern.code}") } - - stopIDFromToShow = "" + // the flag of showing pattern is not necessary anymore, we have set the pattern patternIdToShow = "" + // the flag of selecting from stop needs to be used again when displaying the pattern return p } /** @@ -798,7 +825,7 @@ it.addAll(currentPatterns.map { p->"${p.pattern.directionId} - ${p.pattern.headsign}" }) it.notifyDataSetChanged() } - val patternToShow = filterPatternFromArgs(patterns) + val patternToShow = filterPatternFromArgs(currentPatterns) if(patternToShow!=null) { //showPattern(patternToShow) patternShown = patternToShow @@ -816,7 +843,6 @@ Log.d(DEBUG_TAG, "Requesting stops for pattern ${patternWithStops.pattern.code}") viewModel.selectedPatternLiveData.value = patternWithStops viewModel.currentPatternStops.value = patternWithStops.stopsIndices.sortedBy { i-> i.order } - patternShown = patternWithStops viewModel.requestStopsForPatternWithStops(patternWithStops) } @@ -831,12 +857,11 @@ } } Log.d(DEBUG_TAG, "Requesting stops fro pattern $code in position: $pos") + // this triggers the showing on the map / recyclerview if (pos !=-2) patternsSpinner.setSelection(pos) else Log.e(DEBUG_TAG, "Pattern with code $code not found!!") - //request pattern stops from DB - //setPatternAndReqStops(patternWs) } /** @@ -957,12 +982,28 @@ Log.e(DEBUG_TAG, "Stops layer is not started!!") } + var reallyZoomToPattern = zoomToPattern + if(stopIDFromToShow.isNotEmpty()){ + //open the stop + val stopfilt = stopsSorted.filter { s -> s.ID == stopIDFromToShow } + if (stopfilt.isEmpty()){ + Log.e(DEBUG_TAG, "Tried to show stop but it's not in the selected pattern") + } else{ + val stop = stopfilt[0] + openStopInBottomSheet(stop) + + if(stop.hasCoords()) { + reallyZoomToPattern = false + setCameraPosition(stop.latitude!!, stop.longitude!!, 13.5) + } + + } + // Reset this to avoid checking again when showing + stopIDFromToShow = "" + //camera set + } + if(reallyZoomToPattern) zoomToCurrentPattern() - //POINTS LIST IS NOT IN ORDER ANY MORE - //if(!map.overlayManager.contains(stopsOverlay)){ - // map.overlayManager.add(stopsOverlay) - //} - if(zoomToPattern) zoomToCurrentPattern() } private fun initializeRecyclerView(){ diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/LinesGridShowingFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/LinesGridShowingFragment.kt --- a/app/src/main/java/it/reyboz/bustorino/fragments/LinesGridShowingFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/LinesGridShowingFragment.kt @@ -17,6 +17,9 @@ import androidx.recyclerview.widget.RecyclerView import androidx.work.WorkInfo import androidx.work.WorkManager +import com.google.android.flexbox.FlexDirection +import com.google.android.flexbox.FlexboxLayoutManager +import com.google.android.flexbox.JustifyContent import it.reyboz.bustorino.R import it.reyboz.bustorino.adapters.RouteAdapter import it.reyboz.bustorino.adapters.RouteOnlyLineAdapter @@ -30,7 +33,6 @@ import it.reyboz.bustorino.util.ViewUtils import it.reyboz.bustorino.viewmodels.LinesGridShowingViewModel - class LinesGridShowingFragment : ScreenBaseFragment() { @@ -63,6 +65,13 @@ private val linesComparator = Comparator { a,b -> return@Comparator linesNameSorter.compare(a.shortName, b.shortName) } + private val linesPriorityComparator = Comparator> { pa, pb -> + if (pa.second != pb.second){ + return@Comparator pa.second - pb.second + } else{ + return@Comparator linesNameSorter.compare(pa.first.shortName, pb.first.shortName) + } + } private val routeClickListener = RouteAdapter.ItemClicker { fragmentListener.openLineFromStop(it.gtfsId, null) @@ -73,6 +82,13 @@ private val lastQueryEmptyForAgency = HashMap(3) private var openRecyclerView = "AG_URBAN" + private fun getFlexLayoutManager(context: Context): FlexboxLayoutManager{ + val layoutManager = FlexboxLayoutManager(context) + layoutManager.flexDirection = FlexDirection.ROW + layoutManager.justifyContent = JustifyContent.FLEX_START + + return layoutManager + } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -105,37 +121,36 @@ recyView.layoutManager = gridLayoutManager } //init favorites recyclerview - val gridLayoutManager = AutoFitGridLayoutManager( - requireContext().applicationContext, - (utils.convertDipToPixels(context, 70f)).toInt() - ) - favoritesRecyclerView.layoutManager = gridLayoutManager + favoritesRecyclerView.layoutManager = getFlexLayoutManager(requireContext()) + + viewModel.getLinesLiveData().observe(viewLifecycleOwner){ rL -> - viewModel.getLinesLiveData().observe(viewLifecycleOwner){ - //routesList = ArrayList(it) - //routesList.sortWith(linesComparator) routesByAgency.clear() for (k in AGENCIES){ routesByAgency[k] = ArrayList() } - - for(route in it){ + val routesPrioByAg = HashMap>>() + for (ag in AGENCIES){ + routesPrioByAg[ag] = ArrayList() + } + for(p in rL){ + val route = p.first val agency = route.agencyID if(agency !in routesByAgency.keys){ Log.e(DEBUG_TAG, "The agency $agency is not present in the predefined agencies (${routesByAgency.keys})") } routesByAgency[agency]?.add(route) + routesPrioByAg[agency]!!.add(p) } //zip agencies and recyclerviews - Companion.AGENCIES.zip(recViews) { ag, recView -> - routesByAgency[ag]?.let { routeList -> - if (routeList.size > 0) { - routeList.sortWith(linesComparator) - //val adapter = RouteOnlyLineAdapter(it.map { rt -> rt.shortName }) - val adapter = RouteAdapter(routeList, routeClickListener) + AGENCIES.zip(recViews) { ag, recView -> + routesPrioByAg[ag]?.let { routePrioList -> + if (routePrioList.isNotEmpty()) { + routePrioList.sortWith(linesPriorityComparator) + val adapter = RouteAdapter(routePrioList.map { it.first }, routeClickListener) val lastQueryEmpty = if(ag in lastQueryEmptyForAgency.keys) lastQueryEmptyForAgency[ag]!! else true if (lastQueryEmpty) recView.adapter = adapter @@ -148,7 +163,7 @@ lastQueryEmptyForAgency[ag] = true } - durations[ag] = if(routeList.size < 20) ViewUtils.DEF_DURATION else 1000 + durations[ag] = if(routePrioList.size < 20) ViewUtils.DEF_DURATION else 1000 } } @@ -390,7 +405,7 @@ companion object { - private const val COLUMN_WIDTH_DP=200 + private const val COLUMN_WIDTH_DP=250 private const val AG_FAV = "fav" private const val AG_URBAN = "gtt:U" private const val AG_EXTRAURB ="gtt:E" diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt --- a/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt @@ -161,6 +161,12 @@ super.onCreate(savedInstanceState) arguments?.let { initialStopToShow = Stop.fromBundle(arguments) + if (initialStopToShow==null){ + + } else if(!initialStopToShow!!.hasCoords()){ + //null the stop if it doesn't have coordinates, we cannot find it + initialStopToShow = null + } } } @@ -352,9 +358,11 @@ if (initialStopToShow!=null){ val s = initialStopToShow!! - mapReady.cameraPosition = CameraPosition.Builder().target( - LatLng(s.latitude!!, s.longitude!!) - ).zoom(DEFAULT_ZOOM).build() + if(s.hasCoords()){ + mapReady.cameraPosition = CameraPosition.Builder().target( + LatLng(s.latitude!!, s.longitude!!) + ).zoom(DEFAULT_ZOOM).build() + } restoredMapCamera.set(true) } else{ var boundsRestored = false @@ -437,6 +445,12 @@ override fun showOpenStopWithSymbolLayer(): Boolean { return false } + override fun hideStopOrBusBottomSheet(){ + if (shownStopInBottomSheet?.ID == initialStopToShow?.ID){ + initialStopToShow = null + } + super.hideStopOrBusBottomSheet() + } override fun onAttach(context: Context) { super.onAttach(context) @@ -536,7 +550,7 @@ private fun showVehicleTripInBottomSheet(veh: String) { val data = updatesByVehDict[veh] ?: return - super.showVehicleTripInBottomSheet(veh) { patternCode -> + super.showVehicleTripInBottomSheet(veh) { patternCode, _ -> map?.let { mapStateViewModel.saveMapState(it) } fragmentListener?.openLineFromVehicle( data.posUpdate.getLineGTFSFormat(), @@ -553,7 +567,8 @@ initialStopToShow?.let{ s-> //show the stop in the bottom sheet if(!initialStopShown && (s.ID in stopsShowing.map { it.ID })) { - openStopInBottomSheet(s) + val stopToShow = stopsShowing.first { it.ID == s.ID } + openStopInBottomSheet(stopToShow) initialStopShown = true } } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/ResultListFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/ResultListFragment.java --- a/app/src/main/java/it/reyboz/bustorino/fragments/ResultListFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/ResultListFragment.java @@ -34,7 +34,6 @@ import android.view.ViewGroup; import android.widget.*; -import com.google.android.material.floatingactionbutton.FloatingActionButton; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.FiveTNormalizer; @@ -167,7 +166,7 @@ }); String displayName = getArguments().getString(ArrivalsFragment.STOP_TITLE); setTextViewMessage(String.format( - getString(R.string.passages), displayName)); + getString(R.string.passages_fill), displayName)); break; default: throw new IllegalStateException("Argument passed was not of a supported type"); diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/ScreenBaseFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/ScreenBaseFragment.java --- a/app/src/main/java/it/reyboz/bustorino/fragments/ScreenBaseFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/ScreenBaseFragment.java @@ -3,8 +3,6 @@ import android.Manifest; import android.content.Context; import android.content.SharedPreferences; -import android.os.Bundle; -import android.view.Gravity; import android.view.View; import android.view.inputmethod.InputMethodManager; import android.widget.Toast; diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesGridShowingViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesGridShowingViewModel.kt --- a/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesGridShowingViewModel.kt +++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesGridShowingViewModel.kt @@ -39,15 +39,24 @@ fun getLineQueryValue():String{ return queryLiveData.value ?: "" } - private val filteredLinesLiveData = MediatorLiveData>() - fun getLinesLiveData(): LiveData> { - return filteredLinesLiveData - } + private val filteredLinesLiveData = MediatorLiveData>>() + fun getLinesLiveData() = filteredLinesLiveData - private fun filterLinesForQuery(lines: List, query: String): List{ + private fun filterLinesForQuery(lines: List, query: String): ArrayList>{ val result= lines.filter { r-> query.lowercase() in r.shortName.lowercase() } - - return result + //val filterDescr = lines.filter { r -> query.lowercase() in r.description.lowercase() } + val out = ArrayList>() + for (r in result){ + out.add(Pair(r,1)) + } + for (r: GtfsRoute in lines) { + if (query.lowercase() in r.description.lowercase()) { + if (r !in result){ + out.add(Pair(r,2)) + } + } + } + return out } init { diff --git a/app/src/main/res/drawable/ic_star_filled.xml b/app/src/main/res/drawable/ic_star_filled.xml --- a/app/src/main/res/drawable/ic_star_filled.xml +++ b/app/src/main/res/drawable/ic_star_filled.xml @@ -1,6 +1,6 @@ diff --git a/app/src/main/res/layout/entry_line_num_descr.xml b/app/src/main/res/layout/entry_line_name_description.xml rename from app/src/main/res/layout/entry_line_num_descr.xml rename to app/src/main/res/layout/entry_line_name_description.xml --- a/app/src/main/res/layout/entry_line_num_descr.xml +++ b/app/src/main/res/layout/entry_line_name_description.xml @@ -4,7 +4,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" app:cardCornerRadius="5dp" - android:layout_margin="4dp" + android:layout_margin="8dp" > - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_arrivals.xml b/app/src/main/res/layout/fragment_arrivals.xml --- a/app/src/main/res/layout/fragment_arrivals.xml +++ b/app/src/main/res/layout/fragment_arrivals.xml @@ -13,37 +13,79 @@ android:layout_width="match_parent" android:layout_height="wrap_content" app:cardCornerRadius="5dp" - app:cardElevation="4dp" + app:cardElevation="6dp" android:layout_alignParentTop="true" android:layout_marginStart="8sp" android:layout_marginEnd="8sp" android:layout_marginTop="5sp" - android:padding="5sp" + android:padding="8sp" > - - - - + android:layout_height="wrap_content"> + + + + + + diff --git a/app/src/main/res/menu/menu_arrivals_line_item.xml b/app/src/main/res/menu/menu_arrivals_line_item.xml new file mode 100644 --- /dev/null +++ b/app/src/main/res/menu/menu_arrivals_line_item.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -41,7 +41,7 @@ Vérifiez cocher au moins un élément à importer ! Importer les favoris depuis une sauvegarde Importer les préférences depuis une sauvegarde - Arrivées à: %1$s + Arrivées à: %1$s En savoir plus Rencontrer l\'auteur Aide 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 @@ -18,7 +18,8 @@ Nessun passaggio trovato alla fermata Ricerca arrivi da %1$s Errore di lettura del sito 5T/GTT (dannato sito!) - Fermata: %1$s + Fermata: %1$s + Fermata: Linea Linee Linee urbane @@ -45,6 +46,8 @@ Codice sorgente Licenza Incontra l\'autore + Mostra linea + Vedi direzione Fermata aggiunta ai preferiti Impossibile aggiungere ai preferiti (memoria piena o database corrotto?)! Preferiti diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -140,7 +140,7 @@ Voer bushalte nummer in Check je internetverbinding! Te korte naam, typ meer karakters en probeer opnieuw - Aankomsten om: %1$s + Aankomsten om: %1$s Lijnen: %1$s Meer over Favorieten 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 @@ -31,7 +31,8 @@ Error parsing the 5T/GTT website (damn site!) Name too short, type more characters and retry - Arrivals at: %1$s + Arrivals at: %1$s + Arrivals at: Choose the bus stop… Line Lines @@ -142,6 +143,8 @@ Cannot add to favorites (storage full or corrupted database?)! View on a map + Open line + Show full direction Cannot find any application to show it in Cannot find the position of the stop