Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ captures
!*.xcworkspace/contents.xcworkspacedata
**/xcshareddata/WorkspaceSettings.xcsettings
xcuserdata
node_modules
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import io.ktor.client.plugins.auth.providers.BearerTokens
import io.ktor.client.plugins.auth.providers.bearer
import io.ktor.client.plugins.compression.ContentEncoding
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.HttpRequestRetry
import io.ktor.client.plugins.defaultRequest
import io.ktor.client.plugins.logging.DEFAULT
import io.ktor.client.plugins.logging.LogLevel
Expand Down Expand Up @@ -133,6 +134,19 @@ val appModule = module {
}
}

single(named("kartaViewClient")) {
HttpClient {
install(ContentNegotiation) {
json(Json { ignoreUnknownKeys = true })
}
install(HttpRequestRetry) {
retryOnServerErrors(maxRetries = 3)
retryOnException(maxRetries = 3, retryOnTimeout = true)
exponentialDelay()
}
}
}

single<Res> { Res }
single<FileSystem> { SystemFileSystem }
single<ResourceProvider> { DefaultResourceProvider(androidContext(), get()) }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package de.westnordost.streetcomplete.data.karta_view.domain.model

import kotlinx.serialization.Serializable

@Serializable
data class PhotoLookupResponse(
val result: PhotoLookupResult? = null
)

@Serializable
data class PhotoLookupResult(
val data: List<PhotoLookup> = emptyList()
)

