diff --git a/app/src/androidTest/java/org/scottishtecharmy/soundscape/GeocoderTest.kt b/app/src/androidTest/java/org/scottishtecharmy/soundscape/GeocoderTest.kt index d93f000e4..94419ba1f 100644 --- a/app/src/androidTest/java/org/scottishtecharmy/soundscape/GeocoderTest.kt +++ b/app/src/androidTest/java/org/scottishtecharmy/soundscape/GeocoderTest.kt @@ -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) @@ -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) diff --git a/app/src/main/java/org/scottishtecharmy/soundscape/audio/TtsEngine.kt b/app/src/main/java/org/scottishtecharmy/soundscape/audio/TtsEngine.kt index 6098497c7..f7aa5bebf 100644 --- a/app/src/main/java/org/scottishtecharmy/soundscape/audio/TtsEngine.kt +++ b/app/src/main/java/org/scottishtecharmy/soundscape/audio/TtsEngine.kt @@ -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( diff --git a/app/src/main/java/org/scottishtecharmy/soundscape/components/LocationItem.kt b/app/src/main/java/org/scottishtecharmy/soundscape/components/LocationItem.kt index 0673c1ce5..e405b2411 100644 --- a/app/src/main/java/org/scottishtecharmy/soundscape/components/LocationItem.kt +++ b/app/src/main/java/org/scottishtecharmy/soundscape/components/LocationItem.kt @@ -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) { diff --git a/app/src/main/java/org/scottishtecharmy/soundscape/components/MainSearchBar.kt b/app/src/main/java/org/scottishtecharmy/soundscape/components/MainSearchBar.kt index 501f34590..918f99dcb 100644 --- a/app/src/main/java/org/scottishtecharmy/soundscape/components/MainSearchBar.kt +++ b/app/src/main/java/org/scottishtecharmy/soundscape/components/MainSearchBar.kt @@ -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 @@ -35,10 +36,12 @@ 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) @@ -46,15 +49,24 @@ import org.scottishtecharmy.soundscape.ui.theme.spacing fun MainSearchBar( searchText: String, isSearching: Boolean, + searchInProgress: Boolean, itemList: List, 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(null) } + + LaunchedEffect(searchText) { if (searchText != textFieldValue.text) { textFieldValue = textFieldValue.copy( @@ -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 } } @@ -90,11 +115,13 @@ 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) { @@ -102,7 +129,6 @@ fun MainSearchBar( } }, placeholder = { Text(stringResource(id = R.string.search_choose_destination)) }, - modifier = Modifier.focusRequester(focusRequester), leadingIcon = { when { !isSearching -> { @@ -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() + } } } } @@ -191,6 +241,7 @@ fun MainSearchPreview() { MainSearchBar( searchText = "", isSearching = false, + searchInProgress = false, emptyList(), SearchFunctions(null), {}, @@ -203,6 +254,7 @@ fun MainSearchPreviewSearching() { MainSearchBar( searchText = "Monaco", isSearching = true, + searchInProgress = false, emptyList(), SearchFunctions(null), {}, @@ -210,5 +262,16 @@ fun MainSearchPreviewSearching() { ) } - - +@Preview(showBackground = true) +@Composable +fun MainSearchPreviewSearchInProgress() { + MainSearchBar( + searchText = "Monaco", + isSearching = true, + searchInProgress = true, + emptyList(), + SearchFunctions(null), + {}, + LngLatAlt() + ) +} diff --git a/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/utils/geocoders/PhotonGeocoder.kt b/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/utils/geocoders/PhotonGeocoder.kt index d68db44e0..a45980be3 100644 --- a/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/utils/geocoders/PhotonGeocoder.kt +++ b/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/utils/geocoders/PhotonGeocoder.kt @@ -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 @@ -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()) { 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? { diff --git a/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/utils/geocoders/TileSearch.kt b/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/utils/geocoders/TileSearch.kt index f7a456846..3f509dc5e 100644 --- a/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/utils/geocoders/TileSearch.kt +++ b/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/utils/geocoders/TileSearch.kt @@ -39,6 +39,14 @@ class TileSearch(val offlineExtractPath: String, val stringCache = mutableMapOf>() + private fun cacheIndex(x: Int, y: Int) : Long{ + return x.toLong() + (y.toLong().shl(32)) + } + private fun trimCache(keepSet: MutableSet) { + // Remove all tiles which aren't in the keepSet + stringCache.keys.removeAll { !keepSet.contains(it) } + } + fun findNearestNamedWay(location: LngLatAlt, name: String?) : Way? { val nearestWays = gridState.getFeatureTree(TreeId.ROADS).getNearestCollection( @@ -213,13 +221,12 @@ class TileSearch(val offlineExtractPath: String, val searchResults = mutableListOf() val searchResultLimit = 8 - val needleWithoutSettlement = if(housenumber.isNotEmpty()) - generateWithoutSettlement(normalizedNeedle, settlementNames) - else - null + val needleWithoutSettlement = generateWithoutSettlement(normalizedNeedle, settlementNames) + val tilesUsed = mutableSetOf() while (turnCount < maxTurns) { - val tileIndex = x.toLong() + (y.toLong().shl(32)) + val tileIndex = cacheIndex(x, y) var cache = stringCache[tileIndex] + tilesUsed.add(tileIndex) if (cache == null) { // Load the tile and add all of its String to a cache cache = mutableListOf() @@ -293,6 +300,9 @@ class TileSearch(val offlineExtractPath: String, } } } + // Free up any tiles that we no longer use + trimCache(tilesUsed) + // We have some rough results, but we need to get precise locations for each and remove any // duplicates due to tile boundary overlap and roads crossing tiles val ruler = CheapRuler(location.latitude) @@ -513,23 +523,20 @@ class TileSearch(val offlineExtractPath: String, a.score.compareTo(b.score) } .fold(mutableListOf()) { accumulator, result -> - if(accumulator.size < 5) { - // Check if we already have this exact name at approximately the same location - val isDuplicate = accumulator.any { - it.string == result.string && ruler.distance( - it.location, - result.location - ) < 100.0 - } - if (!isDuplicate) { - accumulator.add(result) - } + // Check if we already have this exact name at approximately the same location + val isDuplicate = accumulator.any { + it.string == result.string && ruler.distance( + it.location, + result.location + ) < 100.0 + } + if (!isDuplicate) { + accumulator.add(result) } accumulator } - return whittledResults.map { result -> - + val streetResults = whittledResults.map { result -> val mvt = MvtFeature() mvt.name = result.properties.get("name") as? String? mvt.properties = result.properties @@ -615,73 +622,21 @@ class TileSearch(val offlineExtractPath: String, } } } - -// // We could decode just the housenumbers layer in the tile, but that only works for -// // direct matches and not interpolation. We could try and interpolate from known values, -// // but that's more special code which seems like a bad idea. -// for(result in searchResults) { -// val tileData = reader?.getTile(MAX_ZOOM_LEVEL, result.tileX, result.tileY) -// if (tileData != null) { -// val tile = decompressTile(reader.tileCompression, tileData) -// if(tile != null) { -// for(layer in tile.layersList) { -// // Was the string found in transportation or POI? TODO: Or both? -// if(layer.name == "housenumber"){ -// // We need to look for the feature. First search by street. -// for (feature in layer.featuresList) { -// var firstInPair = true -// var key = "" -// var street = "" -// var housenumber = "" -// for (tag in feature.tagsList) { -// if (firstInPair) { -// key = layer.getKeys(tag) -// } else { -// val raw = layer.getValues(tag) -// if (raw.hasStringValue()) { -// if(key == "street") { -// street = raw.stringValue.toString() -// } else if(key == "housenumber") { -// housenumber = raw.stringValue.toString() -// } -// } -// } -// firstInPair = !firstInPair -// } -// if((normalizeForSearch(street) == result.string) && housenumber == "12") { -// if (feature.type == VectorTile.Tile.GeomType.POINT) { -// val points = parseGeometry(true, feature.geometryList) -// for (point in points) { -// if (point.isNotEmpty()) { -// val coordinates = convertGeometry( -// result.tileX, -// result.tileY, -// MAX_ZOOM_LEVEL, -// point -// ) -// for (coordinate in coordinates) { -// detailedResults.add( -// DetailedSearchResult( -// result.score, -// "$housenumber $street", -// coordinate -// ) -// ) -// break -// } -// } -// } -// break -// } -// } -// } -// } -// } -//// result.string = stringValue -// } -// } -// } - + Pair(mvt, result) + }.fold(mutableListOf>()) { accumulator, result -> + // Check if we already have this exact name at approximately the same location + val isDuplicate = accumulator.any { + it.second.string == result.second.string && ruler.distance( + it.second.location, + result.second.location + ) < 100.0 + } + if (!isDuplicate) { + accumulator.add(result) + } + accumulator + } + return streetResults.map { (mvt, result) -> val ld = mvt.toLocationDescription(LocationSource.OfflineGeocoder) ld ?: LocationDescription( name = result.string, diff --git a/app/src/main/java/org/scottishtecharmy/soundscape/screens/home/home/Home.kt b/app/src/main/java/org/scottishtecharmy/soundscape/screens/home/home/Home.kt index 3b4fd502e..7d4027a2b 100644 --- a/app/src/main/java/org/scottishtecharmy/soundscape/screens/home/home/Home.kt +++ b/app/src/main/java/org/scottishtecharmy/soundscape/screens/home/home/Home.kt @@ -187,6 +187,7 @@ fun Home( MainSearchBar( searchText = searchText, isSearching = state.isSearching, + searchInProgress = state.searchInProgress, itemList = state.searchItems.orEmpty(), searchFunctions = searchFunctions, onItemClick = { item -> diff --git a/app/src/main/java/org/scottishtecharmy/soundscape/viewmodels/home/HomeState.kt b/app/src/main/java/org/scottishtecharmy/soundscape/viewmodels/home/HomeState.kt index 3577c41df..4d21aa584 100644 --- a/app/src/main/java/org/scottishtecharmy/soundscape/viewmodels/home/HomeState.kt +++ b/app/src/main/java/org/scottishtecharmy/soundscape/viewmodels/home/HomeState.kt @@ -13,6 +13,7 @@ data class HomeState( var beaconState: BeaconState? = null, var streetPreviewState: StreetPreviewState = StreetPreviewState(StreetPreviewEnabled.OFF), var isSearching: Boolean = false, + var searchInProgress: Boolean = false, var searchItems: List? = null, var routesTabSelected: Boolean = true, var currentRouteData: RoutePlayerState = RoutePlayerState(), diff --git a/app/src/main/java/org/scottishtecharmy/soundscape/viewmodels/home/HomeViewModel.kt b/app/src/main/java/org/scottishtecharmy/soundscape/viewmodels/home/HomeViewModel.kt index a7b45ecb5..b6e554633 100644 --- a/app/src/main/java/org/scottishtecharmy/soundscape/viewmodels/home/HomeViewModel.kt +++ b/app/src/main/java/org/scottishtecharmy/soundscape/viewmodels/home/HomeViewModel.kt @@ -236,13 +236,10 @@ class HomeViewModel fun onTriggerSearch() { viewModelScope.launch { + _state.update { it.copy(searchInProgress = true) } val result = soundscapeServiceConnection.soundscapeService?.searchResult(searchText.value) - _state.update { - it.copy( - searchItems = result - ) - } + _state.update { it.copy(searchItems = result,searchInProgress = false) } } } diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index 0896a4f34..f73a42acd 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -14,4 +14,20 @@ Kõnesünteesimootor Hääled Helistiilid + Praegune + Algne + Erk lamp + Mahe lamp + Kombatav + Kõll + Kolks + Signaal + Signaal (aeglane) + Signaal (väga aeglane) + Vasarake + Vasarake (aeglane) + Vasarake (väga aeglane) + Helimajakas + Summuta helimajakas + summuta helimajakas topeltpuudutusega. diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 49052288d..4906d9fb4 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -525,7 +525,7 @@ Marqueurs et itinéraires supprimés Informations sur la nouvelle version Fermer les informations sur la nouvelle version - \nCette version apporte deux changements majeurs. Tout d’abord, la prise en charge des *Cartes hors ligne* a été ajoutée, ce qui permet à Soundscape d’exécuter la majorité de ses fonctionnalités sans accès réseau. Dans cette version initiale, la *Recherche* nécessite toujours une connexion, mais *Emplacements à proximité*, les *notifications audio* et la *carte graphique* utiliseront les cartes hors ligne téléchargées.\n\nUn grand nombre de régions cartographiques préconstruites couvrent désormais l’ensemble de la planète. De nombreux lieux se trouvent dans plus d’une région de carte hors ligne, l’utilisateur peut donc choisir celles qu’il souhaite télécharger et utiliser. Plusieurs cartes hors ligne peuvent être téléchargées et Soundscape basculera automatiquement entre elles au fur et à mesure que l’utilisateur se déplace.\n\nLes régions cartographiques peuvent être des pays entiers, des états ou des regroupements de grandes villes et agglomérations. Leur taille varie selon le niveau de cartographie de la région et la superficie physique couverte. Si vous êtes le premier utilisateur à télécharger une région, un court délai peut précéder le début du téléchargement. Ce délai peut atteindre quelques minutes pendant que le serveur prépare la carte pour la rendre disponible au téléchargement.\n\nLes cartes hors ligne actuellement téléchargées sont visibles via *Menu*, *Cartes hors ligne*. Cet écran affiche également les cartes disponibles au téléchargement pour votre position actuelle.\n\nL’écran *Détails de l’emplacement* contient aussi un lien vers les cartes hors ligne : si vous souhaitez télécharger les cartes d’un lieu où vous allez vous rendre, recherchez cet endroit puis appuyez sur le bouton *Cartes hors ligne à proximité* pour voir quelles cartes sont disponibles pour cet emplacement. Les cartes qui ne sont plus nécessaires peuvent être supprimées en sélectionnant l’une des *Cartes déjà téléchargées*, puis *Supprimer la carte hors ligne*.\n\nLe second changement majeur est que les tuiles cartographiques couvrent désormais une surface quatre fois plus grande que dans les versions précédentes. Cela signifie que des points d’intérêt plus lointains sont maintenant répertoriés dans *Emplacements à proximité*.\n\nComme toujours, veuillez signaler tout problème, même minime, via l’option *Contacter le support* dans le menu principal.\n + \nCette version apporte deux changements majeurs. Tout d’abord, la prise en charge des *Cartes hors ligne* a été ajoutée, ce qui permet à Soundscape d’exécuter la majorité de ses fonctionnalités sans accès réseau. Dans cette version initiale, la *Recherche* nécessite toujours une connexion, mais *Emplacements à proximité*, les *notifications audio* et la *carte graphique* utiliseront les cartes hors ligne téléchargées.\n\nUn grand nombre de régions cartographiques préconstruites couvrent désormais l’ensemble de la planète. De nombreux lieux se trouvent dans plus d’une région de carte hors ligne, l’utilisateur peut donc choisir celles qu’il souhaite télécharger et utiliser. Plusieurs cartes hors ligne peuvent être téléchargées et Soundscape basculera automatiquement entre elles au fur et à mesure que l’utilisateur se déplace.\n\nLes régions cartographiques peuvent être des pays entiers, des états ou des regroupements de grandes villes et agglomérations. Leur taille varie selon le niveau de cartographie de la région et la superficie physique couverte. Si vous êtes le premier utilisateur à télécharger une région, un court délai peut précéder le début du téléchargement. Ce délai peut atteindre quelques minutes pendant que le serveur prépare la carte pour la rendre disponible au téléchargement.\n\nLes cartes hors ligne actuellement téléchargées sont visibles via *Menu*, *Cartes hors ligne*. Cet écran affiche également les cartes disponibles au téléchargement pour votre position actuelle.\n\nL’écran *Détails de l’emplacement* contient aussi un lien vers les cartes hors ligne : si vous souhaitez télécharger les cartes d’un lieu où vous allez vous rendre, recherchez cet endroit puis appuyez sur le bouton *Cartes hors ligne à proximité* pour voir quelles cartes sont disponibles pour cet emplacement. Les cartes qui ne sont plus nécessaires peuvent être supprimées en sélectionnant l’une des *Cartes déjà téléchargées*, puis *Supprimer la carte hors ligne*.\n\nLe second changement majeur est que les tuiles cartographiques couvrent désormais une surface quatre fois plus grande que dans les versions précédentes. Cela signifie que des points d’intérêt plus lointains sont maintenant répertoriés dans *Emplacements à proximité*.\n\nComme toujours, veuillez signaler tout problème, même minime, via l’option *Contacter le support* dans le menu principal.\n Licences open source Terminal de ferry %s Terminal de ferry diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ee8247968..dfb7d31b7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -30,6 +30,10 @@ "Offline" "Auto" + +"No search results found" + +"Searching..." "Speaking Rate" @@ -917,7 +921,9 @@ " The main changes in this release are for *Search* and describing the current address. When there is network available, *Search* will use Android as well as our own Photon server to find places. The main improvements here are firstly that searching by street number should work much better in many parts of the world. Secondly, we have also added offline search which uses any downloaded maps to search for addresses and points of interest. Offline search works up to around 5 miles around the user. -Tips for searching +Tips for searching: + +Offline search requires offline maps to be downloaded. Download them from the Settings/Offline maps screen. Searching can be forced to be offline only in the settings. The default Auto setting means that it uses online search when there's network available and offline search when there's none. @@ -925,8 +931,6 @@ When searching for a street address, including the town name is important for on Mapping of street numbers in the OpenStreetMap data that we use varies depending on location. Offline search will use street numbers when they are available, though an offline map downloaded in the app after January 19th 2026 is required. -As always please report any problems, however small they may be via the *Contact Support* option in the main menu. - As always please report any problems, however small they may be via the *Contact Support* option in the main menu. " diff --git a/app/src/test/java/org/scottishtecharmy/soundscape/SearchTest.kt b/app/src/test/java/org/scottishtecharmy/soundscape/SearchTest.kt index 8d3a492c0..160163521 100644 --- a/app/src/test/java/org/scottishtecharmy/soundscape/SearchTest.kt +++ b/app/src/test/java/org/scottishtecharmy/soundscape/SearchTest.kt @@ -35,6 +35,9 @@ class SearchTest { assertEquals("5 Buchanan Street", results[0].name) assertEquals("5 Buchanan Street\nMilngavie\n", results[0].description) + results = offlineGeocoder.getAddressFromLocationName( "12 roselea drive", currentLocation, null)!! + assertEquals(1, results.size) + results = offlineGeocoder.getAddressFromLocationName("5 buchanan street mulngaviy", currentLocation, null)!! assertEquals("5 Buchanan Street", results[0].name) assertEquals("5 Buchanan Street\nMilngavie\n", results[0].description) @@ -59,6 +62,10 @@ class SearchTest { assertEquals("8 Roselea Drive", results[0].name) assertEquals("8 Roselea Drive\nMilngavie\n", results[0].description) + results = offlineGeocoder.getAddressFromLocationName( "20 roselea drive milngavie", currentLocation, null)!! + assertEquals("20 Roselea Drive", results[0].name) + assertEquals("20 Roselea Drive\nMilngavie\n", results[0].description) + results = offlineGeocoder.getAddressFromLocationName( "greggs ", currentLocation, null)!! assertEquals("Greggs", results[0].name) assertEquals("6 Douglas Street\nMilngavie\n", results[0].description)