diff --git a/.github/workflows/nightly.yaml b/.github/workflows/nightly.yaml index 79e8c7108..fdb1a2643 100644 --- a/.github/workflows/nightly.yaml +++ b/.github/workflows/nightly.yaml @@ -265,8 +265,14 @@ jobs: # See run-tests.yaml: linking iosSimulatorArm64Test fails because of # maplibre-compose's embedded linker flags. xcodebuild archive below # builds via the Xcode/SPM toolchain that resolves MapLibre. + # + # --configure-on-demand keeps Gradle from configuring :app, which this + # job doesn't need: on the self-hosted Mac, AGP's NdkLocator hits a + # File.list() == null on $ANDROID_HOME/ndk and throws an unguarded NPE + # ("A problem occurred configuring project ':app'"). Same fix as the + # ios-build/ios-firebase jobs in build-app.yaml. - name: Compile shared Kotlin/Native tests for iOS simulator - run: ./gradlew :shared:compileTestKotlinIosSimulatorArm64 + run: ./gradlew --configure-on-demand :shared:compileTestKotlinIosSimulatorArm64 - name: Setup iOS signing uses: ./.github/actions/setup-ios-signing diff --git a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/placesnearby/PlacesNearbyList.kt b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/placesnearby/PlacesNearbyList.kt index d7b0b4fc4..0dcb0f8e1 100644 --- a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/placesnearby/PlacesNearbyList.kt +++ b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/placesnearby/PlacesNearbyList.kt @@ -68,7 +68,7 @@ fun PlacesNearbyList( color = MaterialTheme.colorScheme.outlineVariant ) } - locationDescription.process(localizedStrings) + locationDescription.process(localizedStrings, uiState.countryCode) LocationItem( item = locationDescription, decoration = LocationItemDecoration( diff --git a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/placesnearby/PlacesNearbyUiState.kt b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/placesnearby/PlacesNearbyUiState.kt index 996cdd8c9..84cf0f120 100644 --- a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/placesnearby/PlacesNearbyUiState.kt +++ b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/placesnearby/PlacesNearbyUiState.kt @@ -6,6 +6,12 @@ import org.scottishtecharmy.soundscape.screens.home.data.LocationDescription data class PlacesNearbyUiState( var userLocation: LngLatAlt? = null, + /** + * The country whose address conventions every place in the list is written in, looked up once + * from [userLocation]. Everything nearby is by definition in the same country as the user, so + * the rows share the one answer instead of each asking the country boundaries themselves. + */ + var countryCode: String? = null, var level: Int = 0, var nearbyPlaces: FeatureCollection = FeatureCollection(), var nearbyIntersections: FeatureCollection = FeatureCollection(), diff --git a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/placesnearby/PlacesNearbyViewModel.kt b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/placesnearby/PlacesNearbyViewModel.kt index 0103bcb27..d1b88da0f 100644 --- a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/placesnearby/PlacesNearbyViewModel.kt +++ b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/placesnearby/PlacesNearbyViewModel.kt @@ -14,9 +14,14 @@ import kotlinx.coroutines.withContext import org.scottishtecharmy.soundscape.audio.AudioTour import org.scottishtecharmy.soundscape.geoengine.GridState import org.scottishtecharmy.soundscape.geoengine.TreeId +import org.scottishtecharmy.soundscape.geoengine.utils.rulers.CheapRuler import org.scottishtecharmy.soundscape.geojsonparser.geojson.FeatureCollection import org.scottishtecharmy.soundscape.geojsonparser.geojson.LngLatAlt import org.scottishtecharmy.soundscape.services.ServiceConnection +import org.scottishtecharmy.soundscape.utils.addressCountryCode + +/** How far the user has to move before the country their address is written in is looked up again. */ +private const val COUNTRY_LOOKUP_DISTANCE = 5000.0 /** * Shared ViewModel backing the PlacesNearby screen on both Android and iOS. @@ -68,6 +73,25 @@ open class PlacesNearbyViewModel( private data class LocationAndGridState(val location: LngLatAlt?, val gridState: GridState?) + private var countryLookupLocation: LngLatAlt? = null + private var countryCode: String? = null + + /** + * The country [location] is in, looked up from the country boundaries the first time and then + * only again once the user has moved far enough for the answer to plausibly have changed. A + * location update arrives every second or so, and a country is not something you walk out of. + */ + private fun countryCodeFor(location: LngLatAlt): String? { + val lastLookup = countryLookupLocation + if ((lastLookup == null) || + (CheapRuler(location.latitude).distance(lastLookup, location) > COUNTRY_LOOKUP_DISTANCE) + ) { + countryLookupLocation = location + countryCode = addressCountryCode(location) + } + return countryCode + } + @OptIn(ExperimentalCoroutinesApi::class) private fun startMonitoring() { monitorJob?.cancel() @@ -80,7 +104,10 @@ open class PlacesNearbyViewModel( ) }.collect { locationAndGrid -> if (locationAndGrid.location != null) { - internalUiState.update { it.copy(userLocation = locationAndGrid.location) } + val country = countryCodeFor(locationAndGrid.location) + internalUiState.update { + it.copy(userLocation = locationAndGrid.location, countryCode = country) + } } if (locationAndGrid.gridState != null) { val (pois, intersections) = withContext(locationAndGrid.gridState.treeContext) { diff --git a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/markers_routes/screens/addandeditroutescreen/AddWaypointsList.kt b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/markers_routes/screens/addandeditroutescreen/AddWaypointsList.kt index db3390287..c690bbfd2 100644 --- a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/markers_routes/screens/addandeditroutescreen/AddWaypointsList.kt +++ b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/markers_routes/screens/addandeditroutescreen/AddWaypointsList.kt @@ -161,7 +161,7 @@ fun AddWaypointsList( color = MaterialTheme.colorScheme.outlineVariant ) } - locationDescription.process(localizedStrings) + locationDescription.process(localizedStrings, placesNearbyUiState.countryCode) LocationItem( item = locationDescription, decoration = LocationItemDecoration( diff --git a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/utils/FeatureLocationExt.kt b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/utils/FeatureLocationExt.kt index f09e609fb..e54476d6c 100644 --- a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/utils/FeatureLocationExt.kt +++ b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/utils/FeatureLocationExt.kt @@ -79,11 +79,11 @@ internal fun addressCountryCode(location: LngLatAlt): String = * null if it isn't one. The parts of it are the [mvt]'s own for a feature from the grid, and in * [properties] for a search result. */ -private fun japaneseAddress(mvt: MvtFeature?, properties: Map, location: LngLatAlt): String? { +private fun japaneseAddress(mvt: MvtFeature?, properties: Map, countryCode: () -> String): String? { fun part(field: String?, key: String) = field ?: (properties[key] as? String) val quarter = part(mvt?.quarter, "quarter") val neighbourhood = part(mvt?.neighbourhood, "neighbourhood") - if (((quarter == null) && (neighbourhood == null)) || (addressCountryCode(location) != "JP")) return null + if (((quarter == null) && (neighbourhood == null)) || (countryCode() != "JP")) return null return JapaneseAddress.address( quarter, neighbourhood, @@ -112,7 +112,17 @@ private fun streetAddressLine(formattedAddress: String, road: String?, houseNumb } } -fun LocationDescription.process(strings: LocalizedStrings? = null) { +/** + * Fills in the name and address a [LocationDescription] shows, from the feature it was deferred + * from. Deferred because a list of a thousand nearby POIs only ever formats the handful of rows + * which reach the screen. + * + * [countryCode] is the country whose address conventions to write the address in, for a caller + * which already knows it - a list of places all within a kilometre or two of the user needs only + * one boundary lookup between them, not one per row. Left out, the feature's own location is + * looked up, which is what a search result somewhere else in the world needs. + */ +fun LocationDescription.process(strings: LocalizedStrings? = null, countryCode: String? = null) { if (feature != null) { feature?.let { feature -> var address = false @@ -122,6 +132,7 @@ fun LocationDescription.process(strings: LocalizedStrings? = null) { val mvt = (feature as? MvtFeature) var nameLocal: String? = null var blockAddress: String? = null + val addressCountry by lazy { countryCode ?: addressCountryCode(location) } feature.properties?.let { properties -> properties.forEach { (key, value) -> @@ -170,7 +181,7 @@ fun LocationDescription.process(strings: LocalizedStrings? = null) { } // Most Japanese buildings are numbered within their block, and their address is // the ward and then that - "北区, 梅田三丁目1-1" - with no street in it - blockAddress = japaneseAddress(mvt, properties, location) + blockAddress = japaneseAddress(mvt, properties) { addressCountry } blockAddress?.let { jsonFields.remove("road") jsonFields["house_number"] = it @@ -199,7 +210,7 @@ fun LocationDescription.process(strings: LocalizedStrings? = null) { var fallbackCountryCode: String? = null if (!jsonFields.containsKey("country_code")) - fallbackCountryCode = addressCountryCode(location) + fallbackCountryCode = addressCountry if (fallbackCountryCode?.isEmpty() == true) fallbackCountryCode = "GB" val formattedAddress = try {