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
8 changes: 7 additions & 1 deletion .github/workflows/nightly.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ fun PlacesNearbyList(
color = MaterialTheme.colorScheme.outlineVariant
)
}
locationDescription.process(localizedStrings)
locationDescription.process(localizedStrings, uiState.countryCode)
LocationItem(
item = locationDescription,
decoration = LocationItemDecoration(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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()
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ fun AddWaypointsList(
color = MaterialTheme.colorScheme.outlineVariant
)
}
locationDescription.process(localizedStrings)
locationDescription.process(localizedStrings, placesNearbyUiState.countryCode)
LocationItem(
item = locationDescription,
decoration = LocationItemDecoration(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Any?>, location: LngLatAlt): String? {
private fun japaneseAddress(mvt: MvtFeature?, properties: Map<String, Any?>, 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,
Expand Down Expand Up @@ -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
Expand All @@ -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) ->
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down