diff --git a/android/app/build.gradle b/android/app/build.gradle index 93b5e6af5a..51108c9185 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -87,7 +87,7 @@ def getCommitMessage() { def osName = System.properties['os.name'].toLowerCase() project.ext.appId = 'tj.tourism.rebus' -project.ext.appName = 'Organic Maps' +project.ext.appName = 'Tourism' java { toolchain { diff --git a/android/app/src/main/java/app/tourism/AuthActivity.kt b/android/app/src/main/java/app/tourism/AuthActivity.kt index 6069ae9576..de3d3d6289 100644 --- a/android/app/src/main/java/app/tourism/AuthActivity.kt +++ b/android/app/src/main/java/app/tourism/AuthActivity.kt @@ -28,7 +28,7 @@ class AuthActivity : ComponentActivity() { super.onCreate(savedInstanceState) lifecycleScope.launch { - placesRepository.downloadAllDataIfFirstTime() + placesRepository.downloadAllData() } enableEdgeToEdge( statusBarStyle = SystemBarStyle.dark(resources.getColor(R.color.black_primary)), diff --git a/android/app/src/main/java/app/tourism/MainActivity.kt b/android/app/src/main/java/app/tourism/MainActivity.kt index a962c99320..a744b5638b 100644 --- a/android/app/src/main/java/app/tourism/MainActivity.kt +++ b/android/app/src/main/java/app/tourism/MainActivity.kt @@ -70,12 +70,15 @@ class MainActivity : ComponentActivity() { lifecycleScope.launch { profileVM.profileDataResource.collectLatest { if (it is Resource.Success) { - it.data?.language?.let { lang -> - changeSystemAppLanguage(this@MainActivity, lang) - userPreferences.setLanguage(lang) - } - it.data?.theme?.let { theme -> - themeVM.setTheme(theme) + it.data?.apply { + language?.let { lang -> + changeSystemAppLanguage(this@MainActivity, lang) + userPreferences.setLanguage(lang) + } + theme?.let { theme -> + themeVM.setTheme(theme) + } + userPreferences.setUserId(id.toString()) } } if (it is Resource.Error) { diff --git a/android/app/src/main/java/app/tourism/data/db/Database.kt b/android/app/src/main/java/app/tourism/data/db/Database.kt index 7d34f0990a..517bd92669 100644 --- a/android/app/src/main/java/app/tourism/data/db/Database.kt +++ b/android/app/src/main/java/app/tourism/data/db/Database.kt @@ -8,13 +8,22 @@ import app.tourism.data.db.dao.HashesDao import app.tourism.data.db.dao.PlacesDao import app.tourism.data.db.dao.ReviewsDao import app.tourism.data.db.entities.CurrencyRatesEntity +import app.tourism.data.db.entities.FavoriteToSyncEntity import app.tourism.data.db.entities.HashEntity import app.tourism.data.db.entities.PlaceEntity import app.tourism.data.db.entities.ReviewEntity +import app.tourism.data.db.entities.ReviewPlannedToPostEntity @Database( - entities = [PlaceEntity::class, ReviewEntity::class, HashEntity::class, CurrencyRatesEntity::class], - version = 2, + entities = [ + PlaceEntity::class, + ReviewEntity::class, + ReviewPlannedToPostEntity::class, + FavoriteToSyncEntity::class, + HashEntity::class, + CurrencyRatesEntity::class + ], + version = 8, exportSchema = false ) @TypeConverters(Converters::class) diff --git a/android/app/src/main/java/app/tourism/data/db/dao/HashesDao.kt b/android/app/src/main/java/app/tourism/data/db/dao/HashesDao.kt index 34d31739a4..a6c757adf4 100644 --- a/android/app/src/main/java/app/tourism/data/db/dao/HashesDao.kt +++ b/android/app/src/main/java/app/tourism/data/db/dao/HashesDao.kt @@ -15,7 +15,7 @@ interface HashesDao { suspend fun insertHashes(hashes: List) @Query("SELECT * FROM hashes WHERE categoryId = :id") - suspend fun getHash(id: Long): HashEntity + suspend fun getHash(id: Long): HashEntity? @Query("SELECT * FROM hashes") suspend fun getHashes(): List diff --git a/android/app/src/main/java/app/tourism/data/db/dao/PlacesDao.kt b/android/app/src/main/java/app/tourism/data/db/dao/PlacesDao.kt index 3c6caa35ca..2b72f32805 100644 --- a/android/app/src/main/java/app/tourism/data/db/dao/PlacesDao.kt +++ b/android/app/src/main/java/app/tourism/data/db/dao/PlacesDao.kt @@ -1,9 +1,11 @@ package app.tourism.data.db.dao import androidx.room.Dao +import androidx.room.Delete import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import app.tourism.data.db.entities.FavoriteToSyncEntity import app.tourism.data.db.entities.PlaceEntity import kotlinx.coroutines.flow.Flow @@ -19,6 +21,9 @@ interface PlacesDao { @Query("DELETE FROM places WHERE categoryId = :categoryId") suspend fun deleteAllPlacesByCategory(categoryId: Long) + @Query("SELECT * FROM places WHERE UPPER(name) LIKE UPPER(:q)") + fun search(q: String= ""): Flow> + @Query("SELECT * FROM places WHERE categoryId = :categoryId") fun getPlacesByCategoryId(categoryId: Long): Flow> @@ -29,11 +34,17 @@ interface PlacesDao { fun getPlaceById(placeId: Long): Flow @Query("SELECT * FROM places WHERE isFavorite = 1 AND UPPER(name) LIKE UPPER(:q)") - fun getFavoritePlaces(q: String = ""): Flow> + fun getFavoritePlacesFlow(q: String = ""): Flow> + + @Query("SELECT * FROM places WHERE isFavorite = 1 AND UPPER(name) LIKE UPPER(:q)") + fun getFavoritePlaces(q: String = ""): List @Query("UPDATE places SET isFavorite = :isFavorite WHERE id = :placeId") suspend fun setFavorite(placeId: Long, isFavorite: Boolean) - @Query("SELECT * FROM places WHERE UPPER(name) LIKE UPPER(:q)") - fun search(q: String= ""): Flow> + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun addFavoriteToSync(favoriteToSyncEntity: FavoriteToSyncEntity) + + @Query("DELETE FROM favorites_to_sync WHERE placeId = :placeId") + suspend fun removeFavoriteToSync(placeId: Long) } diff --git a/android/app/src/main/java/app/tourism/data/db/dao/ReviewsDao.kt b/android/app/src/main/java/app/tourism/data/db/dao/ReviewsDao.kt index 5d67e70190..61e9b9aef7 100644 --- a/android/app/src/main/java/app/tourism/data/db/dao/ReviewsDao.kt +++ b/android/app/src/main/java/app/tourism/data/db/dao/ReviewsDao.kt @@ -6,6 +6,7 @@ import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import app.tourism.data.db.entities.ReviewEntity +import app.tourism.data.db.entities.ReviewPlannedToPostEntity import kotlinx.coroutines.flow.Flow @Dao @@ -15,10 +16,13 @@ interface ReviewsDao { suspend fun insertReview(review: ReviewEntity) @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertReviews(review: List) + suspend fun insertReviews(reviews: List) - @Delete - suspend fun deleteReview(review: ReviewEntity) + @Query("DELETE FROM reviews WHERE id = :id") + suspend fun deleteReview(id: Long) + + @Query("DELETE FROM reviews WHERE id = :idsList") + suspend fun deleteReviews(idsList: List) @Query("DELETE FROM reviews") suspend fun deleteAllReviews() @@ -28,4 +32,16 @@ interface ReviewsDao { @Query("SELECT * FROM reviews WHERE placeId = :placeId") fun getReviewsForPlace(placeId: Long): Flow> + + @Query("UPDATE reviews SET deletionPlanned = :deletionPlanned WHERE id = :id") + fun markReviewForDeletion(id: Long, deletionPlanned: Boolean = true) + + @Query("SELECT * FROM reviews WHERE deletionPlanned = 1") + fun getReviewsPlannedForDeletion(): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertReviewPlannedToPost(review: ReviewPlannedToPostEntity) + + @Query("SELECT * FROM reviews_planned_to_post") + fun getReviewsPlannedToPost(): List } diff --git a/android/app/src/main/java/app/tourism/data/db/entities/FavoriteToSyncEntity.kt b/android/app/src/main/java/app/tourism/data/db/entities/FavoriteToSyncEntity.kt new file mode 100644 index 0000000000..617e007e5e --- /dev/null +++ b/android/app/src/main/java/app/tourism/data/db/entities/FavoriteToSyncEntity.kt @@ -0,0 +1,10 @@ +package app.tourism.data.db.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "favorites_to_sync") +data class FavoriteToSyncEntity( + @PrimaryKey val placeId: Long, + val isFavorite: Boolean, +) \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/data/db/entities/PlaceEntity.kt b/android/app/src/main/java/app/tourism/data/db/entities/PlaceEntity.kt index ba428d3339..7b211de18d 100644 --- a/android/app/src/main/java/app/tourism/data/db/entities/PlaceEntity.kt +++ b/android/app/src/main/java/app/tourism/data/db/entities/PlaceEntity.kt @@ -16,7 +16,7 @@ data class PlaceEntity( val description: String, val cover: String, val gallery: List, - @Embedded val coordinates: CoordinatesEntity, + @Embedded val coordinates: CoordinatesEntity?, val rating: Double, val isFavorite: Boolean ) { @@ -26,7 +26,7 @@ data class PlaceEntity( rating = rating, excerpt = excerpt, description = description, - placeLocation = coordinates.toPlaceLocation(name), + placeLocation = coordinates?.toPlaceLocation(name), cover = cover, pics = gallery, isFavorite = isFavorite diff --git a/android/app/src/main/java/app/tourism/data/db/entities/ReviewEntity.kt b/android/app/src/main/java/app/tourism/data/db/entities/ReviewEntity.kt index 33c54b6346..141377a4cb 100644 --- a/android/app/src/main/java/app/tourism/data/db/entities/ReviewEntity.kt +++ b/android/app/src/main/java/app/tourism/data/db/entities/ReviewEntity.kt @@ -13,7 +13,8 @@ data class ReviewEntity( val comment: String, val date: String, val rating: Int, - val images: List + val images: List, + val deletionPlanned: Boolean = false, ) { fun toReview() = Review( id = id, @@ -23,5 +24,6 @@ data class ReviewEntity( date = date, comment = comment, picsUrls = images, + deletionPlanned = deletionPlanned, ) } diff --git a/android/app/src/main/java/app/tourism/data/db/entities/ReviewToPublishEntity.kt b/android/app/src/main/java/app/tourism/data/db/entities/ReviewToPublishEntity.kt new file mode 100644 index 0000000000..adc099c943 --- /dev/null +++ b/android/app/src/main/java/app/tourism/data/db/entities/ReviewToPublishEntity.kt @@ -0,0 +1,25 @@ +package app.tourism.data.db.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey +import app.tourism.domain.models.details.ReviewToPost +import java.io.File +import java.net.URI + +@Entity(tableName = "reviews_planned_to_post") +data class ReviewPlannedToPostEntity( + @PrimaryKey(autoGenerate = true) val id: Long? = null, + val placeId: Long, + val comment: String, + val rating: Int, + val images: List, +) { + fun toReviewsPlannedToPostDto(): ReviewToPost { + val imageFiles = images.map { File(it) } + imageFiles.first().path + + return ReviewToPost( + placeId, comment, rating, imageFiles + ) + } +} diff --git a/android/app/src/main/java/app/tourism/data/dto/FavoritesIdsDto.kt b/android/app/src/main/java/app/tourism/data/dto/FavoritesIdsDto.kt new file mode 100644 index 0000000000..b6924bf049 --- /dev/null +++ b/android/app/src/main/java/app/tourism/data/dto/FavoritesIdsDto.kt @@ -0,0 +1,5 @@ +package app.tourism.data.dto + +data class FavoritesIdsDto( + val marks: List +) diff --git a/android/app/src/main/java/app/tourism/data/dto/HashDto.kt b/android/app/src/main/java/app/tourism/data/dto/HashDto.kt new file mode 100644 index 0000000000..cb9407a1c9 --- /dev/null +++ b/android/app/src/main/java/app/tourism/data/dto/HashDto.kt @@ -0,0 +1,3 @@ +package app.tourism.data.dto + +data class HashDto(val hash: String) \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/data/dto/place/CoordinatesDto.kt b/android/app/src/main/java/app/tourism/data/dto/place/CoordinatesDto.kt index f2b5bd3584..aaf792a747 100644 --- a/android/app/src/main/java/app/tourism/data/dto/place/CoordinatesDto.kt +++ b/android/app/src/main/java/app/tourism/data/dto/place/CoordinatesDto.kt @@ -3,13 +3,19 @@ package app.tourism.data.dto.place import app.tourism.data.dto.PlaceLocation data class CoordinatesDto( - val latitude: String, - val longitude: String + val latitude: String?, + val longitude: String? ) { - fun toPlaceLocation(name: String) = - PlaceLocation( - name, - latitude.toDouble(), - longitude.toDouble() - ) -} \ No newline at end of file + fun toPlaceLocation(name: String): PlaceLocation? { + try { + return PlaceLocation( + name, + latitude!!.toDouble(), + longitude!!.toDouble() + ) + } catch (e: Exception) { + e.printStackTrace() + return null + } + } +} diff --git a/android/app/src/main/java/app/tourism/data/dto/place/PlaceDto.kt b/android/app/src/main/java/app/tourism/data/dto/place/PlaceDto.kt index 52b7c2426e..bbbe6565da 100644 --- a/android/app/src/main/java/app/tourism/data/dto/place/PlaceDto.kt +++ b/android/app/src/main/java/app/tourism/data/dto/place/PlaceDto.kt @@ -1,14 +1,13 @@ package app.tourism.data.dto.place -import app.tourism.domain.models.common.PlaceShort import app.tourism.domain.models.details.PlaceFull data class PlaceDto( val id: Long, val name: String, - val coordinates: CoordinatesDto, + val coordinates: CoordinatesDto?, val cover: String, - val feedbacks: List = emptyList(), + val feedbacks: List?, val gallery: List, val rating: String, val short_description: String, @@ -20,10 +19,10 @@ data class PlaceDto( rating = rating.toDouble(), excerpt = short_description, description = long_description, - placeLocation = coordinates.toPlaceLocation(name), + placeLocation = coordinates?.toPlaceLocation(name), cover = cover, pics = gallery, isFavorite = isFavorite, - reviews = feedbacks.map { it.toReview() } + reviews = feedbacks?.map { it.toReview() } ?: emptyList() ) } \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/data/dto/place/ReviewIdsDto.kt b/android/app/src/main/java/app/tourism/data/dto/place/ReviewIdsDto.kt new file mode 100644 index 0000000000..6067b17775 --- /dev/null +++ b/android/app/src/main/java/app/tourism/data/dto/place/ReviewIdsDto.kt @@ -0,0 +1,5 @@ +package app.tourism.data.dto.place + +data class ReviewIdsDto( + val feedbacks: List, +) diff --git a/android/app/src/main/java/app/tourism/data/dto/place/ReviewsDto.kt b/android/app/src/main/java/app/tourism/data/dto/place/ReviewsDto.kt new file mode 100644 index 0000000000..704004a45a --- /dev/null +++ b/android/app/src/main/java/app/tourism/data/dto/place/ReviewsDto.kt @@ -0,0 +1,3 @@ +package app.tourism.data.dto.place + +data class ReviewsDto(val data: List) diff --git a/android/app/src/main/java/app/tourism/data/dto/profile/User.kt b/android/app/src/main/java/app/tourism/data/dto/profile/User.kt index acf98ff20c..a2dc181630 100644 --- a/android/app/src/main/java/app/tourism/data/dto/profile/User.kt +++ b/android/app/src/main/java/app/tourism/data/dto/profile/User.kt @@ -13,6 +13,7 @@ data class User( val username: String ) { fun toPersonalData() = PersonalData( + id = id, fullName = full_name, country = country, pfpUrl = avatar, diff --git a/android/app/src/main/java/app/tourism/data/prefs/UserPreferences.kt b/android/app/src/main/java/app/tourism/data/prefs/UserPreferences.kt index 4b8ebc1d1a..652d779f0f 100644 --- a/android/app/src/main/java/app/tourism/data/prefs/UserPreferences.kt +++ b/android/app/src/main/java/app/tourism/data/prefs/UserPreferences.kt @@ -35,6 +35,9 @@ class UserPreferences(context: Context) { fun getToken() = sharedPref.getString("token", "") fun setToken(value: String?) = sharedPref.edit { putString("token", value) } + + fun getUserId() = sharedPref.getString("user_id", "") + fun setUserId(value: String?) = sharedPref.edit { putString("user_id", value) } } data class Language(val code: String, val name: String) diff --git a/android/app/src/main/java/app/tourism/data/remote/NetworkUtils.kt b/android/app/src/main/java/app/tourism/data/remote/NetworkUtils.kt index a5815041ec..750f9442b1 100644 --- a/android/app/src/main/java/app/tourism/data/remote/NetworkUtils.kt +++ b/android/app/src/main/java/app/tourism/data/remote/NetworkUtils.kt @@ -1,5 +1,9 @@ package app.tourism.data.remote +import android.content.Context +import android.net.ConnectivityManager +import android.util.Log +import androidx.core.content.ContextCompat.getSystemService import app.tourism.domain.models.SimpleResponse import app.tourism.domain.models.resource.Resource import com.google.gson.Gson @@ -11,6 +15,7 @@ import retrofit2.HttpException import retrofit2.Response import java.io.IOException + suspend inline fun FlowCollector>.handleGenericCall( call: () -> Response, mapper: (T) -> R, @@ -23,18 +28,21 @@ suspend inline fun FlowCollector>.handleGenericCall( if (response.isSuccessful) emit(Resource.Success(body)) else emit(response.parseError()) } catch (e: HttpException) { + e.printStackTrace() emit( Resource.Error( message = "Упс! Что-то пошло не так." ) ) } catch (e: IOException) { + e.printStackTrace() emit( Resource.Error( message = "Не удается соединиться с сервером, проверьте интернет подключение" ) ) } catch (e: Exception) { + e.printStackTrace() emit(Resource.Error(message = "Упс! Что-то пошло не так.")) } } @@ -47,12 +55,15 @@ suspend inline fun handleResponse(call: () -> Response): Resource return Resource.Success(body) } else return response.parseError() } catch (e: HttpException) { + e.printStackTrace() return Resource.Error(message = "Упс! Что-то пошло не так.") } catch (e: IOException) { + e.printStackTrace() return Resource.Error( message = "Не удается соединиться с сервером, проверьте интернет подключение" ) } catch (e: Exception) { + e.printStackTrace() return Resource.Error(message = "Упс! Что-то пошло не так.") } } @@ -67,10 +78,16 @@ inline fun Response.parseError(): Resource { Resource.Error(message = response?.message ?: "") } catch (e: JSONException) { - println(e.message) + e.printStackTrace() Resource.Error(e.toString()) } } fun String.toFormDataRequestBody() = this.toRequestBody("multipart/form-data".toMediaTypeOrNull()) +fun isOnline(context: Context): Boolean { + val cm = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager? + val netInfo = cm!!.activeNetworkInfo + return netInfo != null && netInfo.isConnected() +} \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/data/remote/TourismApi.kt b/android/app/src/main/java/app/tourism/data/remote/TourismApi.kt index 153530f1cb..1437aec7e5 100644 --- a/android/app/src/main/java/app/tourism/data/remote/TourismApi.kt +++ b/android/app/src/main/java/app/tourism/data/remote/TourismApi.kt @@ -3,8 +3,11 @@ package app.tourism.data.remote import app.tourism.data.dto.AllDataDto import app.tourism.data.dto.CategoryDto import app.tourism.data.dto.FavoritesDto +import app.tourism.data.dto.FavoritesIdsDto +import app.tourism.data.dto.HashDto import app.tourism.data.dto.auth.AuthResponseDto -import app.tourism.data.dto.place.ReviewDto +import app.tourism.data.dto.place.ReviewIdsDto +import app.tourism.data.dto.place.ReviewsDto import app.tourism.data.dto.profile.LanguageDto import app.tourism.data.dto.profile.ThemeDto import app.tourism.data.dto.profile.UserData @@ -17,11 +20,13 @@ import retrofit2.http.DELETE import retrofit2.http.Field import retrofit2.http.FormUrlEncoded import retrofit2.http.GET +import retrofit2.http.HTTP import retrofit2.http.Multipart import retrofit2.http.POST import retrofit2.http.PUT import retrofit2.http.Part import retrofit2.http.Path +import retrofit2.http.Query interface TourismApi { // region auth @@ -72,7 +77,10 @@ interface TourismApi { // region places @GET("marks/{id}") - suspend fun getPlacesByCategory(@Path("id") id: Long): Response + suspend fun getPlacesByCategory( + @Path("id") id: Long, + @Query("hash") hash: String + ): Response @GET("marks/all") suspend fun getAllPlaces(): Response @@ -83,15 +91,15 @@ interface TourismApi { suspend fun getFavorites(): Response @POST("favourite-marks") - suspend fun addFavorites(@Body ids: List): Response + suspend fun addFavorites(@Body ids: FavoritesIdsDto): Response - @DELETE("favourite-marks") - suspend fun removeFromFavorites(@Body ids: List): Response + @HTTP(method = "DELETE", path = "favourite-marks", hasBody = true) + suspend fun removeFromFavorites(@Body ids: FavoritesIdsDto): Response // endregion favorites // region reviews @GET("feedbacks/{id}") - suspend fun getReviewsByPlaceId(id: Long): Response> + suspend fun getReviewsByPlaceId(@Path("id") id: Long): Response @Multipart @POST("feedbacks") @@ -102,10 +110,9 @@ interface TourismApi { @Part images: List? = null ): Response - @DELETE("feedbacks/{mark_id}") - suspend fun deleteReview( - @Path("mark_id") placeId: Long, - @Body reviewsIds: List, + @HTTP(method = "DELETE", path = "feedbacks", hasBody = true) + suspend fun deleteReviews( + @Body feedbacks: ReviewIdsDto, ): Response // endregion reviews } \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/data/remote/WifiReceiver.kt b/android/app/src/main/java/app/tourism/data/remote/WifiReceiver.kt new file mode 100644 index 0000000000..552ea22ff7 --- /dev/null +++ b/android/app/src/main/java/app/tourism/data/remote/WifiReceiver.kt @@ -0,0 +1,37 @@ +package app.tourism.data.remote + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.net.ConnectivityManager +import android.net.ConnectivityManager.NetworkCallback +import android.net.Network +import android.net.NetworkRequest +import app.tourism.data.repositories.PlacesRepository +import app.tourism.data.repositories.ReviewsRepository +import javax.inject.Inject + +class WifiReceiver : BroadcastReceiver() { + @Inject + lateinit var reviewsRepository: ReviewsRepository + + @Inject + lateinit var placesRepository: PlacesRepository + + override fun onReceive(context: Context, intent: Intent) { + val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val builder = NetworkRequest.Builder() + + cm.registerNetworkCallback( + builder.build(), + object : NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + + reviewsRepository.syncReviews() + placesRepository.syncFavorites() + } + } + ) + } +} \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/data/repositories/PlacesRepository.kt b/android/app/src/main/java/app/tourism/data/repositories/PlacesRepository.kt index 6f7a8f1ed8..07a98a344b 100644 --- a/android/app/src/main/java/app/tourism/data/repositories/PlacesRepository.kt +++ b/android/app/src/main/java/app/tourism/data/repositories/PlacesRepository.kt @@ -3,9 +3,12 @@ package app.tourism.data.repositories import android.content.Context import app.organicmaps.R import app.tourism.data.db.Database +import app.tourism.data.db.entities.FavoriteToSyncEntity import app.tourism.data.db.entities.HashEntity import app.tourism.data.db.entities.PlaceEntity import app.tourism.data.db.entities.ReviewEntity +import app.tourism.data.dto.FavoritesIdsDto +import app.tourism.data.dto.HashDto import app.tourism.data.dto.place.PlaceDto import app.tourism.data.remote.TourismApi import app.tourism.data.remote.handleGenericCall @@ -24,14 +27,14 @@ import kotlinx.coroutines.flow.flow class PlacesRepository( private val api: TourismApi, - private val db: Database, + db: Database, @ApplicationContext private val context: Context, ) { private val placesDao = db.placesDao private val reviewsDao = db.reviewsDao private val hashesDao = db.hashesDao - fun downloadAllDataIfFirstTime(): Flow> = flow { + fun downloadAllData(): Flow> = flow { val hashes = hashesDao.getHashes() val favoritesResponse = handleResponse { api.getFavorites() } @@ -93,6 +96,8 @@ class PlacesRepository( SimpleResponse(message = context.getString(R.string.great_success)) } ) + } else { + emit(Resource.Success(SimpleResponse(message = context.getString(R.string.great_success)))) } } @@ -103,27 +108,24 @@ class PlacesRepository( } } - fun getPlacesByCategory(id: Long): Flow>> = channelFlow { + fun getPlacesByCategoryFromDbFlow(id: Long): Flow>> = channelFlow { + placesDao.getPlacesByCategoryId(categoryId = id) + .collectLatest { placeEntities -> + send(Resource.Success(placeEntities.map { it.toPlaceShort() })) + } + } + + suspend fun getPlacesByCategoryFromApiIfThereIsChange(id: Long) { val hash = hashesDao.getHash(id) - if (hash.value.isNotBlank()) { - placesDao.getPlacesByCategoryId(categoryId = id) - .collectLatest { placeEntities -> - send(Resource.Success(placeEntities.map { it.toPlaceShort() })) - } - } + val favorites = placesDao.getFavoritePlaces("") + val resource = + handleResponse { api.getPlacesByCategory(id, hash?.value ?: "") } - var favorites = listOf() - placesDao.getFavoritePlaces("").collectLatest { - favorites = it - } - - val resource = handleResponse { api.getPlacesByCategory(id) } - if (resource is Resource.Success) { + if (hash != null && resource is Resource.Success) resource.data?.let { categoryDto -> - if (hash.value != categoryDto.hash) { + if (categoryDto.data.isNotEmpty()) { // update places - hashesDao.insertHash(hash.copy(value = categoryDto.hash)) placesDao.deleteAllPlacesByCategory(categoryId = id) val places = categoryDto.data.map { placeDto -> @@ -142,9 +144,12 @@ class PlacesRepository( } reviewsDao.deleteAllReviews() reviewsDao.insertReviews(reviewsEntities) + + // update hash + hashesDao.insertHash(hash.copy(value = categoryDto.hash)) } } - } + } fun getTopPlaces(id: Long): Flow>> = channelFlow { @@ -162,7 +167,7 @@ class PlacesRepository( } fun getFavorites(q: String): Flow>> = channelFlow { - placesDao.getFavoritePlaces("%$q%") + placesDao.getFavoritePlacesFlow("%$q%") .collectLatest { placeEntities -> send(Resource.Success(placeEntities.map { it.toPlaceShort() })) } @@ -170,5 +175,23 @@ class PlacesRepository( suspend fun setFavorite(placeId: Long, isFavorite: Boolean) { placesDao.setFavorite(placeId, isFavorite) + + val favoritesIdsDto = FavoritesIdsDto(marks = listOf(placeId)) + + val favoriteToSyncEntity = FavoriteToSyncEntity(placeId, isFavorite) + placesDao.addFavoriteToSync(favoriteToSyncEntity) + val response: Resource = if (isFavorite) + handleResponse { api.addFavorites(favoritesIdsDto) } + else + handleResponse { api.removeFromFavorites(favoritesIdsDto) } + + if (response is Resource.Success) + placesDao.removeFavoriteToSync(favoriteToSyncEntity.placeId) + else if (response is Resource.Error) + placesDao.addFavoriteToSync(favoriteToSyncEntity) + } + + fun syncFavorites() { + } } diff --git a/android/app/src/main/java/app/tourism/data/repositories/ReviewsRepository.kt b/android/app/src/main/java/app/tourism/data/repositories/ReviewsRepository.kt index 0a8c894666..e9f934cb4b 100644 --- a/android/app/src/main/java/app/tourism/data/repositories/ReviewsRepository.kt +++ b/android/app/src/main/java/app/tourism/data/repositories/ReviewsRepository.kt @@ -1,22 +1,31 @@ package app.tourism.data.repositories +import android.content.Context import app.tourism.data.db.Database +import app.tourism.data.dto.place.ReviewIdsDto import app.tourism.data.remote.TourismApi import app.tourism.data.remote.handleResponse +import app.tourism.data.remote.isOnline import app.tourism.data.remote.toFormDataRequestBody import app.tourism.domain.models.SimpleResponse import app.tourism.domain.models.details.Review import app.tourism.domain.models.details.ReviewToPost import app.tourism.domain.models.resource.Resource +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.launch import okhttp3.MediaType.Companion.toMediaType import okhttp3.MultipartBody import okhttp3.RequestBody.Companion.asRequestBody +import java.io.File class ReviewsRepository( + @ApplicationContext val context: Context, private val api: TourismApi, private val db: Database, ) { @@ -29,42 +38,101 @@ class ReviewsRepository( } } - fun postReview(review: ReviewToPost): Flow> = flow { - val imageMultiparts = mutableListOf() - review.images.forEach { - val requestBody = it.asRequestBody("image/*".toMediaType()) - val imageMultipart = - MultipartBody.Part.createFormData("images[]", it.name, requestBody) - imageMultiparts.add(imageMultipart) - } - - emit(Resource.Loading()) - val postReviewResponse = handleResponse { - api.postReview( - placeId = review.placeId.toString().toFormDataRequestBody(), - comment = review.comment.toFormDataRequestBody(), - points = review.rating.toString().toFormDataRequestBody(), - images = imageMultiparts - ) - } - emit(postReviewResponse) - - if (postReviewResponse is Resource.Success) { - updateReviewsForDb(review.placeId) + suspend fun getReviewsFromApi(id: Long) { + val getReviewsResponse = handleResponse { api.getReviewsByPlaceId(id) } + if (getReviewsResponse is Resource.Success) { + reviewsDao.deleteAllPlaceReviews(id) + getReviewsResponse.data?.data?.map { it.toReview().toReviewEntity() } + ?.let { reviewsDao.insertReviews(it) } } } - fun deleteReviews(placeId: Long, reviewsIds: List): Flow> = - flow { - val deleteReviewsResponse = handleResponse { - api.deleteReview(placeId, reviewsIds) + fun postReview(review: ReviewToPost): Flow> = flow { + if (isOnline(context)) { + emit(Resource.Loading()) + val postReviewResponse = handleResponse { + api.postReview( + placeId = review.placeId.toString().toFormDataRequestBody(), + comment = review.comment.toFormDataRequestBody(), + points = review.rating.toString().toFormDataRequestBody(), + images = getMultipartFromImageFiles(review.images) + ) } - emit(deleteReviewsResponse) + emit(postReviewResponse) - if (deleteReviewsResponse is Resource.Success) { - updateReviewsForDb(placeId) + if (postReviewResponse is Resource.Success) { + updateReviewsForDb(review.placeId) + } + } else { + reviewsDao.insertReviewPlannedToPost(review.toReviewPlannedToPostEntity()) + } + } + + fun deleteReview(id: Long): Flow> = + flow { + reviewsDao.markReviewForDeletion(id) + val deleteReviewResponse = + handleResponse { + api.deleteReviews(ReviewIdsDto(listOf(id))) + } + + if (deleteReviewResponse is Resource.Success) { + reviewsDao.deleteReview(id) + } + emit(deleteReviewResponse) + +// val token = UserPreferences(context).getToken() +// +// val client = OkHttpClient() +// val mediaType = "application/json".toMediaType() +// val body = "{\n \"feedbacks\": [$id]\n}".toRequestBody(mediaType) +// val request = Request.Builder() +// .url("http://192.168.1.80:8888/api/feedbacks") +// .method("DELETE", body) +// .addHeader("Accept", "application/json") +// .addHeader("Content-Type", "application/json") +// .addHeader("Authorization", "Bearer $token") +// .build() +// val response = client.newCall(request).execute() + + } + + fun syncReviews() { + val coroutineScope = CoroutineScope(Dispatchers.IO) + coroutineScope.launch { + deleteReviewsThatWereNotDeletedOnTheServer() + publishReviewsThatWereNotPublished() + } + } + + private suspend fun deleteReviewsThatWereNotDeletedOnTheServer() { + val reviews = reviewsDao.getReviewsPlannedForDeletion() + if (reviews.isEmpty()) { + val reviewsIds = reviews.map { it.id } + val response = handleResponse { api.deleteReviews(ReviewIdsDto(reviewsIds)) } + if (response is Resource.Success) { + reviewsDao.deleteReviews(reviewsIds) } } + // todo + } + + private suspend fun publishReviewsThatWereNotPublished() { + val reviewsPlannedToPostEntities = reviewsDao.getReviewsPlannedToPost() + if (reviewsPlannedToPostEntities.isEmpty()) { + val reviews = reviewsPlannedToPostEntities.map { it.toReviewsPlannedToPostDto() } + reviews.forEach { + CoroutineScope(Dispatchers.IO).launch { + api.postReview( + placeId = it.placeId.toString().toFormDataRequestBody(), + comment = it.comment.toFormDataRequestBody(), + points = it.rating.toString().toFormDataRequestBody(), + images = getMultipartFromImageFiles(it.images) + ) + } + } + } + } private suspend fun updateReviewsForDb(id: Long) { val getReviewsResponse = handleResponse { @@ -72,6 +140,20 @@ class ReviewsRepository( } if (getReviewsResponse is Resource.Success) { reviewsDao.deleteAllPlaceReviews(id) + val reviews = + getReviewsResponse.data?.data?.map { it.toReview().toReviewEntity() } ?: listOf() + reviewsDao.insertReviews(reviews) } } + + private fun getMultipartFromImageFiles(imageFiles: List): MutableList { + val imagesMultipart = mutableListOf() + imageFiles.forEach { + val requestBody = it.asRequestBody("image/*".toMediaType()) + val imageMultipart = + MultipartBody.Part.createFormData("images[]", it.name, requestBody) + imagesMultipart.add(imageMultipart) + } + return imagesMultipart + } } diff --git a/android/app/src/main/java/app/tourism/di/RepositoriesModule.kt b/android/app/src/main/java/app/tourism/di/RepositoriesModule.kt index 9507bd4146..9435fb57c5 100644 --- a/android/app/src/main/java/app/tourism/di/RepositoriesModule.kt +++ b/android/app/src/main/java/app/tourism/di/RepositoriesModule.kt @@ -43,7 +43,7 @@ object RepositoriesModule { db: Database, @ApplicationContext context: Context, ): ReviewsRepository { - return ReviewsRepository(api, db) + return ReviewsRepository(context, api, db) } diff --git a/android/app/src/main/java/app/tourism/domain/models/details/PlaceFull.kt b/android/app/src/main/java/app/tourism/domain/models/details/PlaceFull.kt index 9c85ed4078..826a2badff 100644 --- a/android/app/src/main/java/app/tourism/domain/models/details/PlaceFull.kt +++ b/android/app/src/main/java/app/tourism/domain/models/details/PlaceFull.kt @@ -10,7 +10,7 @@ data class PlaceFull( val rating: Double, val excerpt: String, val description: String, - val placeLocation: PlaceLocation, + val placeLocation: PlaceLocation?, val cover: String, val pics: List = emptyList(), val reviews: List? = null, @@ -33,7 +33,7 @@ data class PlaceFull( excerpt = excerpt, description = description, gallery = pics, - coordinates = placeLocation.toCoordinatesEntity(), + coordinates = placeLocation?.toCoordinatesEntity(), cover = cover, isFavorite = isFavorite, ) diff --git a/android/app/src/main/java/app/tourism/domain/models/details/Review.kt b/android/app/src/main/java/app/tourism/domain/models/details/Review.kt index ceaee1a727..456d987246 100644 --- a/android/app/src/main/java/app/tourism/domain/models/details/Review.kt +++ b/android/app/src/main/java/app/tourism/domain/models/details/Review.kt @@ -10,6 +10,7 @@ data class Review( val date: String? = null, val comment: String? = null, val picsUrls: List = emptyList(), + val deletionPlanned: Boolean = false, ) { fun toReviewEntity() = ReviewEntity( id = id, @@ -18,6 +19,7 @@ data class Review( placeId = placeId, date = date ?: "", rating = rating, - images = picsUrls + images = picsUrls, + deletionPlanned = deletionPlanned ) } diff --git a/android/app/src/main/java/app/tourism/domain/models/details/ReviewToPost.kt b/android/app/src/main/java/app/tourism/domain/models/details/ReviewToPost.kt index 23e6e2b6ac..1929c7c680 100644 --- a/android/app/src/main/java/app/tourism/domain/models/details/ReviewToPost.kt +++ b/android/app/src/main/java/app/tourism/domain/models/details/ReviewToPost.kt @@ -1,5 +1,6 @@ package app.tourism.domain.models.details +import app.tourism.data.db.entities.ReviewPlannedToPostEntity import java.io.File data class ReviewToPost( @@ -7,4 +8,15 @@ data class ReviewToPost( val comment: String, val rating: Int, val images: List, -) \ No newline at end of file +) { + fun toReviewPlannedToPostEntity(): ReviewPlannedToPostEntity { + val imagesPaths = images.map { it.path } + + return ReviewPlannedToPostEntity( + placeId = placeId, + comment = comment, + rating = rating, + images = imagesPaths + ) + } +} \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/domain/models/profile/PersonalData.kt b/android/app/src/main/java/app/tourism/domain/models/profile/PersonalData.kt index 1ed95df0e4..2b6eab5ef0 100644 --- a/android/app/src/main/java/app/tourism/domain/models/profile/PersonalData.kt +++ b/android/app/src/main/java/app/tourism/domain/models/profile/PersonalData.kt @@ -1,6 +1,7 @@ package app.tourism.domain.models.profile data class PersonalData( + val id: Long, val fullName: String, val country: String, val pfpUrl: String?, diff --git a/android/app/src/main/java/app/tourism/ui/common/ui_state/EmptyList.kt b/android/app/src/main/java/app/tourism/ui/common/ui_state/EmptyList.kt new file mode 100644 index 0000000000..90082d8e13 --- /dev/null +++ b/android/app/src/main/java/app/tourism/ui/common/ui_state/EmptyList.kt @@ -0,0 +1,24 @@ +package app.tourism.ui.common.ui_state + +import androidx.compose.foundation.layout.Box +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import app.organicmaps.R +import app.tourism.ui.theme.TextStyles + + +@Composable +fun EmptyList(modifier: Modifier = Modifier) { + Box( + modifier = modifier, + contentAlignment = Alignment.Center + ) { + Text( + text = stringResource(id = R.string.empty_list), + style = TextStyles.h2 + ) + } +} \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/ui/common/ui_state/NetworkError.kt b/android/app/src/main/java/app/tourism/ui/common/ui_state/Error.kt similarity index 93% rename from android/app/src/main/java/app/tourism/ui/common/ui_state/NetworkError.kt rename to android/app/src/main/java/app/tourism/ui/common/ui_state/Error.kt index 3bdc05de2b..48eeaf5b2b 100644 --- a/android/app/src/main/java/app/tourism/ui/common/ui_state/NetworkError.kt +++ b/android/app/src/main/java/app/tourism/ui/common/ui_state/Error.kt @@ -25,14 +25,13 @@ import app.tourism.ui.common.buttons.PrimaryButton import app.tourism.ui.theme.TextStyles @Composable -fun NetworkError( +fun Error( modifier: Modifier = Modifier, errorMessage: String? = null, status: Boolean = true, onEntireScreen: Boolean = true, onRetry: (() -> Unit)? = null ) { - println("error message: $errorMessage") if (status) { Column( modifier = if (onEntireScreen) modifier @@ -55,7 +54,7 @@ fun NetworkError( Text( text = errorMessage ?: stringResource(id = if (onEntireScreen) R.string.no_network else R.string.smth_went_wrong), - style = TextStyles.h1, + style = TextStyles.h3, textAlign = Center ) @@ -83,8 +82,8 @@ fun NetworkError( @Composable fun NetworkError_preview() { Column { - NetworkError(status = true, onEntireScreen = false) {} + Error(status = true, onEntireScreen = false) {} VerticalSpace(height = 16.dp) - NetworkError(status = true) {} + Error(status = true) {} } } diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/categories/categories/CategoriesViewModel.kt b/android/app/src/main/java/app/tourism/ui/screens/main/categories/categories/CategoriesViewModel.kt index dc15c1a2d5..fab4819dc0 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/categories/categories/CategoriesViewModel.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/categories/categories/CategoriesViewModel.kt @@ -3,6 +3,7 @@ package app.tourism.ui.screens.main.categories.categories import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.compose.viewModel import app.organicmaps.R import app.tourism.data.repositories.PlacesRepository import app.tourism.domain.models.categories.PlaceCategory @@ -63,18 +64,22 @@ class CategoriesViewModel @Inject constructor( } private fun onCategoryChangeGetPlaces() { - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch { _selectedCategory.collectLatest { item -> item?.key?.let { id -> val categoryId = id as Long - placesRepository.getPlacesByCategory(categoryId).collectLatest { resource -> - if (resource is Resource.Success) { - resource.data?.let { _places.value = it } + placesRepository.getPlacesByCategoryFromApiIfThereIsChange(categoryId) + placesRepository.getPlacesByCategoryFromDbFlow(categoryId) + .collectLatest { resource -> + if (resource is Resource.Success) { + resource.data?.let { _places.value = it } + } } - } + } } } + } init { diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/favorites/favorites/FavoritesScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/main/favorites/favorites/FavoritesScreen.kt index 9e671b29f7..1f0523a454 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/favorites/favorites/FavoritesScreen.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/favorites/favorites/FavoritesScreen.kt @@ -28,6 +28,7 @@ import app.tourism.ui.common.nav.AppTopBar import app.tourism.ui.common.nav.SearchTopBar import app.tourism.ui.common.nav.TopBarActionData import app.tourism.ui.common.special.PlacesItem +import app.tourism.ui.common.ui_state.EmptyList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -88,19 +89,24 @@ fun FavoritesScreen( VerticalSpace(16.dp) } - items(places, key = { it.id }) { item -> - Column(Modifier.animateItem()) { - PlacesItem( - place = item, - onPlaceClick = { onPlaceClick(item.id) }, - isFavorite = item.isFavorite, - onFavoriteChanged = { isFavorite -> - favoritesVM.setFavoriteChanged(item, isFavorite) - }, - ) - VerticalSpace(height = 16.dp) + if (places.isNotEmpty()) + items(places, key = { it.id }) { item -> + Column(Modifier.animateItem()) { + PlacesItem( + place = item, + onPlaceClick = { onPlaceClick(item.id) }, + isFavorite = item.isFavorite, + onFavoriteChanged = { isFavorite -> + favoritesVM.setFavoriteChanged(item, isFavorite) + }, + ) + VerticalSpace(height = 16.dp) + } + } + else + item { + EmptyList() } - } item { Column { @@ -109,4 +115,4 @@ fun FavoritesScreen( } } } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/home/HomeScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/main/home/HomeScreen.kt index c44300bbb0..3b76bb5a47 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/home/HomeScreen.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/home/HomeScreen.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -31,6 +32,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle @@ -42,7 +44,9 @@ import androidx.hilt.navigation.compose.hiltViewModel import app.organicmaps.R import app.tourism.Constants import app.tourism.domain.models.common.PlaceShort +import app.tourism.domain.models.resource.Resource import app.tourism.drawOverlayForTextBehind +import app.tourism.ui.ObserveAsEvents import app.tourism.ui.common.AppSearchBar import app.tourism.ui.common.BorderedItem import app.tourism.ui.common.HorizontalSpace @@ -51,10 +55,13 @@ import app.tourism.ui.common.SpaceForNavBar import app.tourism.ui.common.VerticalSpace import app.tourism.ui.common.nav.AppTopBar import app.tourism.ui.common.nav.TopBarActionData +import app.tourism.ui.common.ui_state.Error +import app.tourism.ui.common.ui_state.Loading import app.tourism.ui.screens.main.categories.categories.CategoriesViewModel import app.tourism.ui.screens.main.categories.categories.HorizontalSingleChoice import app.tourism.ui.theme.TextStyles import app.tourism.ui.theme.getStarColor +import app.tourism.ui.utils.showToast @Composable fun HomeScreen( @@ -65,10 +72,20 @@ fun HomeScreen( homeVM: HomeViewModel = hiltViewModel(), categoriesVM: CategoriesViewModel, ) { + val context = LocalContext.current + val query = homeVM.query.collectAsState().value val sights = homeVM.sights.collectAsState().value val restaurants = homeVM.restaurants.collectAsState().value + val downloadResponse = homeVM.downloadResponse.collectAsState().value + + ObserveAsEvents(flow = homeVM.uiEventsChannelFlow) { event -> + when (event) { + is UiEvent.ShowToast -> context.showToast(event.message) + } + } + LaunchedEffect(true) { categoriesVM.setSelectedCategory(null) } @@ -88,51 +105,67 @@ fun HomeScreen( }, contentWindowInsets = WindowInsets(left = 0.dp, right = 0.dp, top = 0.dp, bottom = 0.dp) ) { paddingValues -> - Column( - Modifier - .padding(paddingValues) - .verticalScroll(rememberScrollState()) - ) { - Column(Modifier.padding(horizontal = Constants.SCREEN_PADDING)) { + if (downloadResponse is Resource.Success) + Column( + Modifier + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + ) { + Column(Modifier.padding(horizontal = Constants.SCREEN_PADDING)) { + VerticalSpace(height = 16.dp) + + AppSearchBar( + modifier = Modifier.fillMaxWidth(), + query = query, + onQueryChanged = { homeVM.setQuery(it) }, + onSearchClicked = onSearchClick, + onClearClicked = { homeVM.clearSearchField() }, + ) + } VerticalSpace(height = 16.dp) - AppSearchBar( - modifier = Modifier.fillMaxWidth(), - query = query, - onQueryChanged = { homeVM.setQuery(it) }, - onSearchClicked = onSearchClick, - onClearClicked = { homeVM.clearSearchField() }, + Categories(categoriesVM, onCategoryClicked) + VerticalSpace(height = 24.dp) + + HorizontalPlaces( + title = stringResource(id = R.string.sights), + items = sights, + onPlaceClick = { item -> + onPlaceClick(item.id) + }, + setFavoriteChanged = { item, isFavorite -> + homeVM.setFavoriteChanged(item, isFavorite) + }, ) + VerticalSpace(height = 24.dp) + + HorizontalPlaces( + title = stringResource(id = R.string.restaurants), + items = restaurants, + onPlaceClick = { item -> + onPlaceClick(item.id) + }, + setFavoriteChanged = { item, isFavorite -> + homeVM.setFavoriteChanged(item, isFavorite) + }, + ) + + SpaceForNavBar() } - VerticalSpace(height = 16.dp) - - Categories(categoriesVM, onCategoryClicked) - VerticalSpace(height = 24.dp) - - HorizontalPlaces( - title = stringResource(id = R.string.sights), - items = sights, - onPlaceClick = { item -> - onPlaceClick(item.id) - }, - setFavoriteChanged = { item, isFavorite -> - homeVM.setFavoriteChanged(item, isFavorite) - }, + if (downloadResponse is Resource.Loading) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text(text = stringResource(id = R.string.plz_wait_dowloading)) + VerticalSpace(height = 16.dp) + Loading(onEntireScreen = false) + } + } + } + if (downloadResponse is Resource.Error) { + Error( + errorMessage = downloadResponse.message + ?: stringResource(id = R.string.smth_went_wrong), ) - VerticalSpace(height = 24.dp) - - HorizontalPlaces( - title = stringResource(id = R.string.restaurants), - items = restaurants, - onPlaceClick = { item -> - onPlaceClick(item.id) - }, - setFavoriteChanged = { item, isFavorite -> - homeVM.setFavoriteChanged(item, isFavorite) - }, - ) - - SpaceForNavBar() } } } diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/home/HomeViewModel.kt b/android/app/src/main/java/app/tourism/ui/screens/main/home/HomeViewModel.kt index 9ab15f40bf..17c9de40e8 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/home/HomeViewModel.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/home/HomeViewModel.kt @@ -1,5 +1,6 @@ package app.tourism.ui.screens.main.home +import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import app.tourism.data.repositories.PlacesRepository @@ -8,6 +9,7 @@ import app.tourism.domain.models.categories.PlaceCategory import app.tourism.domain.models.common.PlaceShort import app.tourism.domain.models.resource.Resource import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow @@ -19,6 +21,7 @@ import javax.inject.Inject @HiltViewModel class HomeViewModel @Inject constructor( + @ApplicationContext val context: Context, private val placesRepository: PlacesRepository ) : ViewModel() { private val uiChannel = Channel() @@ -65,11 +68,11 @@ class HomeViewModel @Inject constructor( } } - private val _downloadResponse = MutableStateFlow?>(null) + private val _downloadResponse = MutableStateFlow>(Resource.Idle()) val downloadResponse = _downloadResponse.asStateFlow() - private fun downloadAllDataIfFirstTime() { + private fun downloadAllData() { viewModelScope.launch(Dispatchers.IO) { - placesRepository.downloadAllDataIfFirstTime().collectLatest { + placesRepository.downloadAllData().collectLatest { _downloadResponse.value = it } } @@ -82,7 +85,7 @@ class HomeViewModel @Inject constructor( } init { - downloadAllDataIfFirstTime() + downloadAllData() getTopSights() getTopRestaurants() } diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/PlaceScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/PlaceScreen.kt index 3e919fcf60..aea465da8a 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/PlaceScreen.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/PlaceScreen.kt @@ -49,7 +49,12 @@ fun PlaceDetailsScreen( title = it.name, picUrl = it.cover, isFavorite = it.isFavorite, - onFavoriteChanged = { isFavorite -> placeVM.setFavoriteChanged(id, isFavorite) }, + onFavoriteChanged = { isFavorite -> + placeVM.setFavoriteChanged( + id, + isFavorite + ) + }, onMapClick = onMapClick, onBackClick = onBackClick, ) @@ -84,7 +89,7 @@ fun PlaceDetailsScreen( DescriptionScreen( description = place.description, onCreateRoute = { - onCreateRoute(place.placeLocation) + place.placeLocation?.let { it1 -> onCreateRoute(it1) } }, ) } diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsScreen.kt index 3422f7f972..752d4eb73c 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsScreen.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsScreen.kt @@ -23,12 +23,14 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import app.organicmaps.R import app.tourism.Constants +import app.tourism.ui.ObserveAsEvents import app.tourism.ui.common.HorizontalSpace import app.tourism.ui.common.VerticalSpace import app.tourism.ui.common.special.RatingBar @@ -36,6 +38,8 @@ import app.tourism.ui.screens.main.place_details.reviews.components.PostReview import app.tourism.ui.screens.main.place_details.reviews.components.Review import app.tourism.ui.theme.TextStyles import app.tourism.ui.theme.getStarColor +import app.tourism.ui.utils.showToast +import app.tourism.ui.utils.showYesNoAlertDialog import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlin.math.roundToInt @@ -49,6 +53,7 @@ fun ReviewsScreen( onMoreClick: (picsUrls: List) -> Unit, reviewsVM: ReviewsViewModel = hiltViewModel(), ) { + val context = LocalContext.current val scope = rememberCoroutineScope() val sheetState = rememberModalBottomSheetState() @@ -57,6 +62,10 @@ fun ReviewsScreen( val userReview = reviewsVM.userReview.collectAsState().value val reviews = reviewsVM.reviews.collectAsState().value + ObserveAsEvents(flow = reviewsVM.uiEventsChannelFlow) { event -> + if (event is UiEvent.ShowToast) context.showToast(event.message) + } + LazyColumn( contentPadding = PaddingValues(Constants.SCREEN_PADDING), ) { @@ -64,39 +73,39 @@ fun ReviewsScreen( item { Column { VerticalSpace(height = 16.dp) - Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - modifier = Modifier.size(30.dp), - painter = painterResource(id = R.drawable.star), - contentDescription = null, - tint = getStarColor(), - ) - HorizontalSpace(width = 8.dp) - Text(text = "%.1f".format(rating) + "/5", style = TextStyles.h1) - } Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - TextButton( - onClick = { - showReviewBottomSheet = true - scope.launch { - // Have to do add this delay, because bottom sheet doesn't expand fully itself - // and expands with duration after showReviewBottomSheet is set to true - delay(300L) - sheetState.expand() - } - }, - ) { - Text(text = stringResource(id = R.string.compose_review)) + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + modifier = Modifier.size(30.dp), + painter = painterResource(id = R.drawable.star), + contentDescription = null, + tint = getStarColor(), + ) + HorizontalSpace(width = 8.dp) + Text(text = "%.1f".format(rating) + "/5", style = TextStyles.h1) } - RatingBar(rating = it.roundToInt()) + if (userReview == null) { + TextButton( + onClick = { + showReviewBottomSheet = true + scope.launch { + // Have to do add this delay, because bottom sheet doesn't expand fully itself + // and expands with duration after showReviewBottomSheet is set to true + delay(300L) + sheetState.expand() + } + }, + ) { + Text(text = stringResource(id = R.string.compose_review)) + } + } } - VerticalSpace(height = 24.dp) TextButton( modifier = Modifier.align(Alignment.End), @@ -113,9 +122,15 @@ fun ReviewsScreen( userReview?.let { item { Review( - review = userReview, + review = it, onMoreClick = onMoreClick, - onDeleteClick = {} + onDeleteClick = { + showYesNoAlertDialog( + context = context, + title = context.getString(R.string.deletion_warning), + onPositiveButtonClick = { reviewsVM.deleteReview() }, + ) + } ) } } diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsViewModel.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsViewModel.kt index ef3cdb665d..325748c24c 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsViewModel.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsViewModel.kt @@ -1,11 +1,15 @@ package app.tourism.ui.screens.main.place_details.reviews +import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import app.organicmaps.R +import app.tourism.data.prefs.UserPreferences import app.tourism.data.repositories.ReviewsRepository import app.tourism.domain.models.details.Review import app.tourism.domain.models.resource.Resource import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow @@ -17,7 +21,9 @@ import javax.inject.Inject @HiltViewModel class ReviewsViewModel @Inject constructor( - private val reviewsRepository: ReviewsRepository + @ApplicationContext val context: Context, + private val reviewsRepository: ReviewsRepository, + private val userPreferences: UserPreferences, ) : ViewModel() { private val uiChannel = Channel() val uiEventsChannelFlow = uiChannel.receiveAsFlow() @@ -30,11 +36,33 @@ class ReviewsViewModel @Inject constructor( fun getReviews(id: Long) { viewModelScope.launch(Dispatchers.IO) { - reviewsRepository.getReviewsForPlace(id).collectLatest { - if (it is Resource.Success) { - it.data?.let { _reviews.value = it } + reviewsRepository.getReviewsForPlace(id).collectLatest { resource -> + if (resource is Resource.Success) { + resource.data?.let { reviewList -> + _reviews.value = reviewList + _userReview.value = reviewList.firstOrNull { + it.user.id == userPreferences.getUserId()?.toLong() + } + } + } + } + } + viewModelScope.launch(Dispatchers.IO) { + reviewsRepository.getReviewsFromApi(id) + } + } + + fun deleteReview() { + viewModelScope.launch(Dispatchers.IO) { + _userReview.value?.id?.let { + reviewsRepository.deleteReview(it).collectLatest { + if (it is Resource.Success) { + uiChannel.send(UiEvent.ShowToast(context.getString(R.string.review_deleted))) + } } } } } } + +enum class DeleteReviewStatus { DELETED, IN_PROCESS } \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/components/Review.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/components/Review.kt index 6e78448df5..e178a072c1 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/components/Review.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/components/Review.kt @@ -38,7 +38,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import app.organicmaps.R -import app.tourism.Constants import app.tourism.domain.models.details.Review import app.tourism.domain.models.details.User import app.tourism.ui.common.HorizontalSpace @@ -56,7 +55,7 @@ fun Review( modifier: Modifier = Modifier, review: Review, onMoreClick: (picsUrls: List) -> Unit, - onDeleteClick: ((reviewId: Long) -> Unit)? = null, + onDeleteClick: (() -> Unit)? = null, ) { Column { HorizontalDivider(color = MaterialTheme.colorScheme.surface) @@ -68,19 +67,21 @@ fun Review( horizontalArrangement = Arrangement.SpaceBetween ) { User(modifier = Modifier.weight(1f), user = review.user) - review.date?.let { - Text(text = it, style = TextStyles.b2, color = getHintColor()) + if (review.deletionPlanned) { + Text(stringResource(id = R.string.deletionPlanned)) + } else { + review.date?.let { + Text(text = it, style = TextStyles.b2, color = getHintColor()) + } } } VerticalSpace(height = 16.dp) - review.rating.let { - RatingBar( - rating = it, - size = 24.dp, - ) - VerticalSpace(height = 16.dp) - } + RatingBar( + rating = review.rating, + size = 24.dp, + ) + VerticalSpace(height = 16.dp) val maxPics = 3 val theresMore = review.picsUrls.size > maxPics @@ -104,8 +105,17 @@ fun Review( VerticalSpace(height = 16.dp) } - review.comment?.let { - Comment(comment = it) + if(!review.comment.isNullOrBlank()) { + Comment(comment = review.comment) + VerticalSpace(height = 16.dp) + } + + onDeleteClick?.let { + TextButton( + onClick = { onDeleteClick() }, + ) { + Text(text = stringResource(id = R.string.delete_review)) + } VerticalSpace(height = 16.dp) } } @@ -127,8 +137,9 @@ fun User(modifier: Modifier = Modifier, user: User) { .clip(CircleShape), url = user.pfpUrl, ) - HorizontalSpace(width = 8.dp) + HorizontalSpace(width = 12.dp) Column { + VerticalSpace(height = 6.dp) Text( text = user.name, style = TextStyles.h4, @@ -136,19 +147,18 @@ fun User(modifier: Modifier = Modifier, user: User) { maxLines = 1, overflow = TextOverflow.Ellipsis, ) - user.countryCodeName.let { - CountryAsLabel( - Modifier.fillMaxWidth(), - user.countryCodeName, - contentColor = MaterialTheme.colorScheme.onBackground.toArgb(), - ) - } + CountryAsLabel( + Modifier.fillMaxWidth(), + user.countryCodeName, + contentColor = MaterialTheme.colorScheme.onBackground.toArgb(), + ) } } } @Composable fun Comment(modifier: Modifier = Modifier, comment: String) { + var hasOverflown by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) } val shape = RoundedCornerShape(20.dp) @@ -159,11 +169,7 @@ fun Comment(modifier: Modifier = Modifier, comment: String) { .background(color = MaterialTheme.colorScheme.surface, shape = shape) .clip(shape) .clickable { onClick() } - .padding( - start = Constants.SCREEN_PADDING, - end = Constants.SCREEN_PADDING, - top = Constants.SCREEN_PADDING, - ) + .padding(start = 16.dp, end = 16.dp, top = 16.dp) .then(modifier), ) { Text( @@ -171,9 +177,16 @@ fun Comment(modifier: Modifier = Modifier, comment: String) { style = TextStyles.h4.copy(fontWeight = FontWeight.W400), maxLines = if (expanded) 6969 else 2, overflow = TextOverflow.Ellipsis, + onTextLayout = { + if (it.hasVisualOverflow) hasOverflown = true + } ) - TextButton(onClick = { onClick() }, contentPadding = PaddingValues(0.dp)) { - Text(text = stringResource(id = if (expanded) R.string.less else R.string.more)) + if (hasOverflown) { + TextButton(onClick = { onClick() }, contentPadding = PaddingValues(0.dp)) { + Text(text = stringResource(id = if (expanded) R.string.less else R.string.more)) + } + } else { + VerticalSpace(height = 16.dp) } } } @@ -185,7 +198,7 @@ fun ReviewPic(modifier: Modifier = Modifier, url: String) { modifier = Modifier .width(73.dp) .height(65.dp) - .clip(RoundedCornerShape(4.dp)) + .clip(imageShape) .then(modifier), url = url, ) @@ -221,4 +234,6 @@ fun Modifier.getImageProperties() = this .width(73.dp) .height(65.dp) - .clip(RoundedCornerShape(4.dp)) \ No newline at end of file + .clip(imageShape) + +val imageShape = RoundedCornerShape(4.dp) \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/ui/utils/AlertDialogs.kt b/android/app/src/main/java/app/tourism/ui/utils/AlertDialogs.kt new file mode 100644 index 0000000000..261e0e8f0f --- /dev/null +++ b/android/app/src/main/java/app/tourism/ui/utils/AlertDialogs.kt @@ -0,0 +1,22 @@ +package app.tourism.ui.utils + +import android.content.Context +import app.organicmaps.R + +fun showYesNoAlertDialog(context: Context, title: String, onPositiveButtonClick: () -> Unit) { + android.app.AlertDialog.Builder(context) + .setMessage(title) + .setNegativeButton(context.getString(R.string.no)) { _, _ -> } + .setPositiveButton(context.getString(R.string.yes)) { _, _ -> + onPositiveButtonClick() + } + .create().show() +} + +fun showMessageInAlertDialog(context: Context, title: String, message: String) { + android.app.AlertDialog.Builder(context) + .setTitle(title) + .setMessage(message) + .setPositiveButton(context.getString(R.string.ok)) { _, _ -> } + .create().show() +} diff --git a/android/app/src/main/res/layout/ccp_as_country_label.xml b/android/app/src/main/res/layout/ccp_as_country_label.xml index 64a21013c8..2161e45105 100644 --- a/android/app/src/main/res/layout/ccp_as_country_label.xml +++ b/android/app/src/main/res/layout/ccp_as_country_label.xml @@ -4,7 +4,7 @@ android:id="@+id/ccp" android:layout_width="match_parent" android:layout_height="wrap_content" - android:paddingVertical="12dp" + android:paddingVertical="6dp" app:ccp_textSize="15sp" app:ccp_showArrow="false" app:ccp_autoDetectLanguage="true" diff --git a/android/app/src/main/res/values-ru/strings.xml b/android/app/src/main/res/values-ru/strings.xml index 0551a7f914..1f3465b4e8 100644 --- a/android/app/src/main/res/values-ru/strings.xml +++ b/android/app/src/main/res/values-ru/strings.xml @@ -1,2224 +1,2229 @@ - - + - - - Назад - - Отмена - - Удалить - Загрузить карты - - Ошибка загрузки. Нажмите, чтобы повторить попытку - - Загружается… - - Километры - - МБ - ГБ - - Мили - - Мое местоположение - - Не сейчас - - Поиск - - Поиск на карте - - Геолокация выключена в настройках устройства. Пожалуйста, включите её для удобного использования программы. - - Показать на карте - - Ошибка загрузки - - Попробуйте ещё раз - Про Organic Maps - - Бесплатно для всех, сделано с любовью - - • Без рекламы, без трекинга, без слежки - - • Не разряжает батарею, работает в автономном режиме - - • Быстрые, минималистичные, разработанные сообществом - - Приложение с открытым исходным кодом, созданное энтузиастами и волонтёрами. - - Настройки местоположения - Закрыть - Для работы приложения необходим аппаратно ускоренный OpenGL. К сожалению, ваше устройство не поддерживается. - Загрузить - - Отключите USB кабель или вставьте SD-карту - - Недостаточно свободного места на SD карте/в памяти устройства для использования программы - Перед началом работы разрешите нам загрузить общую карту мира на ваше устройство.\nЭто потребует %s данных. - Перейти на карту - Пока загружается %s,\nвы можете пользоваться картой. - Загрузить %s? - Обновить %s? - - Приостановить - - Продолжить - - Не удалось загрузить %s - - Добавить список - - Цвет метки - - Название списка меток - - Метки - - Метки и треки - - Мои Метки - - Название - - Адрес - - Список - - Настройки - - Сохранять карты в - - Выберите место, где будут храниться загруженные карты - - Загруженные карты - - Внутренний скрытый накопитель - - Внутренний общий накопитель - - SD-карта - - Внешний общий накопитель - - %1$s свободно из %2$s - - Переместить карты? - - Ошибка перемещения файлов карт - - Это может занять несколько минут.\nПожалуйста, подождите… - - Единицы измерения - - Использовать километры или мили + + + Назад + + Отмена + + Удалить + Загрузить карты + + Ошибка загрузки. Нажмите, чтобы повторить попытку + + Загружается… + + Километры + + МБ + ГБ + + Мили + + Мое местоположение + + Не сейчас + + Поиск + + Поиск на карте + + Геолокация выключена в настройках устройства. Пожалуйста, включите её для удобного использования программы. + + Показать на карте + + Ошибка загрузки + + Попробуйте ещё раз + Про Organic Maps + + Бесплатно для всех, сделано с любовью + + • Без рекламы, без трекинга, без слежки + + • Не разряжает батарею, работает в автономном режиме + + • Быстрые, минималистичные, разработанные сообществом + + Приложение с открытым исходным кодом, созданное энтузиастами и волонтёрами. + + Настройки местоположения + Закрыть + Для работы приложения необходим аппаратно ускоренный OpenGL. К сожалению, ваше устройство не поддерживается. + Загрузить + + Отключите USB кабель или вставьте SD-карту + + Недостаточно свободного места на SD карте/в памяти устройства для использования программы + Перед началом работы разрешите нам загрузить общую карту мира на ваше устройство.\nЭто потребует %s данных. + Перейти на карту + Пока загружается %s,\nвы можете пользоваться картой. + Загрузить %s? + Обновить %s? + + Приостановить + + Продолжить + + Не удалось загрузить %s + + Добавить список + + Цвет метки + + Название списка меток + + Метки + + Метки и треки + + Мои Метки + + Название + + Адрес + + Список + + Настройки + + Сохранять карты в + + Выберите место, где будут храниться загруженные карты + + Загруженные карты + + Внутренний скрытый накопитель + + Внутренний общий накопитель + + SD-карта + + Внешний общий накопитель + + %1$s свободно из %2$s + + Переместить карты? + + Ошибка перемещения файлов карт + + Это может занять несколько минут.\nПожалуйста, подождите… + + Единицы измерения + + Использовать километры или мили - - - Где поесть - - Продукты - - Транспорт - - Заправка - - Парковка - - Шоппинг - - Секонд-хенд - - Гостиница - - Достопримечательности - - Развлечения - - Банкомат - - Ночная жизнь - - Отдых с детьми - - Банк - - Аптека - - Больница - - Туалет - - Почта - - Полиция - - WiFi - - Переработка - - Вода - - Для автодомов + + + Где поесть + + Продукты + + Транспорт + + Заправка + + Парковка + + Шоппинг + + Секонд-хенд + + Гостиница + + Достопримечательности + + Развлечения + + Банкомат + + Ночная жизнь + + Отдых с детьми + + Банк + + Аптека + + Больница + + Туалет + + Почта + + Полиция + + WiFi + + Переработка + + Вода + + Для автодомов - - - Примечание - - С вами поделились метками Organic Maps - Здравствуйте!\n\nВ прикреплённом файле мои метки из офлайновых карт Organic Maps. Для того чтобы открыть этот файл, вам потребуется приложение Organic Maps, которое можно установить по ссылке: https://omaps.app/get?kmz\n\nСпасибо! - - Загрузка меток - - Метки успешно загружены! Вы можете увидеть их на карте или в ваших сохранённых метках. - - Файл с метками не был загружен. Возможно, файл повреждён или неисправен. - - Тип файла не поддерживается приложением:\n%1$s - - Не удалось открыть файл %1$s\n\n%2$s - - Редактировать - - Ваше местоположение ещё не определено - - Извините, настройки места хранения карт сейчас недоступны. - - Идет процесс загрузки карт. - - Смотри где я сейчас. Жми %1$s или %2$s - - Смотри мою метку на карте Organic Maps - - Посмотри на карте Organic Maps, где я сейчас нахожусь - - Привет!\n\nЯ сейчас здесь: %1$s. Чтобы увидеть это место на карте Organic Maps, открой эту ссылку %2$s или эту %3$s\n\nСпасибо. - - Поделиться - - Email - - Скопировано в буфер обмена: %s - - Готово - - Данные OpenStreetMap: %s - - Вы уверены, что хотите продолжить? - - Треки - - Длина - Поделиться местоположением - - Общие настройки - - Информация - Навигация - Кнопки масштаба - Показать на карте - - Ночной режим - - Выключен - - Включен - - Автоматически - - Перспективный вид - - 3D здания - - 3D-здания отключаются в режиме энергосбережения - - Голосовые инструкции - - Проговаривать названия улиц - - При включении название улицы или съезда, на который нужно повернуть, будет произноситься вслух. - - Язык подсказок - - Проверить голосовые подсказки (TTS, Text-To-Speech). - - Проверьте громкость или системные настройки преобразования текста в речь, если сейчас ничего не слышно. - - Не доступны - Автозум - Выключено - 1 час - 2 часа - 6 часов - 12 часов - 1 сутки - Расстояние - Посмотреть на карте - - Меню - - Вебсайт - - Новости - - Связаться с нами - - Оценить приложение - - Справка - - Вопросы и ответы - - Поддержать деньгами - - Помочь проекту - - Копирайт - - Сообщить о проблеме - - Улучшите направление стрелки, перемещая телефон восьмёркой, чтобы откалибровать компас. - - Двигайте телефон восьмёркой, чтобы откалибровать компас и зафиксировать направление стрелки на карте. - - Обновить все - - Отменить все - - Загруженные - - В очереди - Возле меня - - Карт - Загрузить все - Загружается: - - Чтобы удалить карту, пожалуйста, остановите навигацию. - - Маршрут может быть проложен только внутри карты одного региона. - - Загрузить карту - - Повторить - - Удалить карту - - Обновить карту - - Службы определения местоположения в Google Play - - Быстрое определение приблизительного местоположения с помощью Bluetooth, WiFi или мобильной сети - - Загрузите все карты по пути следования - - Для создания маршрута необходимо загрузить и обновить все карты на пути следования. - - Недостаточно места - - Пожалуйста, включите геолокацию - Сохранить - Ваше описание (текст или html) - создать - - Красный - - Жёлтый - - Синий - - Зелёный - - Пурпурный - - Оранжевый - - Коричневый - - Розовый - - Тёмно-пурпурный - - Голубой - - Сине-зелёный - - Изумрудный - - Лайм - - Тёмно-оранжевый - - Серый - - Серо-голубой + + + Примечание + + С вами поделились метками Organic Maps + Здравствуйте!\n\nВ прикреплённом файле мои метки из офлайновых карт Organic Maps. Для того чтобы открыть этот файл, вам потребуется приложение Organic Maps, которое можно установить по ссылке: https://omaps.app/get?kmz\n\nСпасибо! + + Загрузка меток + + Метки успешно загружены! Вы можете увидеть их на карте или в ваших сохранённых метках. + + Файл с метками не был загружен. Возможно, файл повреждён или неисправен. + + Тип файла не поддерживается приложением:\n%1$s + + Не удалось открыть файл %1$s\n\n%2$s + + Редактировать + + Ваше местоположение ещё не определено + + Извините, настройки места хранения карт сейчас недоступны. + + Идет процесс загрузки карт. + + Смотри где я сейчас. Жми %1$s или %2$s + + Смотри мою метку на карте Organic Maps + + Посмотри на карте Organic Maps, где я сейчас нахожусь + + Привет!\n\nЯ сейчас здесь: %1$s. Чтобы увидеть это место на карте Organic Maps, открой эту ссылку %2$s или эту %3$s\n\nСпасибо. + + Поделиться + + Email + + Скопировано в буфер обмена: %s + + Готово + + Данные OpenStreetMap: %s + + Вы уверены, что хотите продолжить? + + Треки + + Длина + Поделиться местоположением + + Общие настройки + + Информация + Навигация + Кнопки масштаба + Показать на карте + + Ночной режим + + Выключен + + Включен + + Автоматически + + Перспективный вид + + 3D здания + + 3D-здания отключаются в режиме энергосбережения + + Голосовые инструкции + + Проговаривать названия улиц + + При включении название улицы или съезда, на который нужно повернуть, будет произноситься вслух. + + Язык подсказок + + Проверить голосовые подсказки (TTS, Text-To-Speech). + + Проверьте громкость или системные настройки преобразования текста в речь, если сейчас ничего не слышно. + + Не доступны + Автозум + Выключено + 1 час + 2 часа + 6 часов + 12 часов + 1 сутки + Расстояние + Посмотреть на карте + + Меню + + Вебсайт + + Новости + + Связаться с нами + + Оценить приложение + + Справка + + Вопросы и ответы + + Поддержать деньгами + + Помочь проекту + + Копирайт + + Сообщить о проблеме + + Улучшите направление стрелки, перемещая телефон восьмёркой, чтобы откалибровать компас. + + Двигайте телефон восьмёркой, чтобы откалибровать компас и зафиксировать направление стрелки на карте. + + Обновить все + + Отменить все + + Загруженные + + В очереди + Возле меня + + Карт + Загрузить все + Загружается: + + Чтобы удалить карту, пожалуйста, остановите навигацию. + + Маршрут может быть проложен только внутри карты одного региона. + + Загрузить карту + + Повторить + + Удалить карту + + Обновить карту + + Службы определения местоположения в Google Play + + Быстрое определение приблизительного местоположения с помощью Bluetooth, WiFi или мобильной сети + + Загрузите все карты по пути следования + + Для создания маршрута необходимо загрузить и обновить все карты на пути следования. + + Недостаточно места + + Пожалуйста, включите геолокацию + Сохранить + Ваше описание (текст или html) + создать + + Красный + + Жёлтый + + Синий + + Зелёный + + Пурпурный + + Оранжевый + + Коричневый + + Розовый + + Тёмно-пурпурный + + Голубой + + Сине-зелёный + + Изумрудный + + Лайм + + Тёмно-оранжевый + + Серый + + Серо-голубой - - При движении по маршруту помните: - — Дорожная обстановка, ПДД и знаки приоритетнее советов приложения; - — Карта может быть неточной, а предложенный маршрут не всегда оптимален; - — Предлагаемые маршруты — лишь рекомендации; - — Будьте внимательны с маршрутами в приграничных зонах: в построенных программой маршрутах иногда возможны пересечения границ в неположенных местах; - Будьте внимательны на дорогах и берегите себя! - Проверьте сигнал GPS - Маршрут не построен. Текущая геопозиция не определена. - Пожалуйста, проверьте сигнал GPS. Для улучшения точности геопозиции включите Wi-Fi. - Включите режим определения геопозиции - Текущая геопозиция не определена. Для построения маршрута включите режим определения геопозиции. - Маршрут не найден - Не получилось построить маршрут. - Пожалуйста, измените начальную или конечную точку маршрута. - Измените начальную точку маршрута - Маршрут не построен. Не определена начальная точка маршрута. - Пожалуйста, выберите начальную точку маршрута ближе к дороге. - Измените конечную точку маршрута - Маршрут не построен. Не определена конечная точка маршрута. - Пожалуйста, выберите конечную точку маршрута ближе к дороге. - Не определена промежуточная точка маршрута. - Пожалуйста, измените промежуточную точку маршрута. - Системная ошибка - Не удалось проложить маршрут из-за ошибки приложения. - Попробуйте снова - Загрузить карту и построить более оптимальный маршрут с пересечением границы карты? - Для построения более оптимального маршрута с пересечением границы требуется загрузить карту. + + При движении по маршруту помните: + — Дорожная обстановка, ПДД и знаки приоритетнее советов приложения; + — Карта может быть неточной, а предложенный маршрут не всегда оптимален; + — Предлагаемые маршруты — лишь рекомендации; + — Будьте внимательны с маршрутами в приграничных зонах: в построенных программой маршрутах иногда возможны пересечения границ в неположенных местах; + Будьте внимательны на дорогах и берегите себя! + Проверьте сигнал GPS + Маршрут не построен. Текущая геопозиция не определена. + Пожалуйста, проверьте сигнал GPS. Для улучшения точности геопозиции включите Wi-Fi. + Включите режим определения геопозиции + Текущая геопозиция не определена. Для построения маршрута включите режим определения геопозиции. + Маршрут не найден + Не получилось построить маршрут. + Пожалуйста, измените начальную или конечную точку маршрута. + Измените начальную точку маршрута + Маршрут не построен. Не определена начальная точка маршрута. + Пожалуйста, выберите начальную точку маршрута ближе к дороге. + Измените конечную точку маршрута + Маршрут не построен. Не определена конечная точка маршрута. + Пожалуйста, выберите конечную точку маршрута ближе к дороге. + Не определена промежуточная точка маршрута. + Пожалуйста, измените промежуточную точку маршрута. + Системная ошибка + Не удалось проложить маршрут из-за ошибки приложения. + Попробуйте снова + Загрузить карту и построить более оптимальный маршрут с пересечением границы карты? + Для построения более оптимального маршрута с пересечением границы требуется загрузить карту. - - Для поиска мест и построения маршрутов скачайте карту, и интернет вам больше не понадобится. - Выбрать карту - - Показать - - Скрыть - Категории - История - К сожалению, мы ничего не нашли. - Попробуйте изменить условия поиска. - История поиска - Быстрый доступ к последним поисковым запросам. - Очистить историю поиска - - Википедия - Ваше местоположение - Начать - Отсюда - Сюда - Навигация возможна только из текущего местоположения. - Хотите перестроить маршрут от вашего местоположения? - - Далее - - С - - До - Добавить расписание - Удалить расписание - - Весь день (24 часа) - Открыто - Закрыто - Добавить перерыв - Время работы - Расширенный режим - Простой режим - Перерыв - Примеры значений - Исправьте ошибку - Местоположение - Пожалуйста, напишите подробно о проблеме, чтобы сообщество OpenStreetMap исправило ошибку. - Или сделайте это самостоятельно на сайте https://www.openstreetmap.org/ - Отправить - Проблема - Места не существует - Закрыто на ремонт - Повторяющееся место - Автоматическая загрузка - - Ежедневно - Круглосуточно - Сегодня закрыто - Закрыто - Сегодня - Открывается через %s - Закроется через %s - Закрыто - Редактировать время работы - Не зарегистрированы в OpenStreetMap? - Зарегистрироваться - Войти - Войти в OpenStreetMap - Пароль - Забыли пароль? - Выйти - Редактировать место - Добавить язык - Улица - - Номер дома - Подробнее - - Добавить улицу - - Введите название улицы - Выбрать язык - Выбрать улицу - Почтовый индекс - Кухня - Выбрать кухню - - Эл. почта или имя пользователя - Добавить телефон - Этаж - Вместе с картой удалятся и внесённые вами правки на этой карте. - Обновите карты - Для построения маршрутов необходимо обновить все карты и построить маршрут заново. - Найти карту - Проверьте настройки и убедитесь, что устройство подключено к интернету. - Недостаточно места - Удалите ненужные данные - Произошла ошибка при авторизации. - Учтённые правки - Потяните карту, чтобы выбрать правильное местоположение объекта. - Редактирование - Добавление - Название места - - На местном языке - Категория - Подробное описание проблемы - Другая проблема - Добавить организацию - Объект не может находиться в этом месте - - Созданные сообществом данные OpenStreetMap по состоянию на %s. Узнайте больше о том, как редактировать и обновлять карту, на сайте OpenStreetMap.org. - Авторизуйтесь в OpenStreetMap.org, чтобы ваши изменения увидели другие пользователи. - - %1$d из %2$d - Загрузить через сотовую связь? - На некоторых тарифных планах или в роуминге это может привести к значительным расходам. - Введите корректный номер дома - Количество этажей (максимум %d) - - Количество этажей не должно превышать %d - Почтовый индекс - Введите корректный почтовый индекс - - Неизвестное место - Отправить заметку редакторам OSM - Подробный комментарий - Предложенные вами изменения на карте будут отправлены в OpenStreetMap. Опишите дополнительные сведения об объекте, которые Organic Maps не позволяет отредактировать. - Подробнее об OpenStreetMap - Владелец - Нет подходящей категории? - Organic Maps позволяет добавлять на карту только простые типы объектов, то есть никаких городов, дорог, озер, контуров зданий. Пожалуйста, добавляйте такие категории на сайте OpenStreetMap.org. Также рекомендуем ознакомиться с нашими подробными пошаговыми инструкциями и другими приложениями для редактирования карты. - У вас нет загруженных карт - Загрузите необходимые карты, чтобы находить места и пользоваться навигацией без интернета. - - м - - км - - км/ч - - ми - - фут - ми/ч - ч - мин - Ещё - - Фотографии, отзывы, бронирование - - Реферальный бонус, полученный за каждое бронирование по этой ссылке, идёт на разработку Organic Maps. - - Подробнее на Kayak - Редактировать метку - Коментарий… - Сбросить все локальные правки? - Сбросить - Удалить добавленный вами объект? - Удалить - Места не существует - - Пожалуйста, укажите причину удаления - - Введите корректный номер телефона - Введите корректный веб-адрес - Введите корректный email - Введите корректный веб-адрес Facebook страницы или имя пользователя - Введите корректный веб-адрес Instagram страницы или имя пользователя - Введите корректный веб-адрес Twitter страницы или имя пользователя - Введите корректный веб-адрес VK страницы или имя пользователя - Введите корректный веб-адрес LINE страницы или LINE ID - Добавить место в OpenStreetMap - - Отправить всем пользователям? - - Убедитесь, что вы не ввели личные данные. - Редакторы OpenStreetMap проверят изменения и свяжутся с вами, если у них возникнут вопросы. - Cтоп - - Принять - - Отклонить - Загружать дополнительную информацию через мобильный интернет? - Всегда - Только сегодня - Не сегодня - Мобильный интернет - - Мобильный интернет требуется для уведомлений об обновлении карты и для отображения более подробной информации о местах и метках. - Никогда не использовать - Всегда спрашивать - Для отображения пробок необходимо обновить карты. - Увеличить шрифт на карте - Обновите Organic Maps - - Данные о пробках недоступны - Включить запись логов - - Отправить отзыв - Подсказки озвучиваются системным синтезатором речи (TTS). На многих устройствах используется Google TTS, его можно загрузить или обновить в Google Play (https://play.google.com/store/apps/details?id=com.google.android.tts) - Для некоторых языков, возможно, необходимо установить дополнительный синтезатор речи (TTS) из магазина приложений (Google Play, Galaxy Store, App Gallery, FDroid).\nЧтобы настроить синтезатор речи, перейдите в Настройки → Язык и ввод → Синтез речи.\nЗдесь можно установить дополнительные языковые пакеты или выбрать синтезатор речи. - Более подробная информация — в этом руководстве. - Латинская транслитерация - Узнать больше - - Выход - Добавьте стартовую точку, чтобы построить маршрут - Добавьте конечную точку, чтобы построить маршрут - Удалить - Заехать - - Пожалуйста войдите в аккаунт OpenStreetMap.org, чтобы публиковать ваши изменения карты. Подробности по ссылке. - Проблема с доступом к хранилищу - Внешняя память устройства недоступна, возможно SD карта была удалена, повреждена или файловая система доступна только для чтения. Проверьте это и свяжитесь, пожалуйста, с нами support\@organicmaps.app - Эмуляция ошибки с внешней памятью - Вход - Пожалуйста, введите название правильно - Списки - - Спрятать все - Показать все - Создать новый список - - Импортировать метки и треки - Не удалось поделиться из-за ошибки приложения - Ошибка при попытке поделиться - Нельзя делиться пустыми списками - Имя списка не может быть пустым - Введите имя списка, пожалуйста - Новый список - Такое имя уже занято - Выберите, пожалуйста, другое имя - Пожалуйста, подождите… - Номер телефона - Профиль OpenStreetMap - - %d файла были найдены. Вы увидите их после конвертации. - %d файл был найден. Вы увидите его после конвертации. - %d файлов было найдено. Вы увидите их после конвертации. - - - %d объекта - %d объект - %d объектов - - - %d места - %d место - %d мест - - - %d трека - %d трек - %d треков - - - Приватность - Политика конфиденциальности - Условия использования - Пробки - Метро - Стили и слои карты - Карта метро недоступна - Список пустой - Чтобы добавить метку, нажмите на место на карте, а затем на иконку звёздочки - …ещё - Экспорт KMZ - Экспорт GPX - Удалить список - Публичный доступ - Ограниченный доступ - Добавьте описание (текст или html) - Личный - Камеры скорости - Описание места - - Загрузка карт - - Предупреждать при превышении скорости - - Всегда предупреждать - - Никогда не предупреждать - Режим энергосбережения - Если режим энергосбережения включён, приложение будет отключать энергозатратные функции в зависимости от текущего заряда телефона - Никогда - Авто - Максимальное энергосбережение - Данная настройка включается для записи действий в целях диагностики, чтобы помочь нашей команде выявить проблемы с приложением. Временно включайте эту настройку только для отправки детальной информации о найденной вами проблеме в приложении через кнопку \"Сообщить о проблеме\". - Редактируется онлайн - Настройки объезда - - Избегать платных дорог - - Избегать грунтовые дороги - - Избегать паромы - Избегать автомагистрали - Невозможно построить маршрут - К сожалению, мы не смогли построить маршрут с выбранными опциями. Измените настройки и повторите попытку - Настроить пути объезда - Настройки объезда включены - Платная дорога - Грунтовая дорога - Паромная переправа - - Да - - Нет - - Есть - - Нет - - Вместимость: %s - Вы прибыли! - Ок - - Сортировать… - - Сортировать метки - - По умолчанию - - По типу - - По расстоянию - - По дате - - По имени - Неделю назад - Месяц назад - Больше месяца назад - Больше года назад - Рядом со мной - Другие + + Для поиска мест и построения маршрутов скачайте карту, и интернет вам больше не понадобится. + Выбрать карту + + Показать + + Скрыть + Категории + История + К сожалению, мы ничего не нашли. + Попробуйте изменить условия поиска. + История поиска + Быстрый доступ к последним поисковым запросам. + Очистить историю поиска + + Википедия + Ваше местоположение + Начать + Отсюда + Сюда + Навигация возможна только из текущего местоположения. + Хотите перестроить маршрут от вашего местоположения? + + Далее + + С + + До + Добавить расписание + Удалить расписание + + Весь день (24 часа) + Открыто + Закрыто + Добавить перерыв + Время работы + Расширенный режим + Простой режим + Перерыв + Примеры значений + Исправьте ошибку + Местоположение + Пожалуйста, напишите подробно о проблеме, чтобы сообщество OpenStreetMap исправило ошибку. + Или сделайте это самостоятельно на сайте https://www.openstreetmap.org/ + Отправить + Проблема + Места не существует + Закрыто на ремонт + Повторяющееся место + Автоматическая загрузка + + Ежедневно + Круглосуточно + Сегодня закрыто + Закрыто + Сегодня + Открывается через %s + Закроется через %s + Закрыто + Редактировать время работы + Не зарегистрированы в OpenStreetMap? + Зарегистрироваться + Войти + Войти в OpenStreetMap + Пароль + Забыли пароль? + Выйти + Редактировать место + Добавить язык + Улица + + Номер дома + Подробнее + + Добавить улицу + + Введите название улицы + Выбрать язык + Выбрать улицу + Почтовый индекс + Кухня + Выбрать кухню + + Эл. почта или имя пользователя + Добавить телефон + Этаж + Вместе с картой удалятся и внесённые вами правки на этой карте. + Обновите карты + Для построения маршрутов необходимо обновить все карты и построить маршрут заново. + Найти карту + Проверьте настройки и убедитесь, что устройство подключено к интернету. + Недостаточно места + Удалите ненужные данные + Произошла ошибка при авторизации. + Учтённые правки + Потяните карту, чтобы выбрать правильное местоположение объекта. + Редактирование + Добавление + Название места + + На местном языке + Категория + Подробное описание проблемы + Другая проблема + Добавить организацию + Объект не может находиться в этом месте + + Созданные сообществом данные OpenStreetMap по состоянию на %s. Узнайте больше о том, как редактировать и обновлять карту, на сайте OpenStreetMap.org. + Авторизуйтесь в OpenStreetMap.org, чтобы ваши изменения увидели другие пользователи. + + %1$d из %2$d + Загрузить через сотовую связь? + На некоторых тарифных планах или в роуминге это может привести к значительным расходам. + Введите корректный номер дома + Количество этажей (максимум %d) + + Количество этажей не должно превышать %d + Почтовый индекс + Введите корректный почтовый индекс + + Неизвестное место + Отправить заметку редакторам OSM + Подробный комментарий + Предложенные вами изменения на карте будут отправлены в OpenStreetMap. Опишите дополнительные сведения об объекте, которые Organic Maps не позволяет отредактировать. + Подробнее об OpenStreetMap + Владелец + Нет подходящей категории? + Organic Maps позволяет добавлять на карту только простые типы объектов, то есть никаких городов, дорог, озер, контуров зданий. Пожалуйста, добавляйте такие категории на сайте OpenStreetMap.org. Также рекомендуем ознакомиться с нашими подробными пошаговыми инструкциями и другими приложениями для редактирования карты. + У вас нет загруженных карт + Загрузите необходимые карты, чтобы находить места и пользоваться навигацией без интернета. + + м + + км + + км/ч + + ми + + фут + ми/ч + ч + мин + Ещё + + Фотографии, отзывы, бронирование + + Реферальный бонус, полученный за каждое бронирование по этой ссылке, идёт на разработку Organic Maps. + + Подробнее на Kayak + Редактировать метку + Коментарий… + Сбросить все локальные правки? + Сбросить + Удалить добавленный вами объект? + Удалить + Места не существует + + Пожалуйста, укажите причину удаления + + Введите корректный номер телефона + Введите корректный веб-адрес + Введите корректный email + Введите корректный веб-адрес Facebook страницы или имя пользователя + Введите корректный веб-адрес Instagram страницы или имя пользователя + Введите корректный веб-адрес Twitter страницы или имя пользователя + Введите корректный веб-адрес VK страницы или имя пользователя + Введите корректный веб-адрес LINE страницы или LINE ID + Добавить место в OpenStreetMap + + Отправить всем пользователям? + + Убедитесь, что вы не ввели личные данные. + Редакторы OpenStreetMap проверят изменения и свяжутся с вами, если у них возникнут вопросы. + Cтоп + + Принять + + Отклонить + Загружать дополнительную информацию через мобильный интернет? + Всегда + Только сегодня + Не сегодня + Мобильный интернет + + Мобильный интернет требуется для уведомлений об обновлении карты и для отображения более подробной информации о местах и метках. + Никогда не использовать + Всегда спрашивать + Для отображения пробок необходимо обновить карты. + Увеличить шрифт на карте + Обновите Organic Maps + + Данные о пробках недоступны + Включить запись логов + + Отправить отзыв + Подсказки озвучиваются системным синтезатором речи (TTS). На многих устройствах используется Google TTS, его можно загрузить или обновить в Google Play (https://play.google.com/store/apps/details?id=com.google.android.tts) + Для некоторых языков, возможно, необходимо установить дополнительный синтезатор речи (TTS) из магазина приложений (Google Play, Galaxy Store, App Gallery, FDroid).\nЧтобы настроить синтезатор речи, перейдите в Настройки → Язык и ввод → Синтез речи.\nЗдесь можно установить дополнительные языковые пакеты или выбрать синтезатор речи. + Более подробная информация — в этом руководстве. + Латинская транслитерация + Узнать больше + + Выход + Добавьте стартовую точку, чтобы построить маршрут + Добавьте конечную точку, чтобы построить маршрут + Удалить + Заехать + + Пожалуйста войдите в аккаунт OpenStreetMap.org, чтобы публиковать ваши изменения карты. Подробности по ссылке. + Проблема с доступом к хранилищу + Внешняя память устройства недоступна, возможно SD карта была удалена, повреждена или файловая система доступна только для чтения. Проверьте это и свяжитесь, пожалуйста, с нами support\@organicmaps.app + Эмуляция ошибки с внешней памятью + Вход + Пожалуйста, введите название правильно + Списки + + Спрятать все + Показать все + Создать новый список + + Импортировать метки и треки + Не удалось поделиться из-за ошибки приложения + Ошибка при попытке поделиться + Нельзя делиться пустыми списками + Имя списка не может быть пустым + Введите имя списка, пожалуйста + Новый список + Такое имя уже занято + Выберите, пожалуйста, другое имя + Пожалуйста, подождите… + Номер телефона + Профиль OpenStreetMap + + %d файла были найдены. Вы увидите их после конвертации. + %d файл был найден. Вы увидите его после конвертации. + %d файлов было найдено. Вы увидите их после конвертации. + + + %d объекта + %d объект + %d объектов + + + %d места + %d место + %d мест + + + %d трека + %d трек + %d треков + + + Приватность + Политика конфиденциальности + Условия использования + Пробки + Метро + Стили и слои карты + Карта метро недоступна + Список пустой + Чтобы добавить метку, нажмите на место на карте, а затем на иконку звёздочки + …ещё + Экспорт KMZ + Экспорт GPX + Удалить список + Публичный доступ + Ограниченный доступ + Добавьте описание (текст или html) + Личный + Камеры скорости + Описание места + + Загрузка карт + + Предупреждать при превышении скорости + + Всегда предупреждать + + Никогда не предупреждать + Режим энергосбережения + Если режим энергосбережения включён, приложение будет отключать энергозатратные функции в зависимости от текущего заряда телефона + Никогда + Авто + Максимальное энергосбережение + Данная настройка включается для записи действий в целях диагностики, чтобы помочь нашей команде выявить проблемы с приложением. Временно включайте эту настройку только для отправки детальной информации о найденной вами проблеме в приложении через кнопку \"Сообщить о проблеме\". + Редактируется онлайн + Настройки объезда + + Избегать платных дорог + + Избегать грунтовые дороги + + Избегать паромы + Избегать автомагистрали + Невозможно построить маршрут + К сожалению, мы не смогли построить маршрут с выбранными опциями. Измените настройки и повторите попытку + Настроить пути объезда + Настройки объезда включены + Платная дорога + Грунтовая дорога + Паромная переправа + + Да + + Нет + + Есть + + Нет + + Вместимость: %s + Вы прибыли! + Ок + + Сортировать… + + Сортировать метки + + По умолчанию + + По типу + + По расстоянию + + По дате + + По имени + Неделю назад + Месяц назад + Больше месяца назад + Больше года назад + Рядом со мной + Другие - - Еда - Достопримечательности - Музеи - Парки - Плавание - Горы - Животные - Отели - Здания - Деньги - Магазины - Парковки - Заправки - Медицина - Искать в списке - Святые места - Выбрать список - Навигация на метро ещё недоступна в данном регионе - Маршрут метро не найден - Выберите начальную или конечную точку маршрута ближе к станции метро - Высоты - Чтобы воспользоваться линиями высот, обновите или загрузите карту нужной местности - Линии высот пока не доступны в этом регионе - Подъём - Спуск - Мин. высота - Макс. высота - Сложность - Расст.: - В пути: - Увеличьте карту, чтобы увидеть изолинии - Загрузка - Скачать карту мира - - Не могу создать папку и переместить файлы на устройстве - - Ошибка диска - - Ошибка подключения - - Отсоедините USB кабель - Держи экран включённым - - Если эта функция включена, то при отображении карты экран будет всегда включен. - - Показывать на экране блокировки - - Если эта функция включена, вам не нужно каждый раз разблокировать устройство во время работы приложения. - - Картографические данные из OpenStreetMap - - https://t.me/OrganicMapsRu - - https://organicmaps.app/ru/ - - https://wiki.openstreetmap.org/wiki/RU:О_проекте - - Спасибо, что пользуетесь нашими картами, созданными сообществом! - - Благодаря вашим пожертвованиям и поддержке мы сможем создать лучшие карты на свете! - - Вам нравится наше приложение? Поддержите его развитие деньгами! Пока ещё не нравится? Пожалуйста, сообщите нам, почему, и мы это исправим! - - Если вы знаете толкового разработчика программного обеспечения, попросите его реализовать нужную вам функцию для приложения. - - Знаете ли вы, что любое место на карте можно выбрать, подержав там палец в течение секунды? - - А вы знали, что своё местоположение на карте можно выбрать? - - Вы можете помочь перевести наше приложение на ваш язык. - - Наше приложение разработано несколькими энтузиастами и сообществом. - - Вы можете легко исправить и улучшить данные карты. - - Наша главная цель — создать быстрые, конфиденциальные и простые в использовании карты, которые вам понравятся. - - Сейчас Вы используете Organic Maps на экране телефона - - Сейчас Вы используете Organic Maps на экране автомобиля - - Вы подключены к Android Auto - - Продолжить в телефоне - - Продолжить в авто - - Этому приложению необходим доступ к вашему местоположению для навигации. - - Разрешить - - Подключено к автомобилю - - Активный отдых - - Веб-браузер недоступен - Громкость - - Экспортировать все метки и треки - - Системные настройки синтеза речи - - Настройки синтеза речи не найдены, вы уверены, что ваше устройство поддерживает их? - С окошком для водителей - Очистите поиск - Приблизить - - Отдалить - - Ссылка на меню - - Посмотреть меню + + Еда + Достопримечательности + Музеи + Парки + Плавание + Горы + Животные + Отели + Здания + Деньги + Магазины + Парковки + Заправки + Медицина + Искать в списке + Святые места + Выбрать список + Навигация на метро ещё недоступна в данном регионе + Маршрут метро не найден + Выберите начальную или конечную точку маршрута ближе к станции метро + Высоты + Чтобы воспользоваться линиями высот, обновите или загрузите карту нужной местности + Линии высот пока не доступны в этом регионе + Подъём + Спуск + Мин. высота + Макс. высота + Сложность + Расст.: + В пути: + Увеличьте карту, чтобы увидеть изолинии + Загрузка + Скачать карту мира + + Не могу создать папку и переместить файлы на устройстве + + Ошибка диска + + Ошибка подключения + + Отсоедините USB кабель + Держи экран включённым + + Если эта функция включена, то при отображении карты экран будет всегда включен. + + Показывать на экране блокировки + + Если эта функция включена, вам не нужно каждый раз разблокировать устройство во время работы приложения. + + Картографические данные из OpenStreetMap + + https://t.me/OrganicMapsRu + + https://organicmaps.app/ru/ + + https://wiki.openstreetmap.org/wiki/RU:О_проекте + + Спасибо, что пользуетесь нашими картами, созданными сообществом! + + Благодаря вашим пожертвованиям и поддержке мы сможем создать лучшие карты на свете! + + Вам нравится наше приложение? Поддержите его развитие деньгами! Пока ещё не нравится? Пожалуйста, сообщите нам, почему, и мы это исправим! + + Если вы знаете толкового разработчика программного обеспечения, попросите его реализовать нужную вам функцию для приложения. + + Знаете ли вы, что любое место на карте можно выбрать, подержав там палец в течение секунды? + + А вы знали, что своё местоположение на карте можно выбрать? + + Вы можете помочь перевести наше приложение на ваш язык. + + Наше приложение разработано несколькими энтузиастами и сообществом. + + Вы можете легко исправить и улучшить данные карты. + + Наша главная цель — создать быстрые, конфиденциальные и простые в использовании карты, которые вам понравятся. + + Сейчас Вы используете Organic Maps на экране телефона + + Сейчас Вы используете Organic Maps на экране автомобиля + + Вы подключены к Android Auto + + Продолжить в телефоне + + Продолжить в авто + + Этому приложению необходим доступ к вашему местоположению для навигации. + + Разрешить + + Подключено к автомобилю + + Активный отдых + + Веб-браузер недоступен + Громкость + + Экспортировать все метки и треки + + Системные настройки синтеза речи + + Настройки синтеза речи не найдены, вы уверены, что ваше устройство поддерживает их? + С окошком для водителей + Очистите поиск + Приблизить + + Отдалить + + Ссылка на меню + + Посмотреть меню - - Адрес/Блок - Адрес/Блок - Адрес/Блок - Канатная дорога - Канатная дорога - Кресельная канатная дорога - Бугельная канатная дорога - Канатная дорога - Канатная дорога - Канатная дорога - Аэрокосмическая инфраструктура - Аэропорт - Международный аэропорт - Перрон - Выход на посадку - Вертолётная площадка - Взлётно-посадочная полоса - Рулёжная дорожка - Терминал - Объекты инфраструктуры - Центр искусств - Банкомат - Банк - Бар - Барбекю-гриль - Скамейка - Велопарковка - Велопрокат - Станция ремонта велосипедов - Пивная под открытым небом - Бордель - Обмен валюты - Автовокзал - Кафе - Прокат авто - Каршеринг - Автомойка - Казино - Азартные игры - Игровой центр для взрослых - Аркада - Зарядная станция - Станция зарядки велосипедов - Зарядная станция для автомобилей - Детская комната - Кинотеатр - Боулинг - Поликлиника - Колледж - Культурно-досуговый центр - Сжатый воздух - Конференц-центр - Суд - Стоматология - Врач - Питьевая вода - Питьевая вода - Автошкола - Выставочный центр - Денежные переводы - Музыкальная школа - Языковая школа - Посольство - Фастфуд - Паром - Пожарная часть - Ресторанный дворик - Фонтан - АЗС - - Кладбище - - Христианское кладбище - Больница - Охотничья вышка - Мороженое - Интернет-кафе - Детсад - Библиотека - Погрузочный док - Рынок - Мотопарковка - Ночной клуб - Дом престарелых - Парковка - Парковка - Многоэтажная парковка - Многоэтажная парковка - Частная парковка - Частная парковка - Частная парковка - Парковка - Подземный паркинг - Подземный паркинг - Частная подземная парковка - Придорожная парковка - Придорожная парковка - Частная придорожная парковка - Парковочная полоса - Парковочная полоса - Частная парковочная полоса - Въезд на парковку - Въезд на частную парковку - Въезд на парковку - Парковочное место - Парковочное место - Парковочное место - Парковочное место - Парковочное место для инвалидов - Терминал оплаты - Аптека - Храм - Храм - Церковь - Церковь Иисуса Христа Святых последних дней - Зала Царства Свидетелей Иеговы - Храм - Синагога - Мечеть - Святилище - Храм - Полиция - Почтовый ящик - Почта - Тюрьма - Паб - Книгообмен + + Адрес/Блок + Адрес/Блок + Адрес/Блок + Канатная дорога + Канатная дорога + Кресельная канатная дорога + Бугельная канатная дорога + Канатная дорога + Канатная дорога + Канатная дорога + Аэрокосмическая инфраструктура + Аэропорт + Международный аэропорт + Перрон + Выход на посадку + Вертолётная площадка + Взлётно-посадочная полоса + Рулёжная дорожка + Терминал + Объекты инфраструктуры + Центр искусств + Банкомат + Банк + Бар + Барбекю-гриль + Скамейка + Велопарковка + Велопрокат + Станция ремонта велосипедов + Пивная под открытым небом + Бордель + Обмен валюты + Автовокзал + Кафе + Прокат авто + Каршеринг + Автомойка + Казино + Азартные игры + Игровой центр для взрослых + Аркада + Зарядная станция + Станция зарядки велосипедов + Зарядная станция для автомобилей + Детская комната + Кинотеатр + Боулинг + Поликлиника + Колледж + Культурно-досуговый центр + Сжатый воздух + Конференц-центр + Суд + Стоматология + Врач + Питьевая вода + Питьевая вода + Автошкола + Выставочный центр + Денежные переводы + Музыкальная школа + Языковая школа + Посольство + Фастфуд + Паром + Пожарная часть + Ресторанный дворик + Фонтан + АЗС + + Кладбище + + Христианское кладбище + Больница + Охотничья вышка + Мороженое + Интернет-кафе + Детсад + Библиотека + Погрузочный док + Рынок + Мотопарковка + Ночной клуб + Дом престарелых + Парковка + Парковка + Многоэтажная парковка + Многоэтажная парковка + Частная парковка + Частная парковка + Частная парковка + Парковка + Подземный паркинг + Подземный паркинг + Частная подземная парковка + Придорожная парковка + Придорожная парковка + Частная придорожная парковка + Парковочная полоса + Парковочная полоса + Частная парковочная полоса + Въезд на парковку + Въезд на частную парковку + Въезд на парковку + Парковочное место + Парковочное место + Парковочное место + Парковочное место + Парковочное место для инвалидов + Терминал оплаты + Аптека + Храм + Храм + Церковь + Церковь Иисуса Христа Святых последних дней + Зала Царства Свидетелей Иеговы + Храм + Синагога + Мечеть + Святилище + Храм + Полиция + Почтовый ящик + Почта + Тюрьма + Паб + Книгообмен - - Приём вторсырья - Контейнер для вторсырья - Контейнер для вторсырья - Батарейки - Одежда - Стеклотара - Бумага - Пластик - Пластиковые бутылки - Металлолом - Электроотходы - Картон - Жестяные и алюминиевые банки - Обувь - Органика / Пищевые отходы - Тетрапак и аналоги - Ресторан - Слив для туалетов транспортных средств - Школа - - Навес - - Навес - - Хижина для ночлега - - Бивачный навес - Общественная баня - Душ - Стриптиз-клуб - Такси - Телефон - Театр - Туалет - Туалет - Администрация - Университет - Торговый автомат - Автомат с сигаретами - Кофейный автомат - Автомат с презервативами - Автомат с напитками - Автомат с едой - Газетный автомат - Паркомат - Автомат по продаже билетов - Автомат со сладостями - Пакеты для экскрементов - Почтомат - Техосмотр автомобиля - Топливная колонка - Ветеринарная клиника - Урна - Мусорный контейнер - Станция перевалки отходов - Вода для автодомов - Преграда - Блок - Столбик - Погранконтроль - Цепь - Городская стена - Велосипедный барьер - Дренажная канава - Оборонительный ров - Сточные воды - Проход - Забор - Ворота - Живая изгородь - Ворота - Шлагбаум - Поддерживающая стена - Перелаз - Турникет - Шлагбаум - Пункт оплаты - Стена - Граница - Административная граница - - Граница страны - - Граница региона - - Граница региона - Национальный парк - Земли коренных народов - Заповедная зона - Заповедная зона - Заповедная зона - Заповедная зона - Заповедная зона - Заповедная зона - Заповедная зона - Здание - - Адрес - Здание - Здание - Гараж - Ж/д вокзал - Склад - Могила - Мастерская - Пчеловод - Кузница - Крафтовая пивоварня - Кейтеринг - Столяр - Кондитер - Электрик - Ремонт электроники - Садовник - Мельница - Ремесленная мастерская - - Отопление, вентиляция и кондиционирование - Изготовление ключей - Слесарь - Металлоконструкции - Маляр - Фотограф - Магазин фотоаппаратов - Сантехник - Лесопилка - Ремонт обуви - Винодельня - Ателье - Африканская кухня - Американская кухня - Арабская кухня - Аргентинская кухня - Азиатская кухня - Австрийская кухня - Бeйглы - Балканская кухня - Барбекю - Баварская кухня - Говядина - Бразильская кухня - Завтраки - Чай с шариками - Бургеры - Домашняя таверна (бушеншанк) - Кондитерская - Карибская кухня - Блюда из курицы - Китайская кухня - Кофе - Тонкие блинчики - Хорватская кухня - Блюда карри - Деликатесы - Дайнер - Пончики - Эфиопская кухня - Филиппинская кухня - Ресторан высокой кухни - Рыбный ресторан - Рыба и картофель фри - Французская кухня - Блюда во фритюре - Грузинская кухня - Немецкая кухня - Греческая кухня - Гриль - Винная таверна (хойригер) - Хот-доги - Венгерская кухня - Кафе-мороженое - Индийская кухня - Индонезийская кухня - Международная кухня - Ирландская кухня - Итальянская кухня - Итальянская, пиццерия - Японская кухня - Кебабы - Корейская - Лаосская кухня - Ливанская кухня - Местная кухня - Мадагаскарская кухня - Малазийская кухня - Средиземноморская кухня - Мексиканская кухня - Марокканская кухня - Лапшичная - Восточная кухня - Блинная - Паста - Иранская кухня - Перуанская кухня - Пиццерия - Польская кухня - Португальская кухня - Рамен - Региональная кухня - Русская кухня - Сэндвичи - Сосисочная - Несладкие блинчики - Морепродукты - Соба - Испанская кухня - Стейк-хаус - Суши - Тапас-бар - Чайная - Тайская кухня - Турецкая кухня - Веганская кухня - Вегетарианская кухня - Вьетнамская кухня - Экстренная служба - Пункт аварийного сбора - Дефибриллятор - Пожарный гидрант - Телефон для экстренных вызовов - - Вход - - Главный вход - Выход - Бесплатно - Медицинская лаборатория - Физиотерапевт - Альтернативная медицина - Аудиология - Центр донорства крови - Оптометрия - Подиатрия - Психотерапия - Сбор анализов - Логопедия + + Приём вторсырья + Контейнер для вторсырья + Контейнер для вторсырья + Батарейки + Одежда + Стеклотара + Бумага + Пластик + Пластиковые бутылки + Металлолом + Электроотходы + Картон + Жестяные и алюминиевые банки + Обувь + Органика / Пищевые отходы + Тетрапак и аналоги + Ресторан + Слив для туалетов транспортных средств + Школа + + Навес + + Навес + + Хижина для ночлега + + Бивачный навес + Общественная баня + Душ + Стриптиз-клуб + Такси + Телефон + Театр + Туалет + Туалет + Администрация + Университет + Торговый автомат + Автомат с сигаретами + Кофейный автомат + Автомат с презервативами + Автомат с напитками + Автомат с едой + Газетный автомат + Паркомат + Автомат по продаже билетов + Автомат со сладостями + Пакеты для экскрементов + Почтомат + Техосмотр автомобиля + Топливная колонка + Ветеринарная клиника + Урна + Мусорный контейнер + Станция перевалки отходов + Вода для автодомов + Преграда + Блок + Столбик + Погранконтроль + Цепь + Городская стена + Велосипедный барьер + Дренажная канава + Оборонительный ров + Сточные воды + Проход + Забор + Ворота + Живая изгородь + Ворота + Шлагбаум + Поддерживающая стена + Перелаз + Турникет + Шлагбаум + Пункт оплаты + Стена + Граница + Административная граница + + Граница страны + + Граница региона + + Граница региона + Национальный парк + Земли коренных народов + Заповедная зона + Заповедная зона + Заповедная зона + Заповедная зона + Заповедная зона + Заповедная зона + Заповедная зона + Здание + + Адрес + Здание + Здание + Гараж + Ж/д вокзал + Склад + Могила + Мастерская + Пчеловод + Кузница + Крафтовая пивоварня + Кейтеринг + Столяр + Кондитер + Электрик + Ремонт электроники + Садовник + Мельница + Ремесленная мастерская + + Отопление, вентиляция и кондиционирование + Изготовление ключей + Слесарь + Металлоконструкции + Маляр + Фотограф + Магазин фотоаппаратов + Сантехник + Лесопилка + Ремонт обуви + Винодельня + Ателье + Африканская кухня + Американская кухня + Арабская кухня + Аргентинская кухня + Азиатская кухня + Австрийская кухня + Бeйглы + Балканская кухня + Барбекю + Баварская кухня + Говядина + Бразильская кухня + Завтраки + Чай с шариками + Бургеры + Домашняя таверна (бушеншанк) + Кондитерская + Карибская кухня + Блюда из курицы + Китайская кухня + Кофе + Тонкие блинчики + Хорватская кухня + Блюда карри + Деликатесы + Дайнер + Пончики + Эфиопская кухня + Филиппинская кухня + Ресторан высокой кухни + Рыбный ресторан + Рыба и картофель фри + Французская кухня + Блюда во фритюре + Грузинская кухня + Немецкая кухня + Греческая кухня + Гриль + Винная таверна (хойригер) + Хот-доги + Венгерская кухня + Кафе-мороженое + Индийская кухня + Индонезийская кухня + Международная кухня + Ирландская кухня + Итальянская кухня + Итальянская, пиццерия + Японская кухня + Кебабы + Корейская + Лаосская кухня + Ливанская кухня + Местная кухня + Мадагаскарская кухня + Малазийская кухня + Средиземноморская кухня + Мексиканская кухня + Марокканская кухня + Лапшичная + Восточная кухня + Блинная + Паста + Иранская кухня + Перуанская кухня + Пиццерия + Польская кухня + Португальская кухня + Рамен + Региональная кухня + Русская кухня + Сэндвичи + Сосисочная + Несладкие блинчики + Морепродукты + Соба + Испанская кухня + Стейк-хаус + Суши + Тапас-бар + Чайная + Тайская кухня + Турецкая кухня + Веганская кухня + Вегетарианская кухня + Вьетнамская кухня + Экстренная служба + Пункт аварийного сбора + Дефибриллятор + Пожарный гидрант + Телефон для экстренных вызовов + + Вход + + Главный вход + Выход + Бесплатно + Медицинская лаборатория + Физиотерапевт + Альтернативная медицина + Аудиология + Центр донорства крови + Оптометрия + Подиатрия + Психотерапия + Сбор анализов + Логопедия - - Дорога - Конная дорожка - - Мост - Конная дорожка - - Тоннель - Выделенная автобусная дорога - - Мост - - Тоннель - Остановка - Строящаяся дорога - Велодорожка - - Мост - Велодорожка - - Велотоннель - Лифт - Пешеходная дорожка - Тротуар - Пешеходный переход - Пешеходная зона - - Пешеходный мост - - Пешеходный тоннель - Брод - Жилая зона - - Мост - - Тоннель - Автомагистраль - - Автомобильный мост - - Автомобильный тоннель - Съезд - Съезд с автомагистрали - - Мост - - Тоннель - Тропа - - Сложная или плохо видимая тропа - - Очень сложная или неразличимая тропа - Велопешеходная дорожка - Велопешеходная дорожка - - Мост - Конная тропа - - Тоннель - Пешеходная улица - Пешеходная зона - - Пешеходный мост - - Пешеходный тоннель - Шоссе - - Мост - - Тоннель - Съезд с шоссе - - Мост - - Тоннель - Гоночный трек - Улица - Улица - - Мост - - Тоннель - Зона отдыха - Дорога - - Мост - - Мост - - Тоннель - Автодорога - - Мост - - Тоннель - Съезд с автодороги - - Мост - - Тоннель - Проезд - Проезд - - Мост - Подъезд - Парковочный проезд - - Тоннель - Зона обслуживания - Камера скорости - Лестница - - Мост - - Тоннель - Дорога - - Мост - - Тоннель - Съезд с дороги - - Мост - - Тоннель - Грунтовка - Грунтовка - - Мост - Грунтовка - Грунтовка - - Тоннель - Светофор - Трасса - - Мост - - Тоннель - Съезд с трассы - - Мост - - Тоннель - Небольшая дорога - Небольшая дорога - - Мост - - Тоннель - Велодорожка - Пешеходная дорожка - Жилая зона - Автомагистраль - Тропа - Пешеходная улица - Шоссе - Улица - Автодорога - Проезд - Дорога - Лестница - Грунтовка - Трасса - Небольшая дорога + + Дорога + Конная дорожка + + Мост + Конная дорожка + + Тоннель + Выделенная автобусная дорога + + Мост + + Тоннель + Остановка + Строящаяся дорога + Велодорожка + + Мост + Велодорожка + + Велотоннель + Лифт + Пешеходная дорожка + Тротуар + Пешеходный переход + Пешеходная зона + + Пешеходный мост + + Пешеходный тоннель + Брод + Жилая зона + + Мост + + Тоннель + Автомагистраль + + Автомобильный мост + + Автомобильный тоннель + Съезд + Съезд с автомагистрали + + Мост + + Тоннель + Тропа + + Сложная или плохо видимая тропа + + Очень сложная или неразличимая тропа + Велопешеходная дорожка + Велопешеходная дорожка + + Мост + Конная тропа + + Тоннель + Пешеходная улица + Пешеходная зона + + Пешеходный мост + + Пешеходный тоннель + Шоссе + + Мост + + Тоннель + Съезд с шоссе + + Мост + + Тоннель + Гоночный трек + Улица + Улица + + Мост + + Тоннель + Зона отдыха + Дорога + + Мост + + Мост + + Тоннель + Автодорога + + Мост + + Тоннель + Съезд с автодороги + + Мост + + Тоннель + Проезд + Проезд + + Мост + Подъезд + Парковочный проезд + + Тоннель + Зона обслуживания + Камера скорости + Лестница + + Мост + + Тоннель + Дорога + + Мост + + Тоннель + Съезд с дороги + + Мост + + Тоннель + Грунтовка + Грунтовка + + Мост + Грунтовка + Грунтовка + + Тоннель + Светофор + Трасса + + Мост + + Тоннель + Съезд с трассы + + Мост + + Тоннель + Небольшая дорога + Небольшая дорога + + Мост + + Тоннель + Велодорожка + Пешеходная дорожка + Жилая зона + Автомагистраль + Тропа + Пешеходная улица + Шоссе + Улица + Автодорога + Проезд + Дорога + Лестница + Грунтовка + Трасса + Небольшая дорога - - Исторический объект - Исторический самолет - Исторический якорь - Археологический памятник - Поле боя - Пограничный камень - Пушка - Замок - Каструм - Замок - Укреплённая церковь - Крепость - Городище - Кремль - Усадьба - Дворец - Замки Японии - Шато - Городские ворота - Городская стена - Форт - Виселица - Исторический локомотив - Мемориал - Памятный крест - Памятная доска - Скульптура - Статуя - Камни преткновения - Исторический камень - Военный мемориал - Историческая шахта - Памятник - Позорный столб - Руины - Корабль - Исторический танк - Гробница - Христианский крест - Святыня - Кораблекрушение - Интернет - Интернет - Перекрёсток - Кольцо - Кольцо - Землепользование - Земельные участки - Резервуар - Земля для застройки - - Кладбище - - Христианское кладбище - Церковный двор - Коммерческая застройка - Стройка - Образовательные учреждения - Сельскохозяйственная земля - Сельскохозяйственная земля - Поле - Клумба - Лес - Хвойный лес - Лиственный лес - Смешанный лес - Гаражи - Газон - Земля для застройки - Теплицы - Промзона - Свалка - Луг - Военная зона - Сад - Карьер - Железнодорожные сооружения - База отдыха - Водоём - Жилая зона - Зона торговли - Соляной пруд - Парк - Виноградник - Место для отдыха - Общественная земля - Место для выгула собак - Фитнес-клуб - Спортивные снаряды - Танцпол - Сад - Частный сад - Площадка для гольфа - Минигольф - Хакерспейс - Каток - Причал - Заповедник - Сидения на открытом воздухе - Парк - Парк - Парк - Парк - Стол для пикника - Спортплощадка - Детская площадка - Зона для отдыха - Сауна - Лодочный спуск - Спорткомплекс - Скалодром - Йога-центр - Стадион - Плавательный бассейн - Плавательный бассейн - Беговая дорожка - Беговая дорожка - Аквапарк - Пляжный курорт - Искусственное сооружение - Волнорез - Тур - Заводская труба - Просека - Геодезический пункт - Флагшток - Маяк - Мачта/вышка - Пирс - Трубопровод - Наземный трубопровод - Элеватор - Резервуар - Камера наблюдения - Башня - Вышка связи - Очистные сооружения - Водопроводный кран - Водонапорная башня - Колодец - Ветряная мельница - Промышленное производство - Военные объекты - Бункер - Перевал - Природа - - Каменная порода - - Галька - - Каменистая осыпь - Залив - Пляж - Песчаный пляж - Галечный пляж - Мыс - Пещера - Утёс - Обрыв - Насыпь - Береговая линия - Пустыня - Гейзер - Ледник - Луг - Пустошь - Горячий источник - Озеро - Шлюзовая камера - Пруд - Водохранилище - Резервуар - Река - Суша - Луг - Сад - Гора - Седловина - Камень - Заросли - Родник - Пролив - Ряд деревьев - Виноградник - Вулкан - Водоём - Болотистая местность - Торфяное болото - Болотистая местность - Тупик - Офис - Организация - Агентство недвижимости - Госучреждение - Страховая компания - Адвокат - Общественная организация - Телекоммуникационная компания - Эко - Эко - Город - Столица - Город - Город - Столица - Город - Город - Город - Город - Город - Город - Город - Континент - Страна - Округ - Ферма - Посёлок - Остров - Остров - Хутор - Местность - Микрорайон - Океан - Район - Море - Площадь - Штат - Штат - Район - Город - Деревня - Энергетика - Генератор - Солнечный генератор - Ветрогенератор - Газотурбинная электростанция - Гидроэлектростанция - Линия электропередач - Подземная линия электропередач - Линия электропередачи низкого напряжения - Электростанция - Угольная электростанция - Газотурбинная электростанция - Гидроэлектростанция - Солнечная электростанция - Ветряная электростанция - Электростанция - Подстанция - Опора ЛЭП - Поверхность - Плохая асфальтированная - Хорошая асфальтированная - Плохая неасфальтированная - Хорошая неасфальтированная - Общественный транспорт - Платформа - Ж/Д - Заброшенная железная дорога - Заброшенный железнодорожный мост - Заброшенный железнодорожный туннель - Строящаяся железная дорога - Пешеходный переход - Неиспользуемая железная дорога - Фуникулер - Фуникулер - Фуникулер - Ж/д станция - Железнодорожный переезд - Скоростной трамвай - Скоростной трамвай - Скоростной трамвай - Монорельсовая железная дорога - Монорельсовая железная дорога - Монорельсовая железная дорога - Узкоколейка - Узкоколейка - Узкоколейка - Железнодорожная платформа - Законсервированная Ж/Д - Законсервированная Ж/Д - Законсервированная Ж/Д - Железнодорожный путь - Высокоскоростная железная дорога - Туристическая железная дорога - Железная дорога - - Второстепенная железная дорога - - Служебная железная дорога - Подъездной Ж/Д путь - - Вспомогательный Ж/Д путь - Железнодорожный мост - Железнодорожный мост - Железнодорожный мост - Железнодорожный мост - Железнодорожный мост - Железнодорожный мост - Железнодорожный мост - Железнодорожный мост - Железнодорожный туннель - Железнодорожный туннель - Железнодорожный туннель - Железнодорожный туннель - Железнодорожный туннель - Железнодорожный туннель - Железнодорожный туннель - Железнодорожный туннель - Ж/д станция - Фуникулер - Ж/д станция - Ж/д станция - Ж/д станция - Ж/д станция - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Метро - Ветка метро - Ветка метро - Ветка метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Вход в метро - Трамвай - Трамвай - Трамвай - Остановка трамвая - Маршрут - Паромная переправа - Магазин - Магазин алкоголя - Булочная - Мебель для ванной - Салон красоты - Напитки - Веломагазин - Букмекерская контора - Книжный магазин - Мясная лавка - Магазин каннабиса - Автосалон - Автомобильные запчасти - СТО - Шиномонтаж - Продажа автодомов - Ковры - Бытовая химия - Магазин шоколада - Магазин одежды - Магазин кофе - Компьютерный магазин - Кондитерская - Продуктовый магазин - Копировальный центр - Косметика - Шторы - Магазин деликатесов - Универмаг - Строительный магазин - Химчистка - Электротехника - Секс-шоп - Магазин тканей - Фермерский магазин - Модные аксессуары - Цветочный магазин - Ритуальные услуги - Магазин мебели - Садовые товары - Газовый магазин - Магазин сувениров - Овощи и фрукты - Бакалея - Парикмахерская - Хозяйственный магазин - Магазин здоровой еды - Магазин трав - Hi-Fi аудио - Бытовые товары - Ювелирный магазин - Киоск - Кухонный магазин - Прачечная - Торговый центр - Массажный салон - Мобильные телефоны - Ростовщик - Магазин мотоциклов - Ремонт мотоциклов - Музыкальный магазин - Музыкальные инструменты - Газетный киоск - Оптика - Магазин снаряжения - Пункт выдачи заказов - Выпечка - Ломбард - Зоотовары - Груминг - Фототовары - Прокат - Прокат велосипедов - Рыбный магазин - Секонд-хенд магазин - Магазин обуви - Магазин спорттоваров - Канцелярский магазин - Супермаркет - Тату-салон - Чайный магазин - Билетная касса - Магазин игрушек - Турагентство - Магазин шин - Магазин полезных мелочей - Магазин видео - Магазин видеоигр - Винный магазин - Сельскохозяйственный магазин - Антиквариат - Магазин бытовой техники - - Художественный магазин - Детский магазин - Магазин сумок - Магазин кроватей - Бутик - Благотворительный магазин - Магазин сыра - Искусства и ремесла - Молочные продукты - Магазин электротоваров - Рыболовный магазин - Украшения для интерьера - Лотерейные билеты - Медикаменты - Пищевые добавки - Краски - Парфюмерия - Швейные принадлежности - Аренда склада - Табак - Торговые поставки - Часы - Оптовый магазин - Спорт - Американский футбол - Стрельба из лука - Лёгкая атлетика - Австралийский футбол - Бейсбол - Баскетбол - Пляжный волейбол - Боулз - Шахматы - Крикет - Кёрлинг - Конный спорт - Гольф - Гимнастика - Гандбол - Различные виды спорта - - Место для дайвинга - Стрельба - Скейтбординг - Лыжи - Футбол - Плавание - Настольный теннис - Теннисный корт - Волейбол - Боулинг - Боулинг - Падель - Футзал - Хоккей с шайбой - Хоккей на траве - Бадминтон - Баскская пелота - Туризм - Аквариум - - Горный приют с обслуживанием - Квартира для отдыха - Произведение искусства - Произведение искусства - Произведение искусства - Произведение искусства - Произведение искусства - Достопримечательность - Вольер для животных - Достопримечательность - Кемпинг - Кемпинг для автодомов - - Коттедж для отдыха - Галерея - Гостевой дом - Хостел - Гостиница - Туринформация - Информационный щит - Указательный столб - Карта - Туристический офис - Центр для посетителей - Мотель - Музей - Место для пикника - Дом отдыха - Парк развлечений - Обзорная площадка - - Домик для туристов - Зоопарк - Контактный зоопарк - Лежачий полицейский - Лежачий полицейский - Лежачий полицейский - Водный путь - Канал - Канал - Рыбоход - Дамба - Ров - Дренажная канава - Водопропускная труба - Причал - Водоотвод - Водоотвод - Водопропускная труба - Шлюз - Река - Река - Река - Река - Река - Река - Водопад - Плотина - Инвалидная коляска - Частично оборудовано для инвалидов - Не оборудовано для инвалидов - Оборудовано для инвалидов - Бугельный подъёмник - Ленточный конвейер - Бугельный подъёмник - Бугельный подъёмник - Бугельный подъёмник - Горнолыжная трасса - Горнолыжная трасса - Продвинутая горнолыжная трасса - Продвинутая горнолыжная трасса - Лёгкая горнолыжная трасса - Лёгкая горнолыжная трасса - Горнолыжная трасса для экспертов - Горнолыжная трасса для экспертов - Горнолыжная трасса для фрирайда - Горнолыжная трасса средней сложности - Горнолыжная трасса средней сложности - Горнолыжная трасса для новичков - Горнолыжная трасса для новичков - Лыжня - Трасса для саней - Трасса для саней - Снежный парк - Зимняя тропа - Соединение между трассами - Маршрут для скитура - Место проведения мероприятий - Аукцион - Коллекции - Комитет по развитию туризма при Правительстве Республики Таджикистан - Упс, что-то пошло не так - Пожалуйста, подождите, идет загрузка карты Таджикистана. Оставайтесь в приложении + + Исторический объект + Исторический самолет + Исторический якорь + Археологический памятник + Поле боя + Пограничный камень + Пушка + Замок + Каструм + Замок + Укреплённая церковь + Крепость + Городище + Кремль + Усадьба + Дворец + Замки Японии + Шато + Городские ворота + Городская стена + Форт + Виселица + Исторический локомотив + Мемориал + Памятный крест + Памятная доска + Скульптура + Статуя + Камни преткновения + Исторический камень + Военный мемориал + Историческая шахта + Памятник + Позорный столб + Руины + Корабль + Исторический танк + Гробница + Христианский крест + Святыня + Кораблекрушение + Интернет + Интернет + Перекрёсток + Кольцо + Кольцо + Землепользование + Земельные участки + Резервуар + Земля для застройки + + Кладбище + + Христианское кладбище + Церковный двор + Коммерческая застройка + Стройка + Образовательные учреждения + Сельскохозяйственная земля + Сельскохозяйственная земля + Поле + Клумба + Лес + Хвойный лес + Лиственный лес + Смешанный лес + Гаражи + Газон + Земля для застройки + Теплицы + Промзона + Свалка + Луг + Военная зона + Сад + Карьер + Железнодорожные сооружения + База отдыха + Водоём + Жилая зона + Зона торговли + Соляной пруд + Парк + Виноградник + Место для отдыха + Общественная земля + Место для выгула собак + Фитнес-клуб + Спортивные снаряды + Танцпол + Сад + Частный сад + Площадка для гольфа + Минигольф + Хакерспейс + Каток + Причал + Заповедник + Сидения на открытом воздухе + Парк + Парк + Парк + Парк + Стол для пикника + Спортплощадка + Детская площадка + Зона для отдыха + Сауна + Лодочный спуск + Спорткомплекс + Скалодром + Йога-центр + Стадион + Плавательный бассейн + Плавательный бассейн + Беговая дорожка + Беговая дорожка + Аквапарк + Пляжный курорт + Искусственное сооружение + Волнорез + Тур + Заводская труба + Просека + Геодезический пункт + Флагшток + Маяк + Мачта/вышка + Пирс + Трубопровод + Наземный трубопровод + Элеватор + Резервуар + Камера наблюдения + Башня + Вышка связи + Очистные сооружения + Водопроводный кран + Водонапорная башня + Колодец + Ветряная мельница + Промышленное производство + Военные объекты + Бункер + Перевал + Природа + + Каменная порода + + Галька + + Каменистая осыпь + Залив + Пляж + Песчаный пляж + Галечный пляж + Мыс + Пещера + Утёс + Обрыв + Насыпь + Береговая линия + Пустыня + Гейзер + Ледник + Луг + Пустошь + Горячий источник + Озеро + Шлюзовая камера + Пруд + Водохранилище + Резервуар + Река + Суша + Луг + Сад + Гора + Седловина + Камень + Заросли + Родник + Пролив + Ряд деревьев + Виноградник + Вулкан + Водоём + Болотистая местность + Торфяное болото + Болотистая местность + Тупик + Офис + Организация + Агентство недвижимости + Госучреждение + Страховая компания + Адвокат + Общественная организация + Телекоммуникационная компания + Эко + Эко + Город + Столица + Город + Город + Столица + Город + Город + Город + Город + Город + Город + Город + Континент + Страна + Округ + Ферма + Посёлок + Остров + Остров + Хутор + Местность + Микрорайон + Океан + Район + Море + Площадь + Штат + Штат + Район + Город + Деревня + Энергетика + Генератор + Солнечный генератор + Ветрогенератор + Газотурбинная электростанция + Гидроэлектростанция + Линия электропередач + Подземная линия электропередач + Линия электропередачи низкого напряжения + Электростанция + Угольная электростанция + Газотурбинная электростанция + Гидроэлектростанция + Солнечная электростанция + Ветряная электростанция + Электростанция + Подстанция + Опора ЛЭП + Поверхность + Плохая асфальтированная + Хорошая асфальтированная + Плохая неасфальтированная + Хорошая неасфальтированная + Общественный транспорт + Платформа + Ж/Д + Заброшенная железная дорога + Заброшенный железнодорожный мост + Заброшенный железнодорожный туннель + Строящаяся железная дорога + Пешеходный переход + Неиспользуемая железная дорога + Фуникулер + Фуникулер + Фуникулер + Ж/д станция + Железнодорожный переезд + Скоростной трамвай + Скоростной трамвай + Скоростной трамвай + Монорельсовая железная дорога + Монорельсовая железная дорога + Монорельсовая железная дорога + Узкоколейка + Узкоколейка + Узкоколейка + Железнодорожная платформа + Законсервированная Ж/Д + Законсервированная Ж/Д + Законсервированная Ж/Д + Железнодорожный путь + Высокоскоростная железная дорога + Туристическая железная дорога + Железная дорога + + Второстепенная железная дорога + + Служебная железная дорога + Подъездной Ж/Д путь + + Вспомогательный Ж/Д путь + Железнодорожный мост + Железнодорожный мост + Железнодорожный мост + Железнодорожный мост + Железнодорожный мост + Железнодорожный мост + Железнодорожный мост + Железнодорожный мост + Железнодорожный туннель + Железнодорожный туннель + Железнодорожный туннель + Железнодорожный туннель + Железнодорожный туннель + Железнодорожный туннель + Железнодорожный туннель + Железнодорожный туннель + Ж/д станция + Фуникулер + Ж/д станция + Ж/д станция + Ж/д станция + Ж/д станция + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Метро + Ветка метро + Ветка метро + Ветка метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Вход в метро + Трамвай + Трамвай + Трамвай + Остановка трамвая + Маршрут + Паромная переправа + Магазин + Магазин алкоголя + Булочная + Мебель для ванной + Салон красоты + Напитки + Веломагазин + Букмекерская контора + Книжный магазин + Мясная лавка + Магазин каннабиса + Автосалон + Автомобильные запчасти + СТО + Шиномонтаж + Продажа автодомов + Ковры + Бытовая химия + Магазин шоколада + Магазин одежды + Магазин кофе + Компьютерный магазин + Кондитерская + Продуктовый магазин + Копировальный центр + Косметика + Шторы + Магазин деликатесов + Универмаг + Строительный магазин + Химчистка + Электротехника + Секс-шоп + Магазин тканей + Фермерский магазин + Модные аксессуары + Цветочный магазин + Ритуальные услуги + Магазин мебели + Садовые товары + Газовый магазин + Магазин сувениров + Овощи и фрукты + Бакалея + Парикмахерская + Хозяйственный магазин + Магазин здоровой еды + Магазин трав + Hi-Fi аудио + Бытовые товары + Ювелирный магазин + Киоск + Кухонный магазин + Прачечная + Торговый центр + Массажный салон + Мобильные телефоны + Ростовщик + Магазин мотоциклов + Ремонт мотоциклов + Музыкальный магазин + Музыкальные инструменты + Газетный киоск + Оптика + Магазин снаряжения + Пункт выдачи заказов + Выпечка + Ломбард + Зоотовары + Груминг + Фототовары + Прокат + Прокат велосипедов + Рыбный магазин + Секонд-хенд магазин + Магазин обуви + Магазин спорттоваров + Канцелярский магазин + Супермаркет + Тату-салон + Чайный магазин + Билетная касса + Магазин игрушек + Турагентство + Магазин шин + Магазин полезных мелочей + Магазин видео + Магазин видеоигр + Винный магазин + Сельскохозяйственный магазин + Антиквариат + Магазин бытовой техники + + Художественный магазин + Детский магазин + Магазин сумок + Магазин кроватей + Бутик + Благотворительный магазин + Магазин сыра + Искусства и ремесла + Молочные продукты + Магазин электротоваров + Рыболовный магазин + Украшения для интерьера + Лотерейные билеты + Медикаменты + Пищевые добавки + Краски + Парфюмерия + Швейные принадлежности + Аренда склада + Табак + Торговые поставки + Часы + Оптовый магазин + Спорт + Американский футбол + Стрельба из лука + Лёгкая атлетика + Австралийский футбол + Бейсбол + Баскетбол + Пляжный волейбол + Боулз + Шахматы + Крикет + Кёрлинг + Конный спорт + Гольф + Гимнастика + Гандбол + Различные виды спорта + + Место для дайвинга + Стрельба + Скейтбординг + Лыжи + Футбол + Плавание + Настольный теннис + Теннисный корт + Волейбол + Боулинг + Боулинг + Падель + Футзал + Хоккей с шайбой + Хоккей на траве + Бадминтон + Баскская пелота + Туризм + Аквариум + + Горный приют с обслуживанием + Квартира для отдыха + Произведение искусства + Произведение искусства + Произведение искусства + Произведение искусства + Произведение искусства + Достопримечательность + Вольер для животных + Достопримечательность + Кемпинг + Кемпинг для автодомов + + Коттедж для отдыха + Галерея + Гостевой дом + Хостел + Гостиница + Туринформация + Информационный щит + Указательный столб + Карта + Туристический офис + Центр для посетителей + Мотель + Музей + Место для пикника + Дом отдыха + Парк развлечений + Обзорная площадка + + Домик для туристов + Зоопарк + Контактный зоопарк + Лежачий полицейский + Лежачий полицейский + Лежачий полицейский + Водный путь + Канал + Канал + Рыбоход + Дамба + Ров + Дренажная канава + Водопропускная труба + Причал + Водоотвод + Водоотвод + Водопропускная труба + Шлюз + Река + Река + Река + Река + Река + Река + Водопад + Плотина + Инвалидная коляска + Частично оборудовано для инвалидов + Не оборудовано для инвалидов + Оборудовано для инвалидов + Бугельный подъёмник + Ленточный конвейер + Бугельный подъёмник + Бугельный подъёмник + Бугельный подъёмник + Горнолыжная трасса + Горнолыжная трасса + Продвинутая горнолыжная трасса + Продвинутая горнолыжная трасса + Лёгкая горнолыжная трасса + Лёгкая горнолыжная трасса + Горнолыжная трасса для экспертов + Горнолыжная трасса для экспертов + Горнолыжная трасса для фрирайда + Горнолыжная трасса средней сложности + Горнолыжная трасса средней сложности + Горнолыжная трасса для новичков + Горнолыжная трасса для новичков + Лыжня + Трасса для саней + Трасса для саней + Снежный парк + Зимняя тропа + Соединение между трассами + Маршрут для скитура + Место проведения мероприятий + Аукцион + Коллекции + Комитет по развитию туризма при Правительстве Республики Таджикистан + Упс, что-то пошло не так + Пожалуйста, подождите, идет загрузка карты Таджикистана. Оставайтесь в приложении Добро пожаловать в Таджикистан - Войти - Регистрация - Вход - Регистрация - Логин - Ф.И.О - Страна - Повторите пароль - Главная - Избранное - Аккаунт - Изменить - Найдено - Популярное в Таджикистане - Популярное в - Описание - Фотогаллерея - Отзывы - Оставить отзыв - Все отзывы - Развернуть - Свернуть - Отзыв - Нажмите, чтобы оценить: - Текст - Загрузить фото - Отправить - Профиль - USD - EUR - RUB - Персональные данные - Язык - Русский - Темная тема - Светлая тема - Выход - Выход - Вы уверенны что хотите выйти? - Изменить данные - Номер телефона - Выберите язык - Попробовать заново - Не удается соединиться с сервером, проверьте интернет подключение + Войти + Регистрация + Вход + Регистрация + Логин + Ф.И.О + Страна + Повторите пароль + Главная + Избранное + Аккаунт + Изменить + Найдено + Популярное в Таджикистане + Популярное в + Описание + Фотогаллерея + Отзывы + Оставить отзыв + Все отзывы + Развернуть + Свернуть + Отзыв + Нажмите, чтобы оценить: + Текст + Загрузить фото + Отправить + Профиль + USD + EUR + RUB + Персональные данные + Язык + Русский + Темная тема + Светлая тема + Выход + Выход + Вы уверенны что хотите выйти? + Изменить данные + Номер телефона + Выберите язык + Попробовать заново + Не удается соединиться с сервером, проверьте интернет подключение Нет фото Таджикистан - Очистить поле поиска + Очистить поле поиска Топ-30 мест - Достопримечательности - Рестораны - Отели + Достопримечательности + Рестораны + Отели Добавить в избранное - Посмотреть маршрут - Пароли не схожи - Неправильный формат имейла - Сохранено - Мне нраится😄 + Посмотреть маршрут + Пароли не схожи + Неправильный формат имейла + Сохранено + Мне нраится😄 + Отзыв успешно удален + Удалить отзыв + Вы уверены что хотите удалить это? + В процессе удаления + Пожалуйста подождите данные скачиваются + Пусто diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 4d48a9875a..f67c98a92b 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -2262,4 +2262,10 @@ Wrong email format Saved Great success😄 + Review was successfully deleted + Delete review + Are you sure you wanna delete this? + Deleting… + Please, wait, data being downloaded + Пусто