@Serializable
data class PhotoLookup(
val imageLthUrl: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import de.westnordost.osmfeatures.Feature
import de.westnordost.osmfeatures.FeatureDictionary
import de.westnordost.streetcomplete.R
import de.westnordost.streetcomplete.data.karta_view.domain.model.CreateSequenceResponse
import de.westnordost.streetcomplete.data.karta_view.domain.model.ImageUploadResponse
import de.westnordost.streetcomplete.data.karta_view.domain.model.PhotoLookupResponse
import de.westnordost.streetcomplete.data.location.SurveyChecker
import de.westnordost.streetcomplete.data.osm.edits.AddElementEditsController
import de.westnordost.streetcomplete.data.osm.edits.ElementEditAction
Expand All @@ -53,6 +53,7 @@ import de.westnordost.streetcomplete.data.quest.Quest
import de.westnordost.streetcomplete.data.quest.QuestKey
import de.westnordost.streetcomplete.data.visiblequests.HideQuestController
import de.westnordost.streetcomplete.data.visiblequests.QuestsHiddenController
import de.westnordost.streetcomplete.data.preferences.Preferences
import de.westnordost.streetcomplete.osm.applyReplacePlaceTo
import de.westnordost.streetcomplete.quests.sidewalk_long_form.AddGenericLong
import de.westnordost.streetcomplete.screens.main.map.Compass
Expand All @@ -65,6 +66,8 @@ import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.forms.MultiPartFormDataContent
import io.ktor.client.request.forms.formData
import io.ktor.client.request.get
import io.ktor.client.request.parameter
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.http.Headers
Expand Down Expand Up @@ -92,7 +95,8 @@ abstract class AbstractOsmQuestForm<T> : AbstractQuestForm(), IsShowingQuestDeta
private val surveyChecker: SurveyChecker by inject()

protected val featureDictionary: FeatureDictionary get() = featureDictionaryLazy.value
private val httpClient: HttpClient by inject()
private val httpClient: HttpClient by inject(named("kartaViewClient"))
private val prefs: Preferences by inject()
private lateinit var cameraLauncher: ActivityResultLauncher<Intent>

// only used for testing / only used for ShowQuestFormsScreen! Found no better way to do this
Expand Down Expand Up @@ -438,30 +442,44 @@ abstract class AbstractOsmQuestForm<T> : AbstractQuestForm(), IsShowingQuestDeta

private fun startKartViewFlow(bitmap: Bitmap?, displayedLocation: Location?) {
viewLifecycleScope.launch {
val sequenceId = createSequence()
sequenceId?.apply {
val isUploaded = uploadImageInSequence(this, bitmap, displayedLocation)
if (isUploaded.first) {
//https://storage13.openstreetcam.org/files/photo/2024/11/19/lth/10206921_53966_673c7da2672cf.jpg
//After storage13 add .openstreetcam.org in the url
// FInd where storage13 is in the first
val first = isUploaded.second?.first?.replace(
"storage13",
"storage13.openstreetcam.org"
)
val url = "https://${first}/lth/${isUploaded.second?.second}"
onImageUrlReceived(url)
closeSequence(this)
val sequenceId = createSequence() ?: return@launch
val uploaded = uploadImageInSequence(sequenceId, bitmap, displayedLocation)
if (uploaded) {
closeSequence(sequenceId)
val lthUrl = getPhotoLthUrl(sequenceId, sequenceIndex = 1)
if (lthUrl != null) {
Log.d("KartViewFlow", lthUrl)
onImageUrlReceived(lthUrl)
} else {
hideProgressbar()
Log.e("KartViewFlow", "Image upload failed")
Log.e("KartViewFlow", "Failed to retrieve photo URL")
showSnackBar("Failed to retrieve photo URL. Please try again later.", view, requireActivity() as ComponentActivity)
}
} else {
hideProgressbar()
Log.e("KartViewFlow", "Image upload failed")
showSnackBar("Image upload failed. Please try again later.", view, requireActivity() as ComponentActivity)
}
}
}

private suspend fun getPhotoLthUrl(sequenceId: String, sequenceIndex: Int): String? {
val token = prefs.kartaViewAccessToken
val response = httpClient.get("https://api.openstreetcam.org/2.0/photo/") {
parameter("access_token", token)
parameter("sequenceId", sequenceId)
parameter("sequenceIndex", sequenceIndex)
}
if (response.status == HttpStatusCode.OK) {
val photoResponse = response.body<PhotoLookupResponse>()
return photoResponse.result?.data?.firstOrNull()?.imageLthUrl
}
Log.e("KartViewFlow", "Photo lookup failed: ${response.status}")
return null
}

private suspend fun closeSequence(sequenceId: String) {
val token = "96aca5c4b80709fc6d9aced613b51905c0fbc37870640d7bdabede269165bde7"
val token = prefs.kartaViewAccessToken
val response =
httpClient.post("https://api.openstreetcam.org/1.0/sequence/finished-uploading/") {
setBody(MultiPartFormDataContent(formData {
Expand All @@ -485,68 +503,47 @@ abstract class AbstractOsmQuestForm<T> : AbstractQuestForm(), IsShowingQuestDeta
sequenceId: String,
bitmap: Bitmap?,
location: Location?,
): Pair<Boolean, Pair<String, String>?> {

val displayedLocation = listener?.displayedMapLocation
if (displayedLocation != null) {
bitmap?.let {
val byteArrayOutputStream = ByteArrayOutputStream()
it.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream)
val byteArray = byteArrayOutputStream.toByteArray()

val response = httpClient.post("https://api.openstreetcam.org/1.0/photo/") {
setBody(MultiPartFormDataContent(formData {
append(
"access_token",
"96aca5c4b80709fc6d9aced613b51905c0fbc37870640d7bdabede269165bde7"
)
append("sequenceId", sequenceId)
append("sequenceIndex", 1)
append(
"coordinate",
"${displayedLocation.latitude},${displayedLocation.longitude}"
)
var finalBearing = 0.0f
finalBearing =
if (displayedLocation.hasBearing() && displayedLocation.bearing != 0f) {
displayedLocation.bearing
} else {
compassBearing.toFloat()
}
append("headers", finalBearing.toInt().toString())
append("photo", byteArray, Headers.build {
append(HttpHeaders.ContentType, "image/jpeg")
append(
HttpHeaders.ContentDisposition,
"filename=\"wework-kartaview.jpg\""
)
})
}))
}
): Boolean {
val displayedLocation = listener?.displayedMapLocation ?: return false
bitmap ?: return false

if (response.status == HttpStatusCode.OK) {
val uploadResponse = response.body<ImageUploadResponse>()
val pair =
Pair(uploadResponse.osv.photo.path, uploadResponse.osv.photo.photoName)
showSnackBar(
"Image Uploaded Successfully",
view,
requireActivity() as ComponentActivity
)
Log.d("UploadImage", "Image uploaded successfully")
return Pair(true, pair)
} else {
Log.e("UploadImage", "Image upload failed: ${response.status}")
showSnackBar(
"Failed to upload image to KartaView. Please try again later " + response.status,
view, requireActivity() as ComponentActivity
)
hideProgressbar()
return Pair(false, null)
}
}
val byteArrayOutputStream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream)
val byteArray = byteArrayOutputStream.toByteArray()

val finalBearing = if (displayedLocation.hasBearing() && displayedLocation.bearing != 0f) {
displayedLocation.bearing
} else {
compassBearing.toFloat()
}

val response = httpClient.post("https://api.openstreetcam.org/1.0/photo/") {
setBody(MultiPartFormDataContent(formData {
append("access_token", prefs.kartaViewAccessToken)
append("sequenceId", sequenceId)
append("sequenceIndex", 1)
append("coordinate", "${displayedLocation.latitude},${displayedLocation.longitude}")
append("headers", finalBearing.toInt().toString())
append("photo", byteArray, Headers.build {
append(HttpHeaders.ContentType, "image/jpeg")
append(HttpHeaders.ContentDisposition, "filename=\"wework-kartaview.jpg\"")
})
}))
}

return if (response.status == HttpStatusCode.OK) {
showSnackBar("Image Uploaded Successfully", view, requireActivity() as ComponentActivity)
Log.d("UploadImage", "Image uploaded successfully")
true
} else {
Log.e("UploadImage", "Image upload failed: ${response.status}")
showSnackBar(
"Failed to upload image to KartaView. Please try again later " + response.status,
view, requireActivity() as ComponentActivity
)
hideProgressbar()
false
}
return Pair(false, null)
}

private fun showSnackBar(message: String, view: View?, componentActivity: ComponentActivity) {
Expand All @@ -556,7 +553,7 @@ abstract class AbstractOsmQuestForm<T> : AbstractQuestForm(), IsShowingQuestDeta
}

private suspend fun createSequence(): String? {
val token = "96aca5c4b80709fc6d9aced613b51905c0fbc37870640d7bdabede269165bde7"
val token = prefs.kartaViewAccessToken
val response = httpClient.post("https://api.openstreetcam.org/1.0/sequence/") {
setBody(MultiPartFormDataContent(formData {
append("access_token", token)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,12 +155,12 @@ class LongFormAdapter<T>(val cameraIntent: () -> Unit) :
override fun afterTextChanged(s: Editable?) {
val text = s.toString()
if (text.isNotBlank()) {
val number = text.toLongOrNull()
val number = text.toFloatOrNull()
if (number == null) {
textInputLayout?.error = "Invalid number"
} else if (number < (minValue?.toLong() ?: 0L)) {
} else if (number < (minValue?.toFloat() ?: 0F)) {
textInputLayout?.error = "Value should be greater than $minValue"
} else if (number > maxValue.toLong()) {
} else if (number > maxValue.toFloat()) {
textInputLayout?.error = "Value should be less than $maxValue"
} else {
textInputLayout?.error = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/enter_value"
android:inputType="number" />
android:inputType="numberDecimal" />
</com.google.android.material.textfield.TextInputLayout>

<ImageView
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ object ApplicationConstants {
const val ATTACH_PHOTO_MAX_SIZE = 1920 // Full HD

// where to send the error reports to
const val ERROR_REPORTS_EMAIL = "streetcomplete_errors@westnordost.de"
const val ERROR_REPORTS_EMAIL = "helpdesk@tdei.us"

/** Which relation types to drop already during download, before persisting. This is a
* performance improvement. Working properly with relations means we have to have it as
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,10 @@ class Preferences(private val prefs: ObservableSettings) {
}
get() = prefs.getBoolean("${environment}_$LOW_BANDWIDTH_MODE_ENABLED", false)

var kartaViewAccessToken: String
set(value) { prefs.putString(KARTAVIEW_ACCESS_TOKEN, value) }
get() = prefs.getString(KARTAVIEW_ACCESS_TOKEN, DEFAULT_KARTAVIEW_ACCESS_TOKEN)

var isFollowModeEnabled: Boolean
set(value) {
prefs.putBoolean("${environment}_$FOLLOW_MODE_ENABLED", value)
Expand Down Expand Up @@ -440,5 +444,8 @@ class Preferences(private val prefs: ObservableSettings) {
private const val LOW_BANDWIDTH_MODE_ENABLED = "low.bandwidth.enabled"
private const val FOLLOW_MODE_ENABLED = "follow.mode.enabled"
private const val DEBUG_MODE_ENABLED = "debug.mode.enabled"
private const val KARTAVIEW_ACCESS_TOKEN = "kartaview.access_token"
private const val DEFAULT_KARTAVIEW_ACCESS_TOKEN =
"96aca5c4b80709fc6d9aced613b51905c0fbc37870640d7bdabede269165bde7"
}
}