Skip to content

Commit e7c1ecc

Browse files
committed
feat: add ecosystem places to search area
1 parent 5e4023c commit e7c1ecc

10 files changed

Lines changed: 354 additions & 63 deletions

File tree

app/src/main/java/com/cornellappdev/transit/models/RouteRepository.kt

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import kotlinx.coroutines.Dispatchers
1212
import kotlinx.coroutines.flow.MutableStateFlow
1313
import kotlinx.coroutines.flow.asStateFlow
1414
import kotlinx.coroutines.launch
15+
import java.util.concurrent.atomic.AtomicLong
1516
import javax.inject.Inject
1617
import javax.inject.Singleton
1718

@@ -60,6 +61,9 @@ class RouteRepository @Inject constructor(
6061
private val _libraryFlow: MutableStateFlow<ApiResponse<List<Library>>> =
6162
MutableStateFlow(ApiResponse.Pending)
6263

64+
// Monotonic token used to ensure only the latest search request updates placeFlow.
65+
private val latestSearchToken = AtomicLong(0L)
66+
6367
init {
6468
fetchAllStops()
6569
if (ECOSYSTEM_FLAG) {
@@ -142,15 +146,30 @@ class RouteRepository @Inject constructor(
142146
* Makes a new call to places related to a query string.
143147
*/
144148
fun makeSearch(query: String) {
149+
val token = latestSearchToken.incrementAndGet()
150+
151+
if (query.isBlank()) {
152+
if (token == latestSearchToken.get()) {
153+
_placeFlow.value = ApiResponse.Success(emptyList())
154+
}
155+
return
156+
}
157+
145158
_placeFlow.value = ApiResponse.Pending
146159
CoroutineScope(Dispatchers.IO).launch {
147160
try {
148161
val placeResponse = appleSearch(SearchQuery(query))
149162
val res = placeResponse.unwrap()
150163
val totalLocations = (res.places ?: emptyList()) + (res.stops ?: (emptyList()))
151-
_placeFlow.value = ApiResponse.Success(totalLocations)
164+
if (token == latestSearchToken.get()) {
165+
_placeFlow.value = ApiResponse.Success(totalLocations)
166+
}
167+
} catch (_: kotlinx.coroutines.CancellationException) {
168+
// Ignore cancellation; latest query owns the flow update.
152169
} catch (e: Exception) {
153-
_placeFlow.value = ApiResponse.Error
170+
if (token == latestSearchToken.get()) {
171+
_placeFlow.value = ApiResponse.Error
172+
}
154173
}
155174
}
156175
}

app/src/main/java/com/cornellappdev/transit/ui/components/LoadingLocationItems.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ package com.cornellappdev.transit.ui.components
22

33
import androidx.compose.foundation.lazy.LazyColumn
44
import androidx.compose.foundation.lazy.items
5+
import androidx.compose.foundation.layout.Arrangement
6+
import androidx.compose.foundation.layout.PaddingValues
57
import androidx.compose.runtime.Composable
8+
import androidx.compose.ui.Modifier
9+
import androidx.compose.ui.unit.dp
610
import com.cornellappdev.transit.models.Place
711
import com.cornellappdev.transit.networking.ApiResponse
812

@@ -25,15 +29,19 @@ fun LoadingLocationItems(searchResult: ApiResponse<List<Place>>, onClick: (Place
2529
if (searchResult.data.isEmpty()) {
2630
LocationNotFound()
2731
} else {
28-
LazyColumn {
32+
LazyColumn(
33+
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 6.dp),
34+
verticalArrangement = Arrangement.spacedBy(12.dp)
35+
) {
2936
items(
3037
searchResult.data
3138
) {
3239
MenuItem(
3340
type = it.type,
3441
label = it.name,
3542
sublabel = it.subLabel,
36-
onClick = { onClick(it) }
43+
onClick = { onClick(it) },
44+
modifier = Modifier
3745
)
3846
}
3947
}

app/src/main/java/com/cornellappdev/transit/ui/components/MenuItem.kt

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import androidx.compose.foundation.clickable
66
import androidx.compose.foundation.layout.Column
77
import androidx.compose.foundation.layout.Row
88
import androidx.compose.foundation.layout.fillMaxWidth
9-
import androidx.compose.foundation.layout.padding
9+
import androidx.compose.foundation.layout.size
1010
import androidx.compose.material3.Text
1111
import androidx.compose.runtime.Composable
1212
import androidx.compose.ui.Alignment
@@ -15,6 +15,8 @@ import androidx.compose.ui.res.painterResource
1515
import androidx.compose.ui.text.style.TextOverflow
1616
import androidx.compose.ui.tooling.preview.Preview
1717
import androidx.compose.ui.unit.dp
18+
import androidx.annotation.DrawableRes
19+
import androidx.compose.foundation.layout.Spacer
1820
import com.cornellappdev.transit.R
1921
import com.cornellappdev.transit.models.PlaceType
2022
import com.cornellappdev.transit.ui.theme.PrimaryText
@@ -23,33 +25,33 @@ import com.cornellappdev.transit.ui.theme.Style
2325

2426
/**
2527
* Card for each entry in the search bar
26-
* @param icon The icon for the item
28+
* @param type The place type used to resolve icon
2729
* @param label The label for the item
2830
* @param sublabel The sublabel for the item
2931
*/
3032
@Composable
31-
fun MenuItem(type: PlaceType, label: String, sublabel: String, onClick: () -> Unit) {
33+
fun MenuItem(
34+
type: PlaceType,
35+
label: String,
36+
sublabel: String,
37+
onClick: () -> Unit,
38+
modifier: Modifier = Modifier
39+
) {
40+
val displayedSublabel = ecosystemSublabelFor(type) ?: sublabel
41+
3242
Row(
33-
Modifier
43+
modifier
3444
.fillMaxWidth()
35-
.padding(horizontal = 16.dp, vertical = 6.dp)
3645
.clickable(onClick = onClick),
3746
verticalAlignment = Alignment.CenterVertically
3847
) {
39-
//TODO: Add icons for each ecosystem type
40-
if (type == PlaceType.APPLE_PLACE) {
41-
Image(
42-
painterResource(R.drawable.location_pin_gray),
43-
contentDescription = "Place",
44-
modifier = Modifier.padding(end = 20.dp),
45-
)
46-
} else {
47-
Image(
48-
painterResource(R.drawable.bus_stop_pin),
49-
contentDescription = "Stop",
50-
modifier = Modifier.padding(end = 20.dp),
51-
)
52-
}
48+
Image(
49+
painterResource(iconForPlaceType(type)),
50+
contentDescription = type.name,
51+
modifier = Modifier
52+
.size(24.dp),
53+
)
54+
Spacer(modifier = Modifier.size(12.dp))
5355
Column() {
5456
Text(
5557
text = label,
@@ -59,7 +61,7 @@ fun MenuItem(type: PlaceType, label: String, sublabel: String, onClick: () -> Un
5961
style = Style.heading3
6062
)
6163
Text(
62-
text = sublabel,
64+
text = displayedSublabel,
6365
color = SecondaryText,
6466
maxLines = 1,
6567
overflow = TextOverflow.Ellipsis,
@@ -69,6 +71,27 @@ fun MenuItem(type: PlaceType, label: String, sublabel: String, onClick: () -> Un
6971
}
7072
}
7173

74+
private fun ecosystemSublabelFor(type: PlaceType): String? = when (type) {
75+
PlaceType.EATERY -> "Eatery"
76+
PlaceType.LIBRARY -> "Library"
77+
PlaceType.PRINTER -> "Printer"
78+
PlaceType.GYM -> "Gym"
79+
else -> null
80+
}
81+
82+
private val searchIconByPlaceType = mapOf(
83+
PlaceType.BUS_STOP to R.drawable.bus_stop_pin,
84+
PlaceType.APPLE_PLACE to R.drawable.location_pin_gray,
85+
PlaceType.EATERY to R.drawable.eatery_icon,
86+
PlaceType.LIBRARY to R.drawable.library_icon,
87+
PlaceType.GYM to R.drawable.gym_icon,
88+
PlaceType.PRINTER to R.drawable.printer_icon
89+
)
90+
91+
@DrawableRes
92+
private fun iconForPlaceType(type: PlaceType): Int {
93+
return searchIconByPlaceType[type] ?: R.drawable.location_pin_gray
94+
}
7295

7396
@Preview(showBackground = true)
7497
@Composable
@@ -79,5 +102,5 @@ private fun PreviewMenuItemBusStop() {
79102
@Preview(showBackground = true)
80103
@Composable
81104
private fun PreviewMenuItemApplePlace() {
82-
MenuItem(PlaceType.APPLE_PLACE, "Apple Place", "Ithaca, NY", {})
105+
MenuItem(PlaceType.EATERY, "Apple Place", "Ithaca, NY", {})
83106
}

app/src/main/java/com/cornellappdev/transit/ui/components/SearchSuggestions.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,7 @@ import androidx.compose.foundation.rememberScrollState
1010
import androidx.compose.runtime.Composable
1111
import androidx.compose.ui.Modifier
1212
import androidx.compose.ui.unit.dp
13-
import androidx.navigation.NavController
1413
import com.cornellappdev.transit.models.Place
15-
import com.cornellappdev.transit.ui.viewmodels.LocationUIState
16-
import com.google.android.gms.maps.model.LatLng
1714

1815
/**
1916
* Display for suggested searches (recents and favorites)
@@ -43,6 +40,7 @@ fun SearchSuggestions(
4340
type = it.type,
4441
label = it.name,
4542
sublabel = it.subLabel,
43+
modifier = Modifier.padding(vertical = 6.dp),
4644
onClick = {
4745
onItemClick(it)
4846
}
@@ -59,6 +57,7 @@ fun SearchSuggestions(
5957
type = it.type,
6058
label = it.name,
6159
sublabel = it.subLabel,
60+
modifier = Modifier.padding(vertical = 6.dp),
6261
onClick = {
6362
onItemClick(it)
6463
}

app/src/main/java/com/cornellappdev/transit/ui/components/home/EcosystemBottomSheetContent.kt

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,6 @@ private fun BottomSheetFilteredContent(
249249
FilterState.LIBRARIES -> {
250250
libraryList(
251251
staticPlaces,
252-
navigateToPlace,
253252
onDetailsClick,
254253
favorites,
255254
onFavoriteStarClick,
@@ -430,6 +429,7 @@ private fun LazyListScope.gymList(
430429
) {
431430
when (gymsApiResponse) {
432431
is ApiResponse.Error -> {
432+
infoItem("Unable to load gyms")
433433
}
434434

435435
is ApiResponse.Pending -> {
@@ -439,6 +439,11 @@ private fun LazyListScope.gymList(
439439
}
440440

441441
is ApiResponse.Success -> {
442+
if (gymsApiResponse.data.isEmpty()) {
443+
infoItem("No gyms available")
444+
return
445+
}
446+
442447
items(gymsApiResponse.data) {
443448
RoundedImagePlaceCard(
444449
imageUrl = it.imageUrl,
@@ -478,12 +483,21 @@ private fun LazyListScope.printerList(
478483
) {
479484
when (staticPlaces.printers) {
480485
is ApiResponse.Error -> {
486+
infoItem("Unable to load printers")
481487
}
482488

483489
is ApiResponse.Pending -> {
490+
item {
491+
CenteredSpinningIndicator()
492+
}
484493
}
485494

486495
is ApiResponse.Success -> {
496+
if (staticPlaces.printers.data.isEmpty()) {
497+
infoItem("No printers available")
498+
return
499+
}
500+
487501
items(staticPlaces.printers.data) {
488502
val place = it.toPlace()
489503
val alert = if (it.location.contains("*")) {
@@ -529,6 +543,7 @@ private fun LazyListScope.eateryList(
529543
) {
530544
when (eateriesApiResponse) {
531545
is ApiResponse.Error -> {
546+
infoItem("Unable to load eateries")
532547
}
533548

534549
is ApiResponse.Pending -> {
@@ -538,6 +553,11 @@ private fun LazyListScope.eateryList(
538553
}
539554

540555
is ApiResponse.Success -> {
556+
if (eateriesApiResponse.data.isEmpty()) {
557+
infoItem("No eateries available")
558+
return
559+
}
560+
541561
items(eateriesApiResponse.data) {
542562
RoundedImagePlaceCard(
543563
imageUrl = it.imageUrl,
@@ -565,20 +585,28 @@ private fun LazyListScope.eateryList(
565585
*/
566586
private fun LazyListScope.libraryList(
567587
staticPlaces: StaticPlaces,
568-
navigateToPlace: (Place) -> Unit,
569588
navigateToDetails: (DetailedEcosystemPlace) -> Unit,
570589
favorites: Set<Place>,
571590
onFavoriteStarClick: (Place) -> Unit,
572591
distanceStringToPlace: (Double?, Double?) -> String,
573592
) {
574593
when (staticPlaces.libraries) {
575594
is ApiResponse.Error -> {
595+
infoItem("Unable to load libraries")
576596
}
577597

578598
is ApiResponse.Pending -> {
599+
item {
600+
CenteredSpinningIndicator()
601+
}
579602
}
580603

581604
is ApiResponse.Success -> {
605+
if (staticPlaces.libraries.data.isEmpty()) {
606+
infoItem("No libraries available")
607+
return
608+
}
609+
582610
items(staticPlaces.libraries.data) {
583611
RoundedImagePlaceCard(
584612
placeholderRes = R.drawable.olin_library,
@@ -596,6 +624,19 @@ private fun LazyListScope.libraryList(
596624
}
597625
}
598626

627+
private fun LazyListScope.infoItem(message: String) {
628+
item {
629+
Row(
630+
modifier = Modifier
631+
.fillMaxWidth()
632+
.padding(vertical = 20.dp),
633+
horizontalArrangement = Arrangement.Center
634+
) {
635+
Text(text = message)
636+
}
637+
}
638+
}
639+
599640
@Composable
600641
private fun StandardCard(
601642
place: Place,

app/src/main/java/com/cornellappdev/transit/ui/screens/HomeScreen.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,8 @@ fun HomeScreen(
178178
// Add search bar
179179
val addSearchBarValue = homeViewModel.addSearchQuery.collectAsStateWithLifecycle().value
180180

181-
// Add search bar query response
182-
val placeQueryResponse = homeViewModel.placeQueryFlow.collectAsStateWithLifecycle().value
181+
// Add search bar query response (backend + ecosystem, ranked by relevance)
182+
val placeQueryResponse = homeViewModel.addSearchResultsFlow.collectAsStateWithLifecycle().value
183183

184184
val filterStateValue = homeViewModel.filterState.collectAsStateWithLifecycle().value
185185

app/src/main/java/com/cornellappdev/transit/ui/screens/RouteScreen.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,7 @@ private fun RouteOptionsSearchSheet(
814814
type = it.type,
815815
label = it.name,
816816
sublabel = it.subLabel,
817+
modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp),
817818
onClick = {
818819
if (isStart) {
819820
routeViewModel.setStartPlace(
@@ -847,6 +848,7 @@ private fun RouteOptionsSearchSheet(
847848
type = it.type,
848849
label = it.name,
849850
sublabel = it.subLabel,
851+
modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp),
850852
onClick = {
851853
if (isStart) {
852854
routeViewModel.setStartPlace(

0 commit comments

Comments
 (0)