Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ class GeocoderTest {
settlementGrid.start(ApplicationProvider.getApplicationContext())

val milngavie = LngLatAlt(-4.317166334292434, 55.941822016283)
geocodeLocation(geocoderList, milngavie, "Greggs Milngavi", gridState, settlementGrid)
geocodeLocation(geocoderList, milngavie, "Honeybee Bakery, Milngavie", gridState, settlementGrid)

val lisbon = LngLatAlt(-9.145010116796168, 38.707989573367804)
Expand Down Expand Up @@ -378,6 +379,7 @@ class GeocoderTest {

val milngavie = LngLatAlt(-4.317166334292434, 55.941822016283)
val milngavieResults = geocoder.getAddressFromLocationName("Honeybee Bakery, Milngavie", milngavie, appContext)
val greggsResults = geocoder.getAddressFromLocationName("Greggs Milngavi", milngavie, appContext)

val lisbon = LngLatAlt(-9.145010116796168, 38.707989573367804)
val lisbonResults = geocoder.getAddressFromLocationName("Taberna Tosca, Lisbon", lisbon, appContext)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ class TtsEngine(val audioEngine: NativeAudioEngine,
val voiceType = sharedPreferences.getString(MainActivity.VOICE_TYPE_KEY, MainActivity.VOICE_TYPE_DEFAULT)!!
if(textToSpeech.voices != null) {
for (voice in textToSpeech.voices) {
if(voice == null) continue
if (voice.name == voiceType) {
if (textToSpeechVoiceType != voice.name) {
Log.d(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,11 @@ fun LocationItem(
contentDescription = if(decoration.index != -1) {
"${decoration.indexDescription} ${decoration.index + 1}. ${item.name}"
} else {
item.name
val description = item.description
if((description != null) && description.startsWith(item.name))
description
else
item.name + ", " + item.description
}
}
if(decoration.reorderable) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.scottishtecharmy.soundscape.components

import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
Expand Down Expand Up @@ -35,26 +36,37 @@ import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
import kotlinx.coroutines.delay
import org.scottishtecharmy.soundscape.R
import org.scottishtecharmy.soundscape.geojsonparser.geojson.LngLatAlt
import org.scottishtecharmy.soundscape.screens.home.SearchFunctions
import org.scottishtecharmy.soundscape.screens.home.data.LocationDescription
import org.scottishtecharmy.soundscape.screens.talkbackLive
import org.scottishtecharmy.soundscape.ui.theme.spacing

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainSearchBar(
searchText: String,
isSearching: Boolean,
searchInProgress: Boolean,
itemList: List<LocationDescription>,
searchFunctions: SearchFunctions,
onItemClick: (LocationDescription) -> Unit,
userLocation: LngLatAlt?
) {
val focusRequester = remember { FocusRequester() }
val searchLocation = remember { userLocation }
val firstItemFocusRequester = remember { FocusRequester() }
val searchTriggered = remember { mutableStateOf(false) }

var textFieldValue by remember {
mutableStateOf(TextFieldValue(searchText, TextRange(searchText.length)))
}

// Track the last search query for which we moved focus to results
var lastFocusedQuery by remember { mutableStateOf<String?>(null) }


LaunchedEffect(searchText) {
if (searchText != textFieldValue.text) {
textFieldValue = textFieldValue.copy(
Expand All @@ -65,9 +77,22 @@ fun MainSearchBar(
}
LaunchedEffect(isSearching) {
if (isSearching) {
focusRequester.requestFocus()
// Force cursor to the end when returning/expanding
textFieldValue = textFieldValue.copy(selection = TextRange(searchText.length))
} else {
// Reset the last focused query when search is closed
lastFocusedQuery = null
}
}

LaunchedEffect(itemList, searchInProgress) {
// Move focus to the first result when search finishes and there are results,
// but only if we haven't already moved focus for this specific query.
if (!searchInProgress && itemList.isNotEmpty() && searchText != lastFocusedQuery) {
// A small delay ensures the UI has composed the results before we try to focus
delay(1000)
firstItemFocusRequester.requestFocus()
lastFocusedQuery = searchText
}
}

Expand All @@ -90,19 +115,20 @@ fun MainSearchBar(
// bar we'll await this being fixed in Material3.
query = textFieldValue.text,
onQueryChange = { newText ->
println("onQueryChange $newText ${newText.length} ${textFieldValue.selection}")
textFieldValue = textFieldValue.copy(text = newText, selection = TextRange(newText.length))
searchFunctions.onSearchTextChange(newText)
},
onSearch = { searchFunctions.onTriggerSearch() },
onSearch = {
searchTriggered.value = true
searchFunctions.onTriggerSearch()
},
expanded = isSearching,
onExpandedChange = { expanded ->
if (expanded != isSearching) {
searchFunctions.onToggleSearch()
}
},
placeholder = { Text(stringResource(id = R.string.search_choose_destination)) },
modifier = Modifier.focusRequester(focusRequester),
leadingIcon = {
when {
!isSearching -> {
Expand Down Expand Up @@ -137,46 +163,70 @@ fun MainSearchBar(
}
}
) {
Column(
modifier =
Modifier
.semantics {
this.collectionInfo =
CollectionInfo(
rowCount = itemList.size, // Total number of items
columnCount = 1, // Single-column list
)
}
.fillMaxSize()
) {
LazyColumn(modifier = Modifier.padding(top = spacing.medium)) {
itemsIndexed(itemList) { index, item ->
Column {
LocationItem(
item = item,
decoration = LocationItemDecoration(
location = true,
source = item.source,
details = EnabledFunction(
true,
{
onItemClick(item)
}
if(searchInProgress) {
Text(
text = stringResource(R.string.search_searching),
modifier = Modifier.talkbackLive()
)
}
else if(itemList.isEmpty()) {
if(searchTriggered.value) {
Text(
text = stringResource(R.string.search_no_results),
modifier = Modifier.talkbackLive()
)
}
} else {
Column(
modifier =
Modifier
.semantics {
this.collectionInfo =
CollectionInfo(
rowCount = itemList.size, // Total number of items
columnCount = 1, // Single-column list
)
),
modifier =
Modifier.semantics {
this.collectionItemInfo =
CollectionItemInfo(
rowSpan = 1,
columnSpan = 1,
rowIndex = index,
columnIndex = 0,
)
},
userLocation = userLocation
)
HorizontalDivider()
}
.fillMaxSize()
) {
LazyColumn(modifier = Modifier.padding(top = spacing.medium)) {
itemsIndexed(itemList) { index, item ->
var modifier =
Modifier.semantics {
this.collectionItemInfo =
CollectionItemInfo(
rowSpan = 1,
columnSpan = 1,
rowIndex = index,
columnIndex = 0,
)
}

if(index == 0) {
println("Set up focusRequester")
modifier = modifier
.focusRequester(firstItemFocusRequester)
.focusable()
}

Column {
LocationItem(
item = item,
decoration = LocationItemDecoration(
location = true,
source = item.source,
details = EnabledFunction(
true,
{
onItemClick(item)
}
)
),
modifier = modifier,
userLocation = searchLocation
)
HorizontalDivider()
}
}
}
}
Expand All @@ -191,6 +241,7 @@ fun MainSearchPreview() {
MainSearchBar(
searchText = "",
isSearching = false,
searchInProgress = false,
emptyList(),
SearchFunctions(null),
{},
Expand All @@ -203,12 +254,24 @@ fun MainSearchPreviewSearching() {
MainSearchBar(
searchText = "Monaco",
isSearching = true,
searchInProgress = false,
emptyList(),
SearchFunctions(null),
{},
LngLatAlt()
)
}



@Preview(showBackground = true)
@Composable
fun MainSearchPreviewSearchInProgress() {
MainSearchBar(
searchText = "Monaco",
isSearching = true,
searchInProgress = true,
emptyList(),
SearchFunctions(null),
{},
LngLatAlt()
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@ import kotlinx.coroutines.withContext
import org.scottishtecharmy.soundscape.components.LocationSource
import org.scottishtecharmy.soundscape.geoengine.UserGeometry
import org.scottishtecharmy.soundscape.geoengine.getPhotonLanguage
import org.scottishtecharmy.soundscape.geoengine.utils.rulers.CheapRuler
import org.scottishtecharmy.soundscape.geojsonparser.geojson.Feature
import org.scottishtecharmy.soundscape.geojsonparser.geojson.LngLatAlt
import org.scottishtecharmy.soundscape.geojsonparser.geojson.Point
import org.scottishtecharmy.soundscape.network.PhotonSearchProvider
import org.scottishtecharmy.soundscape.screens.home.data.LocationDescription
import org.scottishtecharmy.soundscape.utils.Analytics
import org.scottishtecharmy.soundscape.utils.toLocationDescription
import kotlin.collections.fold

/**
* The PhotonGeocoder class abstracts away the use of photon geo-search server for geocoding and
Expand Down Expand Up @@ -48,7 +52,37 @@ class PhotonGeocoder(val applicationContext: Context) : SoundscapeGeocoder() {

// The geocode result includes the location for the POI. In the case of something
// like a park this could be a long way from the point that was passed in.
return searchResult?.features?.mapNotNull{feature -> feature.toLocationDescription(LocationSource.PhotonGeocoder) }

// Photon sometimes returns the same location twice e.g. "Greggs Milngavi" returns it as
// a fast food amenity and a bakery shop
if(searchResult == null) return null

val ruler = CheapRuler(nearbyLocation.latitude)
val deduplicate = searchResult.features
.fold(mutableListOf<Feature>()) { accumulator, result ->
// Check if we already have this exact name at approximately the same location
val point = (result.geometry as? Point)
var isDuplicate = false
if(point != null) {
isDuplicate = accumulator.any {
val otherPoint = (it.geometry as? Point)
if(otherPoint != null) {
it.properties?.get("name") == result.properties?.get("name") &&
ruler.distance(
otherPoint.coordinates,
point.coordinates
) < 100.0
} else
false
}
}
if (!isDuplicate) {
accumulator.add(result)
}
accumulator
}

return deduplicate.mapNotNull{feature -> feature.toLocationDescription(LocationSource.PhotonGeocoder) }
}

override suspend fun getAddressFromLngLat(userGeometry: UserGeometry, localizedContext: Context?) : LocationDescription? {
Expand Down
Loading