diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7d0e518c90..991dc2191e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,9 +1,66 @@ -# Require legal approval for all new graphics -android/app/src/main/res/drawable*/ @organicmaps/legal -android/app/src/main/res/fonts/ @organicmaps/legal -android/app/src/main/res/mipmap*/ @organicmaps/legal -data/*.ttf @organicmaps/legal -data/resources*/ @organicmaps/legal -data/search-icons/ @organicmaps/legal -data/styles/clear/style-*/ @organicmaps/legal -iphone/Maps/Images.xcassets/ @organicmaps/legal +# All non-assigned. +* @organicmaps/mergers +# Visual design. +android/app/src/main/res/drawable*/ @organicmaps/design +android/app/src/main/res/font/ @organicmaps/design +android/app/src/main/res/mipmap*/ @organicmaps/design +data/*.ttf @organicmaps/design +data/resources*/ @organicmaps/design +data/search-icons/ @organicmaps/design +data/styles/default/light/**/*.png @organicmaps/design +data/styles/default/light/**/*.svg @organicmaps/design +data/styles/default/dark/**/*.png @organicmaps/design +data/styles/default/dark/**/*.svg @organicmaps/design +iphone/Maps/Images.xcassets/ @organicmaps/design +# Android. +android/ @organicmaps/android +android/app/src/main/java/app/organicmaps/car/ @organicmaps/android-auto +docs/ANDROID_LOCATION_TEST.md @organicmaps/android +docs/JAVA_STYLE.md @organicmaps/android +# iOS. +iphone/ @organicmaps/ios +xcode/ @organicmaps/ios +docs/OBJC_STYLE.md @organicmaps/ios +# Qt +qt/ @organicmaps/qt +# Rendering +drape/ @organicmaps/rendering +drape_frontend/ @organicmaps/rendering +# Map Data. +tools/python/maps_generator/ @organicmaps/data +generator/ @organicmaps/data +topography_generator/ @organicmaps/data +data/borders/ @organicmaps/data +data/conf/isolines/ @organicmaps/data +docs/SUBWAY_GENERATION.md @organicmaps/data +docs/MAPS.md @organicmaps/data +docs/EXPERIMENTAL_PUBLIC_TRANSPORT_SUPPORT.md @organicmaps/data +# Map Styles. +data/styles/ @organicmaps/styles +data/types.txt @organicmaps/styles +data/visibility.txt @organicmaps/styles +data/mapcss-mapping.csv @organicmaps/styles +data/replaced_tags.txt @organicmaps/styles +data/classificator.txt @organicmaps/styles +data/drules_* @organicmaps/styles +docs/STYLES.md +tools/kothic/ @organicmaps/styles +# DevOps. +.github/workflows @organicmaps/devops +android/*gradle* @organicmaps/devops +docs/RELEASE_MANAGEMENT.md @organicmaps/devops +xcode/fastlane/ @organicmaps/devops +# Growth. +README.md @organicmaps/growth +.github/FUNDING.yml @organicmaps/growth +android/app/src/fdroid/play/ @organicmaps/growth +android/app/src/google/play/ @organicmaps/growth +iphone/metadata/ @organicmaps/growth +# Legal. +LEGAL @organicmaps/legal +LICENSE @organicmaps/legal +NOTICE @organicmaps/legal +CONTRIBUTORS @organicmaps/legal +docs/CODE_OF_CONDUCT.md @organicmaps/legal +docs/DCO.md @organicmaps/legal +docs/GOVERNANCE.md @organicmaps/legal diff --git a/.github/workflows/android-beta.yaml b/.github/workflows/android-beta.yaml index 1ca85c9e06..ae4b3ef480 100644 --- a/.github/workflows/android-beta.yaml +++ b/.github/workflows/android-beta.yaml @@ -56,19 +56,24 @@ jobs: shell: bash run: git submodule update --depth 1 --init --recursive --jobs=$(($(nproc) * 20)) - - name: Checkout private keys - uses: actions/checkout@v4 - with: - repository: ${{ secrets.PRIVATE_REPO }} - ssh-key: ${{ secrets.PRIVATE_SSH_KEY }} - ref: master - path: private.git - - - name: Configure repo with private keys + - name: Restore beta keys shell: bash run: | - ./configure.sh ./private.git - rm -rf ./private.git + echo "$PRIVATE_H" | base64 -d > private.h + echo "$FIREBASE_APP_DISTRIBUTION_JSON" | base64 -d > android/app/firebase-app-distribution.json + echo "$GOOGLE_SERVICES_JSON" | base64 -d > android/app/google-services.json + echo "$SECURE_PROPERTIES" | base64 -d > android/app/secure.properties + echo "$RELEASE_KEYSTORE" | base64 -d > android/app/release.keystore + env: + PRIVATE_H: ${{ secrets.PRIVATE_H }} + FIREBASE_APP_DISTRIBUTION_JSON: ${{ secrets.FIREBASE_APP_DISTRIBUTION_JSON }} + GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }} + SECURE_PROPERTIES: ${{ secrets.SECURE_PROPERTIES }} + RELEASE_KEYSTORE: ${{ secrets.RELEASE_KEYSTORE }} + + - name: Configure repository + shell: bash + run: ./configure.sh - name: Compile shell: bash diff --git a/.github/workflows/android-check.yaml b/.github/workflows/android-check.yaml index 7bb51d0acd..dcff58bab3 100644 --- a/.github/workflows/android-check.yaml +++ b/.github/workflows/android-check.yaml @@ -51,7 +51,7 @@ jobs: shell: bash run: git submodule update --depth 1 --init --recursive --jobs=$(($(nproc) * 20)) - - name: Configure in Open Source mode + - name: Configure repository shell: bash run: ./configure.sh @@ -66,11 +66,11 @@ jobs: strategy: fail-fast: false matrix: - flavor: [WebDebug, FdroidBeta] + flavor: [WebDebug, FdroidDebug] include: - flavor: WebDebug arch: arm64 - - flavor: FdroidBeta + - flavor: FdroidDebug arch: arm32 # Cancels previous jobs if the same branch or PR was updated again. concurrency: @@ -93,7 +93,7 @@ jobs: shell: bash run: git submodule update --depth 1 --init --recursive --jobs=$(($(nproc) * 20)) - - name: Configure in Open Source mode + - name: Configure repository shell: bash run: ./configure.sh diff --git a/.github/workflows/android-monkey.yaml b/.github/workflows/android-monkey.yaml index c0da2212b1..c9b8f202c3 100644 --- a/.github/workflows/android-monkey.yaml +++ b/.github/workflows/android-monkey.yaml @@ -56,19 +56,26 @@ jobs: shell: bash run: git submodule update --depth 1 --init --recursive --jobs=$(($(nproc) * 20)) - - name: Checkout private keys - uses: actions/checkout@v4 - with: - repository: ${{ secrets.PRIVATE_REPO }} - ssh-key: ${{ secrets.PRIVATE_SSH_KEY }} - ref: master - path: private.git - - - name: Configure repo with private keys + - name: Restore beta keys shell: bash run: | - ./configure.sh ./private.git - rm -rf ./private.git + echo "$PRIVATE_H" | base64 -d > private.h + echo "$FIREBASE_TEST_LAB_JSON" | base64 -d > android/app/firebase-test-lab.json + echo "$FIREBASE_APP_DISTRIBUTION_JSON" | base64 -d > android/app/firebase-app-distribution.json + echo "$GOOGLE_SERVICES_JSON" | base64 -d > android/app/google-services.json + echo "$SECURE_PROPERTIES" | base64 -d > android/app/secure.properties + echo "$RELEASE_KEYSTORE" | base64 -d > android/app/release.keystore + env: + PRIVATE_H: ${{ secrets.PRIVATE_H }} + FIREBASE_TEST_LAB_JSON: ${{ secrets.FIREBASE_TEST_LAB_JSON }} + FIREBASE_APP_DISTRIBUTION_JSON: ${{ secrets.FIREBASE_APP_DISTRIBUTION_JSON }} + GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }} + SECURE_PROPERTIES: ${{ secrets.SECURE_PROPERTIES }} + RELEASE_KEYSTORE: ${{ secrets.RELEASE_KEYSTORE }} + + - name: Configure repository + shell: bash + run: ./configure.sh - name: Compile shell: bash diff --git a/.github/workflows/android-release-metadata.yaml b/.github/workflows/android-release-metadata.yaml index 004fd549f3..37fe9b59e2 100644 --- a/.github/workflows/android-release-metadata.yaml +++ b/.github/workflows/android-release-metadata.yaml @@ -26,19 +26,14 @@ jobs: ref: master path: screenshots - - name: Checkout private keys - uses: actions/checkout@v4 - with: - repository: ${{ secrets.PRIVATE_REPO }} - ssh-key: ${{ secrets.PRIVATE_SSH_KEY }} - ref: master - path: private.git - - - name: Configure repo with private keys + - name: Restore release keys shell: bash run: | - ./configure.sh ./private.git - rm -rf ./private.git + echo "$PRIVATE_H" | base64 -d > private.h + echo "$GOOGLE_PLAY_JSON" | base64 -d > android/app/google-play.json + env: + PRIVATE_H: ${{ secrets.PRIVATE_H }} + GOOGLE_PLAY_JSON: ${{ secrets.GOOGLE_PLAY_JSON }} - name: Upload shell: bash diff --git a/.github/workflows/android-release.yaml b/.github/workflows/android-release.yaml index ff18d4b74a..db205b7f51 100644 --- a/.github/workflows/android-release.yaml +++ b/.github/workflows/android-release.yaml @@ -100,19 +100,26 @@ jobs: ref: master path: screenshots - - name: Checkout private keys - uses: actions/checkout@v4 - with: - repository: ${{ secrets.PRIVATE_REPO }} - ssh-key: ${{ secrets.PRIVATE_SSH_KEY }} - ref: master - path: private.git - - - name: Configure repo with private keys + - name: Restore release keys shell: bash run: | - ./configure.sh ./private.git - rm -rf ./private.git + echo "$PRIVATE_H" | base64 -d > private.h + echo "$GOOGLE_PLAY_JSON" | base64 -d > android/app/google-play.json + echo "$HUAWEI_APPGALLERY_JSON" | base64 -d > android/app/huawei-appgallery.json + echo "$AGCONNECT_SERVICES_JSON" | base64 -d > android/app/agconnect-services.json + echo "$SECURE_PROPERTIES" | base64 -d > android/app/secure.properties + echo "$RELEASE_KEYSTORE" | base64 -d > android/app/release.keystore + env: + PRIVATE_H: ${{ secrets.PRIVATE_H }} + GOOGLE_PLAY_JSON: ${{ secrets.GOOGLE_PLAY_JSON }} + HUAWEI_APPGALLERY_JSON: ${{ secrets.HUAWEI_APPGALLERY_JSON }} + AGCONNECT_SERVICES_JSON: ${{ secrets.AGCONNECT_SERVICES_JSON }} + SECURE_PROPERTIES: ${{ secrets.SECURE_PROPERTIES }} + RELEASE_KEYSTORE: ${{ secrets.RELEASE_KEYSTORE }} + + - name: Configure repository + shell: bash + run: ./configure.sh - name: Set up SDK shell: bash diff --git a/.github/workflows/coverage-check.yaml b/.github/workflows/coverage-check.yaml index d5e82ff6d1..833b5180fa 100644 --- a/.github/workflows/coverage-check.yaml +++ b/.github/workflows/coverage-check.yaml @@ -94,7 +94,7 @@ jobs: llvm \ gcovr - - name: Configure + - name: Configure repository shell: bash run: ./configure.sh diff --git a/.github/workflows/ios-beta.yaml b/.github/workflows/ios-beta.yaml index 321142ccc5..5699ce0e3e 100644 --- a/.github/workflows/ios-beta.yaml +++ b/.github/workflows/ios-beta.yaml @@ -52,18 +52,23 @@ jobs: - name: Parallel submodules checkout run: git submodule update --depth 1 --init --recursive --jobs=$(($(sysctl -n hw.logicalcpu) * 20)) - - name: Checkout private keys - uses: actions/checkout@v4 - with: - repository: ${{ secrets.PRIVATE_REPO }} - ssh-key: ${{ secrets.PRIVATE_SSH_KEY }} - ref: master - path: private.git - - - name: Configure repo with private keys + - name: Restore beta keys + shell: bash run: | - ./configure.sh ./private.git - rm -rf ./private.git + mkdir -p xcode/keys + echo "$PRIVATE_H" | base64 -d > private.h + echo "$APPSTORE_JSON" | base64 -d > xcode/keys/appstore.json + echo "$CERTIFICATES_DEV_P12" | base64 -d > xcode/keys/CertificatesDev.p12 + echo "$CERTIFICATES_DISTR_P12" | base64 -d > xcode/keys/CertificatesDistr.p12 + env: + PRIVATE_H: ${{ secrets.PRIVATE_H }} + APPSTORE_JSON: ${{ secrets.APPSTORE_JSON }} + CERTIFICATES_DEV_P12: ${{ secrets.CERTIFICATES_DEV_P12 }} + CERTIFICATES_DISTR_P12: ${{ secrets.CERTIFICATES_DISTR_P12 }} + + - name: Configure repository + shell: bash + run: ./configure.sh - name: Compile and upload to TestFlight run: | diff --git a/.github/workflows/ios-check.yaml b/.github/workflows/ios-check.yaml index d91674f209..db9af68c19 100644 --- a/.github/workflows/ios-check.yaml +++ b/.github/workflows/ios-check.yaml @@ -59,7 +59,7 @@ jobs: shell: bash run: git submodule update --depth 1 --init --recursive --jobs=$(($(sysctl -n hw.logicalcpu) * 20)) - - name: Configure + - name: Configure repository shell: bash run: ./configure.sh diff --git a/.github/workflows/ios-release.yaml b/.github/workflows/ios-release.yaml index 6b698a53d8..7a94b16fe7 100644 --- a/.github/workflows/ios-release.yaml +++ b/.github/workflows/ios-release.yaml @@ -16,20 +16,13 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Checkout private keys - uses: actions/checkout@v4 - with: - repository: ${{ secrets.PRIVATE_REPO }} - ssh-key: ${{ secrets.PRIVATE_SSH_KEY }} - ref: master - path: ./private.git - - - name: Configure repo with private keys + - name: Restore release keys shell: bash run: | - mkdir -p xcode/keys/ - cp -p ./private.git/xcode/keys/appstore.json xcode/keys/ - rm -rf ./private.git + mkdir -p xcode/keys + echo "$APPSTORE_JSON" | base64 -d > xcode/keys/appstore.json + env: + APPSTORE_JSON: ${{ secrets.APPSTORE_JSON }} - name: Checkout screenshots uses: actions/checkout@v4 diff --git a/.github/workflows/linux-check.yaml b/.github/workflows/linux-check.yaml index 805e7aebf0..17adeecbd2 100644 --- a/.github/workflows/linux-check.yaml +++ b/.github/workflows/linux-check.yaml @@ -67,7 +67,7 @@ jobs: libqt6positioning6-plugins \ libqt6positioning6 - - name: Configure + - name: Configure repository shell: bash run: ./configure.sh @@ -134,7 +134,7 @@ jobs: libqt6positioning6-plugins \ libqt6positioning6 - - name: Configure + - name: Configure repository shell: bash run: ./configure.sh diff --git a/.github/workflows/macos-check.yaml b/.github/workflows/macos-check.yaml index caacce95c4..3d6c5232e0 100644 --- a/.github/workflows/macos-check.yaml +++ b/.github/workflows/macos-check.yaml @@ -57,7 +57,7 @@ jobs: run: | HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 brew install ninja qt@6 - - name: Configure + - name: Configure repository shell: bash run: ./configure.sh diff --git a/.gitmodules b/.gitmodules index 30bb39e233..7d35fe8264 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,9 +4,6 @@ [submodule "tools/kothic"] path = tools/kothic url = https://github.com/organicmaps/kothic.git -[submodule "tools/macdeployqtfix"] - path = tools/macdeployqtfix - url = https://github.com/aurelien-rainone/macdeployqtfix.git [submodule "3party/protobuf/protobuf"] path = 3party/protobuf/protobuf url = https://github.com/organicmaps/protobuf.git diff --git a/3party/CMakeLists.txt b/3party/CMakeLists.txt index 97f45e2efa..57d0592dc3 100644 --- a/3party/CMakeLists.txt +++ b/3party/CMakeLists.txt @@ -22,7 +22,6 @@ if (NOT WITH_SYSTEM_PROVIDED_3PARTY) set(EXPAT_DTD OFF) set(EXPAT_NS ON) add_subdirectory(expat/expat) - add_library(expat::expat ALIAS expat) # Configure Jansson library. set(JANSSON_BUILD_DOCS OFF) diff --git a/3party/expat b/3party/expat index 6b3f93c6ca..a0dc7d5efa 160000 --- a/3party/expat +++ b/3party/expat @@ -1 +1 @@ -Subproject commit 6b3f93c6caa0308455beeced0268cfae04df3584 +Subproject commit a0dc7d5efacbe2b744211289c276e2b9168bd4ae diff --git a/CMakeLists.txt b/CMakeLists.txt index aaa5939e7d..30c1a0e496 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.18) +cmake_minimum_required(VERSION 3.22.1) project(omim C CXX) set(CMAKE_CXX_STANDARD 20) @@ -163,6 +163,7 @@ option(SKIP_QT_GUI "Skip building of Qt GUI" OFF) option(USE_PCH "Use precompiled headers" OFF) option(NJOBS "Number of parallel processes" OFF) option(ENABLE_VULKAN_DIAGNOSTICS "Enable Vulkan diagnostics" OFF) +option(ENABLE_TRACE "Enable Tracing" OFF) if (NJOBS) message(STATUS "Number of parallel processes: ${NJOBS}") @@ -219,6 +220,11 @@ if (ENABLE_VULKAN_DIAGNOSTICS) add_definitions(-DENABLE_VULKAN_DIAGNOSTICS) endif() +if (ENABLE_TRACE) + message(STATUS "Tracing is enabled") + add_definitions(-DENABLE_TRACE) +endif() + set(CMAKE_POSITION_INDEPENDENT_CODE ON) # Set environment variables diff --git a/LEGAL b/LEGAL new file mode 100644 index 0000000000..f654703604 --- /dev/null +++ b/LEGAL @@ -0,0 +1,9 @@ +Certain project resources, including but not limited to domain names, trademarks, hosting accounts, payment accounts, and others, are overseen and managed by Organic Maps OÜ. The governance of these digital assets is subject to policies established by Organic Maps OÜ, in compliance with applicable statutory laws. + +Organic Maps OÜ is a legal entity established on 2021-05-01 under the laws of the Republic of Estonia and the European Union, with registration number 16225385. The primary purpose of the entity is to shield the project's members from personal liability and to ensure the legal protection of the project's assets. Official up-to-date information about the entity can be found in the Estonian Business Register: + +https://ariregister.rik.ee/eng/company/16225385/Organic-Maps-O%C3%9C + +Organic Maps OÜ does not require contributors to transfer copyright ownership and does not retain any copyright over the code contributed to the repository. See the NOTICE file and docs/DCO.md for additional information. + +For any legal inquiries, feel free to contact legal@organicmaps.app. diff --git a/README.md b/README.md index 1ec58f6e29..837509e8c1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ -# Organic Maps - - +
+ +
+

Organic Maps

[Organic Maps](https://organicmaps.app) is a free Android & iOS offline maps app for travellers, tourists, drivers, hikers, and cyclists. It uses crowd-sourced [OpenStreetMap](https://www.openstreetmap.org) data and is developed with love by the creators of **MapsWithMe** (later renamed to **Maps.Me**) and by our community. @@ -114,6 +115,14 @@ Beloved institutional sponsors below have provided targeted grants to cover some
Mythic Beasts ISP provides us two virtual servers with 400 TB/month of free bandwidth to host and serve maps downloads and updates. + + + 44+ Technologies + + + 44+ Technologies is providing us with a free dedicated server worth around $12,000/year to serve maps across Vietnam & Southeast Asia. + + FUTO diff --git a/android/app/build.gradle b/android/app/build.gradle index cfa42b20bc..e710eb6550 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -22,7 +22,7 @@ buildscript { googleFirebaseServicesDefault dependencies { - classpath 'com.android.tools.build:gradle:8.6.1' + classpath 'com.android.tools.build:gradle:8.7.2' if (googleFirebaseServicesEnabled) { println('Building with Google Firebase Services') @@ -45,7 +45,6 @@ repositories { } apply plugin: 'com.android.application' -apply from: 'secure.properties' if (googleFirebaseServicesEnabled) { apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.firebase.crashlytics' @@ -84,11 +83,12 @@ def osName = System.properties['os.name'].toLowerCase() project.ext.appId = 'app.organicmaps' project.ext.appName = 'Organic Maps' -java { - toolchain { - languageVersion.set(JavaLanguageVersion.of(17)) - } -} +// I have Java 21 installed, but this doesn't work on MacOS. +//java { +// toolchain { +// languageVersion.set(JavaLanguageVersion.of(17)) +// } +//} android { namespace 'app.organicmaps' @@ -129,6 +129,11 @@ android { enableVulkanDiagnostics = project.getProperty('enableVulkanDiagnostics') } + def enableTrace = 'OFF' + if (project.hasProperty('enableTrace')) { + enableTrace = project.getProperty('enableTrace') + } + cmake { cppFlags '-fexceptions', '-frtti' // There is no sense to enable sections without gcc's --gc-sections flag. @@ -136,7 +141,8 @@ android { '-Wno-extern-c-compat' arguments '-DANDROID_TOOLCHAIN=clang', '-DANDROID_STL=c++_static', "-DOS=$osName", '-DSKIP_TESTS=ON', '-DSKIP_TOOLS=ON', "-DUSE_PCH=$pchFlag", - "-DNJOBS=$njobs", "-DENABLE_VULKAN_DIAGNOSTICS=$enableVulkanDiagnostics" + "-DNJOBS=$njobs", "-DENABLE_VULKAN_DIAGNOSTICS=$enableVulkanDiagnostics", + "-DENABLE_TRACE=$enableTrace" targets 'organicmaps' } } @@ -240,6 +246,11 @@ android { } } + def securityPropertiesFileExists = file('secure.properties').exists() + if (securityPropertiesFileExists) { + apply from: 'secure.properties' + } + signingConfigs { debug { storeFile file('debug.keystore') @@ -249,10 +260,15 @@ android { } release { - storeFile file(spropStoreFile) - storePassword spropStorePassword - keyAlias spropKeyAlias - keyPassword spropKeyPassword + if (securityPropertiesFileExists) { + println('The release signing keys are available') + storeFile file(spropStoreFile) + storePassword spropStorePassword + keyAlias spropKeyAlias + keyPassword spropKeyPassword + } else { + println('The release signing keys are unavailable') + } } } @@ -325,7 +341,7 @@ android { externalNativeBuild { cmake { - version '3.30.3+' + version '3.22.1+' buildStagingDirectory './nativeOutputs' path '../../CMakeLists.txt' } @@ -347,7 +363,7 @@ android { } dependencies { - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.3' // Google Play Location Services // @@ -361,11 +377,14 @@ dependencies { webImplementation 'com.google.android.gms:play-services-location:21.3.0' googleImplementation 'com.google.android.gms:play-services-location:21.3.0' huaweiImplementation 'com.google.android.gms:play-services-location:21.3.0' + // This is the microG project's re-implementation which is permissible on + // F-droid because it's Apache-2.0. + fdroidImplementation 'org.microg.gms:play-services-location:0.3.4.240913' // Google Firebase Services if (googleFirebaseServicesEnabled) { // Import the BoM for the Firebase platform - implementation platform('com.google.firebase:firebase-bom:33.2.0') + implementation platform('com.google.firebase:firebase-bom:33.5.1') // Add the dependencies for the Crashlytics and Analytics libraries // When using the BoM, you don't specify versions in Firebase library dependencies implementation 'com.google.firebase:firebase-crashlytics' @@ -376,18 +395,18 @@ dependencies { // > A failure occurred while executing com.android.build.gradle.internal.tasks.CheckDuplicatesRunnable // We don't use Kotlin, but some dependencies are actively using it. // See https://stackoverflow.com/a/75719642 - implementation 'androidx.core:core:1.13.1' - implementation(platform('org.jetbrains.kotlin:kotlin-bom:2.0.20')) - implementation 'androidx.annotation:annotation:1.8.2' + implementation 'androidx.core:core:1.15.0' + implementation(platform('org.jetbrains.kotlin:kotlin-bom:2.0.21')) + implementation 'androidx.annotation:annotation:1.9.1' implementation 'androidx.appcompat:appcompat:1.7.0' - implementation 'androidx.car.app:app:1.7.0-beta01' - implementation 'androidx.car.app:app-projected:1.7.0-beta01' - implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - implementation 'androidx.fragment:fragment:1.8.2' + implementation 'androidx.car.app:app:1.7.0-beta03' + implementation 'androidx.car.app:app-projected:1.7.0-beta03' + implementation 'androidx.constraintlayout:constraintlayout:2.2.0' + implementation 'androidx.fragment:fragment:1.8.5' implementation 'androidx.preference:preference:1.2.1' implementation 'androidx.recyclerview:recyclerview:1.3.2' - implementation 'androidx.work:work-runtime:2.9.1' - implementation 'androidx.lifecycle:lifecycle-process:2.8.4' + implementation 'androidx.work:work-runtime:2.10.0' + implementation 'androidx.lifecycle:lifecycle-process:2.8.7' implementation 'com.google.android.material:material:1.12.0' // Fix for app/organicmaps/util/FileUploadWorker.java:14: error: cannot access ListenableFuture // https://github.com/organicmaps/organicmaps/issues/6106 @@ -443,7 +462,7 @@ task prepareGoogleReleaseListing { play { enabled.set(false) - track.set('alpha') + track.set('production') defaultToAppBundles.set(true) releaseStatus.set(ReleaseStatus.IN_PROGRESS) serviceAccountCredentials.set(file('google-play.json')) diff --git a/android/app/src/fdroid/java/app/organicmaps/location b/android/app/src/fdroid/java/app/organicmaps/location new file mode 120000 index 0000000000..c3bacf635a --- /dev/null +++ b/android/app/src/fdroid/java/app/organicmaps/location @@ -0,0 +1 @@ +../../../../google/java/app/organicmaps/location \ No newline at end of file diff --git a/android/app/src/fdroid/java/app/organicmaps/location/LocationProviderFactory.java b/android/app/src/fdroid/java/app/organicmaps/location/LocationProviderFactory.java deleted file mode 100644 index 23c31b675a..0000000000 --- a/android/app/src/fdroid/java/app/organicmaps/location/LocationProviderFactory.java +++ /dev/null @@ -1,18 +0,0 @@ -package app.organicmaps.location; - -import android.content.Context; - -import androidx.annotation.NonNull; - -public class LocationProviderFactory -{ - public static boolean isGoogleLocationAvailable(@NonNull @SuppressWarnings("unused") Context context) - { - return false; - } - - public static BaseLocationProvider getProvider(@NonNull Context context, @NonNull BaseLocationProvider.Listener listener) - { - return new AndroidNativeProvider(context, listener); - } -} diff --git a/android/app/src/fdroid/play/listings/ar/release-notes.txt b/android/app/src/fdroid/play/listings/ar/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/ar/release-notes.txt +++ b/android/app/src/fdroid/play/listings/ar/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/az-AZ/release-notes.txt b/android/app/src/fdroid/play/listings/az-AZ/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/az-AZ/release-notes.txt +++ b/android/app/src/fdroid/play/listings/az-AZ/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/be/release-notes.txt b/android/app/src/fdroid/play/listings/be/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/be/release-notes.txt +++ b/android/app/src/fdroid/play/listings/be/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/bg/release-notes.txt b/android/app/src/fdroid/play/listings/bg/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/bg/release-notes.txt +++ b/android/app/src/fdroid/play/listings/bg/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/bn-BD/release-notes.txt b/android/app/src/fdroid/play/listings/bn-BD/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/bn-BD/release-notes.txt +++ b/android/app/src/fdroid/play/listings/bn-BD/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/ca/release-notes.txt b/android/app/src/fdroid/play/listings/ca/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/ca/release-notes.txt +++ b/android/app/src/fdroid/play/listings/ca/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/cs-CZ/release-notes.txt b/android/app/src/fdroid/play/listings/cs-CZ/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/cs-CZ/release-notes.txt +++ b/android/app/src/fdroid/play/listings/cs-CZ/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/da-DK/release-notes.txt b/android/app/src/fdroid/play/listings/da-DK/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/da-DK/release-notes.txt +++ b/android/app/src/fdroid/play/listings/da-DK/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/de-DE/release-notes.txt b/android/app/src/fdroid/play/listings/de-DE/release-notes.txt index 5c73dbb745..3856690eff 100644 --- a/android/app/src/fdroid/play/listings/de-DE/release-notes.txt +++ b/android/app/src/fdroid/play/listings/de-DE/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• Neue OpenStreetMap-Daten vom 22. November +• Hervorhebung des übereinstimmenden Teils der Adresse in Suchergebnissen +• Sortierung des Suchverlaufs nach letzter Nutzung +• Behobene Startabstürze bei einigen älteren Geräten mit Mali-T-GPUs +• Weitere Suchverbesserungen, Übersetzungs-Updates und Fehlerbehebungen diff --git a/android/app/src/fdroid/play/listings/el-GR/release-notes.txt b/android/app/src/fdroid/play/listings/el-GR/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/el-GR/release-notes.txt +++ b/android/app/src/fdroid/play/listings/el-GR/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/en-US/release-notes.txt b/android/app/src/fdroid/play/listings/en-US/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/en-US/release-notes.txt +++ b/android/app/src/fdroid/play/listings/en-US/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/es-ES/release-notes.txt b/android/app/src/fdroid/play/listings/es-ES/release-notes.txt index 5c73dbb745..24d81c2933 100644 --- a/android/app/src/fdroid/play/listings/es-ES/release-notes.txt +++ b/android/app/src/fdroid/play/listings/es-ES/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• Nuevos datos de OpenStreetMap del 22 de noviembre +• Resaltado de la parte coincidente de la dirección en los resultados de búsqueda +• Ordenar el historial de búsquedas por la última vez de uso +• Solucionados los bloqueos al inicio en algunos dispositivos antiguos con GPU Mali-T +• Otras mejoras en la búsqueda, actualizaciones de traducción y correcciones de errores diff --git a/android/app/src/fdroid/play/listings/et/release-notes.txt b/android/app/src/fdroid/play/listings/et/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/et/release-notes.txt +++ b/android/app/src/fdroid/play/listings/et/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/eu-ES/release-notes.txt b/android/app/src/fdroid/play/listings/eu-ES/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/eu-ES/release-notes.txt +++ b/android/app/src/fdroid/play/listings/eu-ES/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/fa/release-notes.txt b/android/app/src/fdroid/play/listings/fa/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/fa/release-notes.txt +++ b/android/app/src/fdroid/play/listings/fa/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/fi-FI/release-notes.txt b/android/app/src/fdroid/play/listings/fi-FI/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/fi-FI/release-notes.txt +++ b/android/app/src/fdroid/play/listings/fi-FI/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/fr-FR/release-notes.txt b/android/app/src/fdroid/play/listings/fr-FR/release-notes.txt index 5c73dbb745..76544a96d5 100644 --- a/android/app/src/fdroid/play/listings/fr-FR/release-notes.txt +++ b/android/app/src/fdroid/play/listings/fr-FR/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• Nouvelles données OpenStreetMap du 22 novembre +• Mise en évidence de la partie correspondante de l'adresse dans les résultats de recherche +• Tri de l'historique des recherches par dernier temps d'utilisation +• Correction des plantages au démarrage sur certains anciens appareils avec des GPU Mali-T +• Autres améliorations de recherche, mises à jour de traduction et corrections de bugs diff --git a/android/app/src/fdroid/play/listings/gl-ES/release-notes.txt b/android/app/src/fdroid/play/listings/gl-ES/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/gl-ES/release-notes.txt +++ b/android/app/src/fdroid/play/listings/gl-ES/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/gu/release-notes.txt b/android/app/src/fdroid/play/listings/gu/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/gu/release-notes.txt +++ b/android/app/src/fdroid/play/listings/gu/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/hi-IN/release-notes.txt b/android/app/src/fdroid/play/listings/hi-IN/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/hi-IN/release-notes.txt +++ b/android/app/src/fdroid/play/listings/hi-IN/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/hr/release-notes.txt b/android/app/src/fdroid/play/listings/hr/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/hr/release-notes.txt +++ b/android/app/src/fdroid/play/listings/hr/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/hu-HU/release-notes.txt b/android/app/src/fdroid/play/listings/hu-HU/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/hu-HU/release-notes.txt +++ b/android/app/src/fdroid/play/listings/hu-HU/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/id/release-notes.txt b/android/app/src/fdroid/play/listings/id/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/id/release-notes.txt +++ b/android/app/src/fdroid/play/listings/id/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/it-IT/release-notes.txt b/android/app/src/fdroid/play/listings/it-IT/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/it-IT/release-notes.txt +++ b/android/app/src/fdroid/play/listings/it-IT/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/iw-IL/release-notes.txt b/android/app/src/fdroid/play/listings/iw-IL/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/iw-IL/release-notes.txt +++ b/android/app/src/fdroid/play/listings/iw-IL/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/ja-JP/release-notes.txt b/android/app/src/fdroid/play/listings/ja-JP/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/ja-JP/release-notes.txt +++ b/android/app/src/fdroid/play/listings/ja-JP/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/ka-GE/release-notes.txt b/android/app/src/fdroid/play/listings/ka-GE/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/ka-GE/release-notes.txt +++ b/android/app/src/fdroid/play/listings/ka-GE/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/kk/release-notes.txt b/android/app/src/fdroid/play/listings/kk/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/kk/release-notes.txt +++ b/android/app/src/fdroid/play/listings/kk/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/km-KH/release-notes.txt b/android/app/src/fdroid/play/listings/km-KH/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/km-KH/release-notes.txt +++ b/android/app/src/fdroid/play/listings/km-KH/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/kn-IN/release-notes.txt b/android/app/src/fdroid/play/listings/kn-IN/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/kn-IN/release-notes.txt +++ b/android/app/src/fdroid/play/listings/kn-IN/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/ko-KR/release-notes.txt b/android/app/src/fdroid/play/listings/ko-KR/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/ko-KR/release-notes.txt +++ b/android/app/src/fdroid/play/listings/ko-KR/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/lo-LA/release-notes.txt b/android/app/src/fdroid/play/listings/lo-LA/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/lo-LA/release-notes.txt +++ b/android/app/src/fdroid/play/listings/lo-LA/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/lt/release-notes.txt b/android/app/src/fdroid/play/listings/lt/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/lt/release-notes.txt +++ b/android/app/src/fdroid/play/listings/lt/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/lv/release-notes.txt b/android/app/src/fdroid/play/listings/lv/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/lv/release-notes.txt +++ b/android/app/src/fdroid/play/listings/lv/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/mk-MK/release-notes.txt b/android/app/src/fdroid/play/listings/mk-MK/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/mk-MK/release-notes.txt +++ b/android/app/src/fdroid/play/listings/mk-MK/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/ml-IN/release-notes.txt b/android/app/src/fdroid/play/listings/ml-IN/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/ml-IN/release-notes.txt +++ b/android/app/src/fdroid/play/listings/ml-IN/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/mr-IN/release-notes.txt b/android/app/src/fdroid/play/listings/mr-IN/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/mr-IN/release-notes.txt +++ b/android/app/src/fdroid/play/listings/mr-IN/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/ms/release-notes.txt b/android/app/src/fdroid/play/listings/ms/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/ms/release-notes.txt +++ b/android/app/src/fdroid/play/listings/ms/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/ne-NP/release-notes.txt b/android/app/src/fdroid/play/listings/ne-NP/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/ne-NP/release-notes.txt +++ b/android/app/src/fdroid/play/listings/ne-NP/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/nl-NL/release-notes.txt b/android/app/src/fdroid/play/listings/nl-NL/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/nl-NL/release-notes.txt +++ b/android/app/src/fdroid/play/listings/nl-NL/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/no-NO/release-notes.txt b/android/app/src/fdroid/play/listings/no-NO/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/no-NO/release-notes.txt +++ b/android/app/src/fdroid/play/listings/no-NO/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/pl-PL/release-notes.txt b/android/app/src/fdroid/play/listings/pl-PL/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/pl-PL/release-notes.txt +++ b/android/app/src/fdroid/play/listings/pl-PL/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/pt-BR/release-notes.txt b/android/app/src/fdroid/play/listings/pt-BR/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/pt-BR/release-notes.txt +++ b/android/app/src/fdroid/play/listings/pt-BR/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/pt-PT/release-notes.txt b/android/app/src/fdroid/play/listings/pt-PT/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/pt-PT/release-notes.txt +++ b/android/app/src/fdroid/play/listings/pt-PT/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/ro/release-notes.txt b/android/app/src/fdroid/play/listings/ro/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/ro/release-notes.txt +++ b/android/app/src/fdroid/play/listings/ro/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/ru-RU/release-notes.txt b/android/app/src/fdroid/play/listings/ru-RU/release-notes.txt index 5c73dbb745..681356d89b 100644 --- a/android/app/src/fdroid/play/listings/ru-RU/release-notes.txt +++ b/android/app/src/fdroid/play/listings/ru-RU/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• Новые данные OpenStreetMap от 22 ноября +• В результатах поиска выделяется совпавшая часть адреса +• История поиска всегда сортируется по времени последнего использования +• Исправлена проблема запуска на некоторых старых устройствах с GPU Mali-T +• Различные улучшения поиска, обновления переводов и исправления ошибок diff --git a/android/app/src/fdroid/play/listings/si-LK/release-notes.txt b/android/app/src/fdroid/play/listings/si-LK/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/si-LK/release-notes.txt +++ b/android/app/src/fdroid/play/listings/si-LK/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/sk/release-notes.txt b/android/app/src/fdroid/play/listings/sk/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/sk/release-notes.txt +++ b/android/app/src/fdroid/play/listings/sk/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/sl/release-notes.txt b/android/app/src/fdroid/play/listings/sl/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/sl/release-notes.txt +++ b/android/app/src/fdroid/play/listings/sl/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/sr/full-description.txt b/android/app/src/fdroid/play/listings/sr/full-description.txt index 00dfb47559..b3d7bb168f 100644 --- a/android/app/src/fdroid/play/listings/sr/full-description.txt +++ b/android/app/src/fdroid/play/listings/sr/full-description.txt @@ -1,53 +1,53 @@ -‣ Our free app does not track you, does not have ads, and it needs your support. -‣ It is constantly being improved by contributors and our small team, in our free time. -‣ If something is wrong or missing on the map, please fix it in OpenStreetMap and see your changes in the future maps update. -‣ If navigation or search doesn't work, please check it on osm.org first, and then email us. We reply to EVERY email, and we'll fix it ASAP! +‣ Наша бесплатна апликација вас не прати, нема рекламе и потребна јој је ваша подршка. +‣ Континуирано се побољшава од стране сарадника и нашег малог тима, у наше слободно време. +‣ Ако нешто није у реду или недостаје на мапи, исправите то на OpenStreetMap-у и видите своје промене у будућим ажурирањима мапа. +‣ Ако навигација или претрага не раде, прво проверите то на osm.org, а затим нам пошаљите е-пошту. Одговарамо на СВАКУ поруку и поправљамо у најкраћем могућем року! -Your feedback and 5-star reviews are the best motivators for us! +Ваше повратне информације и рецензије са 5 звездица су најбољи мотиватори за нас! -Key features: +Кључне карактеристике: -• Free, open-source, no ads, no tracking -• Detailed offline maps with places that don't exist on Google maps, thanks to the OpenStreetMap community -• Cycling routes, hiking trails, and walking paths -• Contour lines, elevation profiles, peaks, and slopes -• Turn-by-turn walking, cycling, and car navigation with voice guidance and Android Auto -• Fast offline search -• Bookmarks and tracks export and import in KML, KMZ, GPX formats -• Dark mode to protect your eyes +• Бесплатно, отвореног кода, без реклама, без праћења +• Детаљне мапе без интернета са местима која не постоје на Google мапама, захваљујући OpenStreetMap заједници +• Бициклистичке стазе, пешачке стазе и стазе за шетњу +• Контурне линије, профили надморске висине, врхови и нагиби +• Пешачење, вожња бицикла и навигација скретање-по-скретање са гласовним навођењем и Андроид Аутом +• Брза претрага без интернета +• Извоз и увоз маркера и путања у KML, KMZ, GPX формату +• Тамни режим за заштиту очију -There is no public transport, satellite maps, and other cool features yet in Organic Maps. But with your help and support, we can make better maps step by step. +Нема јавног превоза, сателитских мапа и других занимљивих функција још у Organic Maps-у. Али из вашу помоћ и подршку, можемо постепено направити боље мапе. -Organic Maps is pure and organic, made with love: +Organic Maps је чиста и органска, направљена с љубављу: -• Blazing fast offline experience -• Respects your privacy -• Saves your battery -• No unexpected mobile data charges -• Simple to use, with only most important features included +• Невероватно брзо искуство без интернета +• Поштује вашу приватност +• Штеди батерију +• Нема неочекиваних трошкова мобилних података +• Једноставан за употребу, са укљученим само најважнијим функцијама -Free from trackers and other bad stuff: +Без праћења и других лоших ствари: -• No ads -• No tracking -• No data collection -• No phoning home -• No annoying registration -• No mandatory tutorials -• No noisy email spam -• No push notifications -• No crapware -• N̶o̶ ̶p̶e̶s̶t̶i̶c̶i̶d̶e̶s̶ Purely organic +• Нема реклама +• Нема праћења +• Нема прикупљања података +• Нема телефонирања кући +• Нема досадне регистрације +• Нема обавезних туторијала +• Нема бучне нежељене е-поште +• Нема пуш обавештења +• Нема crapware-а +• Б̶е̶з̶ ̶п̶е̶с̶т̶и̶ц̶и̶да̶ Чисто органско -At Organic Maps, we believe that privacy is a fundamental human right: +Organic Maps, верује да је приватност основно право човека: -• Organic Maps is an indie community-driven open-source project -• We protect privacy from Big Tech's prying eyes -• Stay safe no matter wherever you are +• Organic Maps пројекат отвореног кода који води заједница +• Штитимо приватност од радозналих очију технолошких гиганата +• Будите безбедни где год да се налазите -Zero trackers and only minimally required permissions are found according to Exodus Privacy Report. +Према Exodus Privacy Report-у, откривено је нула трагача и само минимално потребне дозволе. -Please visit organicmaps.app website for additional details and FAQ, and contact us directly at @OrganicMapsApp in Telegram. +Молимо посетите organicmaps.app веб-сајт за више информација и прочитајте FAQ, контактирајте нас путем Telegram-а директно на адресу @OrganicMapsApp. -Reject surveillance - embrace your freedom. -Give Organic Maps a try! +Одбаците надзор - прихватите своју слободу. +Испробајте Organic Maps! diff --git a/android/app/src/fdroid/play/listings/sr/release-notes.txt b/android/app/src/fdroid/play/listings/sr/release-notes.txt index 5c73dbb745..48669b0de4 100644 --- a/android/app/src/fdroid/play/listings/sr/release-notes.txt +++ b/android/app/src/fdroid/play/listings/sr/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• Нови OpenStreetMap подаци са стањем на дан 22. новембра +• Назначен пронађени део адресе у резултатима претраге +• Сортирање историје претраге по времену последњег коришћења +• Исправке грешака при покретању на неким старијим уређајима са графичким процесорима Mali-T GPUs. +• Друга побољшања за претрагу, исправке превода и исправке грешака diff --git a/android/app/src/fdroid/play/listings/sr/short-description.txt b/android/app/src/fdroid/play/listings/sr/short-description.txt index c2f1533e8e..9336a8e2ae 100644 --- a/android/app/src/fdroid/play/listings/sr/short-description.txt +++ b/android/app/src/fdroid/play/listings/sr/short-description.txt @@ -1 +1 @@ -Open-source, community-driven maps for travelers, tourists, cyclists & hikers +Отворене мапе из заједнице за путнике, туристе, бициклисте и планинаре diff --git a/android/app/src/fdroid/play/listings/sr/title-google.txt b/android/app/src/fdroid/play/listings/sr/title-google.txt index e88ecfea27..81a0bf0eb6 100644 --- a/android/app/src/fdroid/play/listings/sr/title-google.txt +++ b/android/app/src/fdroid/play/listings/sr/title-google.txt @@ -1 +1 @@ -Offline Organic Maps Hike Bike +Organic Maps — офлајн мапе diff --git a/android/app/src/fdroid/play/listings/sr/title.txt b/android/app/src/fdroid/play/listings/sr/title.txt index 1a8a13ec0c..0ea458a7fd 100644 --- a/android/app/src/fdroid/play/listings/sr/title.txt +++ b/android/app/src/fdroid/play/listings/sr/title.txt @@ -1 +1 @@ -Organic Maps Offline Hike, Bike, GPS Navigation +Organic Maps, офлајн мапе и GPS навигатор diff --git a/android/app/src/fdroid/play/listings/sv-SE/release-notes.txt b/android/app/src/fdroid/play/listings/sv-SE/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/sv-SE/release-notes.txt +++ b/android/app/src/fdroid/play/listings/sv-SE/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/th/release-notes.txt b/android/app/src/fdroid/play/listings/th/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/th/release-notes.txt +++ b/android/app/src/fdroid/play/listings/th/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/tr-TR/release-notes.txt b/android/app/src/fdroid/play/listings/tr-TR/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/tr-TR/release-notes.txt +++ b/android/app/src/fdroid/play/listings/tr-TR/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/uk/release-notes.txt b/android/app/src/fdroid/play/listings/uk/release-notes.txt index 5c73dbb745..c34b38352c 100644 --- a/android/app/src/fdroid/play/listings/uk/release-notes.txt +++ b/android/app/src/fdroid/play/listings/uk/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• Нові дані OpenStreetMap від 22 листопада +• Адреса підсвічюється в результатах пошуку +• Історія пошука сортується за часом останнього використання +• Виправлення падіння на деяких старих девайсах із Mali-T GPU +• Деякі покращення якості пошуку, уточнення перекладів, виправлення помилок diff --git a/android/app/src/fdroid/play/listings/ur/release-notes.txt b/android/app/src/fdroid/play/listings/ur/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/ur/release-notes.txt +++ b/android/app/src/fdroid/play/listings/ur/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/vi/release-notes.txt b/android/app/src/fdroid/play/listings/vi/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/vi/release-notes.txt +++ b/android/app/src/fdroid/play/listings/vi/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/zh-CN/release-notes.txt b/android/app/src/fdroid/play/listings/zh-CN/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/zh-CN/release-notes.txt +++ b/android/app/src/fdroid/play/listings/zh-CN/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/zh-HK/release-notes.txt b/android/app/src/fdroid/play/listings/zh-HK/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/zh-HK/release-notes.txt +++ b/android/app/src/fdroid/play/listings/zh-HK/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/listings/zh-TW/release-notes.txt b/android/app/src/fdroid/play/listings/zh-TW/release-notes.txt index 5c73dbb745..05f86f237b 100644 --- a/android/app/src/fdroid/play/listings/zh-TW/release-notes.txt +++ b/android/app/src/fdroid/play/listings/zh-TW/release-notes.txt @@ -1,5 +1,5 @@ -- Edit Tracks: Change name, color, list and description of tracks, or delete them. -- Open in Another App: Select any map point in Organic Maps and open in apps like taxi, delivery, transport, or other map apps. -- Map Data: Updated as of October 1, now with added isolines for Egypt and improved for Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. -- Right-to-left languages: Enhanced the place page, search, and about screens. +• New OpenStreetMap data as of November 22 +• Highlight matched part of the address in search results +• Sort search history by last usage time +• Fixed start-up crashes for some older devices with Mali-T GPUs +• Other search improvements, translation updates & bug fixes diff --git a/android/app/src/fdroid/play/version.yaml b/android/app/src/fdroid/play/version.yaml index f048b4c36d..745d42c337 100644 --- a/android/app/src/fdroid/play/version.yaml +++ b/android/app/src/fdroid/play/version.yaml @@ -1 +1 @@ -version: 2024.09.08-7-FDroid+24090807 +version: 2024.11.27-12-FDroid+24112712 diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index ddc4abbd4c..67ed66ef78 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -443,6 +443,8 @@ android:label="@string/driving_options_title"/> + const & f) { - ASSERT(dynamic_cast(f.get()) != nullptr, ()); - return static_cast(f.get()); + return dynamic_cast(f.get()); } } // namespace @@ -176,7 +175,7 @@ bool Framework::DestroySurfaceOnDetach() } bool Framework::CreateDrapeEngine(JNIEnv * env, jobject jSurface, int densityDpi, bool firstLaunch, - bool launchByDeepLink, uint32_t appVersionCode) + bool launchByDeepLink, uint32_t appVersionCode, bool isCustomROM) { // Vulkan is supported only since Android 8.0, because some Android devices with Android 7.x // have fatal driver issue, which can lead to process termination and whole OS destabilization. @@ -188,57 +187,51 @@ bool Framework::CreateDrapeEngine(JNIEnv * env, jobject jSurface, int densityDpi if (vulkanForbidden) LOG(LWARNING, ("Vulkan API is forbidden on this device.")); + m_vulkanContextFactory.reset(); + m_oglContextFactory.reset(); + ::Framework::DrapeCreationParams p; + if (m_work.LoadPreferredGraphicsAPI() == dp::ApiVersion::Vulkan && !vulkanForbidden) { - m_vulkanContextFactory = - make_unique_dp(appVersionCode, sdkVersion); - if (!CastFactory(m_vulkanContextFactory)->IsVulkanSupported()) + auto vkFactory = make_unique_dp(appVersionCode, sdkVersion, isCustomROM); + if (!vkFactory->IsVulkanSupported()) { LOG(LWARNING, ("Vulkan API is not supported.")); - m_vulkanContextFactory.reset(); } - - if (m_vulkanContextFactory) + else { - auto f = CastFactory(m_vulkanContextFactory); - f->SetSurface(env, jSurface); - if (!f->IsValid()) + vkFactory->SetSurface(env, jSurface); + if (!vkFactory->IsValid()) { LOG(LWARNING, ("Invalid Vulkan API context.")); - m_vulkanContextFactory.reset(); + } + else + { + p.m_apiVersion = dp::ApiVersion::Vulkan; + p.m_surfaceWidth = vkFactory->GetWidth(); + p.m_surfaceHeight = vkFactory->GetHeight(); + + m_vulkanContextFactory = std::move(vkFactory); } } } - AndroidOGLContextFactory * oglFactory = nullptr; if (!m_vulkanContextFactory) { - m_oglContextFactory = make_unique_dp( - new AndroidOGLContextFactory(env, jSurface)); - oglFactory = m_oglContextFactory->CastFactory(); + auto oglFactory = make_unique_dp(env, jSurface); if (!oglFactory->IsValid()) { LOG(LWARNING, ("Invalid GL context.")); return false; } - } - - ::Framework::DrapeCreationParams p; - if (m_vulkanContextFactory) - { - auto f = CastFactory(m_vulkanContextFactory); - p.m_apiVersion = dp::ApiVersion::Vulkan; - p.m_surfaceWidth = f->GetWidth(); - p.m_surfaceHeight = f->GetHeight(); - } - else - { - CHECK(oglFactory != nullptr, ()); p.m_apiVersion = oglFactory->IsSupportedOpenGLES3() ? dp::ApiVersion::OpenGLES3 : dp::ApiVersion::OpenGLES2; p.m_surfaceWidth = oglFactory->GetWidth(); p.m_surfaceHeight = oglFactory->GetHeight(); + + m_oglContextFactory = make_unique_dp(oglFactory.release()); } + p.m_visualScale = static_cast(dp::VisualScale(densityDpi)); // Drape doesn't care about Editor vs Api mode differences. p.m_isChoosePositionMode = m_isChoosePositionMode != ChoosePositionMode::None; @@ -450,6 +443,16 @@ void Framework::Get3dMode(bool & allow3d, bool & allow3dBuildings) m_work.Load3dMode(allow3d, allow3dBuildings); } +void Framework::SetMapLanguageCode(std::string const & languageCode) +{ + m_work.SetMapLanguageCode(languageCode); +} + +std::string Framework::GetMapLanguageCode() +{ + return m_work.GetMapLanguageCode(); +} + void Framework::SetChoosePositionMode(ChoosePositionMode mode, bool isBusiness, m2::PointD const * optionalPosition) { m_isChoosePositionMode = mode; @@ -928,13 +931,15 @@ Java_app_organicmaps_Framework_nativePlacePageActivationListener(JNIEnv *env, jc jni::TScopedLocalRef placePageDataRef(env, nullptr); if (info.IsTrack()) { - auto const elevationInfo = frm()->GetBookmarkManager().MakeElevationInfo(info.GetTrackId()); - placePageDataRef.reset(usermark_helper::CreateElevationInfo(env, elevationInfo)); + // todo: (KK) implement elevation info handling for the proper track selection + auto const & track = frm()->GetBookmarkManager().GetTrack(info.GetTrackId()); + auto const & elevationInfo = track->GetElevationInfo(); + if (elevationInfo.has_value()) + placePageDataRef.reset(usermark_helper::CreateElevationInfo(env, elevationInfo.value())); } - else - { + if (!placePageDataRef) placePageDataRef.reset(usermark_helper::CreateMapObject(env, info)); - } + env->CallVoidMethod(g_placePageActivationListener, activatedId, placePageDataRef.get()); }; auto const closePlacePage = [deactivateId]() @@ -1979,13 +1984,12 @@ Java_app_organicmaps_Framework_nativeMemoryWarning(JNIEnv *, jclass) JNIEXPORT jstring JNICALL Java_app_organicmaps_Framework_nativeGetKayakHotelLink(JNIEnv * env, jclass, jstring countryIsoCode, jstring uri, - jlong firstDaySec, jlong lastDaySec, jboolean isReferral) + jlong firstDaySec, jlong lastDaySec) { string const url = osm::GetKayakHotelURLFromURI(jni::ToNativeString(env, countryIsoCode), jni::ToNativeString(env, uri), static_cast(firstDaySec), - static_cast(lastDaySec), - isReferral); + static_cast(lastDaySec)); return url.empty() ? nullptr : jni::ToJavaString(env, url); } diff --git a/android/app/src/main/cpp/app/organicmaps/Framework.hpp b/android/app/src/main/cpp/app/organicmaps/Framework.hpp index b7bc59ef78..13a4c2a36b 100644 --- a/android/app/src/main/cpp/app/organicmaps/Framework.hpp +++ b/android/app/src/main/cpp/app/organicmaps/Framework.hpp @@ -99,7 +99,7 @@ namespace android void OnCompassUpdated(location::CompassInfo const & info, bool forceRedraw); bool CreateDrapeEngine(JNIEnv * env, jobject jSurface, int densityDpi, bool firstLaunch, - bool launchByDeepLink, uint32_t appVersionCode); + bool launchByDeepLink, uint32_t appVersionCode, bool isCustomROM); bool IsDrapeEngineCreated() const; void UpdateDpi(int dpi); bool DestroySurfaceOnDetach(); @@ -191,6 +191,9 @@ namespace android void Set3dMode(bool allow3d, bool allow3dBuildings); void Get3dMode(bool & allow3d, bool & allow3dBuildings); + void SetMapLanguageCode(std::string const & languageCode); + std::string GetMapLanguageCode(); + void SetChoosePositionMode(ChoosePositionMode mode, bool isBusiness, m2::PointD const * optionalPosition); ChoosePositionMode GetChoosePositionMode(); diff --git a/android/app/src/main/cpp/app/organicmaps/Map.cpp b/android/app/src/main/cpp/app/organicmaps/Map.cpp index a73ff1d1bb..2062ba8abd 100644 --- a/android/app/src/main/cpp/app/organicmaps/Map.cpp +++ b/android/app/src/main/cpp/app/organicmaps/Map.cpp @@ -27,10 +27,11 @@ Java_app_organicmaps_Map_nativeCreateEngine(JNIEnv * env, jclass, jobject surface, jint density, jboolean firstLaunch, jboolean isLaunchByDeepLink, - jint appVersionCode) + jint appVersionCode, + jboolean isCustomROM) { return g_framework->CreateDrapeEngine(env, surface, density, firstLaunch, isLaunchByDeepLink, - base::asserted_cast(appVersionCode)); + base::asserted_cast(appVersionCode), isCustomROM); } JNIEXPORT jboolean JNICALL diff --git a/android/app/src/main/cpp/app/organicmaps/SearchEngine.cpp b/android/app/src/main/cpp/app/organicmaps/SearchEngine.cpp index af2f846bd8..90fc262910 100644 --- a/android/app/src/main/cpp/app/organicmaps/SearchEngine.cpp +++ b/android/app/src/main/cpp/app/organicmaps/SearchEngine.cpp @@ -81,6 +81,17 @@ jobject ToJavaResult(Result const & result, search::ProductInfo const & productI } env->ReleaseIntArrayElements(ranges.get(), rawArr, 0); + jni::TScopedLocalIntArrayRef descRanges(env, env->NewIntArray( + static_cast(result.GetDescHighlightRangesCount() * 2))); + jint * rawArr2 = env->GetIntArrayElements(descRanges, nullptr); + for (size_t i = 0; i < result.GetDescHighlightRangesCount(); i++) + { + auto const & range = result.GetDescHighlightRange(i); + rawArr2[2 * i] = range.first; + rawArr2[2 * i + 1] = range.second; + } + env->ReleaseIntArrayElements(descRanges.get(), rawArr2, 0); + ms::LatLon ll = ms::LatLon::Zero(); if (result.HasPoint()) ll = mercator::ToLatLon(result.GetFeatureCenter()); @@ -89,7 +100,7 @@ jobject ToJavaResult(Result const & result, search::ProductInfo const & productI { jni::TScopedLocalRef name(env, jni::ToJavaString(env, result.GetString())); jni::TScopedLocalRef suggest(env, jni::ToJavaString(env, result.GetSuggestionString())); - return env->NewObject(g_resultClass, g_suggestConstructor, name.get(), suggest.get(), ll.m_lat, ll.m_lon, ranges.get()); + return env->NewObject(g_resultClass, g_suggestConstructor, name.get(), suggest.get(), ll.m_lat, ll.m_lon, ranges.get(),descRanges.get()); } platform::Distance distance; @@ -125,7 +136,7 @@ jobject ToJavaResult(Result const & result, search::ProductInfo const & productI 0/*static_cast(result.GetRankingInfo().m_popularity)*/)); return env->NewObject(g_resultClass, g_resultConstructor, name.get(), desc.get(), ll.m_lat, ll.m_lon, - ranges.get(), popularity.get()); + ranges.get(), descRanges.get(), popularity.get()); } jobjectArray BuildSearchResults(vector const & productInfo, @@ -232,9 +243,9 @@ extern "C" g_resultClass = jni::GetGlobalClassRef(env, "app/organicmaps/search/SearchResult"); g_resultConstructor = jni::GetConstructorID( env, g_resultClass, - "(Ljava/lang/String;Lapp/organicmaps/search/SearchResult$Description;DD[I" + "(Ljava/lang/String;Lapp/organicmaps/search/SearchResult$Description;DD[I[I" "Lapp/organicmaps/search/Popularity;)V"); - g_suggestConstructor = jni::GetConstructorID(env, g_resultClass, "(Ljava/lang/String;Ljava/lang/String;DD[I)V"); + g_suggestConstructor = jni::GetConstructorID(env, g_resultClass, "(Ljava/lang/String;Ljava/lang/String;DD[I[I)V"); g_descriptionClass = jni::GetGlobalClassRef(env, "app/organicmaps/search/SearchResult$Description"); /* Description(FeatureId featureId, String featureType, String region, Distance distance, diff --git a/android/app/src/main/cpp/app/organicmaps/SearchRecents.cpp b/android/app/src/main/cpp/app/organicmaps/SearchRecents.cpp index e7827ffeed..9d5e9f818a 100644 --- a/android/app/src/main/cpp/app/organicmaps/SearchRecents.cpp +++ b/android/app/src/main/cpp/app/organicmaps/SearchRecents.cpp @@ -16,17 +16,12 @@ extern "C" if (items.empty()) return; - auto const & pairBuilder = jni::PairBuilder::Instance(env); auto const listAddMethod = jni::ListBuilder::Instance(env).m_add; for (SearchRequest const & item : items) { - using SLR = jni::TScopedLocalRef; - SLR pair(env, pairBuilder.Create(env, SLR(env, jni::ToJavaString(env, item.first)), - SLR(env, jni::ToJavaString(env, item.second)))); - ASSERT(pair.get(), (jni::DescribeException())); - - env->CallBooleanMethod(result, listAddMethod, pair.get()); + jni::TScopedLocalRef str(env, jni::ToJavaString(env, item.second)); + env->CallBooleanMethod(result, listAddMethod, str.get()); } } diff --git a/android/app/src/main/cpp/app/organicmaps/UserMarkHelper.cpp b/android/app/src/main/cpp/app/organicmaps/UserMarkHelper.cpp index 033f844683..b4fd15e70f 100644 --- a/android/app/src/main/cpp/app/organicmaps/UserMarkHelper.cpp +++ b/android/app/src/main/cpp/app/organicmaps/UserMarkHelper.cpp @@ -75,7 +75,7 @@ jobject CreateMapObject(JNIEnv * env, place_page::Info const & info, int mapObje jni::TScopedLocalRef jTitle(env, jni::ToJavaString(env, info.GetTitle())); jni::TScopedLocalRef jSecondaryTitle(env, jni::ToJavaString(env, info.GetSecondaryTitle())); jni::TScopedLocalRef jSubtitle(env, jni::ToJavaString(env, info.GetSubtitle())); - jni::TScopedLocalRef jAddress(env, jni::ToJavaString(env, info.GetAddress())); + jni::TScopedLocalRef jAddress(env, jni::ToJavaString(env, info.GetSecondarySubtitle())); jni::TScopedLocalRef jApiId(env, jni::ToJavaString(env, parseApi ? info.GetApiUrl() : "")); jni::TScopedLocalRef jWikiDescription(env, jni::ToJavaString(env, info.GetWikiDescription())); jobject mapObject = @@ -119,7 +119,7 @@ jobject CreateBookmark(JNIEnv *env, const place_page::Info &info, jni::TScopedLocalRef jTitle(env, jni::ToJavaString(env, info.GetTitle())); jni::TScopedLocalRef jSecondaryTitle(env, jni::ToJavaString(env, info.GetSecondaryTitle())); jni::TScopedLocalRef jSubtitle(env, jni::ToJavaString(env, info.GetSubtitle())); - jni::TScopedLocalRef jAddress(env, jni::ToJavaString(env, info.GetAddress())); + jni::TScopedLocalRef jAddress(env, jni::ToJavaString(env, info.GetSecondarySubtitle())); jni::TScopedLocalRef jWikiDescription(env, jni::ToJavaString(env, info.GetWikiDescription())); jobject mapObject = env->NewObject( g_bookmarkClazz, ctorId, jFeatureId.get(), static_cast(categoryId), @@ -140,7 +140,7 @@ jobject CreateElevationPoint(JNIEnv * env, ElevationInfo::Point const & point) static jmethodID const pointCtorId = jni::GetConstructorID(env, pointClass, "(DI)V"); return env->NewObject(pointClass, pointCtorId, static_cast(point.m_distance), - static_cast(point.m_altitude)); + static_cast(point.m_point.GetAltitude())); } jobjectArray ToElevationPointArray(JNIEnv * env, ElevationInfo::Points const & points) @@ -164,16 +164,14 @@ jobject CreateElevationInfo(JNIEnv * env, ElevationInfo const & info) jni::GetConstructorID(env, g_elevationInfoClazz, "(JLjava/lang/String;Ljava/lang/String;" "[Lapp/organicmaps/bookmarks/data/ElevationInfo$Point;" "IIIIIJ)V"); - jni::TScopedLocalRef jName(env, jni::ToJavaString(env, info.GetName())); jni::TScopedLocalObjectArrayRef jPoints(env, ToElevationPointArray(env, info.GetPoints())); - return env->NewObject(g_elevationInfoClazz, ctorId, static_cast(info.GetId()), - jName.get(), jPoints.get(), + return env->NewObject(g_elevationInfoClazz, ctorId, + jPoints.get(), static_cast(info.GetAscent()), static_cast(info.GetDescent()), static_cast(info.GetMinAltitude()), static_cast(info.GetMaxAltitude()), - static_cast(info.GetDifficulty()), - static_cast(info.GetDuration())); + static_cast(info.GetDifficulty())); } jobject CreateMapObject(JNIEnv * env, place_page::Info const & info) diff --git a/android/app/src/main/cpp/app/organicmaps/bookmarks/data/BookmarkManager.cpp b/android/app/src/main/cpp/app/organicmaps/bookmarks/data/BookmarkManager.cpp index 072b37d9a4..bf237a4382 100644 --- a/android/app/src/main/cpp/app/organicmaps/bookmarks/data/BookmarkManager.cpp +++ b/android/app/src/main/cpp/app/organicmaps/bookmarks/data/BookmarkManager.cpp @@ -900,6 +900,7 @@ Java_app_organicmaps_bookmarks_data_BookmarkManager_nativeSetElevationActivePoin { auto & bm = frm()->GetBookmarkManager(); bm.SetElevationActivePoint(static_cast(trackId), + {0,0}, // todo(KK): replace with coordinates from the elevation profile point to show selection mark on the track static_cast(distanceInMeters)); } diff --git a/android/app/src/main/cpp/app/organicmaps/editor/Editor.cpp b/android/app/src/main/cpp/app/organicmaps/editor/Editor.cpp index c30c6b0955..a62eb8c21f 100644 --- a/android/app/src/main/cpp/app/organicmaps/editor/Editor.cpp +++ b/android/app/src/main/cpp/app/organicmaps/editor/Editor.cpp @@ -297,14 +297,14 @@ Java_app_organicmaps_editor_Editor_nativeGetNearbyStreets(JNIEnv * env, jclass c } JNIEXPORT jobjectArray JNICALL -Java_app_organicmaps_editor_Editor_nativeGetSupportedLanguages(JNIEnv * env, jclass clazz) +Java_app_organicmaps_editor_Editor_nativeGetSupportedLanguages(JNIEnv * env, jclass clazz, jboolean includeServiceLangs) { using TLang = StringUtf8Multilang::Lang; //public Language(@NonNull String code, @NonNull String name) static jclass const langClass = jni::GetGlobalClassRef(env, "app/organicmaps/editor/data/Language"); static jmethodID const langCtor = jni::GetConstructorID(env, langClass, "(Ljava/lang/String;Ljava/lang/String;)V"); - return jni::ToJavaArray(env, langClass, StringUtf8Multilang::GetSupportedLanguages(), + return jni::ToJavaArray(env, langClass, StringUtf8Multilang::GetSupportedLanguages(includeServiceLangs), [](JNIEnv * env, TLang const & lang) { jni::TScopedLocalRef const code(env, jni::ToJavaString(env, lang.m_code)); @@ -406,8 +406,10 @@ Java_app_organicmaps_editor_Editor_nativeGetAllCreatableFeatureTypes(JNIEnv * en jstring jLang) { std::string const & lang = jni::ToNativeString(env, jLang); - GetFeatureCategories().AddLanguage(lang); - return jni::ToJavaStringArray(env, GetFeatureCategories().GetAllCreatableTypeNames()); + auto & categories = GetFeatureCategories(); + categories.AddLanguage(lang); + categories.AddLanguage("en"); + return jni::ToJavaStringArray(env, categories.GetAllCreatableTypeNames()); } JNIEXPORT jobjectArray JNICALL @@ -416,9 +418,10 @@ Java_app_organicmaps_editor_Editor_nativeSearchCreatableFeatureTypes(JNIEnv * en jstring jLang) { std::string const & lang = jni::ToNativeString(env, jLang); - GetFeatureCategories().AddLanguage(lang); - return jni::ToJavaStringArray(env, - GetFeatureCategories().Search(jni::ToNativeString(env, query))); + auto & categories = GetFeatureCategories(); + categories.AddLanguage(lang); + categories.AddLanguage("en"); + return jni::ToJavaStringArray(env, categories.Search(jni::ToNativeString(env, query))); } JNIEXPORT jobjectArray JNICALL diff --git a/android/app/src/main/cpp/app/organicmaps/editor/OsmOAuth.cpp b/android/app/src/main/cpp/app/organicmaps/editor/OsmOAuth.cpp index fc18cf19f8..9987212c80 100644 --- a/android/app/src/main/cpp/app/organicmaps/editor/OsmOAuth.cpp +++ b/android/app/src/main/cpp/app/organicmaps/editor/OsmOAuth.cpp @@ -112,4 +112,10 @@ Java_app_organicmaps_editor_OsmOAuth_nativeGetHistoryUrl(JNIEnv * env, jclass, j { return jni::ToJavaString(env, OsmOAuth::ServerAuth().GetHistoryURL(jni::ToNativeString(env, user))); } + +JNIEXPORT jstring JNICALL +Java_app_organicmaps_editor_OsmOAuth_nativeGetNotesUrl(JNIEnv * env, jclass, jstring user) +{ + return jni::ToJavaString(env, OsmOAuth::ServerAuth().GetNotesURL(jni::ToNativeString(env, user))); +} } // extern "C" diff --git a/android/app/src/main/cpp/app/organicmaps/settings/MapLanguageCode.cpp b/android/app/src/main/cpp/app/organicmaps/settings/MapLanguageCode.cpp new file mode 100644 index 0000000000..3b1678cd11 --- /dev/null +++ b/android/app/src/main/cpp/app/organicmaps/settings/MapLanguageCode.cpp @@ -0,0 +1,18 @@ +#include "app/organicmaps/Framework.hpp" + +#include "platform/settings.hpp" + +extern "C" +{ +JNIEXPORT void JNICALL +Java_app_organicmaps_settings_MapLanguageCode_setMapLanguageCode(JNIEnv * env, jobject, jstring languageCode) +{ + g_framework->SetMapLanguageCode(jni::ToNativeString(env, languageCode)); +} + +JNIEXPORT jstring JNICALL +Java_app_organicmaps_settings_MapLanguageCode_getMapLanguageCode(JNIEnv * env, jobject) +{ + return jni::ToJavaString(env, g_framework->GetMapLanguageCode()); +} +} diff --git a/android/app/src/main/cpp/app/organicmaps/vulkan/android_vulkan_context_factory.cpp b/android/app/src/main/cpp/app/organicmaps/vulkan/android_vulkan_context_factory.cpp index 7eb1439885..9559d4db6a 100644 --- a/android/app/src/main/cpp/app/organicmaps/vulkan/android_vulkan_context_factory.cpp +++ b/android/app/src/main/cpp/app/organicmaps/vulkan/android_vulkan_context_factory.cpp @@ -86,7 +86,7 @@ public: }; } // namespace -AndroidVulkanContextFactory::AndroidVulkanContextFactory(uint32_t appVersionCode, int sdkVersion) +AndroidVulkanContextFactory::AndroidVulkanContextFactory(uint32_t appVersionCode, int sdkVersion, bool isCustomROM) { if (InitVulkan() == 0) { @@ -165,8 +165,7 @@ AndroidVulkanContextFactory::AndroidVulkanContextFactory(uint32_t appVersionCode dp::SupportManager::Version driverVersion{VK_VERSION_MAJOR(gpuProperties.driverVersion), VK_VERSION_MINOR(gpuProperties.driverVersion), VK_VERSION_PATCH(gpuProperties.driverVersion)}; - if (dp::SupportManager::Instance().IsVulkanForbidden(gpuProperties.deviceName, apiVersion, - driverVersion)) + if (dp::SupportManager::Instance().IsVulkanForbidden(gpuProperties.deviceName, apiVersion, driverVersion, isCustomROM)) { LOG_ERROR_VK("GPU/Driver configuration is not supported."); return; diff --git a/android/app/src/main/cpp/app/organicmaps/vulkan/android_vulkan_context_factory.hpp b/android/app/src/main/cpp/app/organicmaps/vulkan/android_vulkan_context_factory.hpp index dc6798614a..35d16f8fb6 100644 --- a/android/app/src/main/cpp/app/organicmaps/vulkan/android_vulkan_context_factory.hpp +++ b/android/app/src/main/cpp/app/organicmaps/vulkan/android_vulkan_context_factory.hpp @@ -18,7 +18,7 @@ namespace android class AndroidVulkanContextFactory : public dp::GraphicsContextFactory { public: - explicit AndroidVulkanContextFactory(uint32_t appVersionCode, int sdkVersion); + AndroidVulkanContextFactory(uint32_t appVersionCode, int sdkVersion, bool isCustomROM); ~AndroidVulkanContextFactory(); bool IsVulkanSupported() const; diff --git a/android/app/src/main/java/app/organicmaps/Framework.java b/android/app/src/main/java/app/organicmaps/Framework.java index 24cdac37ab..63d70abde3 100644 --- a/android/app/src/main/java/app/organicmaps/Framework.java +++ b/android/app/src/main/java/app/organicmaps/Framework.java @@ -456,10 +456,9 @@ public class Framework * @param uri `$HOTEL_NAME,-c$CITY_ID-h$HOTEL_ID` URI. * @param firstDaySec the epoch seconds of the first day of planned stay. * @param lastDaySec the epoch seconds of the last day of planned stay. - * @param isReferral enable referral code to help the project. * @return a URL to Kayak's hotel page. */ @Nullable public static native String nativeGetKayakHotelLink(@NonNull String countryIsoCode, @NonNull String uri, - long firstDaySec, long lastDaySec, boolean isReferral); + long firstDaySec, long lastDaySec); } diff --git a/android/app/src/main/java/app/organicmaps/Map.java b/android/app/src/main/java/app/organicmaps/Map.java index ce6ead2f53..96d4da0288 100644 --- a/android/app/src/main/java/app/organicmaps/Map.java +++ b/android/app/src/main/java/app/organicmaps/Map.java @@ -10,6 +10,7 @@ import androidx.annotation.Nullable; import app.organicmaps.display.DisplayType; import app.organicmaps.location.LocationHelper; import app.organicmaps.util.Config; +import app.organicmaps.util.ROMUtils; import app.organicmaps.util.UiUtils; import app.organicmaps.util.concurrency.UiThread; import app.organicmaps.util.log.Logger; @@ -170,7 +171,8 @@ public final class Map final LocationHelper locationHelper = LocationHelper.from(context); final boolean firstStart = locationHelper.isInFirstRun(); - if (!nativeCreateEngine(surface, surfaceDpi, firstStart, mLaunchByDeepLink, BuildConfig.VERSION_CODE)) + if (!nativeCreateEngine(surface, surfaceDpi, firstStart, mLaunchByDeepLink, + BuildConfig.VERSION_CODE, ROMUtils.isCustomROM())) { if (mCallbackUnsupported != null) mCallbackUnsupported.report(); @@ -368,7 +370,8 @@ public final class Map private static native boolean nativeCreateEngine(Surface surface, int density, boolean firstLaunch, boolean isLaunchByDeepLink, - int appVersionCode); + int appVersionCode, + boolean isCustomROM); private static native boolean nativeIsEngineCreated(); diff --git a/android/app/src/main/java/app/organicmaps/MwmActivity.java b/android/app/src/main/java/app/organicmaps/MwmActivity.java index 62ba89d994..a7d4bb8c0e 100644 --- a/android/app/src/main/java/app/organicmaps/MwmActivity.java +++ b/android/app/src/main/java/app/organicmaps/MwmActivity.java @@ -1091,6 +1091,7 @@ public class MwmActivity extends BaseMwmFragmentActivity protected void onResume() { super.onResume(); + ThemeSwitcher.INSTANCE.restart(isMapRendererActive()); refreshSearchToolbar(); setFullscreen(isFullscreen()); if (Framework.nativeGetChoosePositionMode() != Framework.ChoosePositionMode.NONE) @@ -1286,7 +1287,9 @@ public class MwmActivity extends BaseMwmFragmentActivity if (isFullscreen()) { closePlacePage(); - showFullscreenToastIfNeeded(); + // Show the toast every time so that users don't forget and don't get trapped in the FS mode. + // TODO(pastk): there are better solutions, see https://github.com/organicmaps/organicmaps/issues/9344 + Toast.makeText(this, R.string.long_tap_toast, Toast.LENGTH_LONG).show(); } } @@ -1308,16 +1311,6 @@ public class MwmActivity extends BaseMwmFragmentActivity Framework.nativeGetChoosePositionMode() == Framework.ChoosePositionMode.NONE; } - private void showFullscreenToastIfNeeded() - { - // Show the toast only once so new behaviour doesn't confuse users - if (!Config.wasLongTapToastShown(this)) - { - Toast.makeText(this, R.string.long_tap_toast, Toast.LENGTH_LONG).show(); - Config.setLongTapToastShown(this, true); - } - } - @Override public boolean onTouch(View view, MotionEvent event) { @@ -1725,7 +1718,8 @@ public class MwmActivity extends BaseMwmFragmentActivity public void openKayakLink(@NonNull String url) { - if (Config.isKayakDisclaimerAccepted() || !Config.isKayakReferralAllowed()) + // The disclaimer is not needed if a user had explicitly opted-in via the setting. + if (Config.isKayakDisclaimerAccepted() || Config.isKayakDisplayEnabled()) { Utils.openUrl(this, url); return; @@ -1735,12 +1729,16 @@ public class MwmActivity extends BaseMwmFragmentActivity mAlertDialog = new MaterialAlertDialogBuilder(this, R.style.MwmTheme_AlertDialog) .setTitle(R.string.how_to_support_us) .setMessage(R.string.dialog_kayak_disclaimer) - .setCancelable(false) + .setCancelable(true) .setPositiveButton(R.string.dialog_kayak_button, (dlg, which) -> { Config.acceptKayakDisclaimer(); Utils.openUrl(this, url); }) .setNegativeButton(R.string.cancel, null) + .setNeutralButton(R.string.dialog_kayak_disable_button, (dlg, which) -> { + Config.setKayakDisplay(false); + UiUtils.hide(findViewById(R.id.ll__place_kayak)); + }) .setOnDismissListener(dialog -> mAlertDialog = null) .show(); } diff --git a/android/app/src/main/java/app/organicmaps/base/BaseToolbarActivity.java b/android/app/src/main/java/app/organicmaps/base/BaseToolbarActivity.java index f8bf306ae2..2fd0f3e797 100644 --- a/android/app/src/main/java/app/organicmaps/base/BaseToolbarActivity.java +++ b/android/app/src/main/java/app/organicmaps/base/BaseToolbarActivity.java @@ -71,7 +71,7 @@ public abstract class BaseToolbarActivity extends BaseMwmFragmentActivity return R.id.fragment_container; } - public void stackFragment(@NonNull Class fragmentClass, + public Fragment stackFragment(@NonNull Class fragmentClass, @Nullable String title, @Nullable Bundle args) { final int resId = getFragmentContentResId(); @@ -99,6 +99,8 @@ public abstract class BaseToolbarActivity extends BaseMwmFragmentActivity toolbar.setTitle(title); } } + + return fragment; } @Override diff --git a/android/app/src/main/java/app/organicmaps/bookmarks/BookmarkListAdapter.java b/android/app/src/main/java/app/organicmaps/bookmarks/BookmarkListAdapter.java index 7d85f6cf85..e7d86df6bf 100644 --- a/android/app/src/main/java/app/organicmaps/bookmarks/BookmarkListAdapter.java +++ b/android/app/src/main/java/app/organicmaps/bookmarks/BookmarkListAdapter.java @@ -69,6 +69,11 @@ public class BookmarkListAdapter extends RecyclerView.Adapter onMoreButtonClicked(text, moreBtn)); moreBtn.setOnClickListener(v -> onMoreButtonClicked(text, moreBtn)); title.setOnClickListener(v -> onMoreButtonClicked(text, moreBtn)); - author.setOnClickListener(v -> onMoreButtonClicked(text, moreBtn)); break; } diff --git a/android/app/src/main/java/app/organicmaps/bookmarks/BookmarksListFragment.java b/android/app/src/main/java/app/organicmaps/bookmarks/BookmarksListFragment.java index 27dc5437d1..19fda9e02a 100644 --- a/android/app/src/main/java/app/organicmaps/bookmarks/BookmarksListFragment.java +++ b/android/app/src/main/java/app/organicmaps/bookmarks/BookmarksListFragment.java @@ -810,7 +810,7 @@ public class BookmarksListFragment extends BaseMwmRecyclerFragment= 0) mCategory = categories.get(index); } + + @Override + public void invalidate() + { + onChanged(); + } } diff --git a/android/app/src/main/java/app/organicmaps/bookmarks/data/Icon.java b/android/app/src/main/java/app/organicmaps/bookmarks/data/Icon.java index 4a0be0ab45..0f348d8fc7 100644 --- a/android/app/src/main/java/app/organicmaps/bookmarks/data/Icon.java +++ b/android/app/src/main/java/app/organicmaps/bookmarks/data/Icon.java @@ -69,7 +69,6 @@ public class Icon implements Parcelable toARGB(115, 115, 115), // gray toARGB(89, 115, 128) }; // bluegray - @interface BookmarkIconType {} static final int BOOKMARK_ICON_TYPE_NONE = 0; /// @note Important! Should be synced with kml/types.hpp/BookmarkIcon @@ -113,10 +112,9 @@ public class Icon implements Parcelable @PredefinedColor private final int mColor; - @BookmarkIconType private final int mType; - public Icon(@PredefinedColor int color, @BookmarkIconType int type) + public Icon(@PredefinedColor int color, int type) { mColor = color; mType = type; diff --git a/android/app/src/main/java/app/organicmaps/bookmarks/data/MapObject.java b/android/app/src/main/java/app/organicmaps/bookmarks/data/MapObject.java index 750c9bb263..869674fb74 100644 --- a/android/app/src/main/java/app/organicmaps/bookmarks/data/MapObject.java +++ b/android/app/src/main/java/app/organicmaps/bookmarks/data/MapObject.java @@ -13,7 +13,6 @@ import androidx.core.os.ParcelCompat; import app.organicmaps.Framework; import app.organicmaps.routing.RoutePointInfo; import app.organicmaps.search.Popularity; -import app.organicmaps.util.Config; import app.organicmaps.util.Utils; import app.organicmaps.widget.placepage.PlacePageData; @@ -292,8 +291,7 @@ public class MapObject implements PlacePageData final Instant firstDay = Instant.now(); final long firstDaySec = firstDay.getEpochSecond(); final long lastDaySec = firstDay.plus(1, ChronoUnit.DAYS).getEpochSecond(); - final boolean isReferral = Config.isKayakReferralAllowed(); - final String res = Framework.nativeGetKayakHotelLink(Utils.getCountryCode(), uri, firstDaySec, lastDaySec, isReferral); + final String res = Framework.nativeGetKayakHotelLink(Utils.getCountryCode(), uri, firstDaySec, lastDaySec); return res == null ? "" : res; } diff --git a/android/app/src/main/java/app/organicmaps/car/CarAppService.java b/android/app/src/main/java/app/organicmaps/car/CarAppService.java index 0a31570162..b6d9ff4570 100644 --- a/android/app/src/main/java/app/organicmaps/car/CarAppService.java +++ b/android/app/src/main/java/app/organicmaps/car/CarAppService.java @@ -27,8 +27,8 @@ import app.organicmaps.routing.NavigationService; public final class CarAppService extends androidx.car.app.CarAppService { - private static final String CHANNEL_ID = "ANDROID_AUTO"; private static final int NOTIFICATION_ID = CarAppService.class.getSimpleName().hashCode(); + public static final String ANDROID_AUTO_NOTIFICATION_CHANNEL_ID = "ANDROID_AUTO"; public static final String API_CAR_HOST = Const.AUTHORITY + ".car"; public static final String ACTION_SHOW_NAVIGATION_SCREEN = Const.ACTION_PREFIX + ".SHOW_NAVIGATION_SCREEN"; @@ -96,11 +96,12 @@ public final class CarAppService extends androidx.car.app.CarAppService private void createNotificationChannel() { final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); - final NotificationChannelCompat notificationChannel = new NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_MIN) - .setName(getString(R.string.car_notification_channel_name)) - .setLightsEnabled(false) // less annoying - .setVibrationEnabled(false) // less annoying - .build(); + final NotificationChannelCompat notificationChannel = + new NotificationChannelCompat.Builder(ANDROID_AUTO_NOTIFICATION_CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_MIN) + .setName(getString(R.string.car_notification_channel_name)) + .setLightsEnabled(false) // less annoying + .setVibrationEnabled(false) // less annoying + .build(); notificationManager.createNotificationChannel(notificationChannel); } @@ -108,7 +109,7 @@ public final class CarAppService extends androidx.car.app.CarAppService private Notification getNotification() { return NavigationService.getNotificationBuilder(this) - .setChannelId(CHANNEL_ID) + .setChannelId(ANDROID_AUTO_NOTIFICATION_CHANNEL_ID) .setContentTitle(getString(R.string.aa_connected_to_car_notification_title)) .build(); } diff --git a/android/app/src/main/java/app/organicmaps/car/CarAppSession.java b/android/app/src/main/java/app/organicmaps/car/CarAppSession.java index fd20c22a7e..781a61b383 100644 --- a/android/app/src/main/java/app/organicmaps/car/CarAppSession.java +++ b/android/app/src/main/java/app/organicmaps/car/CarAppSession.java @@ -20,14 +20,16 @@ import app.organicmaps.car.screens.ErrorScreen; import app.organicmaps.car.screens.MapPlaceholderScreen; import app.organicmaps.car.screens.MapScreen; import app.organicmaps.car.screens.PlaceScreen; -import app.organicmaps.car.screens.RequestPermissionsScreen; import app.organicmaps.car.screens.base.BaseMapScreen; +import app.organicmaps.car.screens.download.DownloadMapsScreen; import app.organicmaps.car.screens.download.DownloadMapsScreenBuilder; import app.organicmaps.car.screens.download.DownloaderHelpers; +import app.organicmaps.car.screens.permissions.RequestPermissionsScreenBuilder; import app.organicmaps.car.util.CarSensorsManager; import app.organicmaps.car.util.CurrentCountryChangedListener; import app.organicmaps.car.util.IntentUtils; import app.organicmaps.car.util.ThemeUtils; +import app.organicmaps.car.util.UserActionRequired; import app.organicmaps.display.DisplayChangedListener; import app.organicmaps.display.DisplayManager; import app.organicmaps.display.DisplayType; @@ -161,7 +163,11 @@ public final class CarAppSession extends Session implements DefaultLifecycleObse mInitFailed = false; try { - MwmApplication.from(getCarContext()).init(() -> Config.setFirstStartDialogSeen(getCarContext())); + MwmApplication.from(getCarContext()).init(() -> { + Config.setFirstStartDialogSeen(getCarContext()); + if (DownloaderHelpers.isWorldMapsDownloadNeeded()) + mScreenManager.push(new DownloadMapsScreenBuilder(getCarContext()).setDownloaderType(DownloadMapsScreenBuilder.DownloaderType.FirstLaunch).build()); + }); } catch (IOException e) { mInitFailed = true; @@ -178,11 +184,8 @@ public final class CarAppSession extends Session implements DefaultLifecycleObse final List screensStack = new ArrayList<>(); screensStack.add(new MapScreen(getCarContext(), mSurfaceRenderer)); - if (DownloaderHelpers.isWorldMapsDownloadNeeded()) - screensStack.add(new DownloadMapsScreenBuilder(getCarContext()).setDownloaderType(DownloadMapsScreenBuilder.DownloaderType.FirstLaunch).build()); - if (!LocationUtils.checkFineLocationPermission(getCarContext())) - screensStack.add(new RequestPermissionsScreen(getCarContext(), mSensorsManager::onStart)); + screensStack.add(RequestPermissionsScreenBuilder.build(getCarContext(), mSensorsManager::onStart)); if (mDisplayManager.isDeviceDisplayUsed()) { @@ -214,7 +217,7 @@ public final class CarAppSession extends Session implements DefaultLifecycleObse mSurfaceRenderer.disable(); final MapPlaceholderScreen mapPlaceholderScreen = new MapPlaceholderScreen(getCarContext()); - if (!isPermissionsOrErrorScreen(topScreen)) + if (topScreen instanceof UserActionRequired) mScreenManager.popToRoot(); mScreenManager.push(mapPlaceholderScreen); @@ -238,6 +241,10 @@ public final class CarAppSession extends Session implements DefaultLifecycleObse @Override public void onPlacePageActivated(@NonNull PlacePageData data) { + // TODO: How maps downloading can trigger place page activation? + if (DownloadMapsScreen.MARKER.equals(mScreenManager.getTop().getMarker())) + return; + final MapObject mapObject = (MapObject) data; // Don't display the PlaceScreen for 'MY_POSITION' or during navigation // TODO (AndrewShkrob): Implement the 'Add stop' functionality @@ -279,9 +286,4 @@ public final class CarAppSession extends Session implements DefaultLifecycleObse mScreenManager.push(placeScreen); } } - - private boolean isPermissionsOrErrorScreen(@NonNull Screen screen) - { - return screen instanceof RequestPermissionsScreen || screen instanceof ErrorScreen; - } } diff --git a/android/app/src/main/java/app/organicmaps/car/screens/ErrorScreen.java b/android/app/src/main/java/app/organicmaps/car/screens/ErrorScreen.java index b6f9f22cf9..cb070c8f91 100644 --- a/android/app/src/main/java/app/organicmaps/car/screens/ErrorScreen.java +++ b/android/app/src/main/java/app/organicmaps/car/screens/ErrorScreen.java @@ -12,8 +12,9 @@ import androidx.car.app.model.Template; import app.organicmaps.R; import app.organicmaps.car.screens.base.BaseScreen; import app.organicmaps.car.util.Colors; +import app.organicmaps.car.util.UserActionRequired; -public class ErrorScreen extends BaseScreen +public class ErrorScreen extends BaseScreen implements UserActionRequired { @StringRes private final int mTitle; diff --git a/android/app/src/main/java/app/organicmaps/car/screens/bookmarks/BookmarksLoader.java b/android/app/src/main/java/app/organicmaps/car/screens/bookmarks/BookmarksLoader.java index 1063116d3d..c5440cd975 100644 --- a/android/app/src/main/java/app/organicmaps/car/screens/bookmarks/BookmarksLoader.java +++ b/android/app/src/main/java/app/organicmaps/car/screens/bookmarks/BookmarksLoader.java @@ -21,6 +21,7 @@ import app.organicmaps.bookmarks.data.BookmarkCategory; import app.organicmaps.bookmarks.data.BookmarkInfo; import app.organicmaps.bookmarks.data.BookmarkManager; import app.organicmaps.bookmarks.data.Icon; +import app.organicmaps.bookmarks.data.SortedBlock; import app.organicmaps.car.util.Colors; import app.organicmaps.car.util.RoutingHelpers; import app.organicmaps.location.LocationHelper; @@ -29,11 +30,14 @@ import app.organicmaps.util.Graphics; import app.organicmaps.util.concurrency.ThreadPool; import app.organicmaps.util.concurrency.UiThread; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.concurrent.Future; -public class BookmarksLoader +class BookmarksLoader implements BookmarkManager.BookmarksSortingListener { public interface OnBookmarksLoaded { @@ -47,32 +51,102 @@ public class BookmarksLoader // each row contains a unique icon, resulting in serialization of each icon. private static final int MAX_BOOKMARKS_SIZE = 50; - public static void load(@NonNull CarContext carContext, @NonNull BookmarkCategory bookmarkCategory, @NonNull OnBookmarksLoaded onBookmarksLoaded) + @Nullable + private Future mBookmarkLoaderTask = null; + + @NonNull + private final CarContext mCarContext; + + @NonNull + private final OnBookmarksLoaded mOnBookmarksLoaded; + + private final long mBookmarkCategoryId; + private final int mBookmarksListSize; + + public BookmarksLoader(@NonNull CarContext carContext, @NonNull BookmarkCategory bookmarkCategory, @NonNull OnBookmarksLoaded onBookmarksLoaded) { - UiThread.run(() -> { - final ConstraintManager constraintManager = carContext.getCarService(ConstraintManager.class); - final int maxCategoriesSize = constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST); - final long bookmarkCategoryId = bookmarkCategory.getId(); - final int bookmarkCategoriesSize = Math.min(bookmarkCategory.getBookmarksCount(), Math.min(maxCategoriesSize, MAX_BOOKMARKS_SIZE)); + final ConstraintManager constraintManager = carContext.getCarService(ConstraintManager.class); + final int maxCategoriesSize = constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST); - final BookmarkInfo[] bookmarks = new BookmarkInfo[bookmarkCategoriesSize]; - for (int i = 0; i < bookmarkCategoriesSize; ++i) - { - final long id = BookmarkManager.INSTANCE.getBookmarkIdByPosition(bookmarkCategoryId, i); - bookmarks[i] = new BookmarkInfo(bookmarkCategoryId, id); - } + mCarContext = carContext; + mOnBookmarksLoaded = onBookmarksLoaded; + mBookmarkCategoryId = bookmarkCategory.getId(); + mBookmarksListSize = Math.min(bookmarkCategory.getBookmarksCount(), Math.min(maxCategoriesSize, MAX_BOOKMARKS_SIZE)); + } - ThreadPool.getWorker().submit(() -> { - final ItemList bookmarksList = createBookmarksList(carContext, bookmarks); - UiThread.run(() -> onBookmarksLoaded.onBookmarksLoaded(bookmarksList)); + public void load() + { + UiThread.runLater(() -> { + BookmarkManager.INSTANCE.addSortingListener(this); + if (sortBookmarks()) + return; + + final List bookmarkIds = new ArrayList<>(); + for (int i = 0; i < mBookmarksListSize; ++i) + bookmarkIds.add(BookmarkManager.INSTANCE.getBookmarkIdByPosition(mBookmarkCategoryId, i)); + loadBookmarks(bookmarkIds); + }); + } + + public void cancel() + { + BookmarkManager.INSTANCE.removeSortingListener(this); + if (mBookmarkLoaderTask != null) + { + mBookmarkLoaderTask.cancel(true); + mBookmarkLoaderTask = null; + } + } + + /** + * Calls BookmarkManager to sort bookmarks. + * + * @return false if the sorting not needed or can't be done. + */ + private boolean sortBookmarks() + { + if (!BookmarkManager.INSTANCE.hasLastSortingType(mBookmarkCategoryId)) + return false; + + final int sortingType = BookmarkManager.INSTANCE.getLastSortingType(mBookmarkCategoryId); + if (sortingType < 0) + return false; + + final Location loc = LocationHelper.from(mCarContext).getSavedLocation(); + final boolean hasMyPosition = loc != null; + if (!hasMyPosition && sortingType == BookmarkManager.SORT_BY_DISTANCE) + return false; + + final double lat = hasMyPosition ? loc.getLatitude() : 0; + final double lon = hasMyPosition ? loc.getLongitude() : 0; + + BookmarkManager.INSTANCE.getSortedCategory(mBookmarkCategoryId, sortingType, hasMyPosition, lat, lon, 0); + + return true; + } + + private void loadBookmarks(@NonNull List bookmarksIds) + { + final BookmarkInfo[] bookmarks = new BookmarkInfo[mBookmarksListSize]; + for (int i = 0; i < mBookmarksListSize && i < bookmarksIds.size(); ++i) + { + final long id = bookmarksIds.get(i); + bookmarks[i] = new BookmarkInfo(mBookmarkCategoryId, id); + } + + mBookmarkLoaderTask = ThreadPool.getWorker().submit(() -> { + final ItemList bookmarksList = createBookmarksList(bookmarks); + UiThread.run(() -> { + cancel(); + mOnBookmarksLoaded.onBookmarksLoaded(bookmarksList); }); }); } @NonNull - private static ItemList createBookmarksList(@NonNull CarContext carContext, @NonNull BookmarkInfo[] bookmarks) + private ItemList createBookmarksList(@NonNull BookmarkInfo[] bookmarks) { - final Location location = LocationHelper.from(carContext).getSavedLocation(); + final Location location = LocationHelper.from(mCarContext).getSavedLocation(); final ItemList.Builder builder = new ItemList.Builder(); final Map iconsCache = new HashMap<>(); for (final BookmarkInfo bookmarkInfo : bookmarks) @@ -91,7 +165,7 @@ public class BookmarksLoader R.dimen.track_circle_size, icon.getResId(), R.dimen.bookmark_icon_size, - carContext); + mCarContext); final CarIcon carIcon = new CarIcon.Builder(IconCompat.createWithBitmap(Graphics.drawableToBitmap(drawable))).build(); iconsCache.put(icon, carIcon); } @@ -105,20 +179,31 @@ public class BookmarksLoader @NonNull private static CharSequence getDescription(@NonNull BookmarkInfo bookmark, @Nullable Location location) { - final SpannableStringBuilder result = new SpannableStringBuilder(" "); + final SpannableStringBuilder result = new SpannableStringBuilder(""); if (location != null) { + result.append(" "); final Distance distance = bookmark.getDistance(location.getLatitude(), location.getLongitude(), 0.0); result.setSpan(DistanceSpan.create(RoutingHelpers.createDistance(distance)), 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); result.setSpan(ForegroundCarColorSpan.create(Colors.DISTANCE), 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } - if (!bookmark.getFeatureType().isEmpty()) - { + if (!bookmark.getFeatureType().isEmpty()) + { + if (result.length() > 0) result.append(" • "); - result.append(bookmark.getFeatureType()); - } + result.append(bookmark.getFeatureType()); } return result; } + + @Override + public void onBookmarksSortingCompleted(@NonNull SortedBlock[] sortedBlocks, long timestamp) + { + final List bookmarkIds = new ArrayList<>(); + for (final SortedBlock block : sortedBlocks) + bookmarkIds.addAll(block.getBookmarkIds()); + loadBookmarks(bookmarkIds); + } } diff --git a/android/app/src/main/java/app/organicmaps/car/screens/bookmarks/BookmarksScreen.java b/android/app/src/main/java/app/organicmaps/car/screens/bookmarks/BookmarksScreen.java index fcaf96ebbd..1263596781 100644 --- a/android/app/src/main/java/app/organicmaps/car/screens/bookmarks/BookmarksScreen.java +++ b/android/app/src/main/java/app/organicmaps/car/screens/bookmarks/BookmarksScreen.java @@ -4,12 +4,16 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.car.app.CarContext; import androidx.car.app.model.Action; +import androidx.car.app.model.CarIcon; import androidx.car.app.model.Header; import androidx.car.app.model.ItemList; import androidx.car.app.model.ListTemplate; import androidx.car.app.model.Template; import androidx.car.app.navigation.model.MapWithContentTemplate; +import androidx.core.graphics.drawable.IconCompat; +import androidx.lifecycle.LifecycleOwner; +import app.organicmaps.R; import app.organicmaps.bookmarks.data.BookmarkCategory; import app.organicmaps.car.SurfaceRenderer; import app.organicmaps.car.screens.base.BaseMapScreen; @@ -20,13 +24,19 @@ public class BookmarksScreen extends BaseMapScreen @NonNull private final BookmarkCategory mBookmarkCategory; + @NonNull + private final BookmarksLoader mBookmarksLoader; + @Nullable private ItemList mBookmarksList = null; + private boolean mIsOnSortingScreen = false; + public BookmarksScreen(@NonNull CarContext carContext, @NonNull SurfaceRenderer surfaceRenderer, @NonNull BookmarkCategory bookmarkCategory) { super(carContext, surfaceRenderer); mBookmarkCategory = bookmarkCategory; + mBookmarksLoader = new BookmarksLoader(carContext, mBookmarkCategory, this::onBookmarksLoaded); } @NonNull @@ -39,12 +49,20 @@ public class BookmarksScreen extends BaseMapScreen return builder.build(); } + @Override + public void onStop(@NonNull LifecycleOwner owner) + { + if (!mIsOnSortingScreen) + mBookmarksLoader.cancel(); + } + @NonNull private Header createHeader() { final Header.Builder builder = new Header.Builder(); builder.setStartHeaderAction(Action.BACK); builder.setTitle(mBookmarkCategory.getName()); + builder.addEndHeaderAction(createSortingAction()); return builder.build(); } @@ -57,14 +75,39 @@ public class BookmarksScreen extends BaseMapScreen if (mBookmarksList == null) { builder.setLoading(true); - BookmarksLoader.load(getCarContext(), mBookmarkCategory, (bookmarksList) -> { - mBookmarksList = bookmarksList; - invalidate(); - }); + mBookmarksLoader.load(); } else builder.setSingleList(mBookmarksList); return builder.build(); } + + @NonNull + private Action createSortingAction() + { + final Action.Builder builder = new Action.Builder(); + builder.setIcon(new CarIcon.Builder(IconCompat.createWithResource(getCarContext(), R.drawable.ic_sort)).build()); + builder.setOnClickListener(() -> { + mIsOnSortingScreen = true; + getScreenManager().pushForResult(new SortingScreen(getCarContext(), getSurfaceRenderer(), mBookmarkCategory), this::onSortingResult); + }); + return builder.build(); + } + + private void onBookmarksLoaded(@NonNull ItemList bookmarksList) + { + mBookmarksList = bookmarksList; + invalidate(); + } + + private void onSortingResult(final Object result) + { + mIsOnSortingScreen = false; + if (Boolean.TRUE.equals(result)) + { + mBookmarksList = null; + invalidate(); + } + } } diff --git a/android/app/src/main/java/app/organicmaps/car/screens/bookmarks/SortingScreen.java b/android/app/src/main/java/app/organicmaps/car/screens/bookmarks/SortingScreen.java new file mode 100644 index 0000000000..0771c09372 --- /dev/null +++ b/android/app/src/main/java/app/organicmaps/car/screens/bookmarks/SortingScreen.java @@ -0,0 +1,159 @@ +package app.organicmaps.car.screens.bookmarks; + +import android.location.Location; + +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; +import androidx.car.app.CarContext; +import androidx.car.app.model.Action; +import androidx.car.app.model.CarIcon; +import androidx.car.app.model.Header; +import androidx.car.app.model.ItemList; +import androidx.car.app.model.ListTemplate; +import androidx.car.app.model.Row; +import androidx.car.app.model.Template; +import androidx.car.app.navigation.model.MapWithContentTemplate; +import androidx.core.graphics.drawable.IconCompat; +import androidx.lifecycle.LifecycleOwner; + +import app.organicmaps.R; +import app.organicmaps.bookmarks.data.BookmarkCategory; +import app.organicmaps.bookmarks.data.BookmarkManager; +import app.organicmaps.car.SurfaceRenderer; +import app.organicmaps.car.screens.base.BaseMapScreen; +import app.organicmaps.car.util.UiHelpers; +import app.organicmaps.location.LocationHelper; + +import java.util.Arrays; +import java.util.stream.IntStream; + +class SortingScreen extends BaseMapScreen +{ + private static final int DEFAULT_SORTING_TYPE = -1; + + @NonNull + private final CarIcon mRadioButtonIcon; + @NonNull + private final CarIcon mRadioButtonSelectedIcon; + + private final long mBookmarkCategoryId; + private final @BookmarkManager.SortingType int mLastSortingType; + + private @BookmarkManager.SortingType int mNewSortingType; + + public SortingScreen(@NonNull CarContext carContext, @NonNull SurfaceRenderer surfaceRenderer, @NonNull BookmarkCategory bookmarkCategory) + { + super(carContext, surfaceRenderer); + mRadioButtonIcon = new CarIcon.Builder(IconCompat.createWithResource(carContext, R.drawable.ic_radio_button_unchecked)).build(); + mRadioButtonSelectedIcon = new CarIcon.Builder(IconCompat.createWithResource(carContext, R.drawable.ic_radio_button_checked)).build(); + + mBookmarkCategoryId = bookmarkCategory.getId(); + mLastSortingType = mNewSortingType = getLastSortingType(); + } + + @NonNull + @Override + public Template onGetTemplate() + { + final MapWithContentTemplate.Builder builder = new MapWithContentTemplate.Builder(); + builder.setMapController(UiHelpers.createMapController(getCarContext(), getSurfaceRenderer())); + builder.setContentTemplate(createSortingTypesListTemplate()); + return builder.build(); + } + + @Override + public void onStop(@NonNull LifecycleOwner owner) + { + super.onStop(owner); + final boolean sortingTypeChanged = mNewSortingType != mLastSortingType; + setResult(sortingTypeChanged); + } + + @NonNull + private Header createHeader() + { + final Header.Builder builder = new Header.Builder(); + builder.setStartHeaderAction(Action.BACK); + builder.setTitle(getCarContext().getString(R.string.sort_bookmarks)); + return builder.build(); + } + + @NonNull + private ListTemplate createSortingTypesListTemplate() + { + final ListTemplate.Builder builder = new ListTemplate.Builder(); + builder.setHeader(createHeader()); + + builder.setSingleList(createSortingTypesList(getAvailableSortingTypes(), getLastAvailableSortingType())); + + return builder.build(); + } + + @NonNull + private ItemList createSortingTypesList(@NonNull final @BookmarkManager.SortingType int[] availableSortingTypes, final int lastSortingType) + { + final ItemList.Builder builder = new ItemList.Builder(); + for (int type : IntStream.concat(IntStream.of(DEFAULT_SORTING_TYPE), Arrays.stream(availableSortingTypes)).toArray()) + { + final Row.Builder rowBuilder = new Row.Builder(); + rowBuilder.setTitle(getCarContext().getString(sortingTypeToStringRes(type))); + if (type == lastSortingType) + rowBuilder.setImage(mRadioButtonSelectedIcon); + else + { + rowBuilder.setImage(mRadioButtonIcon); + rowBuilder.setOnClickListener(() -> { + if (type == DEFAULT_SORTING_TYPE) + BookmarkManager.INSTANCE.resetLastSortingType(mBookmarkCategoryId); + else + BookmarkManager.INSTANCE.setLastSortingType(mBookmarkCategoryId, type); + mNewSortingType = type; + invalidate(); + }); + } + builder.addItem(rowBuilder.build()); + } + return builder.build(); + } + + @StringRes + private int sortingTypeToStringRes(@BookmarkManager.SortingType int sortingType) + { + return switch (sortingType) + { + case BookmarkManager.SORT_BY_TYPE -> R.string.by_type; + case BookmarkManager.SORT_BY_DISTANCE -> R.string.by_distance; + case BookmarkManager.SORT_BY_TIME -> R.string.by_date; + case BookmarkManager.SORT_BY_NAME -> R.string.by_name; + default -> R.string.by_default; + }; + } + + @NonNull + @BookmarkManager.SortingType + private int[] getAvailableSortingTypes() + { + final Location loc = LocationHelper.from(getCarContext()).getSavedLocation(); + final boolean hasMyPosition = loc != null; + return BookmarkManager.INSTANCE.getAvailableSortingTypes(mBookmarkCategoryId, hasMyPosition); + } + + private int getLastSortingType() + { + if (BookmarkManager.INSTANCE.hasLastSortingType(mBookmarkCategoryId)) + return BookmarkManager.INSTANCE.getLastSortingType(mBookmarkCategoryId); + return DEFAULT_SORTING_TYPE; + } + + private int getLastAvailableSortingType() + { + int currentType = getLastSortingType(); + @BookmarkManager.SortingType int[] types = getAvailableSortingTypes(); + for (@BookmarkManager.SortingType int type : types) + { + if (type == currentType) + return currentType; + } + return DEFAULT_SORTING_TYPE; + } +} diff --git a/android/app/src/main/java/app/organicmaps/car/screens/download/DownloaderHelpers.java b/android/app/src/main/java/app/organicmaps/car/screens/download/DownloaderHelpers.java index bcd4ce0c38..7c899f88fd 100644 --- a/android/app/src/main/java/app/organicmaps/car/screens/download/DownloaderHelpers.java +++ b/android/app/src/main/java/app/organicmaps/car/screens/download/DownloaderHelpers.java @@ -20,7 +20,6 @@ public final class DownloaderHelpers @SuppressWarnings("ConstantConditions") public static boolean isWorldMapsDownloadNeeded() { - // TODO: Maps are asynchronously initialized in the core. If the initialization takes a significant amount of time, the downloader screen could potentially be displayed, even if the world maps are present. if (BuildConfig.FLAVOR.equals("fdroid")) return !CountryItem.fill(WORLD_MAPS[0]).present || !CountryItem.fill(WORLD_MAPS[1]).present; return false; diff --git a/android/app/src/main/java/app/organicmaps/car/screens/download/DownloaderScreen.java b/android/app/src/main/java/app/organicmaps/car/screens/download/DownloaderScreen.java index 82c3abd90e..bf898b5e66 100644 --- a/android/app/src/main/java/app/organicmaps/car/screens/download/DownloaderScreen.java +++ b/android/app/src/main/java/app/organicmaps/car/screens/download/DownloaderScreen.java @@ -162,8 +162,8 @@ class DownloaderScreen extends BaseScreen { long downloadedSize = 0; - for (final var item : mMissingMaps.entrySet()) - downloadedSize += item.getValue().downloadedBytes; + for (final CountryItem map : mMissingMaps.values()) + downloadedSize += map.downloadedBytes; return downloadedSize + mDownloadedMapsSize; } @@ -182,7 +182,7 @@ class DownloaderScreen extends BaseScreen private void cancelMapsDownloading() { - for (final var map : mMissingMaps.entrySet()) - MapManager.nativeCancel(map.getKey()); + for (final String map : mMissingMaps.keySet()) + MapManager.nativeCancel(map); } } diff --git a/android/app/src/main/java/app/organicmaps/car/screens/permissions/RequestPermissionsActivity.java b/android/app/src/main/java/app/organicmaps/car/screens/permissions/RequestPermissionsActivity.java new file mode 100644 index 0000000000..c8e934d033 --- /dev/null +++ b/android/app/src/main/java/app/organicmaps/car/screens/permissions/RequestPermissionsActivity.java @@ -0,0 +1,72 @@ +package app.organicmaps.car.screens.permissions; + +import static android.Manifest.permission.ACCESS_COARSE_LOCATION; +import static android.Manifest.permission.ACCESS_FINE_LOCATION; + +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.provider.Settings; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.Nullable; +import androidx.core.app.NotificationManagerCompat; + +import app.organicmaps.R; +import app.organicmaps.base.BaseMwmFragmentActivity; +import app.organicmaps.util.LocationUtils; + +import java.util.Objects; + +public class RequestPermissionsActivity extends BaseMwmFragmentActivity +{ + private static final String[] LOCATION_PERMISSIONS = new String[]{ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION}; + + @Nullable + private ActivityResultLauncher mPermissionsRequest; + + @Override + protected void onSafeCreate(@Nullable Bundle savedInstanceState) + { + super.onSafeCreate(savedInstanceState); + setContentView(R.layout.activity_request_permissions); + + findViewById(R.id.btn_grant_permissions).setOnClickListener(unused -> openAppPermissionSettings()); + mPermissionsRequest = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), + (grantedPermissions) -> closeIfPermissionsGranted()); + mPermissionsRequest.launch(LOCATION_PERMISSIONS); + } + + @Override + protected void onResume() + { + super.onResume(); + closeIfPermissionsGranted(); + } + + @Override + protected void onSafeDestroy() + { + super.onSafeDestroy(); + Objects.requireNonNull(mPermissionsRequest).unregister(); + mPermissionsRequest = null; + } + + private void openAppPermissionSettings() + { + final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setData(Uri.fromParts("package", getPackageName(), null)); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + } + + private void closeIfPermissionsGranted() + { + if (!LocationUtils.checkLocationPermission(this)) + return; + + NotificationManagerCompat.from(this).cancel(RequestPermissionsScreenWithNotification.NOTIFICATION_ID); + finish(); + } +} diff --git a/android/app/src/main/java/app/organicmaps/car/screens/permissions/RequestPermissionsScreenBuilder.java b/android/app/src/main/java/app/organicmaps/car/screens/permissions/RequestPermissionsScreenBuilder.java new file mode 100644 index 0000000000..3b90a793b1 --- /dev/null +++ b/android/app/src/main/java/app/organicmaps/car/screens/permissions/RequestPermissionsScreenBuilder.java @@ -0,0 +1,29 @@ +package app.organicmaps.car.screens.permissions; + +import static android.Manifest.permission.POST_NOTIFICATIONS; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; + +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.car.app.CarContext; +import androidx.car.app.Screen; +import androidx.core.content.ContextCompat; + +import app.organicmaps.util.log.Logger; + +public class RequestPermissionsScreenBuilder +{ + private static final String TAG = RequestPermissionsScreenBuilder.class.getSimpleName(); + + public static Screen build(@NonNull CarContext carContext, @NonNull Runnable permissionsGrantedCallback) + { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && + ContextCompat.checkSelfPermission(carContext, POST_NOTIFICATIONS) != PERMISSION_GRANTED) + { + Logger.w(TAG, "Permission POST_NOTIFICATIONS is not granted, using API-based permissions request"); + return new RequestPermissionsScreenWithApi(carContext, permissionsGrantedCallback); + } + return new RequestPermissionsScreenWithNotification(carContext, permissionsGrantedCallback); + } +} diff --git a/android/app/src/main/java/app/organicmaps/car/screens/RequestPermissionsScreen.java b/android/app/src/main/java/app/organicmaps/car/screens/permissions/RequestPermissionsScreenWithApi.java similarity index 86% rename from android/app/src/main/java/app/organicmaps/car/screens/RequestPermissionsScreen.java rename to android/app/src/main/java/app/organicmaps/car/screens/permissions/RequestPermissionsScreenWithApi.java index 017d9e585b..8cd3a641e2 100644 --- a/android/app/src/main/java/app/organicmaps/car/screens/RequestPermissionsScreen.java +++ b/android/app/src/main/java/app/organicmaps/car/screens/permissions/RequestPermissionsScreenWithApi.java @@ -1,4 +1,4 @@ -package app.organicmaps.car.screens; +package app.organicmaps.car.screens.permissions; import static android.Manifest.permission.ACCESS_COARSE_LOCATION; import static android.Manifest.permission.ACCESS_FINE_LOCATION; @@ -15,21 +15,23 @@ import androidx.core.graphics.drawable.IconCompat; import androidx.lifecycle.LifecycleOwner; import app.organicmaps.R; +import app.organicmaps.car.screens.ErrorScreen; import app.organicmaps.car.screens.base.BaseScreen; import app.organicmaps.car.util.Colors; +import app.organicmaps.car.util.UserActionRequired; import app.organicmaps.util.LocationUtils; import java.util.Arrays; import java.util.List; -public class RequestPermissionsScreen extends BaseScreen +public class RequestPermissionsScreenWithApi extends BaseScreen implements UserActionRequired { private static final List LOCATION_PERMISSIONS = Arrays.asList(ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION); @NonNull private final Runnable mPermissionsGrantedCallback; - public RequestPermissionsScreen(@NonNull CarContext carContext, @NonNull Runnable permissionsGrantedCallback) + public RequestPermissionsScreenWithApi(@NonNull CarContext carContext, @NonNull Runnable permissionsGrantedCallback) { super(carContext); mPermissionsGrantedCallback = permissionsGrantedCallback; @@ -39,7 +41,7 @@ public class RequestPermissionsScreen extends BaseScreen @Override public Template onGetTemplate() { - final MessageTemplate.Builder builder = new MessageTemplate.Builder(getCarContext().getString(R.string.aa_location_permissions_request)); + final MessageTemplate.Builder builder = new MessageTemplate.Builder(getCarContext().getString(R.string.aa_request_permission_activity_text)); final Action grantPermissions = new Action.Builder() .setTitle(getCarContext().getString(R.string.aa_grant_permissions)) .setBackgroundColor(Colors.BUTTON_ACCEPT) @@ -83,4 +85,4 @@ public class RequestPermissionsScreen extends BaseScreen mPermissionsGrantedCallback.run(); finish(); } -} +} \ No newline at end of file diff --git a/android/app/src/main/java/app/organicmaps/car/screens/permissions/RequestPermissionsScreenWithNotification.java b/android/app/src/main/java/app/organicmaps/car/screens/permissions/RequestPermissionsScreenWithNotification.java new file mode 100644 index 0000000000..c9412a899c --- /dev/null +++ b/android/app/src/main/java/app/organicmaps/car/screens/permissions/RequestPermissionsScreenWithNotification.java @@ -0,0 +1,124 @@ +package app.organicmaps.car.screens.permissions; + +import android.Manifest; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Intent; +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresPermission; +import androidx.car.app.CarContext; +import androidx.car.app.model.Action; +import androidx.car.app.model.CarIcon; +import androidx.car.app.model.Header; +import androidx.car.app.model.MessageTemplate; +import androidx.car.app.model.Template; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; +import androidx.core.content.ContextCompat; +import androidx.core.graphics.drawable.IconCompat; +import androidx.lifecycle.LifecycleOwner; + +import app.organicmaps.R; +import app.organicmaps.car.CarAppService; +import app.organicmaps.car.screens.base.BaseScreen; +import app.organicmaps.car.util.UserActionRequired; +import app.organicmaps.util.LocationUtils; +import app.organicmaps.util.concurrency.ThreadPool; +import app.organicmaps.util.concurrency.UiThread; + +import java.util.concurrent.ExecutorService; + +public class RequestPermissionsScreenWithNotification extends BaseScreen implements UserActionRequired +{ + public static final int NOTIFICATION_ID = RequestPermissionsScreenWithNotification.class.getSimpleName().hashCode(); + + @NonNull + private final ExecutorService mBackgroundExecutor; + private boolean mIsPermissionCheckEnabled = true; + @NonNull + private final Runnable mPermissionsGrantedCallback; + + public RequestPermissionsScreenWithNotification(@NonNull CarContext carContext, @NonNull Runnable permissionsGrantedCallback) + { + super(carContext); + mBackgroundExecutor = ThreadPool.getWorker(); + mPermissionsGrantedCallback = permissionsGrantedCallback; + } + + @NonNull + @Override + public Template onGetTemplate() + { + final MessageTemplate.Builder builder = new MessageTemplate.Builder(getCarContext().getString(R.string.aa_location_permissions_request)); + + final Header.Builder headerBuilder = new Header.Builder(); + headerBuilder.setStartHeaderAction(Action.APP_ICON); + headerBuilder.setTitle(getCarContext().getString(R.string.aa_grant_permissions)); + builder.setHeader(headerBuilder.build()); + + builder.setIcon(new CarIcon.Builder(IconCompat.createWithResource(getCarContext(), R.drawable.ic_location_off)).build()); + return builder.build(); + } + + @Override + @RequiresPermission(value = Manifest.permission.POST_NOTIFICATIONS) + public void onStart(@NonNull LifecycleOwner owner) + { + mIsPermissionCheckEnabled = true; + mBackgroundExecutor.execute(this::checkPermissions); + sendPermissionsRequestNotification(); + } + + @Override + public void onStop(@NonNull LifecycleOwner owner) + { + mIsPermissionCheckEnabled = false; + } + + @Override + public void onDestroy(@NonNull LifecycleOwner owner) + { + NotificationManagerCompat.from(getCarContext()).cancel(NOTIFICATION_ID); + } + + private void checkPermissions() + { + if (!mIsPermissionCheckEnabled) + return; + + if (LocationUtils.checkLocationPermission(getCarContext())) + { + UiThread.runLater(() -> { + mPermissionsGrantedCallback.run(); + finish(); + }); + } + else + mBackgroundExecutor.execute(this::checkPermissions); + } + + @RequiresPermission(value = Manifest.permission.POST_NOTIFICATIONS) + private void sendPermissionsRequestNotification() + { + final int FLAG_IMMUTABLE = Build.VERSION.SDK_INT < Build.VERSION_CODES.M ? 0 : PendingIntent.FLAG_IMMUTABLE; + final Intent contentIntent = new Intent(getCarContext(), RequestPermissionsActivity.class); + final PendingIntent pendingIntent = PendingIntent.getActivity(getCarContext(), 0, contentIntent, + PendingIntent.FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE); + + final NotificationCompat.Builder builder = new NotificationCompat.Builder(getCarContext(), CarAppService.ANDROID_AUTO_NOTIFICATION_CHANNEL_ID); + builder.setCategory(NotificationCompat.CATEGORY_NAVIGATION) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setOngoing(true) + .setShowWhen(false) + .setOnlyAlertOnce(true) + .setSmallIcon(R.drawable.ic_my_location) + .setColor(ContextCompat.getColor(getCarContext(), R.color.notification)) + .setContentTitle(getCarContext().getString(R.string.aa_request_permission_notification)) + .setContentIntent(pendingIntent); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + builder.setPriority(NotificationManager.IMPORTANCE_HIGH); + NotificationManagerCompat.from(getCarContext()).notify(NOTIFICATION_ID, builder.build()); + } +} diff --git a/android/app/src/main/java/app/organicmaps/car/util/UserActionRequired.java b/android/app/src/main/java/app/organicmaps/car/util/UserActionRequired.java new file mode 100644 index 0000000000..0c20ecda66 --- /dev/null +++ b/android/app/src/main/java/app/organicmaps/car/util/UserActionRequired.java @@ -0,0 +1,7 @@ +package app.organicmaps.car.util; + +/// Marker interface for screens that require user action to proceed. +/// These screens can't be dropped from AA's screen stack. +public interface UserActionRequired +{ +} diff --git a/android/app/src/main/java/app/organicmaps/content/DataSource.java b/android/app/src/main/java/app/organicmaps/content/DataSource.java index dbeb5a2601..de246bb6b7 100644 --- a/android/app/src/main/java/app/organicmaps/content/DataSource.java +++ b/android/app/src/main/java/app/organicmaps/content/DataSource.java @@ -6,4 +6,6 @@ public interface DataSource { @NonNull D getData(); + + void invalidate(); } diff --git a/android/app/src/main/java/app/organicmaps/editor/Editor.java b/android/app/src/main/java/app/organicmaps/editor/Editor.java index 7d74db9dae..fe84ed490d 100644 --- a/android/app/src/main/java/app/organicmaps/editor/Editor.java +++ b/android/app/src/main/java/app/organicmaps/editor/Editor.java @@ -108,8 +108,7 @@ public final class Editor public static native NamesDataSource nativeGetNamesDataSource(); public static native void nativeSetNames(@NonNull LocalizedName[] names); public static native LocalizedName nativeMakeLocalizedName(String langCode, String name); - public static native Language[] nativeGetSupportedLanguages(); - + public static native Language[] nativeGetSupportedLanguages(boolean includeServiceLangs); public static native LocalizedStreet nativeGetStreet(); public static native void nativeSetStreet(LocalizedStreet street); @NonNull diff --git a/android/app/src/main/java/app/organicmaps/editor/EditorFragment.java b/android/app/src/main/java/app/organicmaps/editor/EditorFragment.java index e28c7aa40c..736c374604 100644 --- a/android/app/src/main/java/app/organicmaps/editor/EditorFragment.java +++ b/android/app/src/main/java/app/organicmaps/editor/EditorFragment.java @@ -406,7 +406,6 @@ public class EditorFragment extends BaseMwmFragment implements View.OnClickListe private void initViews(View view) { final View categoryBlock = view.findViewById(R.id.category); - categoryBlock.setOnClickListener(this); // TODO show icon and fill it when core will implement that UiUtils.hide(categoryBlock.findViewById(R.id.icon)); mCategory = categoryBlock.findViewById(R.id.name); @@ -536,8 +535,6 @@ public class EditorFragment extends BaseMwmFragment implements View.OnClickListe mParent.editStreet(); else if (id == R.id.block_cuisine) mParent.editCuisine(); - else if (id == R.id.category) - mParent.editCategory(); else if (id == R.id.more_names || id == R.id.show_additional_names) { if (!mNamesAdapter.areAdditionalLanguagesShown() || validateNames()) diff --git a/android/app/src/main/java/app/organicmaps/editor/EditorHostFragment.java b/android/app/src/main/java/app/organicmaps/editor/EditorHostFragment.java index 680152967a..bbf000e272 100644 --- a/android/app/src/main/java/app/organicmaps/editor/EditorHostFragment.java +++ b/android/app/src/main/java/app/organicmaps/editor/EditorHostFragment.java @@ -261,16 +261,6 @@ public class EditorHostFragment extends BaseMwmToolbarFragment implements View.O .commit(); } - protected void editCategory() - { - if (!mIsNewObject) - return; - - final Activity host = requireActivity(); - host.finish(); - startActivity(new Intent(host, FeatureCategoryActivity.class)); - } - private void showSearchControls(boolean showSearch) { ((SearchToolbarController) getToolbarController()).showSearchControls(showSearch); diff --git a/android/app/src/main/java/app/organicmaps/editor/LanguagesFragment.java b/android/app/src/main/java/app/organicmaps/editor/LanguagesFragment.java index 7717780da7..261eff5dfe 100644 --- a/android/app/src/main/java/app/organicmaps/editor/LanguagesFragment.java +++ b/android/app/src/main/java/app/organicmaps/editor/LanguagesFragment.java @@ -3,6 +3,7 @@ package app.organicmaps.editor; import android.os.Bundle; import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; import app.organicmaps.base.BaseMwmRecyclerFragment; import app.organicmaps.editor.data.Language; @@ -24,18 +25,24 @@ public class LanguagesFragment extends BaseMwmRecyclerFragment void onLanguageSelected(Language language); } + private Listener mListener; + @NonNull @Override protected LanguagesAdapter createAdapter() { Bundle args = getArguments(); - Set existingLanguages = new HashSet<>(args.getStringArrayList(EXISTING_LOCALIZED_NAMES)); + Set existingLanguages = args != null + ? new HashSet<>(args.getStringArrayList(EXISTING_LOCALIZED_NAMES)) + : new HashSet<>(); List languages = new ArrayList<>(); - for (Language lang : Editor.nativeGetSupportedLanguages()) + for (Language lang : Editor.nativeGetSupportedLanguages(false)) { - if (!existingLanguages.contains(lang.code)) - languages.add(lang); + if (existingLanguages.contains(lang.code)) + continue; + + languages.add(lang); } Collections.sort(languages, Comparator.comparing(lhs -> lhs.name)); @@ -43,9 +50,17 @@ public class LanguagesFragment extends BaseMwmRecyclerFragment return new LanguagesAdapter(this, languages.toArray(new Language[languages.size()])); } + public void setListener(Listener listener) + { + this.mListener = listener; + } + protected void onLanguageSelected(Language language) { - if (getParentFragment() instanceof Listener) - ((Listener) getParentFragment()).onLanguageSelected(language); + Fragment parent = getParentFragment(); + if (parent instanceof Listener) + ((Listener) parent).onLanguageSelected(language); + if (mListener != null) + mListener.onLanguageSelected(language); } } diff --git a/android/app/src/main/java/app/organicmaps/editor/OsmLoginFragment.java b/android/app/src/main/java/app/organicmaps/editor/OsmLoginFragment.java index ec56c2c842..3a6f48b1fd 100644 --- a/android/app/src/main/java/app/organicmaps/editor/OsmLoginFragment.java +++ b/android/app/src/main/java/app/organicmaps/editor/OsmLoginFragment.java @@ -57,8 +57,6 @@ public class OsmLoginFragment extends BaseMwmToolbarFragment registerButton.setOnClickListener((v) -> Utils.openUrl(requireActivity(), Constants.Url.OSM_REGISTER)); mProgress = view.findViewById(R.id.osm_login_progress); final String dataVersion = DateUtils.getShortDateFormatter().format(Framework.getDataVersion()); - ((TextView) view.findViewById(R.id.osm_presentation)) - .setText(getString(R.string.osm_presentation, dataVersion)); if (BuildConfig.FLAVOR.equals("google")) { diff --git a/android/app/src/main/java/app/organicmaps/editor/OsmOAuth.java b/android/app/src/main/java/app/organicmaps/editor/OsmOAuth.java index a8afa2f3b6..0df60c1ccb 100644 --- a/android/app/src/main/java/app/organicmaps/editor/OsmOAuth.java +++ b/android/app/src/main/java/app/organicmaps/editor/OsmOAuth.java @@ -100,6 +100,11 @@ public final class OsmOAuth return nativeGetHistoryUrl(getUsername(context)); } + public static String getNotesUrl(@NonNull Context context) + { + return nativeGetNotesUrl(getUsername(context)); + } + /* Returns 5 strings: ServerURL, ClientId, ClientSecret, Scope, RedirectUri */ @@ -133,6 +138,10 @@ public final class OsmOAuth @NonNull public static native String nativeGetHistoryUrl(String user); + @WorkerThread + @NonNull + public static native String nativeGetNotesUrl(String user); + /** * @return < 0 if failed to get changesets count. */ diff --git a/android/app/src/main/java/app/organicmaps/editor/ProfileFragment.java b/android/app/src/main/java/app/organicmaps/editor/ProfileFragment.java index f548fec4da..ee3a7a697c 100644 --- a/android/app/src/main/java/app/organicmaps/editor/ProfileFragment.java +++ b/android/app/src/main/java/app/organicmaps/editor/ProfileFragment.java @@ -12,14 +12,17 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; - +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; import app.organicmaps.R; import app.organicmaps.base.BaseMwmToolbarFragment; import app.organicmaps.util.UiUtils; import app.organicmaps.util.Utils; +import app.organicmaps.util.WindowInsetUtils; import app.organicmaps.util.concurrency.ThreadPool; import app.organicmaps.util.concurrency.UiThread; import com.google.android.material.dialog.MaterialAlertDialogBuilder; + import java.text.NumberFormat; public class ProfileFragment extends BaseMwmToolbarFragment @@ -57,7 +60,16 @@ public class ProfileFragment extends BaseMwmToolbarFragment mProfileImage = mUserInfoBlock.findViewById(R.id.user_profile_image); view.findViewById(R.id.about_osm).setOnClickListener((v) -> Utils.openUrl(requireActivity(), getString(R.string.osm_wiki_about_url))); view.findViewById(R.id.osm_history).setOnClickListener((v) -> Utils.openUrl(requireActivity(), OsmOAuth.getHistoryUrl(requireContext()))); + view.findViewById(R.id.osm_notes).setOnClickListener((v) -> Utils.openUrl(requireActivity(), OsmOAuth.getNotesUrl(requireContext()))); + View buttonsContainer = view.findViewById(R.id.buttons_container); + ViewCompat.setOnApplyWindowInsetsListener( + buttonsContainer, + new WindowInsetUtils.PaddingInsetsListener + .Builder() + .setInsetsTypeMask(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout()) + .setExcludeTop() + .build()); } private void refreshViews() diff --git a/android/app/src/main/java/app/organicmaps/maplayer/MapButtonsController.java b/android/app/src/main/java/app/organicmaps/maplayer/MapButtonsController.java index e3ae526f3b..8f53e4387d 100644 --- a/android/app/src/main/java/app/organicmaps/maplayer/MapButtonsController.java +++ b/android/app/src/main/java/app/organicmaps/maplayer/MapButtonsController.java @@ -13,6 +13,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.OptIn; import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.lifecycle.Observer; @@ -31,7 +32,6 @@ import app.organicmaps.util.Config; import app.organicmaps.util.ThemeUtils; import app.organicmaps.util.UiUtils; import app.organicmaps.util.WindowInsetUtils; -import app.organicmaps.util.WindowInsetUtils.PaddingInsetsListener; import app.organicmaps.widget.menu.MyPositionButton; import app.organicmaps.widget.placepage.PlacePageViewModel; import com.google.android.material.badge.BadgeDrawable; @@ -157,8 +157,6 @@ public class MapButtonsController extends Fragment mButtonsMap.put(MapButtons.menu, menuButton); if (helpButton != null) mButtonsMap.put(MapButtons.help, helpButton); - - ViewCompat.setOnApplyWindowInsetsListener(mFrame, PaddingInsetsListener.allSides()); return mFrame; } @@ -362,6 +360,18 @@ public class MapButtonsController extends Fragment mSearchWheel.onResume(); updateMenuBadge(); updateLayerButton(); + final WindowInsetUtils.PaddingInsetsListener insetsListener = new WindowInsetUtils.PaddingInsetsListener.Builder() + .setInsetsTypeMask(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout()) + .setAllSides() + .build(); + ViewCompat.setOnApplyWindowInsetsListener(mFrame, insetsListener); + } + + @Override + public void onPause() + { + ViewCompat.setOnApplyWindowInsetsListener(mFrame, null); + super.onPause(); } @Override diff --git a/android/app/src/main/java/app/organicmaps/routing/NavigationController.java b/android/app/src/main/java/app/organicmaps/routing/NavigationController.java index 1c4daa7360..843793731f 100644 --- a/android/app/src/main/java/app/organicmaps/routing/NavigationController.java +++ b/android/app/src/main/java/app/organicmaps/routing/NavigationController.java @@ -1,5 +1,6 @@ package app.organicmaps.routing; +import android.location.Location; import android.text.TextUtils; import android.view.View; import android.widget.ImageView; @@ -13,11 +14,13 @@ import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import app.organicmaps.Framework; import app.organicmaps.R; +import app.organicmaps.location.LocationHelper; import app.organicmaps.maplayer.traffic.TrafficManager; import app.organicmaps.util.UiUtils; import app.organicmaps.util.Utils; -import app.organicmaps.util.WindowInsetUtils; import app.organicmaps.widget.LanesView; +import app.organicmaps.widget.SpeedLimitView; +import app.organicmaps.util.WindowInsetUtils; import app.organicmaps.widget.menu.NavMenu; import com.google.android.material.bottomsheet.BottomSheetBehavior; @@ -38,6 +41,8 @@ public class NavigationController implements TrafficManager.TrafficCallback, @NonNull private final LanesView mLanesView; + @NonNull + private final SpeedLimitView mSpeedLimit; private final NavMenu mNavMenu; View.OnClickListener mOnSettingsClickListener; @@ -75,6 +80,8 @@ public class NavigationController implements TrafficManager.TrafficCallback, mLanesView = topFrame.findViewById(R.id.lanes); + mSpeedLimit = topFrame.findViewById(R.id.nav_speed_limit); + // Show a blank view below the navbar to hide the menu content final View navigationBarBackground = mFrame.findViewById(R.id.nav_bottom_sheet_nav_bar); final View nextTurnContainer = mFrame.findViewById(R.id.nav_next_turn_container); @@ -103,11 +110,13 @@ public class NavigationController implements TrafficManager.TrafficCallback, else UiUtils.hide(mCircleExit); - UiUtils.showIf(info.nextCarDirection.containsNextTurn(), mNextNextTurnFrame); + UiUtils.visibleIf(info.nextCarDirection.containsNextTurn(), mNextNextTurnFrame); if (info.nextCarDirection.containsNextTurn()) info.nextCarDirection.setNextTurnDrawable(mNextNextTurnImage); mLanesView.setLanes(info.lanes); + + updateSpeedLimit(info); } private void updatePedestrian(@NonNull RoutingInfo info) @@ -236,4 +245,14 @@ public class NavigationController implements TrafficManager.TrafficCallback, RoutingController.get().cancel(); } + private void updateSpeedLimit(@NonNull final RoutingInfo info) + { + final Location location = LocationHelper.from(mFrame.getContext()).getSavedLocation(); + if (location == null) { + mSpeedLimit.setSpeedLimitMps(0); + return; + } + mSpeedLimit.setCurrentSpeed(location.getSpeed()); + mSpeedLimit.setSpeedLimitMps(info.speedLimitMps); + } } diff --git a/android/app/src/main/java/app/organicmaps/routing/RoutingPlanController.java b/android/app/src/main/java/app/organicmaps/routing/RoutingPlanController.java index deadbcab41..691681690e 100644 --- a/android/app/src/main/java/app/organicmaps/routing/RoutingPlanController.java +++ b/android/app/src/main/java/app/organicmaps/routing/RoutingPlanController.java @@ -12,6 +12,7 @@ import androidx.annotation.IdRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; import app.organicmaps.Framework; import app.organicmaps.MwmApplication; import app.organicmaps.R; @@ -108,7 +109,11 @@ public class RoutingPlanController extends ToolbarController .getResources().getInteger(R.integer.anim_default); final View menuFrame = activity.findViewById(R.id.menu_frame); - ViewCompat.setOnApplyWindowInsetsListener(menuFrame, PaddingInsetsListener.excludeTop()); + final PaddingInsetsListener insetsListener = new PaddingInsetsListener.Builder() + .setInsetsTypeMask(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout()) + .setExcludeTop() + .build(); + ViewCompat.setOnApplyWindowInsetsListener(menuFrame, insetsListener); } @NonNull diff --git a/android/app/src/main/java/app/organicmaps/search/SearchAdapter.java b/android/app/src/main/java/app/organicmaps/search/SearchAdapter.java index fce85db5b1..f4bf8cb549 100644 --- a/android/app/src/main/java/app/organicmaps/search/SearchAdapter.java +++ b/android/app/src/main/java/app/organicmaps/search/SearchAdapter.java @@ -2,6 +2,10 @@ package app.organicmaps.search; import android.content.Context; import android.content.res.Resources; +import android.graphics.Typeface; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.StyleSpan; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -138,7 +142,7 @@ class SearchAdapter extends RecyclerView.Adapter> pairs = new ArrayList<>(); - nativeGetList(pairs); sRecents.clear(); - - for (Pair pair : pairs) - sRecents.add(pair.second); + nativeGetList(sRecents); } public static int getSize() @@ -39,7 +35,7 @@ public final class SearchRecents public static boolean add(@NonNull String query, @NonNull Context context) { - if (TextUtils.isEmpty(query) || sRecents.contains(query)) + if (TextUtils.isEmpty(query)) return false; nativeAdd(Language.getKeyboardLocale(context), query); @@ -53,7 +49,7 @@ public final class SearchRecents sRecents.clear(); } - private static native void nativeGetList(List> result); + private static native void nativeGetList(List result); private static native void nativeAdd(String locale, String query); private static native void nativeClear(); } diff --git a/android/app/src/main/java/app/organicmaps/search/SearchResult.java b/android/app/src/main/java/app/organicmaps/search/SearchResult.java index 468e29da0e..f903188ece 100644 --- a/android/app/src/main/java/app/organicmaps/search/SearchResult.java +++ b/android/app/src/main/java/app/organicmaps/search/SearchResult.java @@ -32,7 +32,7 @@ public class SearchResult public static final int OPEN_NOW_NO = 2; public static final SearchResult EMPTY = new SearchResult("", "", 0, 0, - new int[] {}); + new int[] {}, new int[] {}); // Used by JNI. @Keep @@ -77,11 +77,12 @@ public class SearchResult // Consecutive pairs of indexes (each pair contains : start index, length), specifying highlighted matches of original query in result public final int[] highlightRanges; + public final int[] descHighlightRanges; @NonNull private final Popularity mPopularity; - public SearchResult(String name, String suggestion, double lat, double lon, int[] highlightRanges) + public SearchResult(String name, String suggestion, double lat, double lon, int[] highlightRanges, int[] descHighlightRanges) { this.name = name; this.suggestion = suggestion; @@ -94,11 +95,12 @@ public class SearchResult else this.type = TYPE_SUGGEST; this.highlightRanges = highlightRanges; + this.descHighlightRanges = descHighlightRanges; mPopularity = Popularity.defaultInstance(); } public SearchResult(String name, Description description, double lat, double lon, int[] highlightRanges, - @NonNull Popularity popularity) + int[] descHighlightRanges, @NonNull Popularity popularity) { this.type = TYPE_RESULT; this.name = name; @@ -108,6 +110,7 @@ public class SearchResult this.lon = lon; this.description = description; this.highlightRanges = highlightRanges; + this.descHighlightRanges = descHighlightRanges; } @NonNull @@ -119,26 +122,39 @@ public class SearchResult return title; } + public void formatText(SpannableStringBuilder builder, int[] ranges) + { + if (ranges != null) + { + final int size = ranges.length / 2; + int index = 0; + for (int i = 0; i < size; i++) + { + final int start = ranges[index++]; + final int len = ranges[index++]; + + builder.setSpan(new StyleSpan(Typeface.BOLD), start, start + len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + } + @NonNull public Spannable getFormattedTitle(@NonNull Context context) { final String title = getTitle(context); final SpannableStringBuilder builder = new SpannableStringBuilder(title); - - if (highlightRanges != null) - { - final int size = highlightRanges.length / 2; - int index = 0; - - for (int i = 0; i < size; i++) - { - final int start = highlightRanges[index++]; - final int len = highlightRanges[index++]; - - builder.setSpan(new StyleSpan(Typeface.BOLD), start, start + len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - } + formatText(builder, highlightRanges); return builder; } + + public Spannable getFormattedAddress(@NonNull Context context) + { + final String address = description != null ? description.region : null; + final SpannableStringBuilder builder = new SpannableStringBuilder(address); + formatText(builder, descHighlightRanges); + + return builder; + } + } diff --git a/android/app/src/main/java/app/organicmaps/settings/DrivingOptionsFragment.java b/android/app/src/main/java/app/organicmaps/settings/DrivingOptionsFragment.java index 7c7c6977be..ec827f45ff 100644 --- a/android/app/src/main/java/app/organicmaps/settings/DrivingOptionsFragment.java +++ b/android/app/src/main/java/app/organicmaps/settings/DrivingOptionsFragment.java @@ -13,6 +13,7 @@ import androidx.appcompat.widget.SwitchCompat; import app.organicmaps.R; import app.organicmaps.base.BaseMwmToolbarFragment; +import app.organicmaps.routing.RoutingController; import app.organicmaps.routing.RoutingOptions; import java.util.ArrayList; @@ -74,8 +75,16 @@ public class DrivingOptionsFragment extends BaseMwmToolbarFragment @Override public boolean onBackPressed() { - requireActivity().setResult(areSettingsNotChanged() ? Activity.RESULT_CANCELED - : Activity.RESULT_OK); + if (areSettingsNotChanged()) + { + requireActivity().setResult(Activity.RESULT_CANCELED); + } + else + { + requireActivity().setResult(Activity.RESULT_OK); + RoutingController.get().rebuildLastRoute(); + } + return super.onBackPressed(); } diff --git a/android/app/src/main/java/app/organicmaps/settings/MapLanguageCode.java b/android/app/src/main/java/app/organicmaps/settings/MapLanguageCode.java new file mode 100644 index 0000000000..385d6d8dda --- /dev/null +++ b/android/app/src/main/java/app/organicmaps/settings/MapLanguageCode.java @@ -0,0 +1,7 @@ +package app.organicmaps.settings; + +public class MapLanguageCode +{ + public static native String getMapLanguageCode(); + public static native void setMapLanguageCode(String locale); +} diff --git a/android/app/src/main/java/app/organicmaps/settings/SettingsPrefsFragment.java b/android/app/src/main/java/app/organicmaps/settings/SettingsPrefsFragment.java index 3018ac65e6..b7d8e35f69 100644 --- a/android/app/src/main/java/app/organicmaps/settings/SettingsPrefsFragment.java +++ b/android/app/src/main/java/app/organicmaps/settings/SettingsPrefsFragment.java @@ -18,7 +18,9 @@ import app.organicmaps.R; import app.organicmaps.downloader.MapManager; import app.organicmaps.downloader.OnmapDownloader; import app.organicmaps.editor.OsmOAuth; +import app.organicmaps.editor.LanguagesFragment; import app.organicmaps.editor.ProfileActivity; +import app.organicmaps.editor.data.Language; import app.organicmaps.help.HelpActivity; import app.organicmaps.location.LocationHelper; import app.organicmaps.location.LocationProviderFactory; @@ -33,7 +35,9 @@ import app.organicmaps.util.log.LogsManager; import app.organicmaps.search.SearchRecents; import com.google.android.material.dialog.MaterialAlertDialogBuilder; -public class SettingsPrefsFragment extends BaseXmlSettingsFragment +import java.util.Locale; + +public class SettingsPrefsFragment extends BaseXmlSettingsFragment implements LanguagesFragment.Listener { @Override protected int getXmlResources() @@ -62,6 +66,7 @@ public class SettingsPrefsFragment extends BaseXmlSettingsFragment initPowerManagementPrefsCallbacks(); initPlayServicesPrefsCallbacks(); initSearchPrivacyPrefsCallbacks(); + initDisplayKayakPrefsCallbacks(); initScreenSleepEnabledPrefsCallbacks(); initShowOnLockScreenPrefsCallbacks(); } @@ -72,6 +77,13 @@ public class SettingsPrefsFragment extends BaseXmlSettingsFragment pref.setSummary(Config.TTS.isEnabled() ? R.string.on : R.string.off); } + private void updateMapLanguageCodeSummary() + { + final Preference pref = getPreference(getString(R.string.pref_map_locale)); + Locale locale = new Locale(MapLanguageCode.getMapLanguageCode()); + pref.setSummary(locale.getDisplayLanguage()); + } + private void updateRoutingSettingsPrefsSummary() { final Preference pref = getPreference(getString(R.string.prefs_routing)); @@ -98,6 +110,7 @@ public class SettingsPrefsFragment extends BaseXmlSettingsFragment updateProfileSettingsPrefsSummary(); updateVoiceInstructionsPrefsSummary(); updateRoutingSettingsPrefsSummary(); + updateMapLanguageCodeSummary(); } @Override @@ -118,6 +131,11 @@ public class SettingsPrefsFragment extends BaseXmlSettingsFragment { startActivity(new Intent(requireActivity(), HelpActivity.class)); } + else if (key.equals(getString(R.string.pref_map_locale))) + { + LanguagesFragment langFragment = (LanguagesFragment)getSettingsActivity().stackFragment(LanguagesFragment.class, getString(R.string.change_map_locale), null); + langFragment.setListener(this); + } } return super.onPreferenceTreeClick(preference); } @@ -284,6 +302,21 @@ public class SettingsPrefsFragment extends BaseXmlSettingsFragment }); } + private void initDisplayKayakPrefsCallbacks() + { + final TwoStatePreference pref = getPreference(getString(R.string.pref_display_kayak)); + + pref.setChecked(Config.isKayakDisplayEnabled()); + pref.setOnPreferenceChangeListener((preference, newValue) -> { + final boolean oldVal = Config.isKayakDisplayEnabled(); + final boolean newVal = (Boolean) newValue; + if (oldVal != newVal) + Config.setKayakDisplay(newVal); + + return true; + }); + } + private void init3dModePrefsCallbacks() { final TwoStatePreference pref = getPreference(getString(R.string.pref_3d_buildings)); @@ -465,11 +498,19 @@ public class SettingsPrefsFragment extends BaseXmlSettingsFragment category.removePreference(preference); } + @Override + public void onLanguageSelected(Language language) + { + MapLanguageCode.setMapLanguageCode(language.code); + getSettingsActivity().onBackPressed(); + } + enum ThemeMode { DEFAULT(R.string.theme_default), NIGHT(R.string.theme_night), - AUTO(R.string.theme_auto); + AUTO(R.string.theme_auto), + NAV_AUTO(R.string.theme_nav_auto); private final int mModeStringId; diff --git a/android/app/src/main/java/app/organicmaps/util/Config.java b/android/app/src/main/java/app/organicmaps/util/Config.java index 75b3a1b503..14eccbcead 100644 --- a/android/app/src/main/java/app/organicmaps/util/Config.java +++ b/android/app/src/main/java/app/organicmaps/util/Config.java @@ -21,6 +21,7 @@ public final class Config private static final String KEY_PREF_USE_GS = "UseGoogleServices"; private static final String KEY_MISC_DISCLAIMER_ACCEPTED = "IsDisclaimerApproved"; + private static final String KEY_PREF_KAYAK_DISPLAY = "DisplayKayak"; private static final String KEY_MISC_KAYAK_ACCEPTED = "IsKayakApproved"; private static final String KEY_MISC_LOCATION_REQUESTED = "LocationRequested"; private static final String KEY_MISC_UI_THEME = "UiTheme"; @@ -34,7 +35,6 @@ public final class Config private static final String KEY_MISC_AGPS_TIMESTAMP = "AGPSTimestamp"; private static final String KEY_DONATE_URL = "DonateUrl"; private static final String KEY_PREF_SEARCH_HISTORY = "SearchHistoryEnabled"; - private static final String KEY_PREF_LONG_TAP_TOAST_SHOWN = "LongTapToastShown"; /** * The total number of app launches. @@ -59,6 +59,12 @@ public final class Config private Config() {} + @SuppressWarnings("ConstantConditions") // BuildConfig + private static boolean isFdroid() + { + return BuildConfig.FLAVOR.equals("fdroid"); + } + private static int getInt(String key, int def) { return nativeGetInt(key, def); @@ -188,7 +194,14 @@ public final class Config public static boolean useGoogleServices() { - return getBool(KEY_PREF_USE_GS, true); + // F-droid users expect non-free networks to be disabled by default + // https://t.me/organicmaps/47334 + // Additionally, in the µG play-services-location library which is used for + // F-droid builds, GMS api availability is stubbed and always returns true. + // https://github.com/microg/GmsCore/issues/2309 + // For more details, see the discussion in + // https://github.com/organicmaps/organicmaps/pull/9575 + return getBool(KEY_PREF_USE_GS, !isFdroid()); } public static void setUseGoogleService(boolean use) @@ -206,6 +219,18 @@ public final class Config setBool(KEY_MISC_DISCLAIMER_ACCEPTED); } + public static boolean isKayakDisplayEnabled() + { + // Kayak is disabled by default in F-Droid build, + // unless a user has already accepted its disclaimer before. + return getBool(KEY_PREF_KAYAK_DISPLAY, !isFdroid() || isKayakDisclaimerAccepted()); + } + + public static void setKayakDisplay(boolean enabled) + { + setBool(KEY_PREF_KAYAK_DISPLAY, enabled); + } + public static boolean isKayakDisclaimerAccepted() { return getBool(KEY_MISC_KAYAK_ACCEPTED); @@ -216,12 +241,6 @@ public final class Config setBool(KEY_MISC_KAYAK_ACCEPTED); } - @SuppressWarnings("ConstantConditions") // BuildConfig - public static boolean isKayakReferralAllowed() - { - return !BuildConfig.FLAVOR.equals("fdroid"); - } - public static boolean isLocationRequested() { return getBool(KEY_MISC_LOCATION_REQUESTED); @@ -257,7 +276,7 @@ public final class Config { String autoTheme = MwmApplication.from(context).getString(R.string.theme_auto); String res = getString(KEY_MISC_UI_THEME_SETTINGS, autoTheme); - if (ThemeUtils.isValidTheme(context, res) || ThemeUtils.isAutoTheme(context, res)) + if (ThemeUtils.isValidTheme(context, res) || ThemeUtils.isAutoTheme(context, res) || ThemeUtils.isNavAutoTheme(context, res)) return res; return autoTheme; @@ -397,18 +416,6 @@ public final class Config .apply(); } - public static boolean wasLongTapToastShown(@NonNull Context context) - { - return MwmApplication.prefs(context).getBoolean(KEY_PREF_LONG_TAP_TOAST_SHOWN, false); - } - - public static void setLongTapToastShown(@NonNull Context context, Boolean newValue) - { - MwmApplication.prefs(context).edit() - .putBoolean(KEY_PREF_LONG_TAP_TOAST_SHOWN, newValue) - .apply(); - } - public static boolean isSearchHistoryEnabled() { return getBool(KEY_PREF_SEARCH_HISTORY, true); diff --git a/android/app/src/main/java/app/organicmaps/util/ROMUtils.java b/android/app/src/main/java/app/organicmaps/util/ROMUtils.java new file mode 100644 index 0000000000..7a0ac7ba85 --- /dev/null +++ b/android/app/src/main/java/app/organicmaps/util/ROMUtils.java @@ -0,0 +1,53 @@ +package app.organicmaps.util; + +import android.util.Log; + +import java.lang.reflect.Method; + +public class ROMUtils +{ + private static final String TAG = "ROMUtils"; + + public static boolean isCustomROM() + { + Method method = null; + try + { + Class systemProperties = Class.forName("android.os.SystemProperties"); + method = systemProperties.getMethod("get", String.class); + } + catch (Exception e) + { + Log.e(TAG, "Error getting SystemProperties: ", e); + } + + if (method == null) + return false; + + // Check common custom ROM properties + String[] customROMIndicators = { + "ro.modversion", + "ro.cm.version", // LineageOS/CyanogenMod-specific + "ro.lineage.build.version", // LineageOS + }; + + for (String prop : customROMIndicators) + { + try + { + String value = (String) method.invoke(null, prop); + if (value != null && !value.isEmpty()) + { + Log.d(TAG, "Custom ROM detected: " + prop + " = " + value); + return true; + } + } + catch (Exception e) + { + Log.e(TAG, "Error invoking method: ", e); + } + } + + return false; + } +} diff --git a/android/app/src/main/java/app/organicmaps/util/SharedPropertiesUtils.java b/android/app/src/main/java/app/organicmaps/util/SharedPropertiesUtils.java index 0d3a801afd..12ad4059ae 100644 --- a/android/app/src/main/java/app/organicmaps/util/SharedPropertiesUtils.java +++ b/android/app/src/main/java/app/organicmaps/util/SharedPropertiesUtils.java @@ -51,10 +51,12 @@ public final class SharedPropertiesUtils */ public static void emulateBadExternalStorage(@NonNull Context context) throws IOException { - SharedPreferences prefs = PreferenceManager - .getDefaultSharedPreferences(MwmApplication.from(context)); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(MwmApplication.from(context)); String key = MwmApplication.from(context).getString(R.string.pref_emulate_bad_external_storage); - if (prefs.getBoolean(key, false)) { + if (prefs.getBoolean(key, false)) + { + // Emulate one time only -> reset setting to run normally next time. + prefs.edit().putBoolean(key, false).apply(); throw new IOException("Bad external storage error injection"); } } diff --git a/android/app/src/main/java/app/organicmaps/util/ThemeSwitcher.java b/android/app/src/main/java/app/organicmaps/util/ThemeSwitcher.java index 86d7d4497c..d70055b0f9 100644 --- a/android/app/src/main/java/app/organicmaps/util/ThemeSwitcher.java +++ b/android/app/src/main/java/app/organicmaps/util/ThemeSwitcher.java @@ -32,18 +32,18 @@ public enum ThemeSwitcher String nightTheme = MwmApplication.from(mContext).getString(R.string.theme_night); String defaultTheme = MwmApplication.from(mContext).getString(R.string.theme_default); String theme = defaultTheme; + Location last = LocationHelper.from(mContext).getSavedLocation(); - if (RoutingController.get().isNavigating()) + boolean navAuto = RoutingController.get().isNavigating() && ThemeUtils.isNavAutoTheme(mContext); + + if (navAuto || ThemeUtils.isAutoTheme(mContext)) { - Location last = LocationHelper.from(mContext).getSavedLocation(); if (last == null) - { theme = Config.getCurrentUiTheme(mContext); - } else { - boolean day = Framework.nativeIsDayTime(System.currentTimeMillis() / 1000, - last.getLatitude(), last.getLongitude()); + long currentTime = System.currentTimeMillis() / 1000; + boolean day = Framework.nativeIsDayTime(currentTime, last.getLatitude(), last.getLongitude()); theme = (day ? defaultTheme : nightTheme); } } @@ -51,7 +51,7 @@ public enum ThemeSwitcher setThemeAndMapStyle(theme); UiThread.cancelDelayedTasks(mAutoThemeChecker); - if (ThemeUtils.isAutoTheme(mContext)) + if (navAuto || ThemeUtils.isAutoTheme(mContext)) UiThread.runLater(mAutoThemeChecker, CHECK_INTERVAL_MS); } }; @@ -79,7 +79,7 @@ public enum ThemeSwitcher { mRendererActive = isRendererActive; String theme = Config.getUiThemeSettings(mContext); - if (ThemeUtils.isAutoTheme(mContext, theme)) + if (ThemeUtils.isAutoTheme(mContext, theme) || ThemeUtils.isNavAutoTheme(mContext, theme)) { mAutoThemeChecker.run(); return; diff --git a/android/app/src/main/java/app/organicmaps/util/ThemeUtils.java b/android/app/src/main/java/app/organicmaps/util/ThemeUtils.java index cd82d02c48..c121c8d313 100644 --- a/android/app/src/main/java/app/organicmaps/util/ThemeUtils.java +++ b/android/app/src/main/java/app/organicmaps/util/ThemeUtils.java @@ -69,7 +69,7 @@ public final class ThemeUtils public static boolean isAutoTheme(@NonNull Context context) { - return isAutoTheme(context, Config.getCurrentUiTheme(context)); + return isAutoTheme(context, Config.getUiThemeSettings(context)); } public static boolean isAutoTheme(@NonNull Context context, String theme) @@ -78,6 +78,17 @@ public final class ThemeUtils return autoTheme.equals(theme); } + public static boolean isNavAutoTheme(@NonNull Context context) + { + return isNavAutoTheme(context, Config.getUiThemeSettings(context)); + } + + public static boolean isNavAutoTheme(@NonNull Context context, String theme) + { + String navAutoTheme = context.getString(R.string.theme_nav_auto); + return navAutoTheme.equals(theme); +} + public static boolean isValidTheme(@NonNull Context context, String theme) { String defaultTheme = context.getString(R.string.theme_default); diff --git a/android/app/src/main/java/app/organicmaps/util/WindowInsetUtils.java b/android/app/src/main/java/app/organicmaps/util/WindowInsetUtils.java index 9bde7da119..f366562223 100644 --- a/android/app/src/main/java/app/organicmaps/util/WindowInsetUtils.java +++ b/android/app/src/main/java/app/organicmaps/util/WindowInsetUtils.java @@ -138,19 +138,29 @@ public final class WindowInsetUtils public static final class PaddingInsetsListener implements OnApplyWindowInsetsListener { + private final int insetsTypeMask; private final boolean top; private final boolean bottom; private final boolean left; private final boolean right; - public PaddingInsetsListener(boolean top, boolean bottom, boolean left, boolean right) + public PaddingInsetsListener(int insetsTypeMask, boolean top, boolean bottom, boolean left, boolean right) { + this.insetsTypeMask = insetsTypeMask; this.top = top; this.bottom = bottom; this.left = left; this.right = right; } + /** + * Creates PaddingInsetsListener with default insetsTypeMask equals TYPE_SAFE_DRAWING + */ + public PaddingInsetsListener(boolean top, boolean bottom, boolean left, boolean right) + { + this(TYPE_SAFE_DRAWING, top, bottom, left, right); + } + public static PaddingInsetsListener allSides() { return new PaddingInsetsListener(true, true, true, true); @@ -170,7 +180,7 @@ public final class WindowInsetUtils @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat windowInsets) { - final Insets insets = windowInsets.getInsets(TYPE_SAFE_DRAWING); + final Insets insets = windowInsets.getInsets(insetsTypeMask); v.setPadding( left ? insets.left : v.getPaddingLeft(), top ? insets.top : v.getPaddingTop(), @@ -178,5 +188,73 @@ public final class WindowInsetUtils bottom ? insets.bottom : v.getPaddingBottom()); return windowInsets; } + + public static class Builder { + + private int mInsetsTypeMask = TYPE_SAFE_DRAWING; + private boolean mTop; + private boolean mBottom; + private boolean mLeft; + private boolean mRight; + + public Builder setInsetsTypeMask(int insetsTypeMask) + { + mInsetsTypeMask = insetsTypeMask; + return this; + } + + public Builder setAllSides() { + mTop = true; + mBottom = true; + mLeft = true; + mRight = true; + return this; + } + + public Builder setExcludeTop() { + mTop = false; + mBottom = true; + mLeft = true; + mRight = true; + return this; + } + + public Builder setExcludeBottom() { + mTop = true; + mBottom = false; + mLeft = true; + mRight = true; + return this; + } + + public Builder setTop(boolean top) + { + mTop = top; + return this; + } + + public Builder setBottom(boolean bottom) + { + mBottom = bottom; + return this; + } + + public Builder setLeft(boolean left) + { + mLeft = left; + return this; + } + + public Builder setRight(boolean right) + { + mRight = right; + return this; + } + + public PaddingInsetsListener build() + { + return new PaddingInsetsListener(mInsetsTypeMask, mTop, mBottom, mLeft, mRight); + } + } } } diff --git a/android/app/src/main/java/app/organicmaps/util/log/LogsManager.java b/android/app/src/main/java/app/organicmaps/util/log/LogsManager.java index d8d4934b3f..1fa9907c09 100644 --- a/android/app/src/main/java/app/organicmaps/util/log/LogsManager.java +++ b/android/app/src/main/java/app/organicmaps/util/log/LogsManager.java @@ -22,6 +22,7 @@ import androidx.core.content.ContextCompat; import app.organicmaps.BuildConfig; import app.organicmaps.MwmApplication; import app.organicmaps.R; +import app.organicmaps.util.ROMUtils; import app.organicmaps.util.StringUtils; import net.jcip.annotations.ThreadSafe; @@ -245,6 +246,7 @@ public final class LogsManager if (!StringUtils.toLowerCase(Build.MODEL).startsWith(StringUtils.toLowerCase(Build.MANUFACTURER))) sb.append(Build.MANUFACTURER).append(' '); sb.append(Build.MODEL).append(" (").append(Build.DEVICE).append(')'); + sb.append("\nIs custom ROM: ").append(ROMUtils.isCustomROM()); sb.append("\nSupported ABIs:"); for (String abi : Build.SUPPORTED_ABIS) sb.append(' ').append(abi); diff --git a/android/app/src/main/java/app/organicmaps/widget/SpeedLimitView.java b/android/app/src/main/java/app/organicmaps/widget/SpeedLimitView.java new file mode 100644 index 0000000000..4cc8b2ff43 --- /dev/null +++ b/android/app/src/main/java/app/organicmaps/widget/SpeedLimitView.java @@ -0,0 +1,221 @@ +package app.organicmaps.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.util.AttributeSet; +import android.util.Pair; +import android.view.View; + +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; + +import app.organicmaps.R; +import app.organicmaps.util.StringUtils; + +public class SpeedLimitView extends View +{ + private interface DefaultValues + { + @ColorInt + int BACKGROUND_COLOR = Color.WHITE; + @ColorInt + int TEXT_COLOR = Color.BLACK; + @ColorInt + int TEXT_ALERT_COLOR = Color.WHITE; + + float BORDER_WIDTH_RATIO = 0.1f; + } + + @ColorInt + private final int mBackgroundColor; + + @ColorInt + private final int mBorderColor; + + @ColorInt + private final int mAlertColor; + + @ColorInt + private final int mTextColor; + + @ColorInt + private final int mTextAlertColor; + + @NonNull + private final Paint mSignBackgroundPaint; + @NonNull + private final Paint mSignBorderPaint; + @NonNull + private final Paint mTextPaint; + + private float mWidth; + private float mHeight; + private float mBackgroundRadius; + private float mBorderRadius; + private float mBorderWidth; + + private double mSpeedLimitMps; + @Nullable + private String mSpeedLimitStr; + + private double mCurrentSpeed; + + public SpeedLimitView(Context context, @Nullable AttributeSet attrs) + { + super(context, attrs); + + try (TypedArray data = context.getTheme() + .obtainStyledAttributes(attrs, R.styleable.SpeedLimitView, 0, 0)) + { + mBackgroundColor = data.getColor(R.styleable.SpeedLimitView_BackgroundColor, DefaultValues.BACKGROUND_COLOR); + mBorderColor = data.getColor(R.styleable.SpeedLimitView_borderColor, ContextCompat.getColor(context, R.color.base_red)); + mAlertColor = data.getColor(R.styleable.SpeedLimitView_alertColor, ContextCompat.getColor(context, R.color.base_red)); + mTextColor = data.getColor(R.styleable.SpeedLimitView_textColor, DefaultValues.TEXT_COLOR); + mTextAlertColor = data.getColor(R.styleable.SpeedLimitView_textAlertColor, DefaultValues.TEXT_ALERT_COLOR); + if (isInEditMode()) + { + mSpeedLimitMps = data.getInt(R.styleable.SpeedLimitView_editModeSpeedLimit, -1); + mSpeedLimitStr = mSpeedLimitMps > 0 ? String.valueOf(((int) mSpeedLimitMps)) : null; + mCurrentSpeed = data.getInt(R.styleable.SpeedLimitView_editModeCurrentSpeed, -1); + } + } + + mSignBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mSignBackgroundPaint.setColor(mBackgroundColor); + + mSignBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mSignBorderPaint.setColor(mBorderColor); + mSignBorderPaint.setStrokeWidth(mBorderWidth); + mSignBorderPaint.setStyle(Paint.Style.STROKE); + + mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mTextPaint.setColor(mTextColor); + mTextPaint.setTextAlign(Paint.Align.CENTER); + mTextPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD)); + } + + public void setSpeedLimitMps(final double speedLimitMps) + { + if (mSpeedLimitMps == speedLimitMps) + return; + + mSpeedLimitMps = speedLimitMps; + if (mSpeedLimitMps <= 0) + { + mSpeedLimitStr = null; + setVisibility(GONE); + return; + } + + final Pair speedLimitAndUnits = StringUtils.nativeFormatSpeedAndUnits(mSpeedLimitMps); + setVisibility(VISIBLE); + mSpeedLimitStr = speedLimitAndUnits.first; + configureTextSize(); + invalidate(); + } + + public void setCurrentSpeed(final double currentSpeed) + { + mCurrentSpeed = currentSpeed; + invalidate(); + } + + @Override + protected void onDraw(@NonNull Canvas canvas) + { + super.onDraw(canvas); + + final boolean alert = mCurrentSpeed > mSpeedLimitMps && mSpeedLimitMps > 0; + + final float cx = mWidth / 2; + final float cy = mHeight / 2; + + drawSign(canvas, cx, cy, alert); + drawText(canvas, cx, cy, alert); + } + + private void drawSign(@NonNull Canvas canvas, float cx, float cy, boolean alert) + { + if (alert) + mSignBackgroundPaint.setColor(mAlertColor); + else + mSignBackgroundPaint.setColor(mBackgroundColor); + + canvas.drawCircle(cx, cy, mBackgroundRadius, mSignBackgroundPaint); + if (!alert) + { + mSignBorderPaint.setStrokeWidth(mBorderWidth); + canvas.drawCircle(cx, cy, mBorderRadius, mSignBorderPaint); + } + } + + private void drawText(@NonNull Canvas canvas, float cx, float cy, boolean alert) + { + if (mSpeedLimitStr == null) + return; + + if (alert) + mTextPaint.setColor(mTextAlertColor); + else + mTextPaint.setColor(mTextColor); + + final Rect textBounds = new Rect(); + mTextPaint.getTextBounds(mSpeedLimitStr, 0, mSpeedLimitStr.length(), textBounds); + final float textY = cy - textBounds.exactCenterY(); + canvas.drawText(mSpeedLimitStr, cx, textY, mTextPaint); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) + { + super.onSizeChanged(w, h, oldw, oldh); + + final float paddingX = (float) (getPaddingLeft() + getPaddingRight()); + final float paddingY = (float) (getPaddingTop() + getPaddingBottom()); + + mWidth = (float) w - paddingX; + mHeight = (float) h - paddingY; + mBackgroundRadius = Math.min(mWidth, mHeight) / 2; + mBorderWidth = mBackgroundRadius * 2 * DefaultValues.BORDER_WIDTH_RATIO; + mBorderRadius = mBackgroundRadius - mBorderWidth / 2; + configureTextSize(); + } + + // Apply binary search to determine the optimal text size that fits within the circular boundary. + private void configureTextSize() + { + if (mSpeedLimitStr == null) + return; + + final String text = mSpeedLimitStr; + final float textRadius = mBorderRadius - mBorderWidth; + final float textMaxSize = 2 * textRadius; + final float textMaxSizeSquared = (float) Math.pow(textMaxSize, 2); + + float lowerBound = 0; + float upperBound = textMaxSize; + float textSize = textMaxSize; + final Rect textBounds = new Rect(); + + while (lowerBound <= upperBound) + { + textSize = (lowerBound + upperBound) / 2; + mTextPaint.setTextSize(textSize); + mTextPaint.getTextBounds(text, 0, text.length(), textBounds); + + if (Math.pow(textBounds.width(), 2) + Math.pow(textBounds.height(), 2) <= textMaxSizeSquared) + lowerBound = textSize + 1; + else + upperBound = textSize - 1; + } + + mTextPaint.setTextSize(Math.max(1, textSize)); + } +} diff --git a/android/app/src/main/java/app/organicmaps/widget/menu/NavMenu.java b/android/app/src/main/java/app/organicmaps/widget/menu/NavMenu.java index 763a3c86d1..9e2b8612b7 100644 --- a/android/app/src/main/java/app/organicmaps/widget/menu/NavMenu.java +++ b/android/app/src/main/java/app/organicmaps/widget/menu/NavMenu.java @@ -213,14 +213,7 @@ public class NavMenu return; Pair speedAndUnits = StringUtils.nativeFormatSpeedAndUnits(last.getSpeed()); - - if (info.speedLimitMps > 0.0) - { - Pair speedLimitAndUnits = StringUtils.nativeFormatSpeedAndUnits(info.speedLimitMps); - mSpeedValue.setText(speedAndUnits.first + "\u202F/\u202F" + speedLimitAndUnits.first); - } - else - mSpeedValue.setText(speedAndUnits.first); + mSpeedValue.setText(speedAndUnits.first); if (info.speedLimitMps > 0.0 && last.getSpeed() > info.speedLimitMps) { diff --git a/android/app/src/main/java/app/organicmaps/widget/placepage/PlacePageButtons.java b/android/app/src/main/java/app/organicmaps/widget/placepage/PlacePageButtons.java index d4fd663135..5fd7ec6b26 100644 --- a/android/app/src/main/java/app/organicmaps/widget/placepage/PlacePageButtons.java +++ b/android/app/src/main/java/app/organicmaps/widget/placepage/PlacePageButtons.java @@ -11,6 +11,7 @@ import androidx.annotation.AttrRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProvider; @@ -44,7 +45,11 @@ public final class PlacePageButtons extends Fragment implements Observer { final String text = items.get(item.getItemId()); - copyToClipboard(context, popupAnchor, text); + copyToClipboard(context, snackbarTarget, popupAnchor, text); return true; }); popup.show(); diff --git a/android/app/src/main/java/app/organicmaps/widget/placepage/sections/PlacePageLinksFragment.java b/android/app/src/main/java/app/organicmaps/widget/placepage/sections/PlacePageLinksFragment.java index e27f3d2f7a..b6754928f8 100644 --- a/android/app/src/main/java/app/organicmaps/widget/placepage/sections/PlacePageLinksFragment.java +++ b/android/app/src/main/java/app/organicmaps/widget/placepage/sections/PlacePageLinksFragment.java @@ -18,6 +18,7 @@ import app.organicmaps.MwmActivity; import app.organicmaps.R; import app.organicmaps.bookmarks.data.MapObject; import app.organicmaps.bookmarks.data.Metadata; +import app.organicmaps.util.Config; import app.organicmaps.util.UiUtils; import app.organicmaps.util.Utils; import app.organicmaps.widget.placepage.PlacePageUtils; @@ -201,8 +202,6 @@ public class PlacePageLinksFragment extends Fragment implements Observer + + + + + + + + + + + + + diff --git a/android/app/src/main/res/layout-land/fragment_osm_login.xml b/android/app/src/main/res/layout-land/fragment_osm_login.xml index f95650d7dc..b764563d97 100644 --- a/android/app/src/main/res/layout-land/fragment_osm_login.xml +++ b/android/app/src/main/res/layout-land/fragment_osm_login.xml @@ -24,161 +24,141 @@ android:clipChildren="false" android:clipToPadding="false" android:orientation="vertical" - android:padding="@dimen/margin_base" tools:ignore="ScrollViewSize"> - - - - - - - + - - - + + + + + + + - - - - - + android:layout_height="match_parent" + android:layout_weight="1" + android:orientation="vertical" + android:padding="@dimen/margin_base"> + + + + + + + + + @@ -110,11 +112,15 @@ - + + + + + diff --git a/iphone/Maps/UI/PlacePage/Components/ElevationProfile/ElevationProfileBuilder.swift b/iphone/Maps/UI/PlacePage/Components/ElevationProfile/ElevationProfileBuilder.swift index aba969e77b..54efc864bb 100644 --- a/iphone/Maps/UI/PlacePage/Components/ElevationProfile/ElevationProfileBuilder.swift +++ b/iphone/Maps/UI/PlacePage/Components/ElevationProfile/ElevationProfileBuilder.swift @@ -1,13 +1,14 @@ +import CoreApi + class ElevationProfileBuilder { - static func build(data: PlacePageData, delegate: ElevationProfileViewControllerDelegate?) -> ElevationProfileViewController { - guard let elevationProfileData = data.elevationProfileData else { - fatalError() - } + static func build(trackInfo: TrackInfo, + elevationProfileData: ElevationProfileData?, + delegate: ElevationProfileViewControllerDelegate?) -> ElevationProfileViewController { let storyboard = UIStoryboard.instance(.placePage) let viewController = storyboard.instantiateViewController(ofType: ElevationProfileViewController.self); let presenter = ElevationProfilePresenter(view: viewController, - data: elevationProfileData, - imperialUnits: Settings.measurementUnits() == .imperial, + trackInfo: trackInfo, + profileData: elevationProfileData, delegate: delegate) viewController.presenter = presenter diff --git a/iphone/Maps/UI/PlacePage/Components/ElevationProfile/ElevationProfileFormatter.swift b/iphone/Maps/UI/PlacePage/Components/ElevationProfile/ElevationProfileFormatter.swift new file mode 100644 index 0000000000..4a17c5c4c5 --- /dev/null +++ b/iphone/Maps/UI/PlacePage/Components/ElevationProfile/ElevationProfileFormatter.swift @@ -0,0 +1,67 @@ +import Chart +import CoreApi + +final class ElevationProfileFormatter { + + private enum Constants { + static let metricToImperialMultiplier: CGFloat = 0.3048 + static var metricAltitudeStep: CGFloat = 50 + static var imperialAltitudeStep: CGFloat = 100 + } + + private let distanceFormatter: DistanceFormatter.Type + private let altitudeFormatter: AltitudeFormatter.Type + private let unitSystemMultiplier: CGFloat + private let altitudeStep: CGFloat + private let units: Units + + init(units: Units = Settings.measurementUnits()) { + self.units = units + self.distanceFormatter = DistanceFormatter.self + self.altitudeFormatter = AltitudeFormatter.self + switch units { + case .metric: + self.altitudeStep = Constants.metricAltitudeStep + self.unitSystemMultiplier = 1 + case .imperial: + self.altitudeStep = Constants.imperialAltitudeStep + self.unitSystemMultiplier = Constants.metricToImperialMultiplier + @unknown default: + fatalError("Unsupported units") + } + } +} + +extension ElevationProfileFormatter: ChartFormatter { + func xAxisString(from value: Double) -> String { + distanceFormatter.distanceString(fromMeters: value) + } + + func yAxisString(from value: Double) -> String { + altitudeFormatter.altitudeString(fromMeters: value) + } + + func yAxisLowerBound(from value: CGFloat) -> CGFloat { + floor((value / unitSystemMultiplier) / altitudeStep) * altitudeStep * unitSystemMultiplier + } + + func yAxisUpperBound(from value: CGFloat) -> CGFloat { + ceil((value / unitSystemMultiplier) / altitudeStep) * altitudeStep * unitSystemMultiplier + } + + func yAxisSteps(lowerBound: CGFloat, upperBound: CGFloat) -> [CGFloat] { + let lower = yAxisLowerBound(from: lowerBound) + let upper = yAxisUpperBound(from: upperBound) + let range = upper - lower + var stepSize = altitudeStep + var stepsCount = Int((range / stepSize).rounded(.up)) + + while stepsCount > 6 { + stepSize *= 2 // Double the step size to reduce the step count + stepsCount = Int((range / stepSize).rounded(.up)) + } + + let steps = stride(from: lower, through: upper, by: stepSize) + return Array(steps) + } +} diff --git a/iphone/Maps/UI/PlacePage/Components/ElevationProfile/ElevationProfilePresenter.swift b/iphone/Maps/UI/PlacePage/Components/ElevationProfile/ElevationProfilePresenter.swift index 682cdc1042..ec18a1f77b 100644 --- a/iphone/Maps/UI/PlacePage/Components/ElevationProfile/ElevationProfilePresenter.swift +++ b/iphone/Maps/UI/PlacePage/Components/ElevationProfile/ElevationProfilePresenter.swift @@ -1,4 +1,5 @@ import Chart +import CoreApi protocol ElevationProfilePresenterProtocol: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { func configure() @@ -9,7 +10,7 @@ protocol ElevationProfilePresenterProtocol: UICollectionViewDataSource, UICollec protocol ElevationProfileViewControllerDelegate: AnyObject { func openDifficultyPopup() - func updateMapPoint(_ distance: Double) + func updateMapPoint(_ point: CLLocationCoordinate2D, distance: Double) } fileprivate struct DescriptionsViewModel { @@ -18,31 +19,38 @@ fileprivate struct DescriptionsViewModel { let imageName: String } -class ElevationProfilePresenter: NSObject { +final class ElevationProfilePresenter: NSObject { private weak var view: ElevationProfileViewProtocol? - private let data: ElevationProfileData + private let trackInfo: TrackInfo + private let profileData: ElevationProfileData? private let delegate: ElevationProfileViewControllerDelegate? private let cellSpacing: CGFloat = 8 private let descriptionModels: [DescriptionsViewModel] - private let chartData: ElevationProfileChartData - private let formatter: ChartFormatter + private let chartData: ElevationProfileChartData? + private let formatter: ElevationProfileFormatter init(view: ElevationProfileViewProtocol, - data: ElevationProfileData, - imperialUnits: Bool, + trackInfo: TrackInfo, + profileData: ElevationProfileData?, + formatter: ElevationProfileFormatter = ElevationProfileFormatter(), delegate: ElevationProfileViewControllerDelegate?) { self.view = view - self.data = data + self.trackInfo = trackInfo + self.profileData = profileData self.delegate = delegate - chartData = ElevationProfileChartData(data) - formatter = ChartFormatter(imperial: imperialUnits) + if let profileData { + self.chartData = ElevationProfileChartData(profileData) + } else { + self.chartData = nil + } + self.formatter = formatter descriptionModels = [ - DescriptionsViewModel(title: L("elevation_profile_ascent"), value: data.ascent, imageName: "ic_em_ascent_24"), - DescriptionsViewModel(title: L("elevation_profile_descent"), value: data.descent, imageName: "ic_em_descent_24"), - DescriptionsViewModel(title: L("elevation_profile_maxaltitude"), value: data.maxAttitude, imageName: "ic_em_max_attitude_24"), - DescriptionsViewModel(title: L("elevation_profile_minaltitude"), value: data.minAttitude, imageName: "ic_em_min_attitude_24") + DescriptionsViewModel(title: L("elevation_profile_ascent"), value: trackInfo.ascent, imageName: "ic_em_ascent_24"), + DescriptionsViewModel(title: L("elevation_profile_descent"), value: trackInfo.descent, imageName: "ic_em_descent_24"), + DescriptionsViewModel(title: L("elevation_profile_max_elevation"), value: trackInfo.maxElevation, imageName: "ic_em_max_attitude_24"), + DescriptionsViewModel(title: L("elevation_profile_min_elevation"), value: trackInfo.minElevation, imageName: "ic_em_min_attitude_24") ] } @@ -54,34 +62,34 @@ class ElevationProfilePresenter: NSObject { extension ElevationProfilePresenter: ElevationProfilePresenterProtocol { func configure() { - if data.difficulty != .disabled { + guard let profileData, let chartData else { + view?.isChartViewHidden = true + view?.isDifficultyHidden = true + view?.isExtendedDifficultyLabelHidden = true + view?.isBottomPanelHidden = true + return + } + view?.isChartViewHidden = false + + if profileData.difficulty != .disabled { view?.isDifficultyHidden = false - view?.setDifficulty(data.difficulty) + view?.setDifficulty(profileData.difficulty) } else { view?.isDifficultyHidden = true } - if data.trackTime != 0, let eta = DateComponentsFormatter.etaString(from: TimeInterval(data.trackTime)) { - view?.isTimeHidden = false - view?.setTrackTime("\(eta)") - } else { - view?.isTimeHidden = true - } - - view?.isBottomPanelHidden = data.trackTime == 0 && data.difficulty == .disabled + view?.isBottomPanelHidden = profileData.difficulty == .disabled view?.isExtendedDifficultyLabelHidden = true - let presentationData = ChartPresentationData(chartData, - formatter: formatter, - useFilter: true) + let presentationData = ChartPresentationData(chartData, formatter: formatter) view?.setChartData(presentationData) - view?.setActivePoint(data.activePoint) - view?.setMyPosition(data.myPosition) + view?.setActivePoint(profileData.activePoint) + view?.setMyPosition(profileData.myPosition) - BookmarksManager.shared().setElevationActivePointChanged(data.trackId) { [weak self] distance in + BookmarksManager.shared().setElevationActivePointChanged(profileData.trackId) { [weak self] distance in self?.view?.setActivePoint(distance) } - BookmarksManager.shared().setElevationMyPositionChanged(data.trackId) { [weak self] distance in + BookmarksManager.shared().setElevationMyPositionChanged(profileData.trackId) { [weak self] distance in self?.view?.setMyPosition(distance) } } @@ -91,13 +99,10 @@ extension ElevationProfilePresenter: ElevationProfilePresenterProtocol { } func onSelectedPointChanged(_ point: CGFloat) { - let x1 = Int(floor(point)) - let x2 = Int(ceil(point)) - let d1: Double = chartData.points[x1].distance - let d2: Double = chartData.points[x2].distance - let dx = Double(point.truncatingRemainder(dividingBy: 1)) - let distance = d1 + (d2 - d1) * dx - delegate?.updateMapPoint(distance) + guard let chartData else { return } + let distance: Double = floor(point) / CGFloat(chartData.points.count) * chartData.maxDistance + let point = chartData.points.first { $0.distance >= distance } ?? chartData.points[0] + delegate?.updateMapPoint(point.coordinates, distance: point.distance) } } @@ -111,7 +116,7 @@ extension ElevationProfilePresenter { func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ElevationProfileDescriptionCell", for: indexPath) as! ElevationProfileDescriptionCell let model = descriptionModels[indexPath.row] - cell.configure(title: model.title, value: formatter.altitudeString(from: Double(model.value)), imageName: model.imageName) + cell.configure(title: model.title, value: formatter.yAxisString(from: Double(model.value)), imageName: model.imageName) return cell } } @@ -133,61 +138,32 @@ extension ElevationProfilePresenter { } fileprivate struct ElevationProfileChartData { - struct Line: IChartLine { - var values: [Int] + + struct Line: ChartLine { + var values: [ChartValue] var name: String var color: UIColor var type: ChartLineType } + fileprivate let chartValues: [ChartValue] fileprivate let chartLines: [Line] fileprivate let distances: [Double] + fileprivate let maxDistance: Double fileprivate let points: [ElevationHeightPoint] init(_ elevationData: ElevationProfileData) { - points = ElevationProfileChartData.rearrangePoints(elevationData.points) - let values = points.map { Int($0.altitude) } - distances = points.map { $0.distance } - let color = UIColor(red: 0.12, green: 0.59, blue: 0.94, alpha: 1) - let lineColor = StyleManager.shared.theme?.colors.chartLine ?? color - let lineShadowColor = StyleManager.shared.theme?.colors.chartShadow ?? color.withAlphaComponent(0.12) - let l1 = Line(values: values, name: "Altitude", color: lineColor, type: .line) - let l2 = Line(values: values, name: "Altitude", color: lineShadowColor, type: .lineArea) + self.points = elevationData.points + self.chartValues = points.map { ChartValue(xValues: $0.distance, y: $0.altitude) } + self.distances = points.map { $0.distance } + self.maxDistance = distances.last ?? 0 + let lineColor = StyleManager.shared.theme?.colors.chartLine ?? .blue + let lineShadowColor = StyleManager.shared.theme?.colors.chartShadow ?? .lightGray + let l1 = Line(values: chartValues, name: "Altitude", color: lineColor, type: .line) + let l2 = Line(values: chartValues, name: "Altitude", color: lineShadowColor, type: .lineArea) chartLines = [l1, l2] } - private static func rearrangePoints(_ points: [ElevationHeightPoint]) -> [ElevationHeightPoint] { - if points.isEmpty { - return [] - } - - var result: [ElevationHeightPoint] = [] - - let distance = points.last?.distance ?? 0 - let step = max(1, points.count > 50 ? floor(distance / Double(points.count)) : floor(distance / 50)) - result.append(points[0]) - var currentDistance = step - var i = 1 - while i < points.count { - let prevPoint = points[i - 1] - let nextPoint = points[i] - if currentDistance > nextPoint.distance { - i += 1 - continue - } - result.append(ElevationHeightPoint(distance: currentDistance, - andAltitude: altBetweenPoints(prevPoint, nextPoint, at: currentDistance))) - currentDistance += step - if currentDistance > nextPoint.distance { - i += 1 - } - } - - result.append(points.last!) - - return result - } - private static func altBetweenPoints(_ p1: ElevationHeightPoint, _ p2: ElevationHeightPoint, at distance: Double) -> Double { @@ -196,56 +172,10 @@ fileprivate struct ElevationProfileChartData { let d = (distance - p1.distance) / (p2.distance - p1.distance) return p1.altitude + round(Double(p2.altitude - p1.altitude) * d) } - } -extension ElevationProfileChartData: IChartData { - public var xAxisValues: [Double] { - distances - } - - public var lines: [IChartLine] { - chartLines - } - - public var type: ChartType { - .regular - } -} - -final class ChartFormatter: IFormatter { - private let distanceFormatter: MKDistanceFormatter - private let altFormatter: MeasurementFormatter - private let timeFormatter: DateComponentsFormatter - private let imperial: Bool - - init(imperial: Bool) { - self.imperial = imperial - - distanceFormatter = MKDistanceFormatter() - distanceFormatter.units = imperial ? .imperial : .metric - distanceFormatter.unitStyle = .abbreviated - - altFormatter = MeasurementFormatter() - altFormatter.unitOptions = [.providedUnit] - - timeFormatter = DateComponentsFormatter() - timeFormatter.allowedUnits = [.day, .hour, .minute] - timeFormatter.unitsStyle = .abbreviated - timeFormatter.maximumUnitCount = 2 - } - - func distanceString(from value: Double) -> String { - distanceFormatter.string(fromDistance: value) - } - - func altitudeString(from value: Double) -> String { - let alt = imperial ? value / 0.3048 : value - let measurement = Measurement(value: alt.rounded(), unit: imperial ? UnitLength.feet : UnitLength.meters) - return altFormatter.string(from: measurement) - } - - func timeString(from value: Double) -> String { - timeFormatter.string(from: value) ?? "" - } +extension ElevationProfileChartData: ChartData { + public var xAxisValues: [Double] { distances } + public var lines: [ChartLine] { chartLines } + public var type: ChartType { .regular } } diff --git a/iphone/Maps/UI/PlacePage/Components/ElevationProfile/ElevationProfileViewController.swift b/iphone/Maps/UI/PlacePage/Components/ElevationProfile/ElevationProfileViewController.swift index 45e1496aef..37d41a4f0f 100644 --- a/iphone/Maps/UI/PlacePage/Components/ElevationProfile/ElevationProfileViewController.swift +++ b/iphone/Maps/UI/PlacePage/Components/ElevationProfile/ElevationProfileViewController.swift @@ -2,13 +2,13 @@ import Chart protocol ElevationProfileViewProtocol: AnyObject { var presenter: ElevationProfilePresenterProtocol? { get set } - + + var isChartViewHidden: Bool { get set } var isExtendedDifficultyLabelHidden: Bool { get set } var isDifficultyHidden: Bool { get set } - var isTimeHidden: Bool { get set } var isBottomPanelHidden: Bool { get set } + func setExtendedDifficultyGrade(_ value: String) - func setTrackTime(_ value: String?) func setDifficulty(_ value: ElevationDifficulty) func setChartData(_ data: ChartPresentationData) func setActivePoint(_ distance: Double) @@ -16,23 +16,27 @@ protocol ElevationProfileViewProtocol: AnyObject { } class ElevationProfileViewController: UIViewController { + + private enum Constants { + static let chartViewVisibleHeight: CGFloat = 176 + static let chartViewHiddenHeight: CGFloat = 20 + static let difficultyVisibleHeight: CGFloat = 60 + static let difficultyHiddenHeight: CGFloat = 20 + } + var presenter: ElevationProfilePresenterProtocol? - @IBOutlet private var chartView: ChartView! - @IBOutlet private var graphViewContainer: UIView! - @IBOutlet private var descriptionCollectionView: UICollectionView! - @IBOutlet private var difficultyView: DifficultyView! - @IBOutlet private var difficultyTitle: UILabel! - @IBOutlet private var extendedDifficultyGradeLabel: UILabel! - @IBOutlet private var trackTimeLabel: UILabel! - @IBOutlet private var trackTimeTitle: UILabel! - @IBOutlet private var extendedGradeButton: UIButton! - @IBOutlet private var diffucultyConstraint: NSLayoutConstraint! + @IBOutlet private weak var chartView: ChartView! + @IBOutlet private weak var graphViewContainer: UIView! + @IBOutlet private weak var descriptionCollectionView: UICollectionView! + @IBOutlet private weak var difficultyView: DifficultyView! + @IBOutlet private weak var difficultyTitle: UILabel! + @IBOutlet private weak var extendedDifficultyGradeLabel: UILabel! + @IBOutlet private weak var extendedGradeButton: UIButton! + @IBOutlet private weak var chartHeightConstraint: NSLayoutConstraint! + @IBOutlet private weak var difficultyConstraint: NSLayoutConstraint! - private let diffucultiVisibleConstraint: CGFloat = 60 - private let diffucultyHiddenConstraint: CGFloat = 10 private var difficultyHidden: Bool = false - private var timeHidden: Bool = false private var bottomPanelHidden: Bool = false override func viewDidLoad() { @@ -60,6 +64,15 @@ class ElevationProfileViewController: UIViewController { } extension ElevationProfileViewController: ElevationProfileViewProtocol { + var isChartViewHidden: Bool { + get { return chartView.isHidden } + set { + chartView.isHidden = newValue + graphViewContainer.isHidden = newValue + chartHeightConstraint.constant = newValue ? Constants.chartViewHiddenHeight : Constants.chartViewVisibleHeight + } + } + var isExtendedDifficultyLabelHidden: Bool { get { return extendedDifficultyGradeLabel.isHidden } set { @@ -77,25 +90,15 @@ extension ElevationProfileViewController: ElevationProfileViewProtocol { } } - var isTimeHidden: Bool { - get { timeHidden } - set { - timeHidden = newValue - trackTimeLabel.isHidden = newValue - trackTimeTitle.isHidden = newValue - } - } - var isBottomPanelHidden: Bool { get { bottomPanelHidden } set { bottomPanelHidden = newValue if newValue == true { - isTimeHidden = true isExtendedDifficultyLabelHidden = true isDifficultyHidden = true } - diffucultyConstraint.constant = newValue ? diffucultyHiddenConstraint : diffucultiVisibleConstraint + difficultyConstraint.constant = newValue ? Constants.difficultyHiddenHeight : Constants.difficultyVisibleHeight } } @@ -103,10 +106,6 @@ extension ElevationProfileViewController: ElevationProfileViewProtocol { extendedDifficultyGradeLabel.text = value } - func setTrackTime(_ value: String?) { - trackTimeLabel.text = value - } - func setDifficulty(_ value: ElevationDifficulty) { difficultyView.difficulty = value } diff --git a/iphone/Maps/UI/PlacePage/Components/PlacePageBookmarkViewController.swift b/iphone/Maps/UI/PlacePage/Components/PlacePageEditBookmarkOrTrackViewController.swift similarity index 67% rename from iphone/Maps/UI/PlacePage/Components/PlacePageBookmarkViewController.swift rename to iphone/Maps/UI/PlacePage/Components/PlacePageEditBookmarkOrTrackViewController.swift index 6983ebd1ae..4f06cf7ee0 100644 --- a/iphone/Maps/UI/PlacePage/Components/PlacePageBookmarkViewController.swift +++ b/iphone/Maps/UI/PlacePage/Components/PlacePageEditBookmarkOrTrackViewController.swift @@ -1,8 +1,14 @@ -protocol PlacePageBookmarkViewControllerDelegate: AnyObject { - func bookmarkDidPressEdit() +protocol PlacePageEditBookmarkOrTrackViewControllerDelegate: AnyObject { + func didPressEdit(_ data: PlacePageEditData) } -class PlacePageBookmarkViewController: UIViewController { +enum PlacePageEditData { + case bookmark(PlacePageBookmarkData) + case track(PlacePageTrackData) +} + +final class PlacePageEditBookmarkOrTrackViewController: UIViewController { + @IBOutlet var stackView: UIStackView! @IBOutlet var spinner: UIImageView! @IBOutlet var editButton: UIButton! @@ -17,30 +23,45 @@ class PlacePageBookmarkViewController: UIViewController { } } - var bookmarkData: PlacePageBookmarkData? { + var data: PlacePageEditData? { didSet { updateViews() } } - weak var delegate: PlacePageBookmarkViewControllerDelegate? + weak var delegate: PlacePageEditBookmarkOrTrackViewControllerDelegate? override func viewDidLoad() { super.viewDidLoad() updateViews() } - func updateViews() { - guard let bookmarkData = bookmarkData else { return } + override func applyTheme() { + super.applyTheme() + updateViews() + } + + // MARK: - Private methods + + private func updateViews() { + guard let data else { return } editButton.isEnabled = true - if let description = bookmarkData.bookmarkDescription { - if bookmarkData.isHtmlDescription { - setHtmlDescription(description) - topConstraint.constant = 16 + switch data { + case .bookmark(let bookmark): + editButton.setTitle(L("placepage_edit_bookmark_button"), for: .normal) + if let description = bookmark.bookmarkDescription { + if bookmark.isHtmlDescription { + setHtmlDescription(description) + topConstraint.constant = 16 + } else { + expandableLabel.text = description + topConstraint.constant = description.count > 0 ? 16 : 0 + } } else { - expandableLabel.text = description - topConstraint.constant = description.count > 0 ? 16 : 0 + topConstraint.constant = 0 } - } else { + case .track: + editButton.setTitle(L("edit_track"), for: .normal) + expandableLabel.isHidden = true topConstraint.constant = 0 } } @@ -85,12 +106,10 @@ class PlacePageBookmarkViewController: UIViewController { spinner.stopRotation() } + // MARK: - Actions + @IBAction func onEdit(_ sender: UIButton) { - delegate?.bookmarkDidPressEdit() - } - - override func applyTheme() { - super.applyTheme() - updateViews() + guard let data else { return } + delegate?.didPressEdit(data) } } diff --git a/iphone/Maps/UI/PlacePage/Components/PlacePageHeader/PlacePageHeaderPresenter.swift b/iphone/Maps/UI/PlacePage/Components/PlacePageHeader/PlacePageHeaderPresenter.swift index ede407ce53..60c59c123b 100644 --- a/iphone/Maps/UI/PlacePage/Components/PlacePageHeader/PlacePageHeaderPresenter.swift +++ b/iphone/Maps/UI/PlacePage/Components/PlacePageHeader/PlacePageHeaderPresenter.swift @@ -44,6 +44,8 @@ extension PlacePageHeaderPresenter: PlacePageHeaderPresenterProtocol { view?.isExpandViewHidden = true view?.isShadowViewHidden = false } + // TODO: (KK) Enable share button for the tracks to share the whole track gpx/kml + view?.isShareButtonHidden = placePagePreviewData.coordinates == nil } func onClosePress() { diff --git a/iphone/Maps/UI/PlacePage/Components/PlacePageHeader/PlacePageHeaderViewController.swift b/iphone/Maps/UI/PlacePage/Components/PlacePageHeader/PlacePageHeaderViewController.swift index 5ad6e2712e..94069e1c7c 100644 --- a/iphone/Maps/UI/PlacePage/Components/PlacePageHeader/PlacePageHeaderViewController.swift +++ b/iphone/Maps/UI/PlacePage/Components/PlacePageHeader/PlacePageHeaderViewController.swift @@ -2,6 +2,7 @@ protocol PlacePageHeaderViewProtocol: AnyObject { var presenter: PlacePageHeaderPresenterProtocol? { get set } var isExpandViewHidden: Bool { get set } var isShadowViewHidden: Bool { get set } + var isShareButtonHidden: Bool { get set } func setTitle(_ title: String?, secondaryTitle: String?) } @@ -72,6 +73,15 @@ extension PlacePageHeaderViewController: PlacePageHeaderViewProtocol { } } + var isShareButtonHidden: Bool { + get { + shareButton.isHidden + } + set { + shareButton.isHidden = newValue + } + } + func setTitle(_ title: String?, secondaryTitle: String?) { titleText = title secondaryText = secondaryTitle diff --git a/iphone/Maps/UI/PlacePage/Components/PlacePageInfoViewController.swift b/iphone/Maps/UI/PlacePage/Components/PlacePageInfoViewController.swift index f913a56ac2..021f3c1dee 100644 --- a/iphone/Maps/UI/PlacePage/Components/PlacePageInfoViewController.swift +++ b/iphone/Maps/UI/PlacePage/Components/PlacePageInfoViewController.swift @@ -74,7 +74,6 @@ class InfoItemViewController: UIViewController { protocol PlacePageInfoViewControllerDelegate: AnyObject { var shouldShowOpenInApp: Bool { get } - func viewWillAppear() func didPressCall() func didPressWebsite() func didPressWebsiteMenu() @@ -144,11 +143,6 @@ class PlacePageInfoViewController: UIViewController { setupViews() } - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - delegate?.viewWillAppear() - } - // MARK: private private func setupViews() { if let openingHours = placePageInfoData.openingHours { @@ -362,28 +356,48 @@ class PlacePageInfoViewController: UIViewController { }) } - var formatId = self.coordinatesFormatId - if let coordFormats = self.placePageInfoData.coordFormats as? Array { - if formatId >= coordFormats.count { - formatId = 0 - } + setupCoordinatesView() + setupOpenWithAppView() + } - coordinatesView = createInfoItem(coordFormats[formatId], - icon: UIImage(named: "ic_placepage_coordinate"), - accessoryImage: UIImage(named: "ic_placepage_change"), - tapHandler: { [unowned self] in - let formatId = (self.coordinatesFormatId + 1) % coordFormats.count - self.coordinatesFormatId = formatId - let coordinates: String = coordFormats[formatId] - self.coordinatesView?.infoLabel.text = coordinates - }, - longPressHandler: { [unowned self] in - let coordinates: String = coordFormats[self.coordinatesFormatId] - self.delegate?.didCopy(coordinates) - }) + private func setupCoordinatesView() { + guard let coordFormats = placePageInfoData.coordFormats as? Array else { return } + var formatId = coordinatesFormatId + if formatId >= coordFormats.count { + formatId = 0 } - setupOpenWithAppView() + func setCoordinatesSelected(formatId: Int) { + coordinatesFormatId = formatId + let coordinates: String = coordFormats[formatId] + coordinatesView?.infoLabel.text = coordinates + } + + func copyCoordinatesToPasteboard() { + let coordinates: String = coordFormats[coordinatesFormatId] + self.delegate?.didCopy(coordinates) + } + + coordinatesView = createInfoItem(coordFormats[formatId], + icon: UIImage(named: "ic_placepage_coordinate"), + accessoryImage: UIImage(named: "ic_placepage_change"), + tapHandler: { [unowned self] in + let formatId = (self.coordinatesFormatId + 1) % coordFormats.count + setCoordinatesSelected(formatId: formatId) + }, + longPressHandler: { + copyCoordinatesToPasteboard() + }) + if #available(iOS 14.0, *) { + let menu = UIMenu(children: coordFormats.enumerated().map { (index, format) in + UIAction(title: format, handler: { _ in + setCoordinatesSelected(formatId: index) + copyCoordinatesToPasteboard() + }) + }) + coordinatesView?.accessoryButton.menu = menu + coordinatesView?.accessoryButton.showsMenuAsPrimaryAction = true + } } private func setupOpenWithAppView() { @@ -435,9 +449,7 @@ class PlacePageInfoViewController: UIViewController { private extension UIStackView { func addArrangedSubviewWithSeparator(_ view: UIView, insets: UIEdgeInsets = .zero) { if !arrangedSubviews.isEmpty { - view.addSeparator(thickness: CGFloat(1.0), - color: StyleManager.shared.theme?.colors.blackDividers, - insets: insets) + view.addSeparator(thickness: CGFloat(1.0), insets: insets) } addArrangedSubview(view) } diff --git a/iphone/Maps/UI/PlacePage/Components/PlacePagePreviewViewController.swift b/iphone/Maps/UI/PlacePage/Components/PlacePagePreviewViewController.swift index 473ba0c9f2..beb3a23348 100644 --- a/iphone/Maps/UI/PlacePage/Components/PlacePagePreviewViewController.swift +++ b/iphone/Maps/UI/PlacePage/Components/PlacePagePreviewViewController.swift @@ -67,7 +67,7 @@ final class PlacePagePreviewViewController: UIViewController { updateViews() } - private func updateViews() { + func updateViews() { if placePagePreviewData.isMyPosition { if let speedAndAltitude = speedAndAltitude { subtitleLabel.text = speedAndAltitude @@ -91,7 +91,7 @@ final class PlacePagePreviewViewController: UIViewController { placePageDirectionView = subtitleDirectionView - if let address = placePagePreviewData.address { + if let address = placePagePreviewData.secondarySubtitle { addressLabel.text = address placePageDirectionView = addressDirectionView } else { @@ -140,81 +140,80 @@ final class PlacePagePreviewViewController: UIViewController { // MARK: private private func configSchedule() { - let now = time_t(Date().timeIntervalSince1970); - - let hourFormatter = DateFormatter() - hourFormatter.locale = Locale.current - hourFormatter.timeStyle = .short - + let now = time_t(Date().timeIntervalSince1970) + + func stringFromTime(_ time: Int) -> String { + DateTimeFormatter.dateString(from: Date(timeIntervalSince1970: TimeInterval(time)), + dateStyle: .none, + timeStyle: .short) + } + switch placePagePreviewData.schedule.state { + case .unknown: + scheduleContainerView.isHidden = true case .allDay: setScheduleLabel(state: L("twentyfour_seven"), stateColor: UIColor.systemGreen, - details: nil); + details: nil) case .open: - let nextTimeClosed = placePagePreviewData.schedule.nextTimeClosed; - let minutesUntilClosed = (nextTimeClosed - now) / 60; - let stringTimeInterval = getTimeIntervalString(minutes: minutesUntilClosed); - let stringTime = hourFormatter.string(from: Date(timeIntervalSince1970: TimeInterval(nextTimeClosed))); - - let details: String?; + let nextTimeClosed = placePagePreviewData.schedule.nextTimeClosed + let minutesUntilClosed = (nextTimeClosed - now) / 60 + let stringTimeInterval = getTimeIntervalString(minutes: minutesUntilClosed) + let stringTime = stringFromTime(nextTimeClosed) + + let details: String? if (minutesUntilClosed < 3 * 60) // Less than 3 hours { - details = String(format: L("closes_in"), stringTimeInterval) + " • " + stringTime; + details = String(format: L("closes_in"), stringTimeInterval) + " • " + stringTime } else if (minutesUntilClosed < 24 * 60) // Less than 24 hours { - details = String(format: L("closes_at"), stringTime); + details = String(format: L("closes_at"), stringTime) } else { - details = nil; + details = nil } setScheduleLabel(state: L("editor_time_open"), stateColor: UIColor.systemGreen, - details: details); + details: details) case .closed: - let nextTimeOpen = placePagePreviewData.schedule.nextTimeOpen; - let nextTimeOpenDate = Date(timeIntervalSince1970: TimeInterval(nextTimeOpen)); + let nextTimeOpen = placePagePreviewData.schedule.nextTimeOpen + let nextTimeOpenDate = Date(timeIntervalSince1970: TimeInterval(nextTimeOpen)) - let minutesUntilOpen = (nextTimeOpen - now) / 60; - let stringTimeInterval = getTimeIntervalString(minutes: minutesUntilOpen); - let stringTime = hourFormatter.string(from: Date(timeIntervalSince1970: TimeInterval(nextTimeOpen))); - - let details: String?; + let minutesUntilOpen = (nextTimeOpen - now) / 60 + let stringTimeInterval = getTimeIntervalString(minutes: minutesUntilOpen) + let stringTime = stringFromTime(nextTimeOpen) + + let details: String? if (minutesUntilOpen < 3 * 60) // Less than 3 hours { - details = String(format: L("opens_in"), stringTimeInterval) + " • " + stringTime; + details = String(format: L("opens_in"), stringTimeInterval) + " • " + stringTime } else if (Calendar.current.isDateInToday(nextTimeOpenDate)) // Today { - details = String(format: L("opens_at"), stringTime); + details = String(format: L("opens_at"), stringTime) } else if (minutesUntilOpen < 24 * 60) // Less than 24 hours { - details = String(format: L("opens_tomorrow_at"), stringTime); + details = String(format: L("opens_tomorrow_at"), stringTime) } else if (minutesUntilOpen < 7 * 24 * 60) // Less than 1 week { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "EEEE"; - let dayOfWeek = dateFormatter.string(from: nextTimeOpenDate); - details = String(format: L("opens_dayoftheweek_at"), dayOfWeek, stringTime); + let dayOfWeek = DateTimeFormatter.dateString(from: nextTimeOpenDate, format: "EEEE") + details = String(format: L("opens_dayoftheweek_at"), dayOfWeek, stringTime) } else { - details = nil; + details = nil } setScheduleLabel(state: L("closed_now"), stateColor: UIColor.systemRed, - details: details); - - case .unknown: - scheduleContainerView.isHidden = true + details: details) @unknown default: fatalError() @@ -222,28 +221,28 @@ final class PlacePagePreviewViewController: UIViewController { } private func getTimeIntervalString(minutes: Int) -> String { - var str = ""; + var str = "" if (minutes >= 60) { - str = String(minutes / 60) + " " + L("hour") + " "; + str = String(minutes / 60) + " " + L("hour") + " " } - str += String(minutes % 60) + " " + L("minute"); - return str; + str += String(minutes % 60) + " " + L("minute") + return str } private func setScheduleLabel(state: String, stateColor: UIColor, details: String?) { - let attributedString = NSMutableAttributedString(); + let attributedString = NSMutableAttributedString() let stateString = NSAttributedString(string: state, attributes: [NSAttributedString.Key.font: UIFont.regular14(), - NSAttributedString.Key.foregroundColor: stateColor]); - attributedString.append(stateString); + NSAttributedString.Key.foregroundColor: stateColor]) + attributedString.append(stateString) if (details != nil) { let detailsString = NSAttributedString(string: " • " + details!, attributes: [NSAttributedString.Key.font: UIFont.regular14(), - NSAttributedString.Key.foregroundColor: UIColor.blackSecondaryText()]); - attributedString.append(detailsString); + NSAttributedString.Key.foregroundColor: UIColor.blackSecondaryText()]) + attributedString.append(detailsString) } - scheduleLabel.attributedText = attributedString; + scheduleLabel.attributedText = attributedString } } diff --git a/iphone/Maps/UI/PlacePage/Components/Products/ProductButton.swift b/iphone/Maps/UI/PlacePage/Components/Products/ProductButton.swift new file mode 100644 index 0000000000..7c65df7371 --- /dev/null +++ b/iphone/Maps/UI/PlacePage/Components/Products/ProductButton.swift @@ -0,0 +1,38 @@ +final class ProductButton: UIButton { + + private var action: () -> Void + + init(title: String, action: @escaping () -> Void) { + self.action = action + super.init(frame: .zero) + self.setup(title: title, action: action) + self.layout() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setup(title: String, action: @escaping () -> Void) { + setStyleAndApply("BlueBackground") + setTitle(title, for: .normal) + setTitleColor(.white, for: .normal) + titleLabel?.font = UIFont.regular14() + titleLabel?.allowsDefaultTighteningForTruncation = true + titleLabel?.adjustsFontSizeToFitWidth = true + titleLabel?.minimumScaleFactor = 0.5 + layer.setCorner(radius: 5.0) + layer.masksToBounds = true + addTarget(self, action: #selector(buttonDidTap), for: .touchUpInside) + } + + private func layout() { + translatesAutoresizingMaskIntoConstraints = false + heightAnchor.constraint(equalToConstant: 30.0).isActive = true + } + + @objc private func buttonDidTap() { + action() + } +} diff --git a/iphone/Maps/UI/PlacePage/Components/Products/ProductsViewController.swift b/iphone/Maps/UI/PlacePage/Components/Products/ProductsViewController.swift new file mode 100644 index 0000000000..6b7ee5f606 --- /dev/null +++ b/iphone/Maps/UI/PlacePage/Components/Products/ProductsViewController.swift @@ -0,0 +1,162 @@ +final class ProductsViewController: UIViewController { + + private enum Constants { + static let spacing: CGFloat = 10 + static let titleLeadingPadding: CGFloat = 12 + static let titleTrailingPadding: CGFloat = 10 + static let descriptionTopPadding: CGFloat = 10 + static let closeButtonSize: CGFloat = 24 + static let closeButtonTrailingPadding: CGFloat = -12 + static let closeButtonTopPadding: CGFloat = 12 + static let stackViewTopPadding: CGFloat = 12 + static let subtitleButtonTopPadding: CGFloat = 4 + static let subtitleButtonBottomPadding: CGFloat = -4 + } + + private let viewModel: ProductsViewModel + private let titleLabel = UILabel() + private let descriptionLabel = UILabel() + private let closeButton = UIButton(type: .system) + private let stackView = UIStackView() + private let leadingSubtitleButton = UIButton(type: .system) + private let trailingSubtitleButton = UIButton(type: .system) + + init(viewModel: ProductsViewModel) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + setupViews() + layout() + } + + private func setupViews() { + view.setStyleAndApply("Background") + setupTitleLabel() + setupDescriptionLabel() + setupCloseButton() + setupProductsStackView() + setupSubtitleButtons() + } + + private func setupTitleLabel() { + titleLabel.text = viewModel.title + titleLabel.font = UIFont.semibold16() + titleLabel.numberOfLines = 1 + titleLabel.translatesAutoresizingMaskIntoConstraints = false + } + + private func setupDescriptionLabel() { + descriptionLabel.text = viewModel.description + descriptionLabel.font = UIFont.regular14() + descriptionLabel.numberOfLines = 0 + descriptionLabel.translatesAutoresizingMaskIntoConstraints = false + } + + private func setupCloseButton() { + closeButton.setStyleAndApply("MWMGray") + closeButton.setImage(UIImage(resource: .icSearchClear), for: .normal) + closeButton.translatesAutoresizingMaskIntoConstraints = false + closeButton.addTarget(self, action: #selector(closeButtonDidTap), for: .touchUpInside) + } + + private func setupProductsStackView() { + stackView.axis = .horizontal + stackView.alignment = .fill + stackView.distribution = .fillEqually + stackView.spacing = Constants.spacing + stackView.translatesAutoresizingMaskIntoConstraints = false + viewModel.products.forEach { product in + let button = ProductButton(title: product.title) { [weak self] in + self?.productButtonDidTap(product) + } + stackView.addArrangedSubview(button) + } + } + + private func setupSubtitleButtons() { + leadingSubtitleButton.setTitle(viewModel.leadingSubtitle, for: .normal) + leadingSubtitleButton.backgroundColor = .clear + leadingSubtitleButton.setTitleColor(.linkBlue(), for: .normal) + leadingSubtitleButton.translatesAutoresizingMaskIntoConstraints = false + leadingSubtitleButton.addTarget(self, action: #selector(leadingSubtitleButtonDidTap), for: .touchUpInside) + + trailingSubtitleButton.setTitle(viewModel.trailingSubtitle, for: .normal) + trailingSubtitleButton.backgroundColor = .clear + trailingSubtitleButton.setTitleColor(.linkBlue(), for: .normal) + trailingSubtitleButton.translatesAutoresizingMaskIntoConstraints = false + trailingSubtitleButton.addTarget(self, action: #selector(trailingSubtitleButtonDidTap), for: .touchUpInside) + } + + private func layout() { + view.addSubview(titleLabel) + view.addSubview(descriptionLabel) + view.addSubview(closeButton) + view.addSubview(stackView) + view.addSubview(leadingSubtitleButton) + view.addSubview(trailingSubtitleButton) + + NSLayoutConstraint.activate([ + titleLabel.topAnchor.constraint(equalTo: closeButton.topAnchor), + titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: Constants.titleLeadingPadding), + titleLabel.trailingAnchor.constraint(equalTo: closeButton.leadingAnchor), + + descriptionLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: Constants.descriptionTopPadding), + descriptionLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), + descriptionLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -Constants.titleTrailingPadding), + + closeButton.widthAnchor.constraint(equalToConstant: Constants.closeButtonSize), + closeButton.heightAnchor.constraint(equalToConstant: Constants.closeButtonSize), + closeButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: Constants.closeButtonTrailingPadding), + closeButton.topAnchor.constraint(equalTo: view.topAnchor, constant: Constants.closeButtonTopPadding), + + stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: Constants.titleLeadingPadding), + stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -Constants.titleLeadingPadding), + stackView.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: Constants.stackViewTopPadding), + + leadingSubtitleButton.topAnchor.constraint(equalTo: stackView.bottomAnchor, constant: Constants.subtitleButtonTopPadding), + leadingSubtitleButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: Constants.titleLeadingPadding), + leadingSubtitleButton.trailingAnchor.constraint(equalTo: view.centerXAnchor), + leadingSubtitleButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: Constants.subtitleButtonBottomPadding), + + trailingSubtitleButton.topAnchor.constraint(equalTo: leadingSubtitleButton.topAnchor, constant: Constants.subtitleButtonTopPadding), + trailingSubtitleButton.leadingAnchor.constraint(equalTo: view.centerXAnchor), + trailingSubtitleButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -Constants.titleLeadingPadding), + trailingSubtitleButton.bottomAnchor.constraint(equalTo: leadingSubtitleButton.bottomAnchor) + ]) + } + + @objc private func closeButtonDidTap() { + viewModel.didClose(reason: .close) + hide() + } + + private func productButtonDidTap(_ product: Product) { + viewModel.didSelectProduct(product) + viewModel.didClose(reason: .selectProduct) + hide() + } + + @objc private func leadingSubtitleButtonDidTap() { + viewModel.didClose(reason: .alreadyDonated) + hide() + } + + @objc private func trailingSubtitleButtonDidTap() { + viewModel.didClose(reason: .remindLater) + hide() + } + + func hide() { + UIView.transition(with: view, duration: kDefaultAnimationDuration / 2, options: .transitionCrossDissolve) { + self.view.isHidden = true + } + } +} diff --git a/iphone/Maps/UI/PlacePage/Components/Products/ProductsViewModel.swift b/iphone/Maps/UI/PlacePage/Components/Products/ProductsViewModel.swift new file mode 100644 index 0000000000..fa63b682fe --- /dev/null +++ b/iphone/Maps/UI/PlacePage/Components/Products/ProductsViewModel.swift @@ -0,0 +1,24 @@ +struct ProductsViewModel { + private let productsManager: ProductsManager.Type + + let title: String = L("support_organic_maps") + let description: String + let leadingSubtitle: String = L("already_donated") + let trailingSubtitle: String = L("remind_me_later") + let products: [Product] + + init(manager: ProductsManager.Type, configuration: ProductsConfiguration) { + self.productsManager = manager + self.description = configuration.placePagePrompt + self.products = configuration.products + } + + func didSelectProduct(_ product: Product) { + UIViewController.topViewController().openUrl(product.link, externally: true) + productsManager.didSelect(product) + } + + func didClose(reason: ProductsPopupCloseReason) { + productsManager.didCloseProductsPopup(with: reason) + } +} diff --git a/iphone/Maps/UI/PlacePage/PlacePage.storyboard b/iphone/Maps/UI/PlacePage/PlacePage.storyboard index 10eae05f46..405c1e31ee 100644 --- a/iphone/Maps/UI/PlacePage/PlacePage.storyboard +++ b/iphone/Maps/UI/PlacePage/PlacePage.storyboard @@ -1,9 +1,9 @@ - + - + @@ -287,27 +287,14 @@ - - - - - - - - - - - - - @@ -376,41 +363,15 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -439,40 +400,14 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -564,40 +499,14 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -636,7 +545,7 @@ diff --git a/iphone/Maps/UI/PlacePage/PlacePageBuilder.swift b/iphone/Maps/UI/PlacePage/PlacePageBuilder.swift index 337f7d4163..0f7795f5d3 100644 --- a/iphone/Maps/UI/PlacePage/PlacePageBuilder.swift +++ b/iphone/Maps/UI/PlacePage/PlacePageBuilder.swift @@ -1,44 +1,52 @@ @objc class PlacePageBuilder: NSObject { - @objc static func build() -> PlacePageViewController { + @objc static func build(for data: PlacePageData) -> PlacePageViewController { let storyboard = UIStoryboard.instance(.placePage) guard let viewController = storyboard.instantiateInitialViewController() as? PlacePageViewController else { fatalError() } - let data = PlacePageData(localizationProvider: OpeinigHoursLocalization()) viewController.isPreviewPlus = data.isPreviewPlus let interactor = PlacePageInteractor(viewController: viewController, data: data, mapViewController: MapViewController.shared()!) - let layout:IPlacePageLayout - if data.elevationProfileData != nil { - layout = PlacePageElevationLayout(interactor: interactor, storyboard: storyboard, data: data) - } else { + let layout: IPlacePageLayout + switch data.objectType { + case .POI, .bookmark: layout = PlacePageCommonLayout(interactor: interactor, storyboard: storyboard, data: data) + case .track: + layout = PlacePageTrackLayout(interactor: interactor, storyboard: storyboard, data: data) + case .trackRecording: + // TODO: Implement PlacePageTrackRecordingLayout + fatalError("PlacePageTrackRecordingLayout is not implemented") + @unknown default: + fatalError() } - let presenter = PlacePagePresenter(view: viewController, interactor: interactor) - + let presenter = PlacePagePresenter(view: viewController) viewController.setLayout(layout) - viewController.presenter = presenter + viewController.interactor = interactor interactor.presenter = presenter layout.presenter = presenter return viewController } - @objc static func update(_ viewController: PlacePageViewController) { - let data = PlacePageData(localizationProvider: OpeinigHoursLocalization()) + @objc static func update(_ viewController: PlacePageViewController, with data: PlacePageData) { viewController.isPreviewPlus = data.isPreviewPlus let interactor = PlacePageInteractor(viewController: viewController, data: data, mapViewController: MapViewController.shared()!) - let layout:IPlacePageLayout - if data.elevationProfileData != nil { - layout = PlacePageElevationLayout(interactor: interactor, storyboard: viewController.storyboard!, data: data) - } else { + let layout: IPlacePageLayout + switch data.objectType { + case .POI, .bookmark: layout = PlacePageCommonLayout(interactor: interactor, storyboard: viewController.storyboard!, data: data) + case .track: + layout = PlacePageTrackLayout(interactor: interactor, storyboard: viewController.storyboard!, data: data) + case .trackRecording: + // TODO: Implement PlacePageTrackRecordingLayout + fatalError("PlacePageTrackRecordingLayout is not implemented") + @unknown default: + fatalError() } - let presenter = PlacePagePresenter(view: viewController, interactor: interactor) - - viewController.presenter = presenter + let presenter = PlacePagePresenter(view: viewController) + viewController.interactor = interactor interactor.presenter = presenter layout.presenter = presenter viewController.updateWithLayout(layout) diff --git a/iphone/Maps/UI/PlacePage/PlacePageInteractor.swift b/iphone/Maps/UI/PlacePage/PlacePageInteractor.swift index 2915e0fd46..a7a657e205 100644 --- a/iphone/Maps/UI/PlacePage/PlacePageInteractor.swift +++ b/iphone/Maps/UI/PlacePage/PlacePageInteractor.swift @@ -1,9 +1,10 @@ protocol PlacePageInteractorProtocol: AnyObject { + func viewWillAppear() func updateTopBound(_ bound: CGFloat, duration: TimeInterval) } class PlacePageInteractor: NSObject { - weak var presenter: PlacePagePresenterProtocol? + var presenter: PlacePagePresenterProtocol? weak var viewController: UIViewController? weak var mapViewController: MapViewController? private let bookmarksManager = BookmarksManager.shared() @@ -22,9 +23,10 @@ class PlacePageInteractor: NSObject { removeFromBookmarksManagerObserverList() } - private func updateBookmarkIfNeeded() { - guard let bookmarkId = placePageData.bookmarkData?.bookmarkId else { return } - guard bookmarksManager.hasBookmark(bookmarkId) else { + private func updatePlacePageIfNeeded() { + let isBookmark = placePageData.bookmarkData != nil && bookmarksManager.hasBookmark(placePageData.bookmarkData!.bookmarkId) + let isTrack = placePageData.trackData != nil && bookmarksManager.hasTrack(placePageData.trackData!.trackId) + guard isBookmark || isTrack else { presenter?.closeAnimated() return } @@ -42,6 +44,15 @@ class PlacePageInteractor: NSObject { } extension PlacePageInteractor: PlacePageInteractorProtocol { + func viewWillAppear() { + // Skip data reloading on the first appearance, to avoid unnecessary updates. + guard viewWillAppearIsCalledForTheFirstTime else { + viewWillAppearIsCalledForTheFirstTime = true + return + } + updatePlacePageIfNeeded() + } + func updateTopBound(_ bound: CGFloat, duration: TimeInterval) { mapViewController?.setPlacePageTopBound(bound, duration: duration) } @@ -53,14 +64,7 @@ extension PlacePageInteractor: PlacePageInfoViewControllerDelegate { var shouldShowOpenInApp: Bool { !OpenInApplication.availableApps.isEmpty } - - func viewWillAppear() { - // Skip data reloading on the first appearance, to avoid unnecessary updates. - guard viewWillAppearIsCalledForTheFirstTime else { return } - viewWillAppearIsCalledForTheFirstTime = true - updateBookmarkIfNeeded() - } - + func didPressCall() { MWMPlacePageManagerHelper.call(placePageData) } @@ -171,11 +175,16 @@ extension PlacePageInteractor: PlacePageButtonsViewControllerDelegate { } } -// MARK: - PlacePageBookmarkViewControllerDelegate +// MARK: - PlacePageEditBookmarkOrTrackViewControllerDelegate -extension PlacePageInteractor: PlacePageBookmarkViewControllerDelegate { - func bookmarkDidPressEdit() { - MWMPlacePageManagerHelper.editBookmark(placePageData) +extension PlacePageInteractor: PlacePageEditBookmarkOrTrackViewControllerDelegate { + func didPressEdit(_ data: PlacePageEditData) { + switch data { + case .bookmark: + MWMPlacePageManagerHelper.editBookmark(placePageData) + case .track: + MWMPlacePageManagerHelper.editTrack(placePageData) + } } } @@ -228,10 +237,36 @@ extension PlacePageInteractor: ActionBarViewControllerDelegate { MWMPlacePageManagerHelper.avoidFerry() case .more: fatalError("More button should've been handled in ActionBarViewContoller") + case .track: + guard placePageData.trackData != nil else { return } + // TODO: This is temporary solution. Remove the dialog and use the MWMPlacePageManagerHelper.removeTrack + // directly here when the track recovery mechanism will be implemented. + showTrackDeletionConfirmationDialog() @unknown default: fatalError() } } + + private func showTrackDeletionConfirmationDialog() { + let alert = UIAlertController(title: nil, message: L("placepage_delete_track_confirmation_alert_message"), preferredStyle: .actionSheet) + let deleteAction = UIAlertAction(title: L("delete"), style: .destructive) { [weak self] _ in + guard let self = self else { return } + guard self.placePageData.trackData != nil else { + fatalError("The track data should not be nil during the track deletion") + } + MWMPlacePageManagerHelper.removeTrack(self.placePageData) + self.presenter?.closeAnimated() + } + let cancelAction = UIAlertAction(title: L("cancel"), style: .cancel) + alert.addAction(deleteAction) + alert.addAction(cancelAction) + guard let viewController else { return } + iPadSpecific { + alert.popoverPresentationController?.sourceView = viewController.view + alert.popoverPresentationController?.sourceRect = viewController.view.frame + } + viewController.present(alert, animated: true) + } } // MARK: - ElevationProfileViewControllerDelegate @@ -241,8 +276,9 @@ extension PlacePageInteractor: ElevationProfileViewControllerDelegate { MWMPlacePageManagerHelper.openElevationDifficultPopup(placePageData) } - func updateMapPoint(_ distance: Double) { - BookmarksManager.shared().setElevationActivePoint(distance, trackId: placePageData.elevationProfileData!.trackId) + func updateMapPoint(_ point: CLLocationCoordinate2D, distance: Double) { + guard let trackId = placePageData.trackData?.trackId else { return } + BookmarksManager.shared().setElevationActivePoint(point, distance: distance, trackId: trackId) } } @@ -267,7 +303,7 @@ extension PlacePageInteractor: PlacePageHeaderViewControllerDelegate { // MARK: - BookmarksObserver extension PlacePageInteractor: BookmarksObserver { func onBookmarksLoadFinished() { - updateBookmarkIfNeeded() + updatePlacePageIfNeeded() } func onBookmarksCategoryDeleted(_ groupId: MWMMarkGroupID) { diff --git a/iphone/Maps/UI/PlacePage/PlacePageLayout/ActionBar/MWMActionBarButton.h b/iphone/Maps/UI/PlacePage/PlacePageLayout/ActionBar/MWMActionBarButton.h index 0263b96164..a82a84c698 100644 --- a/iphone/Maps/UI/PlacePage/PlacePageLayout/ActionBar/MWMActionBarButton.h +++ b/iphone/Maps/UI/PlacePage/PlacePageLayout/ActionBar/MWMActionBarButton.h @@ -2,6 +2,7 @@ typedef NS_ENUM(NSInteger, MWMActionBarButtonType) { MWMActionBarButtonTypeBooking, MWMActionBarButtonTypeBookingSearch, MWMActionBarButtonTypeBookmark, + MWMActionBarButtonTypeTrack, MWMActionBarButtonTypeCall, MWMActionBarButtonTypeDownload, MWMActionBarButtonTypeMore, diff --git a/iphone/Maps/UI/PlacePage/PlacePageLayout/ActionBar/MWMActionBarButton.m b/iphone/Maps/UI/PlacePage/PlacePageLayout/ActionBar/MWMActionBarButton.m index e5f64f85d3..0a7401bafa 100644 --- a/iphone/Maps/UI/PlacePage/PlacePageLayout/ActionBar/MWMActionBarButton.m +++ b/iphone/Maps/UI/PlacePage/PlacePageLayout/ActionBar/MWMActionBarButton.m @@ -17,6 +17,7 @@ NSString *titleForButton(MWMActionBarButtonType type, BOOL isSelected) { case MWMActionBarButtonTypeCall: return L(@"placepage_call_button"); case MWMActionBarButtonTypeBookmark: + case MWMActionBarButtonTypeTrack: return L(isSelected ? @"delete" : @"save"); case MWMActionBarButtonTypeRouteFrom: return L(@"p2p_from_here"); @@ -103,6 +104,10 @@ NSString *titleForButton(MWMActionBarButtonType type, BOOL isSelected) { case MWMActionBarButtonTypeBookmark: [self setupBookmarkButton:isSelected]; break; + case MWMActionBarButtonTypeTrack: + [self.button setImage:[[UIImage imageNamed:@"ic_route_manager_trash"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] forState:UIControlStateNormal]; + self.button.coloring = MWMButtonColoringRed; + break; case MWMActionBarButtonTypeRouteFrom: [self.button setImage:[UIImage imageNamed:@"ic_route_from"] forState:UIControlStateNormal]; break; diff --git a/iphone/Maps/UI/PlacePage/PlacePageLayout/Layouts/PlacePageCommonLayout.swift b/iphone/Maps/UI/PlacePage/PlacePageLayout/Layouts/PlacePageCommonLayout.swift index f15f7f85d1..cc9f44096e 100644 --- a/iphone/Maps/UI/PlacePage/PlacePageLayout/Layouts/PlacePageCommonLayout.swift +++ b/iphone/Maps/UI/PlacePage/PlacePageLayout/Layouts/PlacePageCommonLayout.swift @@ -1,17 +1,7 @@ class PlacePageCommonLayout: NSObject, IPlacePageLayout { - private lazy var distanceFormatter: MKDistanceFormatter = { - let formatter = MKDistanceFormatter() - formatter.unitStyle = .abbreviated - formatter.units = Settings.measurementUnits() == .imperial ? .imperial : .metric - return formatter - }() - - private lazy var unitsFormatter: MeasurementFormatter = { - let formatter = MeasurementFormatter() - formatter.unitOptions = [.providedUnit] - return formatter - }() + private let distanceFormatter = DistanceFormatter.self + private let altitudeFormatter = AltitudeFormatter.self private var placePageData: PlacePageData private var interactor: PlacePageInteractor @@ -53,8 +43,8 @@ class PlacePageCommonLayout: NSObject, IPlacePageLayout { return vc } () - lazy var bookmarkViewController: PlacePageBookmarkViewController = { - let vc = storyboard.instantiateViewController(ofType: PlacePageBookmarkViewController.self) + lazy var editBookmarkViewController: PlacePageEditBookmarkOrTrackViewController = { + let vc = storyboard.instantiateViewController(ofType: PlacePageEditBookmarkOrTrackViewController.self) vc.view.isHidden = true vc.delegate = interactor return vc @@ -67,6 +57,13 @@ class PlacePageCommonLayout: NSObject, IPlacePageLayout { return vc } () + private func productsViewController() -> ProductsViewController? { + let productsManager = FrameworkHelper.self + guard let configuration = productsManager.getProductsConfiguration() else { return nil } + let viewModel = ProductsViewModel(manager: productsManager, configuration: configuration) + return ProductsViewController(viewModel: viewModel) + } + lazy var buttonsViewController: PlacePageButtonsViewController = { let vc = storyboard.instantiateViewController(ofType: PlacePageButtonsViewController.self) vc.buttonsData = placePageData.buttonsData! @@ -95,6 +92,7 @@ class PlacePageCommonLayout: NSObject, IPlacePageLayout { private func configureViewControllers() -> [UIViewController] { var viewControllers = [UIViewController]() + viewControllers.append(wikiDescriptionViewController) if let wikiDescriptionHtml = placePageData.wikiDescriptionHtml { wikiDescriptionViewController.descriptionHtml = wikiDescriptionHtml @@ -103,16 +101,20 @@ class PlacePageCommonLayout: NSObject, IPlacePageLayout { } } - viewControllers.append(bookmarkViewController) + viewControllers.append(editBookmarkViewController) if let bookmarkData = placePageData.bookmarkData { - bookmarkViewController.bookmarkData = bookmarkData - bookmarkViewController.view.isHidden = false + editBookmarkViewController.data = .bookmark(bookmarkData) + editBookmarkViewController.view.isHidden = false } if placePageData.infoData != nil { viewControllers.append(infoViewController) } + if let productsViewController = productsViewController() { + viewControllers.append(productsViewController) + } + if placePageData.buttonsData != nil { viewControllers.append(buttonsViewController) } @@ -181,7 +183,7 @@ extension PlacePageCommonLayout { func updateBookmarkRelatedSections() { var isBookmark = false if let bookmarkData = placePageData.bookmarkData { - bookmarkViewController.bookmarkData = bookmarkData + editBookmarkViewController.data = .bookmark(bookmarkData) isBookmark = true } if let title = placePageData.previewData.title, let headerViewController = headerViewControllers.compactMap({ $0 as? PlacePageHeaderViewController }).first { @@ -191,7 +193,7 @@ extension PlacePageCommonLayout { } presenter?.layoutIfNeeded() UIView.animate(withDuration: kDefaultAnimationDuration) { [unowned self] in - self.bookmarkViewController.view.isHidden = !isBookmark + self.editBookmarkViewController.view.isHidden = !isBookmark } } } @@ -207,12 +209,7 @@ extension PlacePageCommonLayout: MWMLocationObserver { func onLocationUpdate(_ location: CLLocation) { if placePageData.isMyPosition { - /// @todo Use C++ Distance::FormatAltitude function? - let imperial = Settings.measurementUnits() == .imperial - let alt = imperial ? location.altitude / 0.3048 : location.altitude - let altMeasurement = Measurement(value: alt.rounded(), unit: imperial ? UnitLength.feet : UnitLength.meters) - let altString = "▲ \(unitsFormatter.string(from: altMeasurement))" - + let altString = "▲ \(altitudeFormatter.altitudeString(fromMeters: location.altitude))" if location.speed > 0 && location.timestamp.timeIntervalSinceNow >= -2 { let speedMeasure = Measure.init(asSpeed: location.speed) let speedString = "\(LocationManager.speedSymbolFor(location.speed))\(speedMeasure.valueAsString) \(speedMeasure.unit)" @@ -224,7 +221,7 @@ extension PlacePageCommonLayout: MWMLocationObserver { let ppLocation = CLLocation(latitude: placePageData.locationCoordinate.latitude, longitude: placePageData.locationCoordinate.longitude) let distance = location.distance(from: ppLocation) - let formattedDistance = distanceFormatter.string(fromDistance: distance) + let formattedDistance = distanceFormatter.distanceString(fromMeters: distance) previewViewController.updateDistance(formattedDistance) lastLocation = location diff --git a/iphone/Maps/UI/PlacePage/PlacePageLayout/Layouts/PlacePageElevationLayout.swift b/iphone/Maps/UI/PlacePage/PlacePageLayout/Layouts/PlacePageElevationLayout.swift deleted file mode 100644 index 3b49316114..0000000000 --- a/iphone/Maps/UI/PlacePage/PlacePageLayout/Layouts/PlacePageElevationLayout.swift +++ /dev/null @@ -1,57 +0,0 @@ -class PlacePageElevationLayout: IPlacePageLayout { - private var placePageData: PlacePageData - private var interactor: PlacePageInteractor - private let storyboard: UIStoryboard - weak var presenter: PlacePagePresenterProtocol? - - lazy var bodyViewControllers: [UIViewController] = { - return configureViewControllers() - }() - - var actionBar: ActionBarViewController? = nil - - var navigationBar: UIViewController? { - return placePageNavigationViewController - } - - lazy var headerViewControllers: [UIViewController] = { - return [PlacePageHeaderBuilder.build(data: placePageData.previewData, delegate: interactor, headerType: .flexible)] - } () - - lazy var placePageNavigationViewController: PlacePageHeaderViewController = { - return PlacePageHeaderBuilder.build(data: placePageData.previewData, delegate: interactor, headerType: .fixed) - } () - - lazy var elevationMapViewController: ElevationProfileViewController = { - let vc = ElevationProfileBuilder.build(data: placePageData, delegate: interactor) - return vc - } () - - init(interactor: PlacePageInteractor, storyboard: UIStoryboard, data: PlacePageData) { - self.interactor = interactor - self.storyboard = storyboard - self.placePageData = data - } - - private func configureViewControllers() -> [UIViewController] { - var viewControllers = [UIViewController]() - viewControllers.append(elevationMapViewController) - - return viewControllers - } - - func calculateSteps(inScrollView scrollView: UIScrollView, compact: Bool) -> [PlacePageState] { - var steps: [PlacePageState] = [] - let scrollHeight = scrollView.height - let previewHeight = elevationMapViewController.getPreviewHeight() - steps.append(.closed(-scrollHeight)) - guard let previewView = elevationMapViewController.view else { - return steps - } - let previewFrame = scrollView.convert(previewView.bounds, from: previewView) - steps.append(.preview(previewFrame.maxY - scrollHeight - previewHeight)) - steps.append(.expanded(previewFrame.maxY - scrollHeight)) - steps.append(.full(0)) - return steps - } -} diff --git a/iphone/Maps/UI/PlacePage/PlacePageLayout/Layouts/PlacePageTrackLayout.swift b/iphone/Maps/UI/PlacePage/PlacePageLayout/Layouts/PlacePageTrackLayout.swift new file mode 100644 index 0000000000..7faf58fcb4 --- /dev/null +++ b/iphone/Maps/UI/PlacePage/PlacePageLayout/Layouts/PlacePageTrackLayout.swift @@ -0,0 +1,133 @@ +class PlacePageTrackLayout: IPlacePageLayout { + private var placePageData: PlacePageData + private var trackData: PlacePageTrackData + private var interactor: PlacePageInteractor + private let storyboard: UIStoryboard + weak var presenter: PlacePagePresenterProtocol? + + lazy var bodyViewControllers: [UIViewController] = { + return configureViewControllers() + }() + + var actionBar: ActionBarViewController? { + actionBarViewController + } + + var navigationBar: UIViewController? { + placePageNavigationViewController + } + + lazy var headerViewControllers: [UIViewController] = { + [headerViewController, previewViewController] + }() + + lazy var headerViewController: PlacePageHeaderViewController = { + PlacePageHeaderBuilder.build(data: placePageData.previewData, delegate: interactor, headerType: .flexible) + }() + + lazy var previewViewController: PlacePagePreviewViewController = { + let vc = storyboard.instantiateViewController(ofType: PlacePagePreviewViewController.self) + vc.placePagePreviewData = placePageData.previewData + return vc + }() + + lazy var placePageNavigationViewController: PlacePageHeaderViewController = { + return PlacePageHeaderBuilder.build(data: placePageData.previewData, delegate: interactor, headerType: .fixed) + }() + + lazy var editTrackViewController: PlacePageEditBookmarkOrTrackViewController = { + let vc = storyboard.instantiateViewController(ofType: PlacePageEditBookmarkOrTrackViewController.self) + vc.view.isHidden = true + vc.delegate = interactor + return vc + }() + + lazy var elevationMapViewController: ElevationProfileViewController? = { + guard trackData.trackInfo.hasElevationInfo(), + let elevationProfileData = trackData.elevationProfileData else { + return nil + } + return ElevationProfileBuilder.build(trackInfo: trackData.trackInfo, + elevationProfileData: elevationProfileData, + delegate: interactor) + }() + + lazy var actionBarViewController: ActionBarViewController = { + let vc = storyboard.instantiateViewController(ofType: ActionBarViewController.self) + vc.placePageData = placePageData + vc.canAddStop = MWMRouter.canAddIntermediatePoint() + vc.isRoutePlanning = MWMNavigationDashboardManager.shared().state != .hidden + vc.delegate = interactor + return vc + }() + + init(interactor: PlacePageInteractor, storyboard: UIStoryboard, data: PlacePageData) { + self.interactor = interactor + self.storyboard = storyboard + self.placePageData = data + guard let trackData = data.trackData else { + fatalError("PlacePageData must contain trackData for the PlacePageTrackLayout") + } + self.trackData = trackData + } + + private func configureViewControllers() -> [UIViewController] { + var viewControllers = [UIViewController]() + + viewControllers.append(editTrackViewController) + editTrackViewController.view.isHidden = false + editTrackViewController.data = .track(trackData) + + placePageData.onBookmarkStatusUpdate = { [weak self] in + guard let self = self else { return } + self.previewViewController.placePagePreviewData = self.placePageData.previewData + self.updateTrackRelatedSections() + } + + if let elevationMapViewController { + viewControllers.append(elevationMapViewController) + } + + return viewControllers + } + + func calculateSteps(inScrollView scrollView: UIScrollView, compact: Bool) -> [PlacePageState] { + var steps: [PlacePageState] = [] + let scrollHeight = scrollView.height + steps.append(.closed(-scrollHeight)) + guard elevationMapViewController != nil else { + steps.append(.full(0)) + return steps + } + guard let previewView = previewViewController.view else { + return steps + } + let previewFrame = scrollView.convert(previewView.bounds, from: previewView) + steps.append(.preview(previewFrame.maxY - scrollHeight)) + if !compact { + steps.append(.expanded(-scrollHeight * 0.55)) + } + steps.append(.full(0)) + return steps + } +} + +private extension PlacePageTrackLayout { + func updateTrackRelatedSections() { + guard let trackData = placePageData.trackData else { + presenter?.closeAnimated() + return + } + editTrackViewController.data = .track(trackData) + let previewData = placePageData.previewData + if let headerViewController = headerViewControllers.compactMap({ $0 as? PlacePageHeaderViewController }).first { + headerViewController.setTitle(previewData.title, secondaryTitle: previewData.secondaryTitle) + placePageNavigationViewController.setTitle(previewData.title, secondaryTitle: previewData.secondaryTitle) + } + if let previewViewController = headerViewControllers.compactMap({ $0 as? PlacePagePreviewViewController }).first { + previewViewController.placePagePreviewData = previewData + previewViewController.updateViews() + } + presenter?.layoutIfNeeded() + } +} diff --git a/iphone/Maps/UI/PlacePage/PlacePageManager/MWMPlacePageManager.mm b/iphone/Maps/UI/PlacePage/PlacePageManager/MWMPlacePageManager.mm index 8101db24af..725991feb8 100644 --- a/iphone/Maps/UI/PlacePage/PlacePageManager/MWMPlacePageManager.mm +++ b/iphone/Maps/UI/PlacePage/PlacePageManager/MWMPlacePageManager.mm @@ -7,9 +7,8 @@ #import "MWMStorage+UI.h" #import "SwiftBridge.h" #import "MWMMapViewControlsManager+AddPlace.h" -#import "location_util.h" -#import +#import #import #include "platform/downloader_defines.hpp" @@ -87,8 +86,8 @@ using namespace storage; NSString *title = nil; if (data.previewData.title.length > 0) { title = data.previewData.title; - } else if (data.previewData.address.length > 0) { - title = data.previewData.address; + } else if (data.previewData.secondarySubtitle.length > 0) { + title = data.previewData.secondarySubtitle; } else if (data.previewData.subtitle.length > 0) { title = data.previewData.subtitle; } else if (data.bookmarkData != nil) { @@ -121,8 +120,8 @@ using namespace storage; NSString *title = nil; if (data.previewData.title.length > 0) { title = data.previewData.title; - } else if (data.previewData.address.length > 0) { - title = data.previewData.address; + } else if (data.previewData.secondarySubtitle.length > 0) { + title = data.previewData.secondarySubtitle; } else if (data.previewData.subtitle.length > 0) { title = data.previewData.subtitle; } else if (data.bookmarkData != nil) { @@ -185,12 +184,16 @@ using namespace storage; { auto &f = GetFramework(); f.GetBookmarkManager().GetEditSession().DeleteBookmark(data.bookmarkData.bookmarkId); - [MWMFrameworkHelper updateAfterDeleteBookmark]; - [data updateBookmarkStatus]; } +- (void)removeTrack:(PlacePageData *)data +{ + auto &f = GetFramework(); + f.GetBookmarkManager().GetEditSession().DeleteTrack(data.trackData.trackId); +} + - (void)call:(PlacePageData *)data { if (data.infoData.phoneUrl && [UIApplication.sharedApplication canOpenURL:data.infoData.phoneUrl]) { [UIApplication.sharedApplication openURL:data.infoData.phoneUrl options:@{} completionHandler:nil]; @@ -204,6 +207,20 @@ using namespace storage; [[MapViewController sharedController].navigationController pushViewController:editBookmarkController animated:YES]; } +- (void)editTrack:(PlacePageData *)data { + if (data.objectType != PlacePageObjectTypeTrack) { + ASSERT_FAIL("editTrack called for non-track object"); + return; + } + EditTrackViewController * editTrackController = [[EditTrackViewController alloc] initWithTrackId:data.trackData.trackId editCompletion:^(BOOL edited) { + if (!edited) + return; + [MWMFrameworkHelper updatePlacePageData]; + [data updateBookmarkStatus]; + }]; + [[MapViewController sharedController].navigationController pushViewController:editTrackController animated:YES]; +} + - (void)showPlaceDescription:(NSString *)htmlString { [self.ownerViewController openFullPlaceDescriptionWithHtml:htmlString]; diff --git a/iphone/Maps/UI/PlacePage/PlacePageManager/MWMPlacePageManagerHelper.h b/iphone/Maps/UI/PlacePage/PlacePageManager/MWMPlacePageManagerHelper.h index 0b7efa3c19..e5a1c78617 100644 --- a/iphone/Maps/UI/PlacePage/PlacePageManager/MWMPlacePageManagerHelper.h +++ b/iphone/Maps/UI/PlacePage/PlacePageManager/MWMPlacePageManagerHelper.h @@ -28,7 +28,9 @@ + (void)openCatalogMoreItems:(PlacePageData *)data; + (void)addBookmark:(PlacePageData *)data; + (void)removeBookmark:(PlacePageData *)data; ++ (void)removeTrack:(PlacePageData *)data; + (void)editBookmark:(PlacePageData *)data; ++ (void)editTrack:(PlacePageData *)data; + (void)searchBookingHotels:(PlacePageData *)data; + (void)book:(PlacePageData *)data; + (void)routeFrom:(PlacePageData *)data; diff --git a/iphone/Maps/UI/PlacePage/PlacePageManager/MWMPlacePageManagerHelper.mm b/iphone/Maps/UI/PlacePage/PlacePageManager/MWMPlacePageManagerHelper.mm index f6b78cdd17..4feae30af8 100644 --- a/iphone/Maps/UI/PlacePage/PlacePageManager/MWMPlacePageManagerHelper.mm +++ b/iphone/Maps/UI/PlacePage/PlacePageManager/MWMPlacePageManagerHelper.mm @@ -35,7 +35,9 @@ - (void)openCatalogMoreItems:(PlacePageData *)data; - (void)addBookmark:(PlacePageData *)data; - (void)removeBookmark:(PlacePageData *)data; +- (void)removeTrack:(PlacePageData *)data; - (void)editBookmark:(PlacePageData *)data; +- (void)editTrack:(PlacePageData *)data; - (void)searchBookingHotels:(PlacePageData *)data; - (void)book:(PlacePageData *)data; - (void)routeFrom:(PlacePageData *)data; @@ -152,10 +154,18 @@ [[MWMMapViewControlsManager manager].placePageManager removeBookmark:data]; } ++ (void)removeTrack:(PlacePageData *)data { + [[MWMMapViewControlsManager manager].placePageManager removeTrack:data]; +} + + (void)editBookmark:(PlacePageData *)data { [[MWMMapViewControlsManager manager].placePageManager editBookmark:data]; } ++ (void)editTrack:(PlacePageData *)data { + [[MWMMapViewControlsManager manager].placePageManager editTrack:data]; +} + + (void)searchBookingHotels:(PlacePageData *)data { [[MWMMapViewControlsManager manager].placePageManager searchBookingHotels:data]; } diff --git a/iphone/Maps/UI/PlacePage/PlacePagePresenter.swift b/iphone/Maps/UI/PlacePage/PlacePagePresenter.swift index 881bfc57b1..133e550f83 100644 --- a/iphone/Maps/UI/PlacePage/PlacePagePresenter.swift +++ b/iphone/Maps/UI/PlacePage/PlacePagePresenter.swift @@ -3,18 +3,14 @@ protocol PlacePagePresenterProtocol: AnyObject { func layoutIfNeeded() func showNextStop() func closeAnimated() - func updateTopBound(_ bound: CGFloat, duration: TimeInterval) func showAlert(_ alert: UIAlertController) } class PlacePagePresenter: NSObject { private weak var view: PlacePageViewProtocol! - private let interactor: PlacePageInteractorProtocol - init(view: PlacePageViewProtocol, - interactor: PlacePageInteractorProtocol) { + init(view: PlacePageViewProtocol) { self.view = view - self.interactor = interactor } } @@ -37,10 +33,6 @@ extension PlacePagePresenter: PlacePagePresenterProtocol { view.closeAnimated(completion: nil) } - func updateTopBound(_ bound: CGFloat, duration: TimeInterval) { - interactor.updateTopBound(bound, duration: duration) - } - func showAlert(_ alert: UIAlertController) { view.showAlert(alert) } diff --git a/iphone/Maps/UI/PlacePage/PlacePageViewController.swift b/iphone/Maps/UI/PlacePage/PlacePageViewController.swift index 590d3635bd..d4da6c0c27 100644 --- a/iphone/Maps/UI/PlacePage/PlacePageViewController.swift +++ b/iphone/Maps/UI/PlacePage/PlacePageViewController.swift @@ -1,5 +1,5 @@ protocol PlacePageViewProtocol: AnyObject { - var presenter: PlacePagePresenterProtocol! { get set } + var interactor: PlacePageInteractorProtocol! { get set } func setLayout(_ layout: IPlacePageLayout) func closeAnimated(completion: (() -> Void)?) @@ -35,7 +35,7 @@ final class PlacePageScrollView: UIScrollView { stackView.distribution = .fill return stackView }() - var presenter: PlacePagePresenterProtocol! + var interactor: PlacePageInteractorProtocol! var beginDragging = false var rootViewController: MapViewController { MapViewController.shared()! @@ -69,6 +69,11 @@ final class PlacePageScrollView: UIScrollView { previousTraitCollection = traitCollection } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + interactor?.viewWillAppear() + } + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) updatePreviewOffset() @@ -77,7 +82,7 @@ final class PlacePageScrollView: UIScrollView { override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) // Update layout when the device was rotated but skip when the appearance was changed. - if self.previousTraitCollection != nil, previousTraitCollection?.userInterfaceStyle == traitCollection.userInterfaceStyle { + if self.previousTraitCollection != nil, previousTraitCollection?.userInterfaceStyle == traitCollection.userInterfaceStyle, previousTraitCollection?.verticalSizeClass != traitCollection.verticalSizeClass { DispatchQueue.main.async { self.updateSteps() self.showLastStop() @@ -172,8 +177,8 @@ final class PlacePageScrollView: UIScrollView { private func setupLayout(_ layout: IPlacePageLayout) { setLayout(layout) - layout.headerViewControllers.forEach({ addToHeader($0) }) - layout.bodyViewControllers.forEach({ addToBody($0) }) + fillHeader(with: layout.headerViewControllers) + fillBody(with: layout.bodyViewControllers) beginDragging = false if let actionBar = layout.actionBar { @@ -184,6 +189,26 @@ final class PlacePageScrollView: UIScrollView { } } + private func fillHeader(with viewControllers: [UIViewController]) { + viewControllers.forEach { [self] viewController in + if !stackView.arrangedSubviews.contains(headerStackView) { + stackView.addArrangedSubview(headerStackView) + } + headerStackView.addArrangedSubview(viewController.view) + } + headerStackView.addSeparator(.bottom) + } + + private func fillBody(with viewControllers: [UIViewController]) { + viewControllers.forEach { [self] viewController in + addChild(viewController) + stackView.addArrangedSubview(viewController.view) + viewController.didMove(toParent: self) + viewController.view.addSeparator(.top) + viewController.view.addSeparator(.bottom) + } + } + private func cleanupLayout() { layout?.actionBar?.view.removeFromSuperview() layout?.navigationBar?.view.removeFromSuperview() @@ -201,6 +226,55 @@ final class PlacePageScrollView: UIScrollView { return result } + private func addActionBar(_ actionBarViewController: UIViewController) { + addChild(actionBarViewController) + actionBarViewController.view.translatesAutoresizingMaskIntoConstraints = false + actionBarContainerView.addSubview(actionBarViewController.view) + actionBarViewController.didMove(toParent: self) + NSLayoutConstraint.activate([ + actionBarViewController.view.leadingAnchor.constraint(equalTo: actionBarContainerView.leadingAnchor), + actionBarViewController.view.topAnchor.constraint(equalTo: actionBarContainerView.topAnchor), + actionBarViewController.view.trailingAnchor.constraint(equalTo: actionBarContainerView.trailingAnchor), + actionBarViewController.view.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor) + ]) + } + + private func addNavigationBar(_ header: UIViewController) { + header.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(header.view) + addChild(header) + NSLayoutConstraint.activate([ + header.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + header.view.topAnchor.constraint(equalTo: view.topAnchor), + header.view.trailingAnchor.constraint(equalTo: view.trailingAnchor) + ]) + } + + private func scrollTo(_ point: CGPoint, animated: Bool = true, forced: Bool = false, completion: (() -> Void)? = nil) { + if alternativeSizeClass(iPhone: beginDragging, iPad: true) && !forced { + return + } + if forced { + beginDragging = true + } + let scrollPosition = CGPoint(x: point.x, y: min(scrollView.contentSize.height - scrollView.height, point.y)) + let bound = view.height + scrollPosition.y + if animated { + updateTopBound(bound, duration: kDefaultAnimationDuration) + UIView.animate(withDuration: kDefaultAnimationDuration, animations: { [weak scrollView] in + scrollView?.contentOffset = scrollPosition + self.layoutIfNeeded() + }) { complete in + if complete { + completion?() + } + } + } else { + scrollView?.contentOffset = scrollPosition + completion?() + } + } + private func showLastStop() { if let lastStop = scrollSteps.last { scrollTo(CGPoint(x: 0, y: lastStop.offset), forced: true) @@ -209,7 +283,7 @@ final class PlacePageScrollView: UIScrollView { private func updateTopBound(_ bound: CGFloat, duration: TimeInterval) { alternativeSizeClass(iPhone: { - presenter.updateTopBound(bound, duration: duration) + interactor.updateTopBound(bound, duration: duration) }, iPad: {}) } } @@ -237,13 +311,6 @@ extension PlacePageViewController: PlacePageViewProtocol { actionBarHeightConstraint.constant = !value ? Constants.actionBarHeight : .zero } - func addToHeader(_ headerViewController: UIViewController) { - if !stackView.arrangedSubviews.contains(headerStackView) { - stackView.addArrangedSubview(headerStackView) - } - headerStackView.addArrangedSubview(headerViewController.view) - } - func updatePreviewOffset() { updateSteps() if !beginDragging { @@ -252,61 +319,6 @@ extension PlacePageViewController: PlacePageViewProtocol { } } - func addToBody(_ viewController: UIViewController) { - addChild(viewController) - stackView.addArrangedSubview(viewController.view) - viewController.didMove(toParent: self) - } - - func addActionBar(_ actionBarViewController: UIViewController) { - addChild(actionBarViewController) - actionBarViewController.view.translatesAutoresizingMaskIntoConstraints = false - actionBarContainerView.addSubview(actionBarViewController.view) - actionBarViewController.didMove(toParent: self) - NSLayoutConstraint.activate([ - actionBarViewController.view.leadingAnchor.constraint(equalTo: actionBarContainerView.leadingAnchor), - actionBarViewController.view.topAnchor.constraint(equalTo: actionBarContainerView.topAnchor), - actionBarViewController.view.trailingAnchor.constraint(equalTo: actionBarContainerView.trailingAnchor), - actionBarViewController.view.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor) - ]) - } - - func addNavigationBar(_ header: UIViewController) { - header.view.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(header.view) - addChild(header) - NSLayoutConstraint.activate([ - header.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), - header.view.topAnchor.constraint(equalTo: view.topAnchor), - header.view.trailingAnchor.constraint(equalTo: view.trailingAnchor) - ]) - } - - func scrollTo(_ point: CGPoint, animated: Bool = true, forced: Bool = false, completion: (() -> Void)? = nil) { - if alternativeSizeClass(iPhone: beginDragging, iPad: true) && !forced { - return - } - if forced { - beginDragging = true - } - let scrollPosition = CGPoint(x: point.x, y: min(scrollView.contentSize.height - scrollView.height, point.y)) - let bound = view.height + scrollPosition.y - if animated { - updateTopBound(bound, duration: kDefaultAnimationDuration) - UIView.animate(withDuration: kDefaultAnimationDuration, animations: { [weak scrollView] in - scrollView?.contentOffset = scrollPosition - self.layoutIfNeeded() - }) { complete in - if complete { - completion?() - } - } - } else { - scrollView?.contentOffset = scrollPosition - completion?() - } - } - func showNextStop() { if let nextStop = scrollSteps.last(where: { $0.offset > scrollView.contentOffset.y }) { scrollTo(CGPoint(x: 0, y: nextStop.offset), forced: true) diff --git a/iphone/Maps/UI/PlacePage/Util/OpeinigHoursLocalization.swift b/iphone/Maps/UI/PlacePage/Util/OpeinigHoursLocalization.swift index bf107300ce..dab1a5a463 100644 --- a/iphone/Maps/UI/PlacePage/Util/OpeinigHoursLocalization.swift +++ b/iphone/Maps/UI/PlacePage/Util/OpeinigHoursLocalization.swift @@ -1,6 +1,7 @@ import Foundation -class OpeinigHoursLocalization: IOpeningHoursLocalization { +@objcMembers +class OpeinigHoursLocalization: NSObject, IOpeningHoursLocalization { var closedString: String { L("closed") } diff --git a/iphone/Maps/UI/Search/Filters/FilterCollectionHolderCell.swift b/iphone/Maps/UI/Search/Filters/FilterCollectionHolderCell.swift deleted file mode 100644 index ecdcf71e42..0000000000 --- a/iphone/Maps/UI/Search/Filters/FilterCollectionHolderCell.swift +++ /dev/null @@ -1,43 +0,0 @@ -@objc(MWMFilterCollectionHolderCell) -final class FilterCollectionHolderCell: MWMTableViewCell { - @IBOutlet private(set) weak var collectionView: UICollectionView! - @IBOutlet private weak var collectionViewHeight: NSLayoutConstraint! - private weak var tableView: UITableView? - override var frame: CGRect { - didSet { - if frame.size.height < 1 /* minimal correct height */ { - frame.size.height = max(collectionViewHeight.constant, 1) - tableView?.refresh() - } - } - } - - private func layout() { - collectionView.setNeedsLayout() - collectionView.layoutIfNeeded() - if abs(collectionViewHeight.constant - collectionView.contentSize.height) > 2.0 { - let newHeight = collectionView.contentSize.height - collectionViewHeight.constant = newHeight - frame.size.height = newHeight - tableView?.reloadData() - } - } - - @objc func config(tableView: UITableView?) { - self.tableView = tableView - layout() - collectionView.allowsMultipleSelection = true - collectionView.contentInset = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16) - collectionView.reloadData() - } - - override func awakeFromNib() { - super.awakeFromNib() - isSeparatorHidden = true - } - - override func layoutSubviews() { - super.layoutSubviews() - layout() - } -} diff --git a/iphone/Maps/UI/Search/Filters/FilterTypeCell.swift b/iphone/Maps/UI/Search/Filters/FilterTypeCell.swift deleted file mode 100644 index 8d75e6f7f8..0000000000 --- a/iphone/Maps/UI/Search/Filters/FilterTypeCell.swift +++ /dev/null @@ -1,19 +0,0 @@ -@objc(MWMFilterTypeCell) -final class FilterTypeCell: UICollectionViewCell { - - @IBOutlet weak var tagName: UILabel! - - override var isSelected: Bool { - didSet { - backgroundColor = isSelected ? UIColor.linkBlue() : UIColor.white() - tagName.textColor = isSelected ? UIColor.white() : UIColor.blackPrimaryText() - } - } - - override var isHighlighted: Bool { - didSet { - backgroundColor = isHighlighted ? UIColor.linkBlueHighlighted() : UIColor.white() - tagName.textColor = isHighlighted ? UIColor.white() : UIColor.blackPrimaryText() - } - } -} diff --git a/iphone/Maps/UI/Search/Filters/MWMSearchFilterViewController.h b/iphone/Maps/UI/Search/Filters/MWMSearchFilterViewController.h deleted file mode 100644 index 6b641a3886..0000000000 --- a/iphone/Maps/UI/Search/Filters/MWMSearchFilterViewController.h +++ /dev/null @@ -1,7 +0,0 @@ -#import "MWMViewController.h" - -@interface MWMSearchFilterViewController : MWMViewController - -+ (MWMSearchFilterViewController *)controller; - -@end diff --git a/iphone/Maps/UI/Search/Filters/MWMSearchFilterViewController.mm b/iphone/Maps/UI/Search/Filters/MWMSearchFilterViewController.mm deleted file mode 100644 index 838b55f303..0000000000 --- a/iphone/Maps/UI/Search/Filters/MWMSearchFilterViewController.mm +++ /dev/null @@ -1,18 +0,0 @@ -#import "MWMSearchFilterViewController_Protected.h" -#import "SwiftBridge.h" - -@implementation MWMSearchFilterViewController - -+ (MWMSearchFilterViewController *)controller -{ - // Must be implemented in subclasses. - [self doesNotRecognizeSelector:_cmd]; - return nil; -} - -+ (MWMSearchFilterViewController *)controllerWithIdentifier:(NSString *)identifier -{ - auto storyboard = [UIStoryboard instance:MWMStoryboardSearchFilters]; - return [storyboard instantiateViewControllerWithIdentifier:identifier]; -} -@end diff --git a/iphone/Maps/UI/Search/Filters/MWMSearchFilterViewController_Protected.h b/iphone/Maps/UI/Search/Filters/MWMSearchFilterViewController_Protected.h deleted file mode 100644 index ba1b91616c..0000000000 --- a/iphone/Maps/UI/Search/Filters/MWMSearchFilterViewController_Protected.h +++ /dev/null @@ -1,7 +0,0 @@ -#import "MWMSearchFilterViewController.h" - -@interface MWMSearchFilterViewController (Protected) - -+ (MWMSearchFilterViewController *)controllerWithIdentifier:(NSString *)identifier; - -@end diff --git a/iphone/Maps/UI/Search/MWMSearchManager.mm b/iphone/Maps/UI/Search/MWMSearchManager.mm index 44dab5b6ea..87a06641ca 100644 --- a/iphone/Maps/UI/Search/MWMSearchManager.mm +++ b/iphone/Maps/UI/Search/MWMSearchManager.mm @@ -54,7 +54,6 @@ const CGFloat kWidthForiPad = 320; @property(nonatomic) MWMNoMapsViewController *noMapsController; @property(nonatomic) Observers *observers; -@property(nonatomic) NSDateFormatter *dateFormatter; @end @@ -70,8 +69,6 @@ const CGFloat kWidthForiPad = 320; self.state = MWMSearchManagerStateHidden; [MWMSearch addObserver:self]; _observers = [Observers weakObjectsHashTable]; - _dateFormatter = [[NSDateFormatter alloc] init]; - _dateFormatter.dateFormat = @"yyyy-MM-dd"; } return self; } diff --git a/iphone/Maps/UI/Search/SearchBar.swift b/iphone/Maps/UI/Search/SearchBar.swift index f2a845ca24..7bc4033471 100644 --- a/iphone/Maps/UI/Search/SearchBar.swift +++ b/iphone/Maps/UI/Search/SearchBar.swift @@ -21,6 +21,8 @@ final class SearchBar: SolidTouchView { override var tabBarAreaAffectDirections: MWMAvailableAreaAffectDirections { return alternative(iPhone: [], iPad: .left) } + override var trackRecordingButtonAreaAffectDirections: MWMAvailableAreaAffectDirections { return alternative(iPhone: .top, iPad: .left) } + @objc var state: SearchBarState = .ready { didSet { if state != oldValue { diff --git a/iphone/Maps/UI/Settings/Cells/SettingsTableViewiCloudSwitchCell.swift b/iphone/Maps/UI/Settings/Cells/SettingsTableViewiCloudSwitchCell.swift index c25efc9824..d429735a1b 100644 --- a/iphone/Maps/UI/Settings/Cells/SettingsTableViewiCloudSwitchCell.swift +++ b/iphone/Maps/UI/Settings/Cells/SettingsTableViewiCloudSwitchCell.swift @@ -1,7 +1,7 @@ final class SettingsTableViewiCloudSwitchCell: SettingsTableViewDetailedSwitchCell { @objc - func updateWithSynchronizationState(_ state: CloudStorageSynchronizationState) { + func updateWithSynchronizationState(_ state: SynchronizationManagerState) { guard state.isAvailable else { accessoryView = nil accessoryType = .detailButton diff --git a/iphone/Maps/UI/Settings/MWMSettingsViewController.mm b/iphone/Maps/UI/Settings/MWMSettingsViewController.mm index 1d295b4ccf..d4cf08a266 100644 --- a/iphone/Maps/UI/Settings/MWMSettingsViewController.mm +++ b/iphone/Maps/UI/Settings/MWMSettingsViewController.mm @@ -4,7 +4,7 @@ #import "SwiftBridge.h" #import "MWMActivityViewController.h" -#import +#import #include "map/gps_tracker.hpp" @@ -188,7 +188,7 @@ static NSString * const kUDDidShowICloudSynchronizationEnablingAlert = @"kUDDidS isOn:[MWMSettings iCLoudSynchronizationEnabled]]; __weak __typeof(self) weakSelf = self; - [CloudStorageManager.shared addObserver:self synchronizationStateDidChangeHandler:^(CloudStorageSynchronizationState * state) { + [iCloudSynchronizaionManager.shared addObserver:self synchronizationStateDidChangeHandler:^(SynchronizationManagerState * state) { __strong auto strongSelf = weakSelf; [strongSelf.iCloudSynchronizationCell updateWithSynchronizationState:state]; }]; diff --git a/iphone/Maps/UI/Storyboard/Main.storyboard b/iphone/Maps/UI/Storyboard/Main.storyboard index b625fc2aad..a78cd06513 100644 --- a/iphone/Maps/UI/Storyboard/Main.storyboard +++ b/iphone/Maps/UI/Storyboard/Main.storyboard @@ -84,6 +84,9 @@ + @@ -179,6 +182,7 @@ + @@ -188,12 +192,15 @@ + + + @@ -529,23 +536,27 @@ - - - + + + - + - + + + + + - + + - - + - + @@ -652,7 +663,7 @@ - + diff --git a/iphone/Maps/UI/Storyboard/SearchFilters.storyboard b/iphone/Maps/UI/Storyboard/SearchFilters.storyboard deleted file mode 100644 index dc0da0673e..0000000000 --- a/iphone/Maps/UI/Storyboard/SearchFilters.storyboard +++ /dev/null @@ -1,339 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/iphone/Maps/UI/Storyboard/Storyboard.swift b/iphone/Maps/UI/Storyboard/Storyboard.swift index 07b3a5daaa..05ab7ac725 100644 --- a/iphone/Maps/UI/Storyboard/Storyboard.swift +++ b/iphone/Maps/UI/Storyboard/Storyboard.swift @@ -3,7 +3,6 @@ enum Storyboard: Int { case authorization case launchScreen case main - case searchFilters case settings case welcome case sharing @@ -20,7 +19,6 @@ extension UIStoryboard { case .authorization: name = "Authorization" case .launchScreen: name = "LaunchScreen" case .main: name = "Main" - case .searchFilters: name = "SearchFilters" case .settings: name = "Settings" case .welcome: name = "Welcome" case .sharing: name = "BookmarksSharingFlow" diff --git a/iphone/Maps/main.mm b/iphone/Maps/main.mm index 55df103325..98261accb4 100644 --- a/iphone/Maps/main.mm +++ b/iphone/Maps/main.mm @@ -6,8 +6,12 @@ int main(int argc, char * argv[]) { [MWMSettings initializeLogging]; + + NSBundle * mainBundle = [NSBundle mainBundle]; + NSString * appName = [mainBundle objectForInfoDictionaryKey:@"CFBundleName"]; + NSString * bundleId = mainBundle.bundleIdentifier; auto & p = GetPlatform(); - LOG(LINFO, (p.Version(), "started, detected CPU cores:", p.CpuCores())); + LOG(LINFO, (appName.UTF8String, bundleId.UTF8String, p.Version(), "started, detected CPU cores:", p.CpuCores())); int retVal; @autoreleasepool diff --git a/iphone/metadata/ar-SA/release_notes.txt b/iphone/metadata/ar-SA/release_notes.txt index bad114bf41..fd5f15cdf3 100644 --- a/iphone/metadata/ar-SA/release_notes.txt +++ b/iphone/metadata/ar-SA/release_notes.txt @@ -1,3 +1,4 @@ -- iOS 18 Dark Mode: Introduced dark and tinted app icons. -- Map Data: Updated as of October 1, now including isolines for Egypt, Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. +• New OpenStreetMap data as of November 22 +• Fixed bug with Bookmark's description editor +• macOS: map scrolling via mouse and trackpad +• Other search improvements, translation updates & bug fixes diff --git a/iphone/metadata/ca/release_notes.txt b/iphone/metadata/ca/release_notes.txt index bad114bf41..fd5f15cdf3 100644 --- a/iphone/metadata/ca/release_notes.txt +++ b/iphone/metadata/ca/release_notes.txt @@ -1,3 +1,4 @@ -- iOS 18 Dark Mode: Introduced dark and tinted app icons. -- Map Data: Updated as of October 1, now including isolines for Egypt, Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. +• New OpenStreetMap data as of November 22 +• Fixed bug with Bookmark's description editor +• macOS: map scrolling via mouse and trackpad +• Other search improvements, translation updates & bug fixes diff --git a/iphone/metadata/cs/release_notes.txt b/iphone/metadata/cs/release_notes.txt index bad114bf41..fd5f15cdf3 100644 --- a/iphone/metadata/cs/release_notes.txt +++ b/iphone/metadata/cs/release_notes.txt @@ -1,3 +1,4 @@ -- iOS 18 Dark Mode: Introduced dark and tinted app icons. -- Map Data: Updated as of October 1, now including isolines for Egypt, Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. +• New OpenStreetMap data as of November 22 +• Fixed bug with Bookmark's description editor +• macOS: map scrolling via mouse and trackpad +• Other search improvements, translation updates & bug fixes diff --git a/iphone/metadata/da/release_notes.txt b/iphone/metadata/da/release_notes.txt index bad114bf41..fd5f15cdf3 100644 --- a/iphone/metadata/da/release_notes.txt +++ b/iphone/metadata/da/release_notes.txt @@ -1,3 +1,4 @@ -- iOS 18 Dark Mode: Introduced dark and tinted app icons. -- Map Data: Updated as of October 1, now including isolines for Egypt, Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. +• New OpenStreetMap data as of November 22 +• Fixed bug with Bookmark's description editor +• macOS: map scrolling via mouse and trackpad +• Other search improvements, translation updates & bug fixes diff --git a/iphone/metadata/de-DE/release_notes.txt b/iphone/metadata/de-DE/release_notes.txt index bad114bf41..a8ad557cf6 100644 --- a/iphone/metadata/de-DE/release_notes.txt +++ b/iphone/metadata/de-DE/release_notes.txt @@ -1,3 +1,4 @@ -- iOS 18 Dark Mode: Introduced dark and tinted app icons. -- Map Data: Updated as of October 1, now including isolines for Egypt, Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. +• Neue OpenStreetMap-Daten vom 22. November +• Behobener Fehler im Beschreibung-Editor für Lesezeichen +• macOS: Karten-Scrolling mit Maus und Trackpad +• Weitere Suchverbesserungen, Übersetzungs-Updates und Fehlerbehebungen diff --git a/iphone/metadata/el/release_notes.txt b/iphone/metadata/el/release_notes.txt index bad114bf41..fd5f15cdf3 100644 --- a/iphone/metadata/el/release_notes.txt +++ b/iphone/metadata/el/release_notes.txt @@ -1,3 +1,4 @@ -- iOS 18 Dark Mode: Introduced dark and tinted app icons. -- Map Data: Updated as of October 1, now including isolines for Egypt, Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. +• New OpenStreetMap data as of November 22 +• Fixed bug with Bookmark's description editor +• macOS: map scrolling via mouse and trackpad +• Other search improvements, translation updates & bug fixes diff --git a/iphone/metadata/en-AU/release_notes.txt b/iphone/metadata/en-AU/release_notes.txt index bad114bf41..fd5f15cdf3 100644 --- a/iphone/metadata/en-AU/release_notes.txt +++ b/iphone/metadata/en-AU/release_notes.txt @@ -1,3 +1,4 @@ -- iOS 18 Dark Mode: Introduced dark and tinted app icons. -- Map Data: Updated as of October 1, now including isolines for Egypt, Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. +• New OpenStreetMap data as of November 22 +• Fixed bug with Bookmark's description editor +• macOS: map scrolling via mouse and trackpad +• Other search improvements, translation updates & bug fixes diff --git a/iphone/metadata/en-CA/release_notes.txt b/iphone/metadata/en-CA/release_notes.txt index bad114bf41..fd5f15cdf3 100644 --- a/iphone/metadata/en-CA/release_notes.txt +++ b/iphone/metadata/en-CA/release_notes.txt @@ -1,3 +1,4 @@ -- iOS 18 Dark Mode: Introduced dark and tinted app icons. -- Map Data: Updated as of October 1, now including isolines for Egypt, Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. +• New OpenStreetMap data as of November 22 +• Fixed bug with Bookmark's description editor +• macOS: map scrolling via mouse and trackpad +• Other search improvements, translation updates & bug fixes diff --git a/iphone/metadata/en-GB/release_notes.txt b/iphone/metadata/en-GB/release_notes.txt index bad114bf41..fd5f15cdf3 100644 --- a/iphone/metadata/en-GB/release_notes.txt +++ b/iphone/metadata/en-GB/release_notes.txt @@ -1,3 +1,4 @@ -- iOS 18 Dark Mode: Introduced dark and tinted app icons. -- Map Data: Updated as of October 1, now including isolines for Egypt, Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. +• New OpenStreetMap data as of November 22 +• Fixed bug with Bookmark's description editor +• macOS: map scrolling via mouse and trackpad +• Other search improvements, translation updates & bug fixes diff --git a/iphone/metadata/en-US/release_notes.txt b/iphone/metadata/en-US/release_notes.txt index bad114bf41..fd5f15cdf3 100644 --- a/iphone/metadata/en-US/release_notes.txt +++ b/iphone/metadata/en-US/release_notes.txt @@ -1,3 +1,4 @@ -- iOS 18 Dark Mode: Introduced dark and tinted app icons. -- Map Data: Updated as of October 1, now including isolines for Egypt, Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. +• New OpenStreetMap data as of November 22 +• Fixed bug with Bookmark's description editor +• macOS: map scrolling via mouse and trackpad +• Other search improvements, translation updates & bug fixes diff --git a/iphone/metadata/es-ES/release_notes.txt b/iphone/metadata/es-ES/release_notes.txt index bad114bf41..a9dbdd6123 100644 --- a/iphone/metadata/es-ES/release_notes.txt +++ b/iphone/metadata/es-ES/release_notes.txt @@ -1,3 +1,4 @@ -- iOS 18 Dark Mode: Introduced dark and tinted app icons. -- Map Data: Updated as of October 1, now including isolines for Egypt, Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. +• Nuevos datos de OpenStreetMap del 22 de noviembre +• Error corregido en el editor de descripción de marcadores +• macOS: desplazamiento del mapa con mouse y trackpad +• Otras mejoras en la búsqueda, actualizaciones de traducción y correcciones de errores diff --git a/iphone/metadata/es-MX/release_notes.txt b/iphone/metadata/es-MX/release_notes.txt index bad114bf41..a9dbdd6123 100644 --- a/iphone/metadata/es-MX/release_notes.txt +++ b/iphone/metadata/es-MX/release_notes.txt @@ -1,3 +1,4 @@ -- iOS 18 Dark Mode: Introduced dark and tinted app icons. -- Map Data: Updated as of October 1, now including isolines for Egypt, Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. +• Nuevos datos de OpenStreetMap del 22 de noviembre +• Error corregido en el editor de descripción de marcadores +• macOS: desplazamiento del mapa con mouse y trackpad +• Otras mejoras en la búsqueda, actualizaciones de traducción y correcciones de errores diff --git a/iphone/metadata/fi/release_notes.txt b/iphone/metadata/fi/release_notes.txt index bad114bf41..fd5f15cdf3 100644 --- a/iphone/metadata/fi/release_notes.txt +++ b/iphone/metadata/fi/release_notes.txt @@ -1,3 +1,4 @@ -- iOS 18 Dark Mode: Introduced dark and tinted app icons. -- Map Data: Updated as of October 1, now including isolines for Egypt, Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. +• New OpenStreetMap data as of November 22 +• Fixed bug with Bookmark's description editor +• macOS: map scrolling via mouse and trackpad +• Other search improvements, translation updates & bug fixes diff --git a/iphone/metadata/fr-CA/release_notes.txt b/iphone/metadata/fr-CA/release_notes.txt index bad114bf41..7792d8a340 100644 --- a/iphone/metadata/fr-CA/release_notes.txt +++ b/iphone/metadata/fr-CA/release_notes.txt @@ -1,3 +1,4 @@ -- iOS 18 Dark Mode: Introduced dark and tinted app icons. -- Map Data: Updated as of October 1, now including isolines for Egypt, Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. +• Nouvelles données OpenStreetMap du 22 novembre +• Correction d'un bug dans l'éditeur de description des favoris +• macOS : défilement de la carte avec la souris et le trackpad +• Autres améliorations de recherche, mises à jour de traduction et corrections de bugs diff --git a/iphone/metadata/fr-FR/release_notes.txt b/iphone/metadata/fr-FR/release_notes.txt index bad114bf41..7792d8a340 100644 --- a/iphone/metadata/fr-FR/release_notes.txt +++ b/iphone/metadata/fr-FR/release_notes.txt @@ -1,3 +1,4 @@ -- iOS 18 Dark Mode: Introduced dark and tinted app icons. -- Map Data: Updated as of October 1, now including isolines for Egypt, Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. +• Nouvelles données OpenStreetMap du 22 novembre +• Correction d'un bug dans l'éditeur de description des favoris +• macOS : défilement de la carte avec la souris et le trackpad +• Autres améliorations de recherche, mises à jour de traduction et corrections de bugs diff --git a/iphone/metadata/he/release_notes.txt b/iphone/metadata/he/release_notes.txt index bad114bf41..fd5f15cdf3 100644 --- a/iphone/metadata/he/release_notes.txt +++ b/iphone/metadata/he/release_notes.txt @@ -1,3 +1,4 @@ -- iOS 18 Dark Mode: Introduced dark and tinted app icons. -- Map Data: Updated as of October 1, now including isolines for Egypt, Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. +• New OpenStreetMap data as of November 22 +• Fixed bug with Bookmark's description editor +• macOS: map scrolling via mouse and trackpad +• Other search improvements, translation updates & bug fixes diff --git a/iphone/metadata/hi/release_notes.txt b/iphone/metadata/hi/release_notes.txt index bad114bf41..fd5f15cdf3 100644 --- a/iphone/metadata/hi/release_notes.txt +++ b/iphone/metadata/hi/release_notes.txt @@ -1,3 +1,4 @@ -- iOS 18 Dark Mode: Introduced dark and tinted app icons. -- Map Data: Updated as of October 1, now including isolines for Egypt, Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. +• New OpenStreetMap data as of November 22 +• Fixed bug with Bookmark's description editor +• macOS: map scrolling via mouse and trackpad +• Other search improvements, translation updates & bug fixes diff --git a/iphone/metadata/hr/release_notes.txt b/iphone/metadata/hr/release_notes.txt index bad114bf41..fd5f15cdf3 100644 --- a/iphone/metadata/hr/release_notes.txt +++ b/iphone/metadata/hr/release_notes.txt @@ -1,3 +1,4 @@ -- iOS 18 Dark Mode: Introduced dark and tinted app icons. -- Map Data: Updated as of October 1, now including isolines for Egypt, Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. +• New OpenStreetMap data as of November 22 +• Fixed bug with Bookmark's description editor +• macOS: map scrolling via mouse and trackpad +• Other search improvements, translation updates & bug fixes diff --git a/iphone/metadata/hu/release_notes.txt b/iphone/metadata/hu/release_notes.txt index bad114bf41..fd5f15cdf3 100644 --- a/iphone/metadata/hu/release_notes.txt +++ b/iphone/metadata/hu/release_notes.txt @@ -1,3 +1,4 @@ -- iOS 18 Dark Mode: Introduced dark and tinted app icons. -- Map Data: Updated as of October 1, now including isolines for Egypt, Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. +• New OpenStreetMap data as of November 22 +• Fixed bug with Bookmark's description editor +• macOS: map scrolling via mouse and trackpad +• Other search improvements, translation updates & bug fixes diff --git a/iphone/metadata/id/release_notes.txt b/iphone/metadata/id/release_notes.txt index bad114bf41..fd5f15cdf3 100644 --- a/iphone/metadata/id/release_notes.txt +++ b/iphone/metadata/id/release_notes.txt @@ -1,3 +1,4 @@ -- iOS 18 Dark Mode: Introduced dark and tinted app icons. -- Map Data: Updated as of October 1, now including isolines for Egypt, Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. +• New OpenStreetMap data as of November 22 +• Fixed bug with Bookmark's description editor +• macOS: map scrolling via mouse and trackpad +• Other search improvements, translation updates & bug fixes diff --git a/iphone/metadata/it/release_notes.txt b/iphone/metadata/it/release_notes.txt index bad114bf41..fd5f15cdf3 100644 --- a/iphone/metadata/it/release_notes.txt +++ b/iphone/metadata/it/release_notes.txt @@ -1,3 +1,4 @@ -- iOS 18 Dark Mode: Introduced dark and tinted app icons. -- Map Data: Updated as of October 1, now including isolines for Egypt, Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. +• New OpenStreetMap data as of November 22 +• Fixed bug with Bookmark's description editor +• macOS: map scrolling via mouse and trackpad +• Other search improvements, translation updates & bug fixes diff --git a/iphone/metadata/ja/release_notes.txt b/iphone/metadata/ja/release_notes.txt index bad114bf41..fd5f15cdf3 100644 --- a/iphone/metadata/ja/release_notes.txt +++ b/iphone/metadata/ja/release_notes.txt @@ -1,3 +1,4 @@ -- iOS 18 Dark Mode: Introduced dark and tinted app icons. -- Map Data: Updated as of October 1, now including isolines for Egypt, Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. +• New OpenStreetMap data as of November 22 +• Fixed bug with Bookmark's description editor +• macOS: map scrolling via mouse and trackpad +• Other search improvements, translation updates & bug fixes diff --git a/iphone/metadata/ko/release_notes.txt b/iphone/metadata/ko/release_notes.txt index bad114bf41..fd5f15cdf3 100644 --- a/iphone/metadata/ko/release_notes.txt +++ b/iphone/metadata/ko/release_notes.txt @@ -1,3 +1,4 @@ -- iOS 18 Dark Mode: Introduced dark and tinted app icons. -- Map Data: Updated as of October 1, now including isolines for Egypt, Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. +• New OpenStreetMap data as of November 22 +• Fixed bug with Bookmark's description editor +• macOS: map scrolling via mouse and trackpad +• Other search improvements, translation updates & bug fixes diff --git a/iphone/metadata/ms/release_notes.txt b/iphone/metadata/ms/release_notes.txt index bad114bf41..fd5f15cdf3 100644 --- a/iphone/metadata/ms/release_notes.txt +++ b/iphone/metadata/ms/release_notes.txt @@ -1,3 +1,4 @@ -- iOS 18 Dark Mode: Introduced dark and tinted app icons. -- Map Data: Updated as of October 1, now including isolines for Egypt, Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. +• New OpenStreetMap data as of November 22 +• Fixed bug with Bookmark's description editor +• macOS: map scrolling via mouse and trackpad +• Other search improvements, translation updates & bug fixes diff --git a/iphone/metadata/nl-NL/release_notes.txt b/iphone/metadata/nl-NL/release_notes.txt index bad114bf41..fd5f15cdf3 100644 --- a/iphone/metadata/nl-NL/release_notes.txt +++ b/iphone/metadata/nl-NL/release_notes.txt @@ -1,3 +1,4 @@ -- iOS 18 Dark Mode: Introduced dark and tinted app icons. -- Map Data: Updated as of October 1, now including isolines for Egypt, Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. +• New OpenStreetMap data as of November 22 +• Fixed bug with Bookmark's description editor +• macOS: map scrolling via mouse and trackpad +• Other search improvements, translation updates & bug fixes diff --git a/iphone/metadata/no/release_notes.txt b/iphone/metadata/no/release_notes.txt index bad114bf41..fd5f15cdf3 100644 --- a/iphone/metadata/no/release_notes.txt +++ b/iphone/metadata/no/release_notes.txt @@ -1,3 +1,4 @@ -- iOS 18 Dark Mode: Introduced dark and tinted app icons. -- Map Data: Updated as of October 1, now including isolines for Egypt, Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. +• New OpenStreetMap data as of November 22 +• Fixed bug with Bookmark's description editor +• macOS: map scrolling via mouse and trackpad +• Other search improvements, translation updates & bug fixes diff --git a/iphone/metadata/pl/release_notes.txt b/iphone/metadata/pl/release_notes.txt index bad114bf41..fd5f15cdf3 100644 --- a/iphone/metadata/pl/release_notes.txt +++ b/iphone/metadata/pl/release_notes.txt @@ -1,3 +1,4 @@ -- iOS 18 Dark Mode: Introduced dark and tinted app icons. -- Map Data: Updated as of October 1, now including isolines for Egypt, Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. +• New OpenStreetMap data as of November 22 +• Fixed bug with Bookmark's description editor +• macOS: map scrolling via mouse and trackpad +• Other search improvements, translation updates & bug fixes diff --git a/iphone/metadata/pt-BR/release_notes.txt b/iphone/metadata/pt-BR/release_notes.txt index bad114bf41..fd5f15cdf3 100644 --- a/iphone/metadata/pt-BR/release_notes.txt +++ b/iphone/metadata/pt-BR/release_notes.txt @@ -1,3 +1,4 @@ -- iOS 18 Dark Mode: Introduced dark and tinted app icons. -- Map Data: Updated as of October 1, now including isolines for Egypt, Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. +• New OpenStreetMap data as of November 22 +• Fixed bug with Bookmark's description editor +• macOS: map scrolling via mouse and trackpad +• Other search improvements, translation updates & bug fixes diff --git a/iphone/metadata/pt-PT/release_notes.txt b/iphone/metadata/pt-PT/release_notes.txt index bad114bf41..fd5f15cdf3 100644 --- a/iphone/metadata/pt-PT/release_notes.txt +++ b/iphone/metadata/pt-PT/release_notes.txt @@ -1,3 +1,4 @@ -- iOS 18 Dark Mode: Introduced dark and tinted app icons. -- Map Data: Updated as of October 1, now including isolines for Egypt, Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. +• New OpenStreetMap data as of November 22 +• Fixed bug with Bookmark's description editor +• macOS: map scrolling via mouse and trackpad +• Other search improvements, translation updates & bug fixes diff --git a/iphone/metadata/ro/release_notes.txt b/iphone/metadata/ro/release_notes.txt index bad114bf41..fd5f15cdf3 100644 --- a/iphone/metadata/ro/release_notes.txt +++ b/iphone/metadata/ro/release_notes.txt @@ -1,3 +1,4 @@ -- iOS 18 Dark Mode: Introduced dark and tinted app icons. -- Map Data: Updated as of October 1, now including isolines for Egypt, Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. +• New OpenStreetMap data as of November 22 +• Fixed bug with Bookmark's description editor +• macOS: map scrolling via mouse and trackpad +• Other search improvements, translation updates & bug fixes diff --git a/iphone/metadata/ru/release_notes.txt b/iphone/metadata/ru/release_notes.txt index bad114bf41..40ecab36d5 100644 --- a/iphone/metadata/ru/release_notes.txt +++ b/iphone/metadata/ru/release_notes.txt @@ -1,3 +1,4 @@ -- iOS 18 Dark Mode: Introduced dark and tinted app icons. -- Map Data: Updated as of October 1, now including isolines for Egypt, Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. +• Новые данные OpenStreetMap от 22 ноября +• Исправлена проблема с редактированием описаний меток +• macOS: возможность прокрутки карты мышкой или трекпадом +• Различные улучшения поиска, обновления переводов и исправления ошибок diff --git a/iphone/metadata/sk/release_notes.txt b/iphone/metadata/sk/release_notes.txt index bad114bf41..fd5f15cdf3 100644 --- a/iphone/metadata/sk/release_notes.txt +++ b/iphone/metadata/sk/release_notes.txt @@ -1,3 +1,4 @@ -- iOS 18 Dark Mode: Introduced dark and tinted app icons. -- Map Data: Updated as of October 1, now including isolines for Egypt, Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. +• New OpenStreetMap data as of November 22 +• Fixed bug with Bookmark's description editor +• macOS: map scrolling via mouse and trackpad +• Other search improvements, translation updates & bug fixes diff --git a/iphone/metadata/sv/release_notes.txt b/iphone/metadata/sv/release_notes.txt index bad114bf41..fd5f15cdf3 100644 --- a/iphone/metadata/sv/release_notes.txt +++ b/iphone/metadata/sv/release_notes.txt @@ -1,3 +1,4 @@ -- iOS 18 Dark Mode: Introduced dark and tinted app icons. -- Map Data: Updated as of October 1, now including isolines for Egypt, Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. +• New OpenStreetMap data as of November 22 +• Fixed bug with Bookmark's description editor +• macOS: map scrolling via mouse and trackpad +• Other search improvements, translation updates & bug fixes diff --git a/iphone/metadata/th/release_notes.txt b/iphone/metadata/th/release_notes.txt index bad114bf41..fd5f15cdf3 100644 --- a/iphone/metadata/th/release_notes.txt +++ b/iphone/metadata/th/release_notes.txt @@ -1,3 +1,4 @@ -- iOS 18 Dark Mode: Introduced dark and tinted app icons. -- Map Data: Updated as of October 1, now including isolines for Egypt, Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. +• New OpenStreetMap data as of November 22 +• Fixed bug with Bookmark's description editor +• macOS: map scrolling via mouse and trackpad +• Other search improvements, translation updates & bug fixes diff --git a/iphone/metadata/tr/release_notes.txt b/iphone/metadata/tr/release_notes.txt index bad114bf41..fd5f15cdf3 100644 --- a/iphone/metadata/tr/release_notes.txt +++ b/iphone/metadata/tr/release_notes.txt @@ -1,3 +1,4 @@ -- iOS 18 Dark Mode: Introduced dark and tinted app icons. -- Map Data: Updated as of October 1, now including isolines for Egypt, Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. +• New OpenStreetMap data as of November 22 +• Fixed bug with Bookmark's description editor +• macOS: map scrolling via mouse and trackpad +• Other search improvements, translation updates & bug fixes diff --git a/iphone/metadata/uk/release_notes.txt b/iphone/metadata/uk/release_notes.txt index bad114bf41..bac1703bf3 100644 --- a/iphone/metadata/uk/release_notes.txt +++ b/iphone/metadata/uk/release_notes.txt @@ -1,3 +1,4 @@ -- iOS 18 Dark Mode: Introduced dark and tinted app icons. -- Map Data: Updated as of October 1, now including isolines for Egypt, Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. +• Нові дані OpenStreetMap від 22 листопада +• Виправлено помилку при редагуванні опису міток +• macOS: прокрутка карти за допомогою мишки та трекпаду +• Деякі покращення якості пошуку, уточнення перекладів, виправлення помилок diff --git a/iphone/metadata/vi/release_notes.txt b/iphone/metadata/vi/release_notes.txt index bad114bf41..fd5f15cdf3 100644 --- a/iphone/metadata/vi/release_notes.txt +++ b/iphone/metadata/vi/release_notes.txt @@ -1,3 +1,4 @@ -- iOS 18 Dark Mode: Introduced dark and tinted app icons. -- Map Data: Updated as of October 1, now including isolines for Egypt, Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. +• New OpenStreetMap data as of November 22 +• Fixed bug with Bookmark's description editor +• macOS: map scrolling via mouse and trackpad +• Other search improvements, translation updates & bug fixes diff --git a/iphone/metadata/zh-Hans/release_notes.txt b/iphone/metadata/zh-Hans/release_notes.txt index bad114bf41..fd5f15cdf3 100644 --- a/iphone/metadata/zh-Hans/release_notes.txt +++ b/iphone/metadata/zh-Hans/release_notes.txt @@ -1,3 +1,4 @@ -- iOS 18 Dark Mode: Introduced dark and tinted app icons. -- Map Data: Updated as of October 1, now including isolines for Egypt, Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. +• New OpenStreetMap data as of November 22 +• Fixed bug with Bookmark's description editor +• macOS: map scrolling via mouse and trackpad +• Other search improvements, translation updates & bug fixes diff --git a/iphone/metadata/zh-Hant/release_notes.txt b/iphone/metadata/zh-Hant/release_notes.txt index bad114bf41..fd5f15cdf3 100644 --- a/iphone/metadata/zh-Hant/release_notes.txt +++ b/iphone/metadata/zh-Hant/release_notes.txt @@ -1,3 +1,4 @@ -- iOS 18 Dark Mode: Introduced dark and tinted app icons. -- Map Data: Updated as of October 1, now including isolines for Egypt, Adygea, Dagestan. -- KML/KMZ/GPX: Fixed issues with importing files from different apps. +• New OpenStreetMap data as of November 22 +• Fixed bug with Bookmark's description editor +• macOS: map scrolling via mouse and trackpad +• Other search improvements, translation updates & bug fixes diff --git a/kml/kml_tests/gpx_tests.cpp b/kml/kml_tests/gpx_tests.cpp index a503ebd86e..37c2274441 100644 --- a/kml/kml_tests/gpx_tests.cpp +++ b/kml/kml_tests/gpx_tests.cpp @@ -271,7 +271,7 @@ UNIT_TEST(Color) TEST_EQUAL(red, dataFromFile.m_tracksData[0].m_layers[0].m_color.m_rgba, ()); TEST_EQUAL(blue, dataFromFile.m_tracksData[1].m_layers[0].m_color.m_rgba, ()); TEST_EQUAL(black, dataFromFile.m_tracksData[2].m_layers[0].m_color.m_rgba, ()); - TEST_EQUAL(red, dataFromFile.m_tracksData[3].m_layers[0].m_color.m_rgba, ()); + TEST_EQUAL(dataFromFile.m_tracksData.size(), 3, ()); } UNIT_TEST(MultiTrackNames) diff --git a/kml/kml_tests/serdes_tests.cpp b/kml/kml_tests/serdes_tests.cpp index a3729e452c..da74b7900b 100644 --- a/kml/kml_tests/serdes_tests.cpp +++ b/kml/kml_tests/serdes_tests.cpp @@ -872,3 +872,41 @@ UNIT_TEST(Kml_Import_OpenTracks) TEST_GREATER(geom.m_lines[0].size(), 10, ()); } } + +UNIT_TEST(Kml_BadTracks) +{ + std::string_view constexpr input = R"( + + + + 2010-05-28T02:00Z + -122.205712 37.373288 152.000000 + + + 9.42666332 52.94270656 95 + 2022-12-25T13:12:01.914Z + + + 9.42666332 52.94270656 95 + 2022-12-25T13:12:01.914Z + 9.42682572 52.94270115 94 + 2022-12-25T13:12:36Z + + +)"; + + kml::FileData fData; + TEST_NO_THROW( + { + kml::DeserializerKml(fData).Deserialize(MemReader(input)); + }, ()); + + { + TEST_EQUAL(fData.m_tracksData.size(), 1, ()); + auto const & geom = fData.m_tracksData[0].m_geometry; + TEST_EQUAL(geom.m_lines.size(), 1, ()); + TEST_EQUAL(geom.m_lines.size(), geom.m_timestamps.size(), ()); + TEST_EQUAL(geom.m_lines[0].size(), 2, ()); + TEST_EQUAL(geom.m_lines[0].size(), geom.m_timestamps[0].size(), ()); + } +} diff --git a/kml/serdes.cpp b/kml/serdes.cpp index e4b86e6a46..7e66818e0b 100644 --- a/kml/serdes.cpp +++ b/kml/serdes.cpp @@ -59,14 +59,6 @@ std::string_view constexpr kExtendedDataFooter = std::string const kCompilationFooter = "\n"; -std::string GetLocalizableString(LocalizableString const & s, int8_t lang) -{ - auto const it = s.find(lang); - if (it == s.cend()) - return {}; - return it->second; -} - PredefinedColor ExtractPlacemarkPredefinedColor(std::string const & s) { if (s == "#placemark-red") @@ -335,14 +327,17 @@ void SaveCategoryData(Writer & writer, CategoryData const & categoryData, SaveStyle(writer, GetStyleForPredefinedColor(static_cast(i)), kIndent0); // Use CDATA if we have special symbols in the name. - writer << kIndent2 << ""; - SaveStringWithCDATA(writer, GetLocalizableString(categoryData.m_name, kDefaultLang)); - writer << "\n"; + if (auto name = GetDefaultLanguage(categoryData.m_name)) + { + writer << kIndent2 << ""; + SaveStringWithCDATA(writer, *name); + writer << "\n"; + } - if (!categoryData.m_description.empty()) + if (auto const description = GetDefaultLanguage(categoryData.m_description)) { writer << kIndent2 << ""; - SaveStringWithCDATA(writer, GetLocalizableString(categoryData.m_description, kDefaultLang)); + SaveStringWithCDATA(writer, *description); writer << "\n"; } @@ -432,10 +427,10 @@ void SaveBookmarkData(Writer & writer, BookmarkData const & bookmarkData) SaveStringWithCDATA(writer, GetPreferredBookmarkName(bookmarkData, defaultLang)); writer << "\n"; - if (!bookmarkData.m_description.empty()) + if (auto const description = GetDefaultLanguage(bookmarkData.m_description)) { writer << kIndent4 << ""; - SaveStringWithCDATA(writer, GetLocalizableString(bookmarkData.m_description, kDefaultLang)); + SaveStringWithCDATA(writer, *description); writer << "\n"; } @@ -589,14 +584,17 @@ void SaveTrackExtendedData(Writer & writer, TrackData const & trackData) void SaveTrackData(Writer & writer, TrackData const & trackData) { writer << kIndent2 << "\n"; - writer << kIndent4 << ""; - SaveStringWithCDATA(writer, GetLocalizableString(trackData.m_name, kDefaultLang)); - writer << "\n"; + if (auto name = GetDefaultLanguage(trackData.m_name)) + { + writer << kIndent4 << ""; + SaveStringWithCDATA(writer, *name); + writer << "\n"; + } - if (!trackData.m_description.empty()) + if (auto const description = GetDefaultLanguage(trackData.m_description)) { writer << kIndent4 << ""; - SaveStringWithCDATA(writer, GetLocalizableString(trackData.m_description, kDefaultLang)); + SaveStringWithCDATA(writer, *description); writer << "\n"; } @@ -1065,7 +1063,10 @@ void KmlParser::Pop(std::string_view tag) auto & lines = m_geometry.m_lines; ASSERT(!lines.empty(), ()); if (lines.back().size() < 2) + { lines.pop_back(); + m_geometry.m_timestamps.pop_back(); + } } else if (IsProcessTrackCoord()) { diff --git a/kml/serdes_common.cpp b/kml/serdes_common.cpp index af66f94b87..01e644246c 100644 --- a/kml/serdes_common.cpp +++ b/kml/serdes_common.cpp @@ -65,4 +65,14 @@ void SaveStringWithCDATA(Writer & writer, std::string s) else writer << s; } + +std::string const * GetDefaultLanguage(LocalizableString const & lstr) +{ + auto const find = lstr.find(kDefaultLang); + if (find != lstr.end()) { + return &find->second; + } + return nullptr; +} + } // namespace kml diff --git a/kml/serdes_common.hpp b/kml/serdes_common.hpp index 2ad192244d..d017210099 100644 --- a/kml/serdes_common.hpp +++ b/kml/serdes_common.hpp @@ -26,6 +26,7 @@ std::string PointToLineString(geometry::PointWithAltitude const & pt); std::string PointToGxString(geometry::PointWithAltitude const & pt); void SaveStringWithCDATA(Writer & writer, std::string s); +std::string const * GetDefaultLanguage(LocalizableString const & lstr); std::string_view constexpr kIndent0 {}; std::string_view constexpr kIndent2 {" "}; diff --git a/kml/serdes_gpx.cpp b/kml/serdes_gpx.cpp index d5fbb2660a..36366caa16 100644 --- a/kml/serdes_gpx.cpp +++ b/kml/serdes_gpx.cpp @@ -290,10 +290,17 @@ void GpxParser::Pop(std::string_view tag) } else if (tag == gpx::kTrkSeg || tag == gpx::kRte) { - CheckAndCorrectTimestamps(); + if (m_line.size() > 1) + { + CheckAndCorrectTimestamps(); - m_geometry.m_lines.push_back(std::move(m_line)); - m_geometry.m_timestamps.push_back(std::move(m_timestamps)); + m_geometry.m_lines.push_back(std::move(m_line)); + m_geometry.m_timestamps.push_back(std::move(m_timestamps)); + } + + // Clear segment (it may be incomplete). + m_line.clear(); + m_timestamps.clear(); } else if (tag == gpx::kWpt) { @@ -332,8 +339,8 @@ void GpxParser::Pop(std::string_view tag) // Default gpx parser doesn't check points and timestamps count as the kml parser does. for (size_t lineIndex = 0; lineIndex < m_geometry.m_lines.size(); ++lineIndex) { - auto const & pointsSize = m_geometry.m_lines[lineIndex].size(); - auto const & timestampsSize = m_geometry.m_timestamps[lineIndex].size(); + auto const pointsSize = m_geometry.m_lines[lineIndex].size(); + auto const timestampsSize = m_geometry.m_timestamps[lineIndex].size(); ASSERT(!m_geometry.HasTimestampsFor(lineIndex) || pointsSize == timestampsSize, (pointsSize, timestampsSize)); } #endif @@ -461,14 +468,6 @@ std::string GpxParser::BuildDescription() const namespace { -std::optional GetDefaultLanguage(LocalizableString const & lstr) -{ - auto const firstLang = lstr.begin(); - if (firstLang != lstr.end()) - return {firstLang->second}; - return {}; -} - std::string CoordToString(double c) { std::ostringstream ss; @@ -489,11 +488,11 @@ void SaveCategoryData(Writer & writer, CategoryData const & categoryData) { writer << "\n"; if (auto const name = GetDefaultLanguage(categoryData.m_name)) - writer << kIndent2 << "" << name.value() << "\n"; + writer << kIndent2 << "" << *name << "\n"; if (auto const description = GetDefaultLanguage(categoryData.m_description)) { writer << kIndent2 << ""; - SaveStringWithCDATA(writer, description.value()); + SaveStringWithCDATA(writer, *description); writer << "\n"; } writer << "\n"; @@ -508,13 +507,16 @@ void SaveBookmarkData(Writer & writer, BookmarkData const & bookmarkData) if (!name) name = GetDefaultLanguage(bookmarkData.m_name); // Original POI name stored when bookmark was created. if (name) - writer << kIndent2 << "" << *name << "\n"; - + { + writer << kIndent2 << ""; + SaveStringWithCDATA(writer, *name); + writer << "\n"; + } if (auto const description = GetDefaultLanguage(bookmarkData.m_description)) { - writer << kIndent2 << ""; - SaveStringWithCDATA(writer, description.value()); - writer << "\n"; + writer << kIndent2 << ""; + SaveStringWithCDATA(writer, *description); + writer << "\n"; } writer << "\n"; } @@ -539,8 +541,18 @@ void SaveTrackData(Writer & writer, TrackData const & trackData) { writer << "\n"; auto name = GetDefaultLanguage(trackData.m_name); - if (name.has_value()) - writer << kIndent2 << "" << name.value() << "\n"; + if (name) + { + writer << kIndent2 << ""; + SaveStringWithCDATA(writer, *name); + writer << "\n"; + } + if (auto const description = GetDefaultLanguage(trackData.m_description)) + { + writer << kIndent2 << ""; + SaveStringWithCDATA(writer, *description); + writer << "\n"; + } if (auto const color = TrackColor(trackData); color != kDefaultTrackColor) { writer << kIndent2 << "\n" << kIndent4 << ""; diff --git a/kml/types.hpp b/kml/types.hpp index f5d4f11ad3..6a682cce8c 100644 --- a/kml/types.hpp +++ b/kml/types.hpp @@ -92,6 +92,7 @@ inline dp::Color ColorFromPredefinedColor(PredefinedColor color) case None: case Count: return ColorFromPredefinedColor(kml::PredefinedColor::Red); } + UNREACHABLE(); } kml::PredefinedColor GetRandomPredefinedColor(); diff --git a/map/bookmark_manager.cpp b/map/bookmark_manager.cpp index a40b96e72a..9f2a50836d 100644 --- a/map/bookmark_manager.cpp +++ b/map/bookmark_manager.cpp @@ -473,7 +473,7 @@ void BookmarkManager::DeleteCompilations(kml::GroupIdCollection const & compilat Track * BookmarkManager::CreateTrack(kml::TrackData && trackData) { CHECK_THREAD_CHECKER(m_threadChecker, ()); - return AddTrack(std::make_unique(std::move(trackData), false /* interactive */)); + return AddTrack(std::make_unique(std::move(trackData))); } Track const * BookmarkManager::GetTrack(kml::TrackId trackId) const @@ -812,15 +812,6 @@ std::string BookmarkManager::GetLocalizedRegionAddress(m2::PointD const & pt) return m_regionAddressGetter->GetLocalizedRegionAddress(pt); } -ElevationInfo BookmarkManager::MakeElevationInfo(kml::TrackId trackId) const -{ - CHECK_THREAD_CHECKER(m_threadChecker, ()); - auto const track = GetTrack(trackId); - CHECK(track != nullptr, ()); - - return ElevationInfo(*track); -} - void BookmarkManager::UpdateElevationMyPosition(kml::TrackId const & trackId) { CHECK_THREAD_CHECKER(m_threadChecker, ()); @@ -847,6 +838,9 @@ void BookmarkManager::UpdateElevationMyPosition(kml::TrackId const & trackId) } auto const markId = GetTrackSelectionMarkId(trackId); + if (markId == kml::kInvalidTrackId) + return; + auto es = GetEditSession(); auto trackSelectionMark = GetMarkForEdit(markId); @@ -879,16 +873,13 @@ void BookmarkManager::SetElevationMyPositionChangedCallback( m_elevationMyPositionChanged = cb; } -void BookmarkManager::SetElevationActivePoint(kml::TrackId const & trackId, double targetDistance) +void BookmarkManager::SetElevationActivePoint(kml::TrackId const & trackId, m2::PointD pt, double targetDistance) { CHECK_THREAD_CHECKER(m_threadChecker, ()); auto const track = GetTrack(trackId); CHECK(track != nullptr, ()); - m2::PointD pt; - VERIFY(track->GetPoint(targetDistance, pt), (trackId, targetDistance)); - SetTrackSelectionInfo({trackId, pt, targetDistance}, false /* notifyListeners */); m_drapeEngine.SafeCall(&df::DrapeEngine::SelectObject, @@ -930,7 +921,7 @@ Track::TrackSelectionInfo BookmarkManager::FindNearestTrack( for (auto trackId : category.GetUserLines()) { auto const track = GetTrack(trackId); - if (!track->IsInteractive() || (tracksFilter && !tracksFilter(track))) + if (tracksFilter && !tracksFilter(track)) continue; track->UpdateSelectionInfo(touchRect, selectionInfo); @@ -1037,8 +1028,14 @@ void BookmarkManager::SetTrackSelectionInfo(Track::TrackSelectionInfo const & tr CHECK_THREAD_CHECKER(m_threadChecker, ()); CHECK_NOT_EQUAL(trackSelectionInfo.m_trackId, kml::kInvalidTrackId, ()); - auto es = GetEditSession(); auto const markId = GetTrackSelectionMarkId(trackSelectionInfo.m_trackId); + if (markId == kml::kInvalidMarkId) + { + SetTrackSelectionMark(trackSelectionInfo.m_trackId, + trackSelectionInfo.m_trackPoint, + trackSelectionInfo.m_distFromBegM); + return; + } CHECK_NOT_EQUAL(markId, kml::kInvalidMarkId, ()); auto trackSelectionMark = GetMarkForEdit(markId); @@ -1049,22 +1046,6 @@ void BookmarkManager::SetTrackSelectionInfo(Track::TrackSelectionInfo const & tr m_elevationActivePointChanged(); } -void BookmarkManager::SetDefaultTrackSelection(kml::TrackId trackId, bool showInfoSign) -{ - CHECK_THREAD_CHECKER(m_threadChecker, ()); - - auto track = GetTrack(trackId); - CHECK(track != nullptr, ()); - CHECK(track->IsInteractive(), ()); - - auto const [pt, distance] = track->GetCenterPoint(); - - auto es = GetEditSession(); - if (showInfoSign) - SetTrackInfoMark(trackId, pt); - SetTrackSelectionMark(trackId, pt, distance); -} - void BookmarkManager::OnTrackSelected(kml::TrackId trackId) { CHECK_THREAD_CHECKER(m_threadChecker, ()); @@ -1092,8 +1073,7 @@ void BookmarkManager::OnTrackDeselected() auto es = GetEditSession(); auto * trackSelectionMark = GetMarkForEdit(markId); - auto const isVisible = IsVisible(GetTrack(m_selectedTrackId)->GetGroupId()); - trackSelectionMark->SetIsVisible(isVisible); + trackSelectionMark->SetIsVisible(false); m_selectedTrackId = kml::kInvalidTrackId; } @@ -2829,12 +2809,10 @@ void BookmarkManager::CreateCategories(KMLDataCollection && dataCollection, bool } for (auto & trackData : fileData.m_tracksData) { - auto track = std::make_unique(std::move(trackData), group->HasElevationProfile()); + auto track = std::make_unique(std::move(trackData)); auto * t = AddTrack(std::move(track)); t->Attach(groupId); group->m_tracks.insert(t->GetId()); - if (t->IsInteractive()) - SetDefaultTrackSelection(t->GetId(), false /* showInfoSign */); } UpdateTrackMarksVisibility(groupId); UserMarkIdStorage::Instance().EnableSaving(true); diff --git a/map/bookmark_manager.hpp b/map/bookmark_manager.hpp index 0f65e7163c..4847f69eee 100644 --- a/map/bookmark_manager.hpp +++ b/map/bookmark_manager.hpp @@ -407,9 +407,7 @@ public: static std::string GetSortedByTimeBlockName(SortedByTimeBlockType blockType); std::string GetLocalizedRegionAddress(m2::PointD const & pt); - ElevationInfo MakeElevationInfo(kml::TrackId trackId) const; - - void SetElevationActivePoint(kml::TrackId const & trackId, double distanceInMeters); + void SetElevationActivePoint(kml::TrackId const & trackId, m2::PointD pt, double distanceInMeters); // Returns distance from the start of the track to active point in meters. double GetElevationActivePoint(kml::TrackId const & trackId) const; @@ -427,7 +425,6 @@ public: Track::TrackSelectionInfo GetTrackSelectionInfo(kml::TrackId const & trackId) const; void SetTrackSelectionInfo(Track::TrackSelectionInfo const & trackSelectionInfo, bool notifyListeners); - void SetDefaultTrackSelection(kml::TrackId trackId, bool showInfoSign); void OnTrackSelected(kml::TrackId trackId); void OnTrackDeselected(); diff --git a/map/elevation_info.cpp b/map/elevation_info.cpp index 006c35daf6..d057970b89 100644 --- a/map/elevation_info.cpp +++ b/map/elevation_info.cpp @@ -1,71 +1,57 @@ #include "map/elevation_info.hpp" +#include "base/logging.hpp" + #include "geometry/mercator.hpp" -#include "base/string_utils.hpp" - -namespace +ElevationInfo::ElevationInfo(kml::MultiGeometry const & geometry) { -static uint8_t constexpr kMaxDifficulty = ElevationInfo::Difficulty::Hard; - -std::string const kAscentKey = "ascent"; -std::string const kDescentKey = "descent"; -std::string const kLowestPointKey = "lowest_point"; -std::string const kHighestPointKey = "highest_point"; -std::string const kDifficultyKey = "difficulty"; -std::string const kDurationKey = "duration"; - -template -void FillProperty(kml::Properties const & properties, std::string const & key, T & value) -{ - auto const it = properties.find(key); - if (it == properties.cend()) - LOG(LERROR, ("Property not found for key:", key)); - else + double distance = 0; + // Concatenate all segments. + for (size_t lineIndex = 0; lineIndex < geometry.m_lines.size(); ++lineIndex) { - if (!strings::to_any(it->second, value)) - LOG(LERROR, ("Conversion is not possible for key", key, "string representation is", it->second)); + auto const & line = geometry.m_lines[lineIndex]; + if (line.empty()) + { + LOG(LWARNING, ("Empty line in elevation info")); + continue; + } + + if (lineIndex == 0) + { + m_minAltitude = line.front().GetAltitude(); + m_maxAltitude = m_minAltitude; + } + + if (lineIndex > 0) + m_segmentsDistances.emplace_back(distance); + + for (size_t pointIndex = 0; pointIndex < line.size(); ++pointIndex) + { + auto const & currentPoint = line[pointIndex]; + auto const & currentPointAltitude = currentPoint.GetAltitude(); + if (currentPointAltitude < m_minAltitude) + m_minAltitude = currentPointAltitude; + if (currentPointAltitude > m_maxAltitude) + m_maxAltitude = currentPointAltitude; + + if (pointIndex == 0) + { + m_points.emplace_back(currentPoint, distance); + continue; + } + + auto const & previousPoint = line[pointIndex - 1]; + distance += mercator::DistanceOnEarth(previousPoint.GetPoint(), currentPoint.GetPoint()); + m_points.emplace_back(currentPoint, distance); + + auto const deltaAltitude = currentPointAltitude - previousPoint.GetAltitude(); + if (deltaAltitude > 0) + m_ascent += deltaAltitude; + else + m_descent -= deltaAltitude; + } } -} -} // namespace - -ElevationInfo::ElevationInfo(Track const & track) - : m_id(track.GetId()) - , m_name(track.GetName()) -{ - // (Distance, Elevation) chart doesn't have a sence for multiple track's geometry. - auto const & points = track.GetSingleGeometry(); - if (points.empty()) - return; - - m_points.reserve(points.size()); - m_points.emplace_back(0, points[0].GetAltitude()); - double distance = 0.0; - for (size_t i = 1; i < points.size(); ++i) - { - distance += mercator::DistanceOnEarth(points[i - 1].GetPoint(), points[i].GetPoint()); - m_points.emplace_back(distance, points[i].GetAltitude()); - } - - auto const & properties = track.GetData().m_properties; - - FillProperty(properties, kAscentKey, m_ascent); - FillProperty(properties, kDescentKey, m_descent); - FillProperty(properties, kLowestPointKey, m_minAltitude); - FillProperty(properties, kHighestPointKey, m_maxAltitude); - - uint8_t difficulty; - FillProperty(properties, kDifficultyKey, difficulty); - - if (difficulty > kMaxDifficulty) - { - LOG(LWARNING, ("Invalid difficulty value", m_difficulty, "in track", track.GetName())); - m_difficulty = Difficulty ::Unknown; - } - else - { - m_difficulty = static_cast(difficulty); - } - - FillProperty(properties, kDurationKey, m_duration); + /// @todo(KK) Implement difficulty calculation. + m_difficulty = Difficulty::Unknown; } diff --git a/map/elevation_info.hpp b/map/elevation_info.hpp index 95010ac208..9291d8ff28 100644 --- a/map/elevation_info.hpp +++ b/map/elevation_info.hpp @@ -1,28 +1,28 @@ #pragma once -#include "map/track.hpp" +#include "kml/types.hpp" #include "geometry/point_with_altitude.hpp" +#include "geometry/latlon.hpp" #include #include #include -class ElevationInfo +struct ElevationInfo { public: struct Point { - Point(double distance, geometry::Altitude altitude) - : m_distance(distance), m_altitude(altitude) - { - } - - double m_distance; - geometry::Altitude m_altitude; + Point(geometry::PointWithAltitude point, double distance) + : m_point(point), m_distance(distance) + {} + const geometry::PointWithAltitude m_point; + const double m_distance; }; using Points = std::vector; + using SegmentsDistances = std::vector; enum Difficulty : uint8_t { @@ -33,35 +33,31 @@ public: }; ElevationInfo() = default; - explicit ElevationInfo(Track const & track); + explicit ElevationInfo(kml::MultiGeometry const & geometry); - kml::TrackId GetId() const { return m_id; }; - std::string const & GetName() const { return m_name; } size_t GetSize() const { return m_points.size(); }; Points const & GetPoints() const { return m_points; }; - uint16_t GetAscent() const { return m_ascent; } - uint16_t GetDescent() const { return m_descent; } - uint16_t GetMinAltitude() const { return m_minAltitude; } - uint16_t GetMaxAltitude() const { return m_maxAltitude; } + uint32_t GetAscent() const { return m_ascent; } + uint32_t GetDescent() const { return m_descent; } + geometry::Altitude GetMinAltitude() const { return m_minAltitude; } + geometry::Altitude GetMaxAltitude() const { return m_maxAltitude; } uint8_t GetDifficulty() const { return m_difficulty; } - uint32_t GetDuration() const { return m_duration; } + SegmentsDistances const & GetSegmentsDistances() const { return m_segmentsDistances; }; private: - kml::TrackId m_id = kml::kInvalidTrackId; - std::string m_name; // Points with distance from start of the track and altitude. Points m_points; // Ascent in meters. - uint16_t m_ascent = 0; + uint32_t m_ascent = 0; // Descent in meters. - uint16_t m_descent = 0; + uint32_t m_descent = 0; // Altitude in meters. - uint16_t m_minAltitude = 0; + geometry::Altitude m_minAltitude = 0; // Altitude in meters. - uint16_t m_maxAltitude = 0; + geometry::Altitude m_maxAltitude = 0; // Some digital difficulty level with value in range [0-kMaxDifficulty] // or kInvalidDifficulty when difficulty is not found or incorrect. Difficulty m_difficulty = Difficulty::Unknown; - // Duration in seconds. - uint32_t m_duration = 0; + // Distances to the start of each segment. + SegmentsDistances m_segmentsDistances; }; diff --git a/map/framework.cpp b/map/framework.cpp index 5eb0fb6178..b589cd0c18 100644 --- a/map/framework.cpp +++ b/map/framework.cpp @@ -88,23 +88,32 @@ Framework::FixedPosition::FixedPosition() namespace { -char const kMapStyleKey[] = "MapStyleKeyV1"; -char const kAllow3dKey[] = "Allow3d"; -char const kAllow3dBuildingsKey[] = "Buildings3d"; -char const kAllowAutoZoom[] = "AutoZoom"; -char const kTrafficEnabledKey[] = "TrafficEnabled"; -char const kTransitSchemeEnabledKey[] = "TransitSchemeEnabled"; -char const kIsolinesEnabledKey[] = "IsolinesEnabled"; -char const kOutdoorsEnabledKey[] = "OutdoorsEnabled"; -char const kTrafficSimplifiedColorsKey[] = "TrafficSimplifiedColors"; -char const kLargeFontsSize[] = "LargeFontsSize"; -char const kTranslitMode[] = "TransliterationMode"; -char const kPreferredGraphicsAPI[] = "PreferredGraphicsAPI"; -char const kShowDebugInfo[] = "DebugInfo"; +std::string_view constexpr kMapStyleKey = "MapStyleKeyV1"; +std::string_view constexpr kAllow3dKey = "Allow3d"; +std::string_view constexpr kAllow3dBuildingsKey = "Buildings3d"; +std::string_view constexpr kAllowAutoZoom = "AutoZoom"; +std::string_view constexpr kTrafficEnabledKey = "TrafficEnabled"; +std::string_view constexpr kTransitSchemeEnabledKey = "TransitSchemeEnabled"; +std::string_view constexpr kIsolinesEnabledKey = "IsolinesEnabled"; +std::string_view constexpr kOutdoorsEnabledKey = "OutdoorsEnabled"; +std::string_view constexpr kTrafficSimplifiedColorsKey = "TrafficSimplifiedColors"; +std::string_view constexpr kLargeFontsSize = "LargeFontsSize"; +std::string_view constexpr kTranslitMode = "TransliterationMode"; +std::string_view constexpr kPreferredGraphicsAPI = "PreferredGraphicsAPI"; +std::string_view constexpr kShowDebugInfo = "DebugInfo"; +std::string_view constexpr kScreenViewport = "ScreenClipRect"; +std::string_view constexpr kPlacePageProductsPopupCloseTime = "PlacePageProductsPopupCloseTime"; +std::string_view constexpr kPlacePageProductsPopupCloseReason = "PlacePageProductsPopupCloseReason"; +std::string_view constexpr kPlacePageSelectedProduct = "PlacePageSelectedProduct"; + +std::string_view constexpr kProductsPopupCloseReasonCloseStr = "close"; +std::string_view constexpr kProductsPopupCloseReasonRemindLaterStr = "remind_later"; +std::string_view constexpr kProductsPopupCloseReasonAlreadyDonatedStr = "already_donated"; +std::string_view constexpr kProductsPopupCloseReasonSelectProductStr = "select_product"; auto constexpr kLargeFontsScaleFactor = 1.6; size_t constexpr kMaxTrafficCacheSizeBytes = 64 /* Mb */ * 1024 * 1024; -auto constexpr kBuildingCentroidThreshold = 10.0; + // TODO! // To adjust GpsTrackFilter was added secret command "?gpstrackaccuracy:xxx;" @@ -304,7 +313,7 @@ Framework::Framework(FrameworkParams const & params, bool loadMaps) // It's better to use strings from strings.txt instead of hardcoding them here. m_stringsBundle.SetDefaultString("core_entrance", "Entrance"); m_stringsBundle.SetDefaultString("core_exit", "Exit"); - m_stringsBundle.SetDefaultString("core_placepage_unknown_place", "Unknown Place"); + m_stringsBundle.SetDefaultString("core_placepage_unknown_place", "Map Point"); m_stringsBundle.SetDefaultString("core_my_places", "My Places"); m_stringsBundle.SetDefaultString("core_my_position", "My Position"); m_stringsBundle.SetDefaultString("postal_code", "Postal Code"); @@ -648,7 +657,9 @@ void Framework::FillTrackInfo(Track const & track, m2::PointD const & trackPoint { info.SetTrackId(track.GetId()); info.SetBookmarkCategoryId(track.GetGroupId()); + info.SetBookmarkCategoryName(GetBookmarkManager().GetCategoryName(track.GetGroupId())); info.SetMercator(trackPoint); + info.SetTitlesForTrack(track); } search::ReverseGeocoder::Address Framework::GetAddressAtPoint(m2::PointD const & pt) const @@ -876,20 +887,25 @@ void Framework::ShowTrack(kml::TrackId trackId) { auto & bm = GetBookmarkManager(); auto const track = bm.GetTrack(trackId); - if (track == nullptr) - return; + + StopLocationFollow(); auto rect = track->GetLimitRect(); ExpandRectForPreview(rect); - StopLocationFollow(); - ShowRect(rect); + place_page::BuildInfo info; + info.m_trackId = trackId; + info.m_mercator = rect.Center(); - auto es = GetBookmarkManager().GetEditSession(); + m_currentPlacePageInfo = BuildPlacePageInfo(info); + + auto es = bm.GetEditSession(); es.SetIsVisible(track->GetGroupId(), true /* visible */); - if (track->IsInteractive()) - bm.SetDefaultTrackSelection(trackId, true /* showInfoSign */); + if (m_drapeEngine) + m_drapeEngine->SetModelViewCenter(rect.Center(), scales::GetScaleLevel(rect), true /* isAnim */, true /* trackVisibleViewport */); + + ActivateMapSelection(); } void Framework::ShowBookmarkCategory(kml::MarkGroupId categoryId, bool animation) @@ -906,15 +922,6 @@ void Framework::ShowBookmarkCategory(kml::MarkGroupId categoryId, bool animation auto es = bm.GetEditSession(); es.SetIsVisible(categoryId, true /* visible */); - - auto const & trackIds = bm.GetTrackIds(categoryId); - for (auto trackId : trackIds) - { - if (!bm.GetTrack(trackId)->IsInteractive()) - continue; - bm.SetDefaultTrackSelection(trackId, true /* showInfoSign */); - break; - } } void Framework::ShowFeature(FeatureID const & featureId) @@ -958,13 +965,13 @@ void Framework::SaveViewport() { rect = m_currentModelView.GlobalRect(); } - settings::Set("ScreenClipRect", rect); + settings::Set(kScreenViewport, rect); } void Framework::LoadViewport() { m2::AnyRectD rect; - if (settings::Get("ScreenClipRect", rect) && df::GetWorldRect().IsRectInside(rect.GetGlobalRect())) + if (settings::Get(kScreenViewport, rect) && df::GetWorldRect().IsRectInside(rect.GetGlobalRect())) { if (m_drapeEngine != nullptr) m_drapeEngine->SetModelViewAnyRect(rect, false /* isAnim */, false /* useVisibleViewport */); @@ -1575,10 +1582,12 @@ void Framework::CreateDrapeEngine(ref_ptr contextFac Allow3dMode(allow3d, allow3dBuildings); + ApplyMapLanguageCode(GetMapLanguageCode()); + LoadViewport(); if (m_connectToGpsTrack) - GpsTracker::Instance().Connect(bind(&Framework::OnUpdateGpsTrackPointsCallback, this, _1, _2)); + GpsTracker::Instance().Connect(bind(&Framework::OnUpdateGpsTrackPointsCallback, this, _1, _2, _3)); GetBookmarkManager().SetDrapeEngine(make_ref(m_drapeEngine)); m_drapeApi.SetDrapeEngine(make_ref(m_drapeEngine)); @@ -1690,7 +1699,7 @@ void Framework::ConnectToGpsTracker() if (m_drapeEngine) { m_drapeEngine->ClearGpsTrackPoints(); - GpsTracker::Instance().Connect(bind(&Framework::OnUpdateGpsTrackPointsCallback, this, _1, _2)); + GpsTracker::Instance().Connect(bind(&Framework::OnUpdateGpsTrackPointsCallback, this, _1, _2, _3)); } } @@ -1711,10 +1720,17 @@ void Framework::StartTrackRecording() if (m_drapeEngine) { m_drapeEngine->ClearGpsTrackPoints(); - tracker.Connect(bind(&Framework::OnUpdateGpsTrackPointsCallback, this, _1, _2)); + tracker.Connect(bind(&Framework::OnUpdateGpsTrackPointsCallback, this, _1, _2, _3)); } } +void Framework::SetTrackRecordingUpdateHandler(TrackRecordingUpdateHandler && trackRecordingDidUpdate) +{ + m_trackRecordingUpdateHandler = std::move(trackRecordingDidUpdate); + if (m_trackRecordingUpdateHandler) + m_trackRecordingUpdateHandler(GpsTracker::Instance().GetTrackInfo()); +} + void Framework::StopTrackRecording() { m_connectToGpsTrack = false; @@ -1743,7 +1759,8 @@ bool Framework::IsTrackRecordingEnabled() const } void Framework::OnUpdateGpsTrackPointsCallback(vector> && toAdd, - pair const & toRemove) + pair const & toRemove, + GpsTrackInfo const & trackInfo) { ASSERT(m_drapeEngine.get() != nullptr, ()); @@ -1770,6 +1787,9 @@ void Framework::OnUpdateGpsTrackPointsCallback(vectorUpdateGpsTrackPoints(std::move(pointsAdd), std::move(indicesRemove)); + + if (m_trackRecordingUpdateHandler) + m_trackRecordingUpdateHandler(trackInfo); } void Framework::MarkMapStyle(MapStyle mapStyle) @@ -2218,11 +2238,26 @@ place_page::Info Framework::BuildPlacePageInfo(place_page::BuildInfo const & bui FeatureID selectedFeature = buildInfo.m_featureId; auto const isFeatureMatchingEnabled = buildInfo.IsFeatureMatchingEnabled(); + // @TODO: (KK) Enable track selection. + // The isTrackSelectionEnabled should be removed to enable the track selection when the UI will be implemented. + #if defined(TARGET_OS_IPHONE) + bool constexpr isTrackSelectionEnabled = true; + #else + bool constexpr isTrackSelectionEnabled = false; + #endif + // Using VisualParams inside FindTrackInTapPosition/GetDefaultTapRect requires drapeEngine. - if (m_drapeEngine != nullptr && buildInfo.IsTrackMatchingEnabled() && + if (isTrackSelectionEnabled && m_drapeEngine != nullptr && buildInfo.IsTrackMatchingEnabled() && !(isFeatureMatchingEnabled && selectedFeature.IsValid())) { - auto const trackSelectionInfo = FindTrackInTapPosition(buildInfo); + Track::TrackSelectionInfo trackSelectionInfo; + if (buildInfo.m_trackId != kml::kInvalidTrackId) + { + auto const & track = *GetBookmarkManager().GetTrack(buildInfo.m_trackId); + track.UpdateSelectionInfo(track.GetLimitRect(), trackSelectionInfo); + } + else + trackSelectionInfo = FindTrackInTapPosition(buildInfo); if (trackSelectionInfo.m_trackId != kml::kInvalidTrackId) { BuildTrackPlacePage(trackSelectionInfo, outInfo); @@ -2374,6 +2409,7 @@ string Framework::GenerateApiBackUrl(ApiMarkPoint const & point) const return res; } +/* bool Framework::IsDataVersionUpdated() { int64_t storedVersion; @@ -2389,6 +2425,7 @@ void Framework::UpdateSavedDataVersion() { settings::Set("DataVersion", m_storage.GetCurrentDataVersion()); } +*/ int64_t Framework::GetCurrentDataVersion() const { return m_storage.GetCurrentDataVersion(); } @@ -2426,6 +2463,31 @@ void Framework::SaveTransliteration(bool allowTranslit) : Transliteration::Mode::Disabled); } +std::string Framework::GetMapLanguageCode() +{ + return languages::GetCurrentMapLanguage(); +} + +void Framework::SetMapLanguageCode(std::string const & langCode) +{ + settings::Set(settings::kMapLanguageCode, langCode); + if (m_drapeEngine) + ApplyMapLanguageCode(langCode); + + if (m_searchAPI) + m_searchAPI->SetLocale(langCode); +} + +void Framework::ApplyMapLanguageCode(std::string const & langCode) +{ + int8_t langIndex = StringUtf8Multilang::GetLangIndex(langCode); + ASSERT(langIndex != StringUtf8Multilang::kUnsupportedLanguageCode, ()); + if (langIndex == StringUtf8Multilang::kUnsupportedLanguageCode) + langIndex = StringUtf8Multilang::kDefaultCode; + + m_drapeEngine->SetMapLangIndex(langIndex); +} + void Framework::Allow3dMode(bool allow3d, bool allow3dBuildings) { if (m_drapeEngine == nullptr) @@ -3243,7 +3305,7 @@ void Framework::FillDescription(FeatureType & ft, place_page::Info & info) const if (!ft.GetID().m_mwmId.IsAlive()) return; auto const & regionData = ft.GetID().m_mwmId.GetInfo()->GetRegionData(); - auto const deviceLang = StringUtf8Multilang::GetLangIndex(languages::GetCurrentNorm()); + auto const deviceLang = StringUtf8Multilang::GetLangIndex(languages::GetCurrentMapLanguage()); auto const langPriority = feature::GetDescriptionLangPriority(regionData, deviceLang); std::string wikiDescription = m_descriptionsLoader->GetWikiDescription(ft.GetID(), langPriority); @@ -3286,3 +3348,97 @@ void Framework::OnPowerSchemeChanged(power_management::Scheme const actualScheme if (actualScheme == power_management::Scheme::EconomyMaximum && GetTrafficManager().IsEnabled()) GetTrafficManager().SetEnabled(false); } + +bool Framework::ShouldShowProducts() const +{ + auto const connectionStatus = Platform::ConnectionStatus(); + if (connectionStatus == Platform::EConnectionType::CONNECTION_NONE) + return false; + + std::string donateUrl; + if (!settings::Get(settings::kDonateUrl, donateUrl)) // donation is disabled + return false; + + if (!m_usageStats.IsLoyalUser()) + return false; + + if (!storage::IsPointCoveredByDownloadedMaps(GetCurrentPlacePageInfo().GetMercator(), m_storage, *m_infoGetter)) + return false; + + uint64_t popupCloseTime; + std::string productCloseReason; + if (!settings::Get(kPlacePageProductsPopupCloseTime, popupCloseTime) || + !settings::Get(kPlacePageProductsPopupCloseReason, productCloseReason)) + return true; // The popup was never closed. + + auto const now = base::SecondsSinceEpoch(); + auto const timeout = GetTimeoutForReason(FromString(productCloseReason)); + bool const timeoutExpired = popupCloseTime + timeout < now; + if (timeoutExpired) + return true; + + return false; +} + +std::optional Framework::GetProductsConfiguration() const +{ + if (!ShouldShowProducts()) + return nullopt; + return products::GetProductsConfiguration(); +} + +void Framework::DidCloseProductsPopup(ProductsPopupCloseReason reason) const +{ + settings::Set(kPlacePageProductsPopupCloseTime, base::SecondsSinceEpoch()); + settings::Set(kPlacePageProductsPopupCloseReason, std::string(ToString(reason))); +} + +void Framework::DidSelectProduct(products::ProductsConfig::Product const & product) const +{ + settings::Set(kPlacePageSelectedProduct, product.GetTitle()); +} + +uint32_t Framework::GetTimeoutForReason(ProductsPopupCloseReason reason) const +{ + #ifdef DEBUG + uint32_t constexpr kPopupCloseTimeout = 10; + uint32_t constexpr kProductSelectTimeout = 20; + uint32_t constexpr kRemindMeLaterTimeout = 5; + #else + uint32_t constexpr kPopupCloseTimeout = 60 * 60 * 24 * 30; // 30 days + uint32_t constexpr kProductSelectTimeout = 60 * 60 * 24 * 180; // 180 days + uint32_t constexpr kRemindMeLaterTimeout = 60 * 60 * 24 * 3; // 3 days + #endif + switch (reason) + { + case ProductsPopupCloseReason::Close: return kPopupCloseTimeout; + case ProductsPopupCloseReason::RemindLater: return kRemindMeLaterTimeout; + case ProductsPopupCloseReason::AlreadyDonated: return kProductSelectTimeout; + case ProductsPopupCloseReason::SelectProduct: return kProductSelectTimeout; + } + ASSERT(false, ("Unknown reason")); + return kPopupCloseTimeout; +} + +std::string_view Framework::ToString(ProductsPopupCloseReason reason) const +{ + switch (reason) + { + case ProductsPopupCloseReason::Close: return kProductsPopupCloseReasonCloseStr; + case ProductsPopupCloseReason::RemindLater: return kProductsPopupCloseReasonRemindLaterStr; + case ProductsPopupCloseReason::AlreadyDonated: return kProductsPopupCloseReasonAlreadyDonatedStr; + case ProductsPopupCloseReason::SelectProduct: return kProductsPopupCloseReasonSelectProductStr; + } + ASSERT(false, ("Unknown reason")); + return kProductsPopupCloseReasonCloseStr; +} + +Framework::ProductsPopupCloseReason Framework::FromString(std::string const & str) const +{ + if (str == kProductsPopupCloseReasonCloseStr) return ProductsPopupCloseReason::Close; + if (str == kProductsPopupCloseReasonRemindLaterStr) return ProductsPopupCloseReason::RemindLater; + if (str == kProductsPopupCloseReasonAlreadyDonatedStr) return ProductsPopupCloseReason::AlreadyDonated; + if (str == kProductsPopupCloseReasonSelectProductStr) return ProductsPopupCloseReason::SelectProduct; + ASSERT(false, ("Incorrect reason string:", str)); + return ProductsPopupCloseReason::Close; +} diff --git a/map/framework.hpp b/map/framework.hpp index 00eb7e3b8e..9fb29750aa 100644 --- a/map/framework.hpp +++ b/map/framework.hpp @@ -17,6 +17,7 @@ #include "map/track.hpp" #include "map/traffic_manager.hpp" #include "map/transit/transit_reader.hpp" +#include "map/gps_track_collection.hpp" #include "drape_frontend/gui/skin.hpp" #include "drape_frontend/drape_api.hpp" @@ -47,6 +48,7 @@ #include "platform/location.hpp" #include "platform/platform.hpp" #include "platform/distance.hpp" +#include "platform/products.hpp" #include "routing/router.hpp" @@ -434,7 +436,9 @@ public: void ConnectToGpsTracker(); void DisconnectFromGpsTracker(); + using TrackRecordingUpdateHandler = platform::SafeCallback; void StartTrackRecording(); + void SetTrackRecordingUpdateHandler(TrackRecordingUpdateHandler && trackRecordingDidUpdate); void StopTrackRecording(); void SaveTrackRecordingWithName(std::string const & name); bool IsTrackRecordingEmpty() const; @@ -462,15 +466,16 @@ private: TCurrentCountryChanged m_currentCountryChanged; void OnUpdateGpsTrackPointsCallback(std::vector> && toAdd, - std::pair const & toRemove); + std::pair const & toRemove, + GpsTrackInfo const & trackInfo); + + TrackRecordingUpdateHandler m_trackRecordingUpdateHandler; CachingRankTableLoader m_popularityLoader; std::unique_ptr m_descriptionsLoader; public: - using SearchRequest = search::QuerySaver::SearchRequest; - // Moves viewport to the search result and taps on it. void SelectSearchResult(search::Result const & res, bool animation); @@ -673,8 +678,8 @@ private: public: /// @name Data versions - bool IsDataVersionUpdated(); - void UpdateSavedDataVersion(); + // bool IsDataVersionUpdated(); + // void UpdateSavedDataVersion(); int64_t GetCurrentDataVersion() const; public: @@ -686,6 +691,12 @@ public: void Save3dMode(bool allow3d, bool allow3dBuildings); void Load3dMode(bool & allow3d, bool & allow3dBuildings); +private: + void ApplyMapLanguageCode(std::string const & langCode); +public: + static std::string GetMapLanguageCode(); + void SetMapLanguageCode(std::string const & langCode); + void SetLargeFontsSize(bool isLargeSize); bool LoadLargeFontsSize(); @@ -751,4 +762,24 @@ public: // PowerManager::Subscriber override. void OnPowerFacilityChanged(power_management::Facility const facility, bool enabled) override; void OnPowerSchemeChanged(power_management::Scheme const actualScheme) override; + +public: + std::optional GetProductsConfiguration() const; + + enum class ProductsPopupCloseReason + { + Close, + SelectProduct, + AlreadyDonated, + RemindLater + }; + + void DidCloseProductsPopup(ProductsPopupCloseReason reason) const; + void DidSelectProduct(products::ProductsConfig::Product const & product) const; + +private: + bool ShouldShowProducts() const; + uint32_t GetTimeoutForReason(ProductsPopupCloseReason reason) const; + std::string_view ToString(ProductsPopupCloseReason reason) const; + ProductsPopupCloseReason FromString(std::string const & str) const; }; diff --git a/map/gps_track.cpp b/map/gps_track.cpp index c6bdc70414..c808753cda 100644 --- a/map/gps_track.cpp +++ b/map/gps_track.cpp @@ -79,6 +79,11 @@ void GpsTrack::AddPoints(vector const & points) ScheduleTask(); } +GpsTrackInfo GpsTrack::GetTrackInfo() const +{ + return m_collection ? m_collection->GetTrackInfo() : GpsTrackInfo(); +} + void GpsTrack::Clear() { { @@ -298,7 +303,7 @@ void GpsTrack::NotifyCallback(pair const & addedIds, pairGetTrackInfo()); } else { @@ -319,6 +324,6 @@ void GpsTrack::NotifyCallback(pair const & addedIds, pairGetTrackInfo()); } } diff --git a/map/gps_track.hpp b/map/gps_track.hpp index 54f755a407..6ba0ba138f 100644 --- a/map/gps_track.hpp +++ b/map/gps_track.hpp @@ -30,6 +30,9 @@ public: void AddPoint(location::GpsInfo const & point); void AddPoints(std::vector const & points); + /// Returns track statistics + GpsTrackInfo GetTrackInfo() const; + /// Clears any previous tracking info /// @note Callback is called with 'toRemove' points, if some points were removed. void Clear(); @@ -43,7 +46,8 @@ public: /// @note Calling of a GpsTrack.SetCallback function from the callback causes deadlock. using TGpsTrackDiffCallback = std::function> && toAdd, - std::pair const & toRemove)>; + std::pair const & toRemove, + GpsTrackInfo const & trackInfo)>; /// Sets callback on change of gps track. /// @param callback - callback callable object diff --git a/map/gps_track_collection.cpp b/map/gps_track_collection.cpp index a6f2a7fd23..f39c2e964c 100644 --- a/map/gps_track_collection.cpp +++ b/map/gps_track_collection.cpp @@ -2,6 +2,8 @@ #include "base/assert.hpp" +#include "geometry/distance_on_sphere.hpp" + #include namespace @@ -33,22 +35,8 @@ size_t const GpsTrackCollection::kInvalidId = std::numeric_limits::max() GpsTrackCollection::GpsTrackCollection() : m_lastId(0) -{ -} - -size_t GpsTrackCollection::Add(TItem const & item) -{ - if (!m_items.empty() && m_items.back().m_timestamp > item.m_timestamp) - { - // Invalid timestamp order - return kInvalidId; // Nothing was added - } - - m_items.emplace_back(item); - ++m_lastId; - - return m_lastId - 1; -} + , m_trackInfo(GpsTrackInfo()) +{} std::pair GpsTrackCollection::Add(std::vector const & items) { @@ -63,6 +51,27 @@ std::pair GpsTrackCollection::Add(std::vector const & ite if (!m_items.empty() && m_items.back().m_timestamp > item.m_timestamp) continue; + if (m_items.empty()) + { + m_trackInfo.m_maxElevation = item.m_altitude; + m_trackInfo.m_minElevation = item.m_altitude; + } + else + { + auto const & lastItem = m_items.back(); + m_trackInfo.m_length += ms::DistanceOnEarth(lastItem.GetLatLon(), item.GetLatLon()); + m_trackInfo.m_duration = item.m_timestamp - m_items.front().m_timestamp; + + auto const deltaAltitude = item.m_altitude - lastItem.m_altitude; + if (item.m_altitude > lastItem.m_altitude) + m_trackInfo.m_ascent += deltaAltitude; + if (item.m_altitude < lastItem.m_altitude) + m_trackInfo.m_descent -= deltaAltitude; + + m_trackInfo.m_maxElevation = std::max(static_cast(m_trackInfo.m_maxElevation), item.m_altitude); + m_trackInfo.m_minElevation = std::min(static_cast(m_trackInfo.m_minElevation), item.m_altitude); + } + m_items.emplace_back(item); ++added; } @@ -97,6 +106,7 @@ std::pair GpsTrackCollection::Clear(bool resetIds) m_items.clear(); m_items.shrink_to_fit(); + m_trackInfo = {}; if (resetIds) m_lastId = 0; @@ -113,11 +123,3 @@ bool GpsTrackCollection::IsEmpty() const { return m_items.empty(); } - -std::pair GpsTrackCollection::RemoveUntil(std::deque::iterator i) -{ - auto const res = std::make_pair(m_lastId - m_items.size(), - m_lastId - m_items.size() + distance(m_items.begin(), i) - 1); - m_items.erase(m_items.begin(), i); - return res; -} diff --git a/map/gps_track_collection.hpp b/map/gps_track_collection.hpp index 6319026723..140459e729 100644 --- a/map/gps_track_collection.hpp +++ b/map/gps_track_collection.hpp @@ -7,6 +7,16 @@ #include #include +struct GpsTrackInfo +{ + double m_length; + double m_duration; + uint32_t m_ascent; + uint32_t m_descent; + int16_t m_minElevation; + int16_t m_maxElevation; +}; + class GpsTrackCollection final { public: @@ -17,11 +27,6 @@ public: /// Constructor GpsTrackCollection(); - /// Adds new point in the collection. - /// @param item - item to be added. - /// @returns the item unique identifier or kInvalidId if point has incorrect time. - size_t Add(TItem const & item); - /// Adds set of new points in the collection. /// @param items - set of items to be added. /// @returns range of identifiers of added items or pair(kInvalidId,kInvalidId) if nothing was added @@ -41,6 +46,8 @@ public: /// Returns number of items in the collection size_t GetSize() const; + GpsTrackInfo GetTrackInfo() const { return m_trackInfo; } + /// Enumerates items in the collection. /// @param f - callable object, which is called with params - item and item id, /// if f returns false, then enumeration is stopped. @@ -62,14 +69,8 @@ public: } private: - // Removes items in range [m_items.begin(), i) and returnd - // range of identifiers of removed items - std::pair RemoveUntil(std::deque::iterator i); - - // Removes items extra by timestamp - std::pair RemoveExtraItems(); - std::deque m_items; // asc. sorted by timestamp size_t m_lastId; + GpsTrackInfo m_trackInfo; }; diff --git a/map/gps_tracker.hpp b/map/gps_tracker.hpp index 4c07d3664f..38ff7915c2 100644 --- a/map/gps_tracker.hpp +++ b/map/gps_tracker.hpp @@ -17,10 +17,12 @@ public: bool IsEmpty() const; size_t GetTrackSize() const; + GpsTrackInfo GetTrackInfo() const { return m_track.GetTrackInfo(); } using TGpsTrackDiffCallback = std::function> && toAdd, - std::pair const & toRemove)>; + std::pair const & toRemove, + GpsTrackInfo const & trackInfo)>; void Connect(TGpsTrackDiffCallback const & fn); void Disconnect(); diff --git a/map/map_tests/CMakeLists.txt b/map/map_tests/CMakeLists.txt index 0ca98f3c7b..c79a222283 100644 --- a/map/map_tests/CMakeLists.txt +++ b/map/map_tests/CMakeLists.txt @@ -17,6 +17,7 @@ set(SRC search_api_tests.cpp transliteration_test.cpp working_time_tests.cpp + elevation_info_tests.cpp ) omim_add_test(${PROJECT_NAME} ${SRC} REQUIRE_QT REQUIRE_SERVER) diff --git a/map/map_tests/address_tests.cpp b/map/map_tests/address_tests.cpp index 5d64bcd801..e21b4d8500 100644 --- a/map/map_tests/address_tests.cpp +++ b/map/map_tests/address_tests.cpp @@ -39,7 +39,7 @@ void TestAddress(ReverseGeocoder & coder, std::shared_ptr mwmInfo, ms:: StringUtf8Multilang const & streetNames, std::string const & houseNumber) { feature::NameParamsOut out; - feature::GetReadableName({ streetNames, mwmInfo->GetRegionData(), languages::GetCurrentNorm(), + feature::GetReadableName({ streetNames, mwmInfo->GetRegionData(), languages::GetCurrentMapLanguage(), false /* allowTranslit */ }, out); TestAddress(coder, ll, out.primary, houseNumber); @@ -61,7 +61,7 @@ UNIT_TEST(ReverseGeocoder_Smoke) ReverseGeocoder coder(dataSource); - auto const currentLocale = languages::GetCurrentNorm(); + auto const currentLocale = languages::GetCurrentMapLanguage(); { StringUtf8Multilang streetNames; diff --git a/map/map_tests/bookmarks_test.cpp b/map/map_tests/bookmarks_test.cpp index c3d570717b..0107c4a5da 100644 --- a/map/map_tests/bookmarks_test.cpp +++ b/map/map_tests/bookmarks_test.cpp @@ -450,7 +450,7 @@ void CheckPlace(Framework const & fm, std::shared_ptr const & mwmInfo, auto const info = fm.GetAddressAtPoint(mercator::FromLatLon(lat, lon)); feature::NameParamsOut out; - feature::GetReadableName({ streetNames, mwmInfo->GetRegionData(), languages::GetCurrentNorm(), + feature::GetReadableName({ streetNames, mwmInfo->GetRegionData(), languages::GetCurrentMapLanguage(), false /* allowTranslit */ }, out); TEST_EQUAL(info.GetStreetName(), out.primary, ()); @@ -1155,7 +1155,7 @@ UNIT_CLASS_TEST(Runner, TrackParsingTest_1) for (auto const trackId : bmManager.GetTrackIds(catId)) { auto const * track = bmManager.GetTrack(trackId); - auto const & geom = track->GetSingleGeometry(); + auto const & geom = track->GetGeometry(); TEST_EQUAL(geom[0].GetAltitude(), altitudes[i], ()); TEST_EQUAL(names[i], track->GetName(), ()); diff --git a/map/map_tests/elevation_info_tests.cpp b/map/map_tests/elevation_info_tests.cpp new file mode 100644 index 0000000000..9188c1065b --- /dev/null +++ b/map/map_tests/elevation_info_tests.cpp @@ -0,0 +1,136 @@ +#include "testing/testing.hpp" + +#include "map/elevation_info.hpp" + +#include "geometry/point_with_altitude.hpp" +#include "geometry/mercator.hpp" + +#include "kml/types.hpp" + +namespace geometry +{ +using namespace geometry; + +UNIT_TEST(ElevationInfo_EmptyMultiGeometry) +{ + ElevationInfo ei; + TEST_EQUAL(0, ei.GetSize(), ()); + TEST_EQUAL(0, ei.GetAscent(), ()); + TEST_EQUAL(0, ei.GetDescent(), ()); + TEST_EQUAL(ei.GetMinAltitude(), kDefaultAltitudeMeters, ()); + TEST_EQUAL(ei.GetMaxAltitude(), kDefaultAltitudeMeters, ()); +} + +UNIT_TEST(ElevationInfo_FromMultiGeometry) +{ + kml::MultiGeometry geometry; + auto const point1 = PointWithAltitude({0.0, 0.0}, 100); + auto const point2 = PointWithAltitude({1.0, 1.0}, 150); + auto const point3 = PointWithAltitude({2.0, 2.0}, 50); + geometry.AddLine({ + point1, + point2, + point3 + }); + ElevationInfo ei(geometry); + + TEST_EQUAL(3, ei.GetSize(), ()); + TEST_EQUAL(ei.GetMinAltitude(), 50, ()); + TEST_EQUAL(ei.GetMaxAltitude(), 150, ()); + TEST_EQUAL(ei.GetAscent(), 50, ()); // Ascent from 100 -> 150 + TEST_EQUAL(ei.GetDescent(), 100, ()); // Descent from 150 -> 50 + + double distance = 0; + TEST_EQUAL(ei.GetPoints()[0].m_distance, distance, ()); + distance += mercator::DistanceOnEarth(point1, point2); + TEST_EQUAL(ei.GetPoints()[1].m_distance, distance, ()); + distance += mercator::DistanceOnEarth(point2, point3); + TEST_EQUAL(ei.GetPoints()[2].m_distance, distance, ()); +} + +UNIT_TEST(ElevationInfo_NoAltitudePoints) +{ + kml::MultiGeometry geometry; + geometry.AddLine({ + PointWithAltitude({0.0, 0.0}), + PointWithAltitude({1.0, 1.0}), + PointWithAltitude({2.0, 2.0}) + }); + ElevationInfo ei(geometry); + + TEST_EQUAL(3, ei.GetSize(), ()); + TEST_EQUAL(ei.GetMinAltitude(), kDefaultAltitudeMeters, ()); + TEST_EQUAL(ei.GetMaxAltitude(), kDefaultAltitudeMeters, ()); + TEST_EQUAL(ei.GetAscent(), 0, ()); + TEST_EQUAL(ei.GetDescent(), 0, ()); +} + +UNIT_TEST(ElevationInfo_MultipleLines) +{ + kml::MultiGeometry geometry; + geometry.AddLine({ + PointWithAltitude({0.0, 0.0}, 100), + PointWithAltitude({1.0, 1.0}, 150), + PointWithAltitude({1.0, 1.0}, 140) + }); + geometry.AddLine({ + PointWithAltitude({2.0, 2.0}, 50), + PointWithAltitude({3.0, 3.0}, 75), + PointWithAltitude({3.0, 3.0}, 60) + }); + geometry.AddLine({ + PointWithAltitude({4.0, 4.0}, 200), + PointWithAltitude({5.0, 5.0}, 250) + }); + ElevationInfo ei(geometry); + + TEST_EQUAL(8, ei.GetSize(), ()); + TEST_EQUAL(ei.GetMinAltitude(), 50, ()); + TEST_EQUAL(ei.GetMaxAltitude(), 250, ()); + TEST_EQUAL(ei.GetAscent(), 125, ()); // Ascent from 100 -> 150, 50 -> 75, 200 -> 250 + TEST_EQUAL(ei.GetDescent(), 25, ()); // Descent from 150 -> 140, 75 -> 60 +} + +UNIT_TEST(ElevationInfo_SegmentDistances) +{ + kml::MultiGeometry geometry; + geometry.AddLine({ + geometry::PointWithAltitude({0.0, 0.0}), + geometry::PointWithAltitude({1.0, 0.0}) + }); + geometry.AddLine({ + geometry::PointWithAltitude({2.0, 0.0}), + geometry::PointWithAltitude({3.0, 0.0}) + }); + geometry.AddLine({ + geometry::PointWithAltitude({4.0, 0.0}), + geometry::PointWithAltitude({5.0, 0.0}) + }); + + ElevationInfo ei(geometry); + auto const & segmentDistances = ei.GetSegmentsDistances(); + auto const points = ei.GetPoints(); + + TEST_EQUAL(segmentDistances.size(), 2, ()); + TEST_EQUAL(segmentDistances[0], ei.GetPoints()[2].m_distance, ()); + TEST_EQUAL(segmentDistances[1], ei.GetPoints()[4].m_distance, ()); +} + +UNIT_TEST(ElevationInfo_PositiveAndNegativeAltitudes) +{ + kml::MultiGeometry geometry; + geometry.AddLine({ + PointWithAltitude({0.0, 0.0}, -10), + PointWithAltitude({1.0, 1.0}, 20), + PointWithAltitude({2.0, 2.0}, -5), + PointWithAltitude({3.0, 3.0}, 15) + }); + ElevationInfo ei(geometry); + + TEST_EQUAL(4, ei.GetSize(), ()); + TEST_EQUAL(ei.GetMinAltitude(), -10, ()); + TEST_EQUAL(ei.GetMaxAltitude(), 20, ()); + TEST_EQUAL(ei.GetAscent(), 50, ()); // Ascent from -10 -> 20 and -5 -> 15 + TEST_EQUAL(ei.GetDescent(), 25, ()); // Descent from 20 -> -5 +} +} // namespace geometry diff --git a/map/map_tests/gps_track_collection_test.cpp b/map/map_tests/gps_track_collection_test.cpp index 389f4ab5ee..fd54c3b2b5 100644 --- a/map/map_tests/gps_track_collection_test.cpp +++ b/map/map_tests/gps_track_collection_test.cpp @@ -38,9 +38,9 @@ UNIT_TEST(GpsTrackCollection_Simple) for (size_t i = 0; i < 50; ++i) { auto info = MakeGpsTrackInfo(timestamp + i, ms::LatLon(-90 + i, -180 + i), i); - size_t addedId = collection.Add(info); - TEST_EQUAL(addedId, i, ()); - data[addedId] = info; + std::pair addedIds = collection.Add({info}); + TEST_EQUAL(addedIds.second, i, ()); + data[addedIds.second] = info; } TEST_EQUAL(50, collection.GetSize(), ()); diff --git a/map/place_page_info.cpp b/map/place_page_info.cpp index 9242e62994..6da7fe0c0f 100644 --- a/map/place_page_info.cpp +++ b/map/place_page_info.cpp @@ -10,6 +10,8 @@ #include "platform/measurement_utils.hpp" #include "platform/preferred_languages.hpp" #include "platform/utm_mgrs_utils.hpp" +#include "platform/distance.hpp" +#include "platform/duration.hpp" #include "geometry/mercator.hpp" @@ -41,7 +43,7 @@ void Info::SetFromFeatureType(FeatureType & ft) auto const mwmInfo = GetID().m_mwmId.GetInfo(); if (mwmInfo) { - feature::GetPreferredNames({ m_name, mwmInfo->GetRegionData(), languages::GetCurrentNorm(), + feature::GetPreferredNames({ m_name, mwmInfo->GetRegionData(), languages::GetCurrentMapLanguage(), true /* allowTranslit */} , out); } @@ -246,6 +248,20 @@ void Info::SetCustomName(std::string const & name) m_customName = name; } +void Info::SetTitlesForTrack(Track const & track) +{ + m_uiTitle = track.GetName(); + m_uiSubtitle = m_bookmarkCategoryName; + + std::vector statistics; + auto const length = track.GetLengthMeters(); + auto const duration = track.GetDurationInSeconds(); + statistics.push_back(platform::Distance::CreateFormatted(length).ToString()); + if (duration > 0) + statistics.push_back(platform::Duration(duration).GetPlatformLocalizedString()); + m_uiTrackStatistics = strings::JoinStrings(statistics, feature::kFieldsSeparator); +} + void Info::SetCustomNames(std::string const & title, std::string const & subtitle) { m_uiTitle = title; diff --git a/map/place_page_info.hpp b/map/place_page_info.hpp index 14e5226ad7..f7539b7fdb 100644 --- a/map/place_page_info.hpp +++ b/map/place_page_info.hpp @@ -133,7 +133,7 @@ public: std::string const & GetSecondaryTitle() const { return m_uiSecondaryTitle; }; /// Convenient wrapper for type, cuisines, elevation, stars, wifi etc. std::string const & GetSubtitle() const { return m_uiSubtitle; }; - std::string const & GetAddress() const { return m_uiAddress; } + std::string const & GetSecondarySubtitle() const { return !m_uiTrackStatistics.empty() ? m_uiTrackStatistics : m_uiAddress; }; std::string const & GetWikiDescription() const { return m_description; } /// @returns coordinate in DMS format if isDMS is true std::string GetFormattedCoordinate(CoordinatesFormat format) const; @@ -141,6 +141,7 @@ public: /// UI setters void SetCustomName(std::string const & name); void SetTitlesForBookmark(); + void SetTitlesForTrack(Track const & track); void SetCustomNames(std::string const & title, std::string const & subtitle); void SetCustomNameWithCoordinates(m2::PointD const & mercator, std::string const & name); void SetAddress(std::string && address) { m_address = std::move(address); } @@ -227,6 +228,7 @@ private: std::string m_uiSubtitle; std::string m_uiSecondaryTitle; std::string m_uiAddress; + std::string m_uiTrackStatistics; std::string m_description; /// Booking rating string std::string m_localizedRatingString; diff --git a/map/search_api.cpp b/map/search_api.cpp index 148719c847..9e905986bc 100644 --- a/map/search_api.cpp +++ b/map/search_api.cpp @@ -146,7 +146,7 @@ SearchAPI::SearchAPI(DataSource & dataSource, storage::Storage const & storage, , m_infoGetter(infoGetter) , m_delegate(delegate) , m_engine(m_dataSource, GetDefaultCategories(), m_infoGetter, - Engine::Params(languages::GetCurrentTwine() /* locale */, numThreads)) + Engine::Params(languages::GetCurrentMapTwine() /* locale */, numThreads)) { } @@ -422,6 +422,11 @@ void SearchAPI::SetViewportIfPossible(SearchParams & params) params.m_viewport = m_viewport; } +void SearchAPI::SetLocale(std::string const & locale) +{ + m_engine.SetLocale(locale); +} + bool SearchAPI::QueryMayBeSkipped(SearchParams const & prevParams, SearchParams const & currParams) const { diff --git a/map/search_api.hpp b/map/search_api.hpp index 88498cc4cf..41d887f0c4 100644 --- a/map/search_api.hpp +++ b/map/search_api.hpp @@ -21,6 +21,7 @@ #include #include #include +#include class DataSource; @@ -113,6 +114,8 @@ public: void EnableIndexingOfBookmarksDescriptions(bool enable); + void SetLocale(std::string const & locale); + // By default all created bookmarks are saved in BookmarksProcessor // but we do not index them in an attempt to save time and memory. // This method must be used to enable or disable indexing all current and future diff --git a/map/search_mark.cpp b/map/search_mark.cpp index 439442f37e..cdb4a6d417 100644 --- a/map/search_mark.cpp +++ b/map/search_mark.cpp @@ -74,7 +74,6 @@ using SearchMarkType = SearchMarkPoint::SearchMarkType; namespace { -df::ColorConstant const kPoiVisitedMaskColor = "PoiVisitedMask"; df::ColorConstant const kColorConstant = "SearchmarkDefault"; float const kVisitedSymbolOpacity = 0.7f; diff --git a/map/track.cpp b/map/track.cpp index b44153c1d4..fcdd167e55 100644 --- a/map/track.cpp +++ b/map/track.cpp @@ -10,42 +10,6 @@ namespace { -bool GetTrackPoint(std::vector const & points, - std::vector const & lengths, double distanceInMeters, m2::PointD & pt) -{ - CHECK_GREATER_OR_EQUAL(distanceInMeters, 0.0, ()); - CHECK_EQUAL(points.size(), lengths.size(), ()); - - double const kEpsMeters = 1e-2; - if (base::AlmostEqualAbs(distanceInMeters, lengths.front(), kEpsMeters)) - { - pt = points.front().GetPoint(); - return true; - } - - if (base::AlmostEqualAbs(distanceInMeters, lengths.back(), kEpsMeters)) - { - pt = points.back().GetPoint(); - return true; - } - - auto const it = std::lower_bound(lengths.begin(), lengths.end(), distanceInMeters); - if (it == lengths.end()) - return false; - - auto const pointIndex = std::distance(lengths.begin(), it); - auto const length = *it; - - auto const segmentLength = length - lengths[pointIndex - 1]; - auto const k = (segmentLength - (length - distanceInMeters)) / segmentLength; - - auto const & pt1 = points[pointIndex - 1].GetPoint(); - auto const & pt2 = points[pointIndex].GetPoint(); - pt = pt1 + (pt2 - pt1) * k; - - return true; -} - double GetLengthInMeters(kml::MultiGeometry::LineT const & points, size_t pointIndex) { CHECK_LESS(pointIndex, points.size(), (pointIndex, points.size())); @@ -62,33 +26,37 @@ double GetLengthInMeters(kml::MultiGeometry::LineT const & points, size_t pointI } } // namespace -Track::Track(kml::TrackData && data, bool interactive) +Track::Track(kml::TrackData && data) : Base(data.m_id == kml::kInvalidTrackId ? UserMarkIdStorage::Instance().GetNextTrackId() : data.m_id) , m_data(std::move(data)) { m_data.m_id = GetId(); CHECK(m_data.m_geometry.IsValid(), ()); - if (interactive && HasAltitudes()) - CacheDataForInteraction(); } -void Track::CacheDataForInteraction() +void Track::CacheDataForInteraction() const { m_interactionData = InteractionData(); m_interactionData->m_lengths = GetLengthsImpl(); m_interactionData->m_limitRect = GetLimitRectImpl(); } -std::vector Track::GetLengthsImpl() const +std::vector Track::GetLengthsImpl() const { - auto const & line = GetSingleGeometry(); - std::vector lengths(line.size(), 0.0); - for (size_t i = 1; i < line.size(); ++i) + double distance = 0; + std::vector lengths; + for (auto const & line : m_data.m_geometry.m_lines) { - auto const & pt1 = line[i - 1].GetPoint(); - auto const & pt2 = line[i].GetPoint(); - auto const segmentLength = mercator::DistanceOnEarth(pt1, pt2); - lengths[i] = lengths[i - 1] + segmentLength; + Lengths lineLengths; + lineLengths.emplace_back(distance); + for (size_t j = 1; j < line.size(); ++j) + { + auto const & pt1 = line[j - 1].GetPoint(); + auto const & pt2 = line[j].GetPoint(); + distance += mercator::DistanceOnEarth(pt1, pt2); + lineLengths.emplace_back(distance); + } + lengths.emplace_back(std::move(lineLengths)); } return lengths; } @@ -153,7 +121,7 @@ m2::RectD Track::GetLimitRect() const double Track::GetLengthMeters() const { if (m_interactionData) - return m_interactionData->m_lengths.back(); + return m_interactionData->m_lengths.back().back(); double len = 0; for (auto const & line : m_data.m_geometry.m_lines) @@ -161,28 +129,12 @@ double Track::GetLengthMeters() const return len; } -double Track::GetLengthMetersImpl(kml::MultiGeometry::LineT const & line, size_t ptIdx) const +double Track::GetLengthMetersImpl(size_t lineIndex, size_t ptIndex) const { - if (m_interactionData) - { - CHECK_LESS(ptIdx, m_interactionData->m_lengths.size(), ()); - return m_interactionData->m_lengths[ptIdx]; - } - - return GetLengthInMeters(line, ptIdx); -} - -bool Track::IsInteractive() const -{ - return m_interactionData.has_value(); -} - -std::pair Track::GetCenterPoint() const -{ - ASSERT(m_data.m_geometry.IsValid(), ()); - - auto const & line = m_data.m_geometry.m_lines[0]; - return { line[line.size() / 2].GetPoint(), GetLengthMetersImpl(line, line.size() / 2) }; + if (!m_interactionData) + CacheDataForInteraction(); + auto const & lineLengths = m_interactionData->m_lengths[lineIndex]; + return lineLengths[ptIndex]; } void Track::UpdateSelectionInfo(m2::RectD const & touchRect, TrackSelectionInfo & info) const @@ -190,12 +142,13 @@ void Track::UpdateSelectionInfo(m2::RectD const & touchRect, TrackSelectionInfo if (m_interactionData && !m_interactionData->m_limitRect.IsIntersect(touchRect)) return; - for (auto const & line : m_data.m_geometry.m_lines) + for (size_t lineIndex = 0; lineIndex < m_data.m_geometry.m_lines.size(); ++lineIndex) { - for (size_t i = 0; i + 1 < line.size(); ++i) + auto const & line = m_data.m_geometry.m_lines[lineIndex]; + for (size_t ptIndex = 0; ptIndex + 1 < line.size(); ++ptIndex) { - auto pt1 = line[i].GetPoint(); - auto pt2 = line[i + 1].GetPoint(); + auto pt1 = line[ptIndex].GetPoint(); + auto pt2 = line[ptIndex + 1].GetPoint(); if (!m2::Intersect(touchRect, pt1, pt2)) continue; @@ -209,8 +162,8 @@ void Track::UpdateSelectionInfo(m2::RectD const & touchRect, TrackSelectionInfo info.m_trackId = m_data.m_id; info.m_trackPoint = closestPoint; - auto const segDistInMeters = mercator::DistanceOnEarth(line[i].GetPoint(), closestPoint); - info.m_distFromBegM = segDistInMeters + GetLengthMetersImpl(line, i); + auto const segDistInMeters = mercator::DistanceOnEarth(line[ptIndex].GetPoint(), closestPoint); + info.m_distFromBegM = segDistInMeters + GetLengthMetersImpl(lineIndex, ptIndex); } } } @@ -272,16 +225,36 @@ void Track::Detach() m_groupID = kml::kInvalidMarkGroupId; } -bool Track::GetPoint(double distanceInMeters, m2::PointD & pt) const +kml::MultiGeometry::LineT Track::GetGeometry() const { - if (m_interactionData) - return GetTrackPoint(GetSingleGeometry(), m_interactionData->m_lengths, distanceInMeters, pt); - - return GetTrackPoint(GetSingleGeometry(), GetLengthsImpl(), distanceInMeters, pt); + kml::MultiGeometry::LineT geometry; + for (auto const & line : m_data.m_geometry.m_lines) + { + for (size_t i = 0; i < line.size(); ++i) + geometry.emplace_back(line[i]); + } + return geometry; } -kml::MultiGeometry::LineT const & Track::GetSingleGeometry() const +std::optional Track::GetElevationInfo() const { - ASSERT_EQUAL(m_data.m_geometry.m_lines.size(), 1, ()); - return m_data.m_geometry.m_lines[0]; + if (!HasAltitudes()) + return std::nullopt; + if (!m_elevationInfo) + m_elevationInfo = ElevationInfo(GetData().m_geometry); + return m_elevationInfo; +} + +double Track::GetDurationInSeconds() const +{ + double duration = 0.0; + if (!m_data.m_geometry.HasTimestamps()) + return duration; + for (size_t i = 0; i < m_data.m_geometry.m_timestamps.size(); ++i) + { + ASSERT(m_data.m_geometry.HasTimestampsFor(i), ()); + auto const & timestamps = m_data.m_geometry.m_timestamps[i]; + duration += timestamps.back() - timestamps.front(); + } + return duration; } diff --git a/map/track.hpp b/map/track.hpp index 32b9c43c9e..da030c2fdc 100644 --- a/map/track.hpp +++ b/map/track.hpp @@ -2,6 +2,8 @@ #include "kml/types.hpp" +#include "map/elevation_info.hpp" + #include "drape_frontend/user_marks_provider.hpp" #include @@ -9,9 +11,10 @@ class Track : public df::UserLineMark { using Base = df::UserLineMark; + using Lengths = std::vector; public: - Track(kml::TrackData && data, bool interactive); + Track(kml::TrackData && data); kml::MarkGroupId GetGroupId() const override { return m_groupID; } @@ -27,8 +30,8 @@ public: m2::RectD GetLimitRect() const; double GetLengthMeters() const; - bool IsInteractive() const; - + double GetDurationInSeconds() const; + std::optional GetElevationInfo() const; std::pair GetCenterPoint() const; struct TrackSelectionInfo @@ -64,28 +67,27 @@ public: bool GetPoint(double distanceInMeters, m2::PointD & pt) const; - /// @name This functions are valid only for the single line geometry. - /// @{ - kml::MultiGeometry::LineT const & GetSingleGeometry() const; -private: - std::vector GetLengthsImpl() const; - /// @} - m2::RectD GetLimitRectImpl() const; - - void CacheDataForInteraction(); + kml::MultiGeometry::LineT GetGeometry() const; bool HasAltitudes() const; - double GetLengthMetersImpl(kml::MultiGeometry::LineT const & line, size_t ptIdx) const; +private: + std::vector GetLengthsImpl() const; + m2::RectD GetLimitRectImpl() const; + + void CacheDataForInteraction() const; + + double GetLengthMetersImpl(size_t lineIndex, size_t ptIdx) const; kml::TrackData m_data; kml::MarkGroupId m_groupID = kml::kInvalidMarkGroupId; + mutable std::optional m_elevationInfo; struct InteractionData { - std::vector m_lengths; + std::vector m_lengths; m2::RectD m_limitRect; }; - std::optional m_interactionData; + mutable std::optional m_interactionData; mutable bool m_isDirty = true; }; diff --git a/packaging/app.organicmaps.desktop.metainfo.xml b/packaging/app.organicmaps.desktop.metainfo.xml index 4d2e30b394..139b972222 100644 --- a/packaging/app.organicmaps.desktop.metainfo.xml +++ b/packaging/app.organicmaps.desktop.metainfo.xml @@ -115,7 +115,41 @@ - + + +

Highlights:

+
    +
  • OpenStreetMap data as of November 22
  • +
  • Touch pad pinch zoom gesture on Linux
  • +
  • Various search improvements, translation updates & bug fixes
  • +
+
+
+ + +

Highlights:

+
    +
  • OpenStreetMap data as of November 7
  • +
  • Added a map language setting
  • +
  • Touch handling on Linux devices is now feature complete
  • +
  • Added water slides, carousels, ferris wheels, etc
  • +
  • Various search improvements, translation updates & bug fixes
  • +
+
+
+ + +

Highlights:

+
    +
  • OpenStreetMap data as of October 17
  • +
  • More detailed (50m step) altitude isolines for Iceland, India & Morocco
  • +
  • Use a different icon for non-drinking water sources
  • +
  • Added water towers
  • +
  • Fixed Search on Map results prioritization bug
  • +
+
+
+

Highlights:

    diff --git a/platform/CMakeLists.txt b/platform/CMakeLists.txt index 1f6482e547..0e4f40becd 100644 --- a/platform/CMakeLists.txt +++ b/platform/CMakeLists.txt @@ -13,6 +13,8 @@ set(SRC country_file.hpp distance.cpp distance.hpp + duration.cpp + duration.hpp downloader_defines.hpp downloader_utils.cpp downloader_utils.hpp @@ -51,9 +53,12 @@ set(SRC secure_storage.hpp servers_list.cpp servers_list.hpp + products.cpp + products.hpp settings.cpp settings.hpp socket.hpp + trace.hpp string_storage_base.cpp string_storage_base.hpp utm_mgrs_utils.cpp @@ -98,6 +103,7 @@ elseif(${PLATFORM_ANDROID}) platform_android.cpp platform_unix_impl.cpp platform_unix_impl.hpp + trace_android.cpp ) else() # neither iPhone nor Android # Find bash first, on Windows it can be either in Git or in WSL diff --git a/platform/duration.cpp b/platform/duration.cpp new file mode 100644 index 0000000000..64606d7fdf --- /dev/null +++ b/platform/duration.cpp @@ -0,0 +1,134 @@ +#include "duration.hpp" + +#include "base/stl_helpers.hpp" + +/// @todo(KK): move the formatting code from the platform namespace +namespace platform +{ +namespace +{ +using namespace std::chrono; + +static constexpr std::string_view kNoSpace = ""; + +unsigned long SecondsToUnits(seconds duration, Duration::Units unit) +{ + switch (unit) + { + case Duration::Units::Days: return duration_cast(duration).count(); + case Duration::Units::Hours: return duration_cast(duration).count(); + case Duration::Units::Minutes: return duration_cast(duration).count(); + default: UNREACHABLE(); + } +} + +seconds UnitsToSeconds(long value, Duration::Units unit) +{ + switch (unit) + { + case Duration::Units::Days: return days(value); + case Duration::Units::Hours: return hours(value); + case Duration::Units::Minutes: return minutes(value); + default: UNREACHABLE(); + } +} + +std::string_view GetUnitSeparator(Locale const & locale) +{ + static constexpr auto kEmptyNumberUnitSeparatorLocales = std::array + { + "en", "de", "fr", "he", "fa", "ja", "ko", "mr", "th", "tr", "vi", "zh" + }; + bool const isEmptySeparator = base::IsExist(kEmptyNumberUnitSeparatorLocales, locale.m_language); + return isEmptySeparator ? kNoSpace : kNarrowNonBreakingSpace; +} + +std::string_view GetUnitsGroupingSeparator(Locale const & locale) +{ + static constexpr auto kEmptyGroupingSeparatorLocales = std::array + { + "ja", "zh" + }; + bool const isEmptySeparator = base::IsExist(kEmptyGroupingSeparatorLocales, locale.m_language); + return isEmptySeparator ? kNoSpace : kNonBreakingSpace; +} + +bool IsUnitsOrderValid(std::initializer_list units) +{ + return base::IsSortedAndUnique(units.begin(), units.end()); +} +} // namespace + +Duration::Duration(unsigned long seconds) : m_seconds(seconds) +{} + +std::string Duration::GetLocalizedString(std::initializer_list units, Locale const & locale) const +{ + return GetString(std::move(units), GetUnitSeparator(locale), GetUnitsGroupingSeparator(locale)); +} + +std::string Duration::GetPlatformLocalizedString() const +{ + struct InitSeparators + { + std::string_view m_unitSep, m_groupingSep; + InitSeparators() + { + auto const loc = GetCurrentLocale(); + m_unitSep = GetUnitSeparator(loc); + m_groupingSep = GetUnitsGroupingSeparator(loc); + } + }; + static InitSeparators seps; + + return GetString({Units::Days, Units::Hours, Units::Minutes}, seps.m_unitSep, seps.m_groupingSep); +} + +std::string Duration::GetString(std::initializer_list units, std::string_view unitSeparator, + std::string_view groupingSeparator) const +{ + ASSERT(units.size(), ()); + ASSERT(IsUnitsOrderValid(units), ()); + + if (SecondsToUnits(m_seconds, Units::Minutes) == 0) + return std::to_string(0U).append(unitSeparator).append(GetUnitsString(Units::Minutes)); + + std::string formattedTime; + seconds remainingSeconds = m_seconds; + + for (auto const unit : units) + { + const unsigned long unitsCount = SecondsToUnits(remainingSeconds, unit); + if (unitsCount > 0) + { + if (!formattedTime.empty()) + formattedTime.append(groupingSeparator); + formattedTime.append(std::to_string(unitsCount).append(unitSeparator).append(GetUnitsString(unit))); + remainingSeconds -= UnitsToSeconds(unitsCount, unit); + } + } + return formattedTime; +} + +std::string Duration::GetUnitsString(Units unit) +{ + switch (unit) + { + case Units::Minutes: return platform::GetLocalizedString("minute"); + case Units::Hours: return platform::GetLocalizedString("hour"); + case Units::Days: return platform::GetLocalizedString("day"); + default: UNREACHABLE(); + } +} + +std::string DebugPrint(Duration::Units units) +{ + switch (units) + { + case Duration::Units::Days: return "d"; + case Duration::Units::Hours: return "h"; + case Duration::Units::Minutes: return "m"; + default: UNREACHABLE(); + } +} +} // namespace platform diff --git a/platform/duration.hpp b/platform/duration.hpp new file mode 100644 index 0000000000..6ec353fd94 --- /dev/null +++ b/platform/duration.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include "platform/localization.hpp" + +#include +#include +#include + +namespace platform +{ + +class Duration +{ +public: + enum class Units + { + Days = 0, + Hours = 1, + Minutes = 2, + }; + + explicit Duration(unsigned long seconds); + + static std::string GetUnitsString(Units unit); + + std::string GetLocalizedString(std::initializer_list units, Locale const & locale) const; + std::string GetPlatformLocalizedString() const; + +private: + const std::chrono::seconds m_seconds; + + std::string GetString(std::initializer_list units, std::string_view unitSeparator, std::string_view groupingSeparator) const; +}; + +std::string DebugPrint(Duration::Units units); + +} // namespace platform diff --git a/platform/localization.mm b/platform/localization.mm index 6854418b0e..0b4881ffb8 100644 --- a/platform/localization.mm +++ b/platform/localization.mm @@ -39,10 +39,9 @@ std::string GetCurrencySymbol(std::string const & currencyCode) std::string GetLocalizedMyPositionBookmarkName() { - NSDateFormatter * dateFormatter = [[NSDateFormatter alloc] init]; - dateFormatter.dateStyle = NSDateFormatterLongStyle; - dateFormatter.timeStyle = NSDateFormatterShortStyle; NSDate * now = [NSDate date]; - return [dateFormatter stringFromDate:now].UTF8String; + return [NSDateFormatter localizedStringFromDate:now + dateStyle:NSDateFormatterLongStyle + timeStyle:NSDateFormatterShortStyle].UTF8String; } } // namespace platform diff --git a/platform/location.hpp b/platform/location.hpp index cd4706d178..afed47d4a6 100644 --- a/platform/location.hpp +++ b/platform/location.hpp @@ -1,6 +1,7 @@ #pragma once #include "geometry/point2d.hpp" +#include "geometry/latlon.hpp" #include "base/base.hpp" @@ -60,6 +61,7 @@ namespace location bool HasBearing() const { return m_bearing >= 0.0; } bool HasSpeed() const { return m_speed >= 0.0; } bool HasVerticalAccuracy() const { return m_verticalAccuracy >= 0.0; } + ms::LatLon GetLatLon() const { return {m_latitude, m_longitude}; } }; class CompassInfo diff --git a/platform/measurement_utils.cpp b/platform/measurement_utils.cpp index d53e493b68..343cbb6f14 100644 --- a/platform/measurement_utils.cpp +++ b/platform/measurement_utils.cpp @@ -202,7 +202,8 @@ double MpsToUnits(double metersPerSecond, Units units) std::string FormatSpeedNumeric(double metersPerSecond, Units units) { double const unitsPerHour = MpsToUnits(metersPerSecond, units); - return ToStringPrecision(unitsPerHour, unitsPerHour >= 10.0 ? 0 : 1); + double roundedValue = std::round(unitsPerHour); + return std::to_string(static_cast(roundedValue)); } std::string FormatOsmLink(double lat, double lon, int zoom) diff --git a/platform/platform_ios.mm b/platform/platform_ios.mm index b83b37db01..5293673cdd 100644 --- a/platform/platform_ios.mm +++ b/platform/platform_ios.mm @@ -162,12 +162,11 @@ std::string Platform::DeviceModel() const std::string Platform::Version() const { + /// @note Do not change version format, it is parsed on server side. NSBundle * mainBundle = [NSBundle mainBundle]; - NSString * appName = [mainBundle objectForInfoDictionaryKey:@"CFBundleName"]; - NSString * bundleId = mainBundle.bundleIdentifier; NSString * version = [mainBundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; NSString * build = [mainBundle objectForInfoDictionaryKey:@"CFBundleVersion"]; - return std::string{appName.UTF8String} + ' ' + std::string{bundleId.UTF8String} + ' ' + std::string{version.UTF8String} + '-' + build.UTF8String + '-' + OMIM_OS_NAME; + return std::string{version.UTF8String} + '-' + build.UTF8String + '-' + OMIM_OS_NAME; } int32_t Platform::IntVersion() const diff --git a/platform/platform_tests/CMakeLists.txt b/platform/platform_tests/CMakeLists.txt index 81831f3d0c..b3cb32c5f8 100644 --- a/platform/platform_tests/CMakeLists.txt +++ b/platform/platform_tests/CMakeLists.txt @@ -4,6 +4,7 @@ set(SRC apk_test.cpp country_file_tests.cpp distance_tests.cpp + duration_tests.cpp downloader_tests/downloader_test.cpp downloader_utils_tests.cpp get_text_by_id_tests.cpp @@ -13,6 +14,8 @@ set(SRC location_test.cpp measurement_tests.cpp platform_test.cpp + meta_config_tests.cpp + products_tests.cpp utm_mgrs_utils_tests.cpp ) diff --git a/platform/platform_tests/downloader_utils_tests.cpp b/platform/platform_tests/downloader_utils_tests.cpp index 7ea9458247..f2e71b1737 100644 --- a/platform/platform_tests/downloader_utils_tests.cpp +++ b/platform/platform_tests/downloader_utils_tests.cpp @@ -1,10 +1,10 @@ #include "testing/testing.hpp" #include "platform/downloader_utils.hpp" -#include "platform/servers_list.hpp" #include "platform/local_country_file_utils.hpp" #include "platform/mwm_version.hpp" #include "platform/platform.hpp" +#include "platform/servers_list.hpp" #include "base/file_name_utils.hpp" @@ -99,17 +99,18 @@ UNIT_TEST(Downloader_ParseMetaConfig) { "servers": [ "https://url1/", "https://url2/" ], "settings": { - "key1": "value1", - "key2": "value2" + "DonateUrl": "value1", + "NY": "value2", + "key3": "value3" } } )")), ()); TEST_EQUAL(cfg->m_serversList.size(), 2, ()); TEST_EQUAL(cfg->m_serversList[0], "https://url1/", ()); TEST_EQUAL(cfg->m_serversList[1], "https://url2/", ()); - TEST_EQUAL(cfg->m_settings.size(), 2, ()); - TEST_EQUAL(cfg->m_settings["key1"], "value1", ()); - TEST_EQUAL(cfg->m_settings["key2"], "value2", ()); + TEST_EQUAL(cfg->m_settings.size(), 2, ()); // "key3" is ignored + TEST_EQUAL(cfg->m_settings["DonateUrl"], "value1", ()); + TEST_EQUAL(cfg->m_settings["NY"], "value2", ()); TEST(!downloader::ParseMetaConfig(R"(broken json)"), ()); diff --git a/platform/platform_tests/duration_tests.cpp b/platform/platform_tests/duration_tests.cpp new file mode 100644 index 0000000000..0b7436af5e --- /dev/null +++ b/platform/platform_tests/duration_tests.cpp @@ -0,0 +1,114 @@ +#include "testing/testing.hpp" + +#include "platform/duration.hpp" + +#include + +namespace platform +{ +using std::chrono::duration_cast, std::chrono::seconds, std::chrono::minutes, std::chrono::hours, std::chrono::days; + +struct TestData +{ + struct Duration + { + days m_days; + hours m_hours; + minutes m_minutes; + seconds m_seconds; + std::string result; + + Duration(long days, long hours, long minutes, long seconds, std::string const & result) + : m_days(days), m_hours(hours), m_minutes(minutes), m_seconds(seconds), result(result) + {} + + long Seconds() const + { + return (duration_cast(m_days) + + duration_cast(m_hours) + + duration_cast(m_minutes) + + m_seconds).count(); + } + }; + + Locale m_locale; + std::vector m_duration; + + constexpr TestData(Locale locale, std::vector duration) + : m_locale(locale), m_duration(duration) + {} +}; + +Locale GetLocale(std::string const & language) +{ + Locale locale; + locale.m_language = language; + return locale; +} +/* + Localized string cannot be retrieved from the app target bundle during the tests execution + and the platform::GetLocalizedString will return the same string as the input ("minute", "hour" etc). + This is why the expectation strings are not explicit. + */ + +auto const m = Duration::GetUnitsString(Duration::Units::Minutes); +auto const h = Duration::GetUnitsString(Duration::Units::Hours); +auto const d = Duration::GetUnitsString(Duration::Units::Days); + +UNIT_TEST(Duration_AllUnits) +{ + TestData const testData[] = { + {GetLocale("en"), + { + {0, 0, 0, 0, "0" + m}, + {0, 0, 0, 30, "0" + m}, + {0, 0, 0, 59, "0" + m}, + {0, 0, 1, 0, "1" + m}, + {0, 0, 1, 59, "1" + m}, + {0, 0, 60, 0, "1" + h}, + {0, 0, 123, 0, "2" + h + kNonBreakingSpace + "3" + m}, + {0, 3, 0, 0, "3" + h}, + {0, 24, 0, 0, "1" + d}, + {4, 0, 0, 0, "4" + d}, + {1, 2, 3, 0, "1" + d + kNonBreakingSpace + "2" + h + kNonBreakingSpace + "3" + m}, + {1, 0, 15, 0, "1" + d + kNonBreakingSpace + "15" + m}, + {0, 15, 1, 0, "15" + h + kNonBreakingSpace + "1" + m}, + {1, 15, 0, 0, "1" + d + kNonBreakingSpace + "15" + h}, + {15, 0, 10, 0, "15" + d + kNonBreakingSpace + "10" + m}, + {15, 15, 15, 0, "15" + d + kNonBreakingSpace + "15" + h + kNonBreakingSpace + "15" + m} + } + }, + }; + + for (auto const & data : testData) + { + for (auto const & dataDuration : data.m_duration) + { + auto const duration = Duration(dataDuration.Seconds()); + auto durationStr = duration.GetLocalizedString({Duration::Units::Days, Duration::Units::Hours, Duration::Units::Minutes}, data.m_locale); + TEST_EQUAL(durationStr, dataDuration.result, ()); + } + } +} + +UNIT_TEST(Duration_Localization) +{ + TestData const testData[] = { + // en + {GetLocale("en"), {{1, 2, 3, 0, "1" + d + kNonBreakingSpace + "2" + h + kNonBreakingSpace + "3" + m}}}, + // ru (narrow spacing between number and unit) + {GetLocale("ru"), {{1, 2, 3, 0, "1" + kNarrowNonBreakingSpace + d + kNonBreakingSpace + "2" + kNarrowNonBreakingSpace + h + kNonBreakingSpace + "3" + kNarrowNonBreakingSpace + m}}}, + // zh (no spacings) + {GetLocale("zh"), {{1, 2, 3, 0, "1" + d + "2" + h + "3" + m}}} + }; + + for (auto const & data : testData) + { + for (auto const & duration : data.m_duration) + { + auto const durationStr = Duration(duration.Seconds()).GetLocalizedString({Duration::Units::Days, Duration::Units::Hours, Duration::Units::Minutes}, data.m_locale); + TEST_EQUAL(durationStr, duration.result, ()); + } + } +} +} // namespace platform diff --git a/platform/platform_tests/measurement_tests.cpp b/platform/platform_tests/measurement_tests.cpp index 11ab0c9ab9..f87eb1e6f2 100644 --- a/platform/platform_tests/measurement_tests.cpp +++ b/platform/platform_tests/measurement_tests.cpp @@ -67,10 +67,10 @@ UNIT_TEST(FormatOsmLink) UNIT_TEST(FormatSpeedNumeric) { TEST_EQUAL(FormatSpeedNumeric(10, Units::Metric), "36", ()); - TEST_EQUAL(FormatSpeedNumeric(1, Units::Metric), "3.6", ()); + TEST_EQUAL(FormatSpeedNumeric(1, Units::Metric), "4", ()); TEST_EQUAL(FormatSpeedNumeric(10, Units::Imperial), "22", ()); - TEST_EQUAL(FormatSpeedNumeric(1, Units::Imperial), "2.2", ()); + TEST_EQUAL(FormatSpeedNumeric(1, Units::Imperial), "2", ()); } UNIT_TEST(OSMDistanceToMetersString) diff --git a/platform/platform_tests/meta_config_tests.cpp b/platform/platform_tests/meta_config_tests.cpp new file mode 100644 index 0000000000..29baa8515c --- /dev/null +++ b/platform/platform_tests/meta_config_tests.cpp @@ -0,0 +1,107 @@ +#include "testing/testing.hpp" + +#include "platform/products.hpp" +#include "platform/servers_list.hpp" + +#include "cppjansson/cppjansson.hpp" + +using namespace downloader; + +UNIT_TEST(MetaConfig_JSONParser_OldFormat) +{ + std::string oldFormatJson = R"(["http://url1", "http://url2", "http://url3"])"; + auto result = ParseMetaConfig(oldFormatJson); + TEST(result.has_value(), ()); + TEST_EQUAL(result->m_serversList.size(), 3, ()); + TEST_EQUAL(result->m_serversList[0], "http://url1", ()); + TEST_EQUAL(result->m_serversList[1], "http://url2", ()); + TEST_EQUAL(result->m_serversList[2], "http://url3", ()); + TEST(result->m_settings.empty(), ()); + TEST(result->m_productsConfig.empty(), ()); +} + +UNIT_TEST(MetaConfig_JSONParser_InvalidJSON) +{ + std::string invalidJson = R"({"servers": ["http://url1", "http://url2")"; + auto result = ParseMetaConfig(invalidJson); + TEST(!result.has_value(), ()); +} + +UNIT_TEST(MetaConfig_JSONParser_EmptyServersList) +{ + std::string emptyServersJson = R"({"servers": []})"; + auto result = ParseMetaConfig(emptyServersJson); + TEST(!result.has_value(), ()); +} + +UNIT_TEST(MetaConfig_JSONParser_NewFormatWithoutProducts) +{ + std::string newFormatJson = R"({ + "servers": ["http://url1", "http://url2"], + "settings": { + "DonateUrl": "value1", + "key2": "value2" + } + })"; + auto result = ParseMetaConfig(newFormatJson); + TEST(result.has_value(), ()); + TEST_EQUAL(result->m_serversList.size(), 2, ()); + TEST_EQUAL(result->m_serversList[0], "http://url1", ()); + TEST_EQUAL(result->m_serversList[1], "http://url2", ()); + TEST_EQUAL(result->m_settings.size(), 1, ()); + TEST_EQUAL(result->m_settings["DonateUrl"], "value1", ()); + TEST(result->m_productsConfig.empty(), ()); +} + +UNIT_TEST(MetaConfig_JSONParser_NewFormatWithProducts) +{ + std::string newFormatJson = R"({ + "servers": ["http://url1", "http://url2"], + "settings": { + "DonateUrl": "value1", + "key2": "value2" + }, + "productsConfig": { + "placePagePrompt": "prompt1", + "aboutScreenPrompt": "prompt2", + "products": [ + { + "title": "Product 1", + "link": "http://product1" + }, + { + "title": "Product 2", + "link": "http://product2" + } + ] + } + })"; + + auto result = ParseMetaConfig(newFormatJson); + TEST(result.has_value(), ()); + TEST_EQUAL(result->m_serversList.size(), 2, ()); + TEST_EQUAL(result->m_serversList[0], "http://url1", ()); + TEST_EQUAL(result->m_serversList[1], "http://url2", ()); + TEST_EQUAL(result->m_settings.size(), 1, ()); + TEST_EQUAL(result->m_settings["DonateUrl"], "value1", ()); + + TEST(!result->m_productsConfig.empty(), ()); + auto const productsConfigResult = products::ProductsConfig::Parse(result->m_productsConfig); + TEST(productsConfigResult.has_value(), ()); + auto const productsConfig = productsConfigResult.value(); + TEST_EQUAL(productsConfig.GetPlacePagePrompt(), "prompt1", ()); + TEST(productsConfig.HasProducts(), ()); + auto const products = productsConfig.GetProducts(); + TEST_EQUAL(products.size(), 2, ()); +} + +UNIT_TEST(MetaConfig_JSONParser_MissingServersKey) +{ + std::string missingServersJson = R"({ + "settings": { + "key1": "value1" + } + })"; + auto result = ParseMetaConfig(missingServersJson); + TEST(!result.has_value(), ("JSON shouldn't be parsed without 'servers' key")); +} diff --git a/platform/platform_tests/products_tests.cpp b/platform/platform_tests/products_tests.cpp new file mode 100644 index 0000000000..25382932e8 --- /dev/null +++ b/platform/platform_tests/products_tests.cpp @@ -0,0 +1,107 @@ +#include "testing/testing.hpp" + +#include "platform/products.hpp" + +#include "cppjansson/cppjansson.hpp" + +using namespace products; + +UNIT_TEST(ProductsConfig_ValidConfig) +{ + std::string jsonStr = R"({ + "placePagePrompt": "prompt1", + "products": [ + { + "title": "Product 1", + "link": "http://product1" + }, + { + "title": "Product 2", + "link": "http://product2" + } + ] + })"; + + auto const result = ProductsConfig::Parse(jsonStr); + TEST(result.has_value(), ()); + auto const productsConfig = result.value(); + TEST_EQUAL(productsConfig.GetPlacePagePrompt(), "prompt1", ()); + + auto const products = productsConfig.GetProducts(); + TEST_EQUAL(products.size(), 2, ()); + TEST_EQUAL(products[0].GetTitle(), "Product 1", ()); + TEST_EQUAL(products[0].GetLink(), "http://product1", ()); + TEST_EQUAL(products[1].GetTitle(), "Product 2", ()); + TEST_EQUAL(products[1].GetLink(), "http://product2", ()); +} + +UNIT_TEST(ProductsConfig_EmptyPrompts) +{ + std::string jsonStr = R"({ + "aboutScreenPrompt": "", + "products": [ + { + "title": "Product 1", + "link": "http://product1" + }, + { + "title": "Product 2", + "link": "http://product2" + } + ] + })"; + + auto const result = ProductsConfig::Parse(jsonStr); + TEST(result.has_value(), ()); + auto const productsConfig = result.value(); + TEST_EQUAL(productsConfig.GetPlacePagePrompt(), "", ()); + TEST_EQUAL(productsConfig.GetProducts().size(), 2, ()); +} + +UNIT_TEST(ProductsConfig_InvalidProduct) +{ + std::string jsonStr = R"({ + "placePagePrompt": "prompt1", + "products": [ + { + "title": "Product 1" + }, + { + "title": "Product 2", + "link": "http://product2" + } + ] + })"; + + auto const result = ProductsConfig::Parse(jsonStr); + TEST(result.has_value(), ()); + auto const productsConfig = result.value(); + TEST_EQUAL(productsConfig.GetPlacePagePrompt(), "prompt1", ()); + + auto const products = productsConfig.GetProducts(); + TEST_EQUAL(products.size(), 1, ()); + TEST_EQUAL(products[0].GetTitle(), "Product 2", ()); + TEST_EQUAL(products[0].GetLink(), "http://product2", ()); +} + +UNIT_TEST(ProductsConfig_EmptyProducts) +{ + std::string jsonStr = R"({ + "placePagePrompt": "prompt1", + "products": [] + })"; + + auto const result = ProductsConfig::Parse(jsonStr); + TEST(!result.has_value(), ()); +} + +UNIT_TEST(ProductsConfig_MissedProductsField) +{ + std::string jsonStr = R"({ + "placePagePrompt": "prompt1" + })"; + + auto const result = ProductsConfig::Parse(jsonStr); + TEST(!result.has_value(), ()); +} + diff --git a/platform/preferred_languages.cpp b/platform/preferred_languages.cpp index 9d83ce1973..aa96c92e15 100644 --- a/platform/preferred_languages.cpp +++ b/platform/preferred_languages.cpp @@ -1,4 +1,7 @@ #include "platform/preferred_languages.hpp" +#include "platform/settings.hpp" + +#include "coding/string_utf8_multilang.hpp" #include "base/buffer_vector.hpp" #include "base/macros.hpp" @@ -169,9 +172,24 @@ std::string GetCurrentNorm() return Normalize(GetCurrentOrig()); } -std::string GetCurrentTwine() +std::string GetCurrentMapLanguage() +{ + std::string languageCode; + if (!settings::Get(settings::kMapLanguageCode, languageCode) || languageCode.empty()) + { + for (auto const & systemLanguage : GetSystemPreferred()) + { + auto normalizedLang = Normalize(systemLanguage); + if (StringUtf8Multilang::GetLangIndex(normalizedLang) != StringUtf8Multilang::kUnsupportedLanguageCode) + return normalizedLang; + } + return std::string(StringUtf8Multilang::GetLangByCode(StringUtf8Multilang::kDefaultCode)); + } + return languageCode; +} + +std::string GetTwine(std::string const & lang) { - std::string const lang = GetCurrentOrig(); // Special cases for different Chinese variations. if (lang.find("zh") == 0) { @@ -191,4 +209,15 @@ std::string GetCurrentTwine() // Use short (2 or 3 chars) versions for all other languages. return Normalize(lang); } + +std::string GetCurrentTwine() +{ + return GetTwine(GetCurrentOrig()); +} + +std::string GetCurrentMapTwine() +{ + return GetTwine(GetCurrentMapLanguage()); +} + } // namespace languages diff --git a/platform/preferred_languages.hpp b/platform/preferred_languages.hpp index 2288eeda9d..4a2c2d7801 100644 --- a/platform/preferred_languages.hpp +++ b/platform/preferred_languages.hpp @@ -1,5 +1,7 @@ #pragma once +#include "base/buffer_vector.hpp" + #include namespace languages @@ -14,6 +16,7 @@ std::string GetCurrentOrig(); /// @return Current language in out Twine translations compatible format, e.g. "en", "pt" or "zh-Hant". std::string GetCurrentTwine(); +std::string GetCurrentMapTwine(); /// @return Normalized language code for the current user in the form "en", "zh". /// Returned languages are normalized to our supported languages in the core, see @@ -21,4 +24,7 @@ std::string GetCurrentTwine(); /// langs like Danish (da) are not supported in the core too, but used as a locale. std::string Normalize(std::string_view lang); std::string GetCurrentNorm(); +std::string GetCurrentMapLanguage(); + +buffer_vector const & GetSystemPreferred(); } // namespace languages diff --git a/platform/products.cpp b/platform/products.cpp new file mode 100644 index 0000000000..7c651cbd2f --- /dev/null +++ b/platform/products.cpp @@ -0,0 +1,118 @@ +#include "platform/products.hpp" +#include "platform/platform.hpp" + +#include "base/logging.hpp" +#include "base/assert.hpp" +#include "base/string_utils.hpp" + +#include "defines.hpp" + +#include "coding/file_writer.hpp" + +#include "cppjansson/cppjansson.hpp" + +namespace products { + +char const kPlacePagePrompt[] = "placePagePrompt"; +char const kProducts[] = "products"; +char const kProductTitle[] = "title"; +char const kProductLink[] = "link"; + +std::string GetProductsFilePath() +{ + return GetPlatform().SettingsPathForFile(PRODUCTS_SETTINGS_FILE_NAME); +} + +ProductsSettings::ProductsSettings() +{ + std::lock_guard guard(m_mutex); + auto const path = GetProductsFilePath(); + if (Platform::IsFileExistsByFullPath(path)) + { + try + { + std::string outValue; + auto dataReader = GetPlatform().GetReader(path); + dataReader->ReadAsString(outValue); + m_productsConfig = ProductsConfig::Parse(outValue); + } + catch (std::exception const & ex) + { + LOG(LERROR, ("Error reading ProductsConfig file.", ex.what())); + } + } + LOG(LWARNING, ("ProductsConfig file not found.")); +} + +ProductsSettings & ProductsSettings::Instance() +{ + static ProductsSettings instance; + return instance; +} + +std::optional ProductsSettings::Get() +{ + std::lock_guard guard(m_mutex); + return m_productsConfig; +} + +void ProductsSettings::Update(std::string const & jsonStr) +{ + std::lock_guard guard(m_mutex); + if (jsonStr.empty()) + FileWriter::DeleteFileX(GetProductsFilePath()); + else + { + try + { + FileWriter file(GetProductsFilePath()); + file.Write(jsonStr.data(), jsonStr.size()); + m_productsConfig = ProductsConfig::Parse(jsonStr); + } + catch (std::exception const & ex) + { + LOG(LERROR, ("Error writing ProductsConfig file.", ex.what())); + } + } +} + +std::optional ProductsConfig::Parse(std::string const & jsonStr) +{ + const base::Json root(jsonStr.c_str()); + auto const json = root.get(); + auto const productsObj = json_object_get(json, kProducts); + if (!json_is_object(json) || !productsObj || !json_is_array(productsObj)) + { + LOG(LWARNING, ("Failed to parse ProductsConfig:", jsonStr)); + return std::nullopt; + } + + ProductsConfig config; + auto const placePagePrompt = json_object_get(json, kPlacePagePrompt); + if (placePagePrompt && json_is_string(placePagePrompt)) + config.m_placePagePrompt = json_string_value(placePagePrompt); + + for (size_t i = 0; i < json_array_size(productsObj); ++i) + { + json_t * product = json_array_get(productsObj, i); + if (!product || !json_is_object(product)) + { + LOG(LWARNING, ("Failed to parse Product:", jsonStr)); + continue; + } + json_t * title = json_object_get(product, kProductTitle); + json_t * link = json_object_get(product, kProductLink); + if (title && link && json_is_string(title) && json_is_string(link)) + config.m_products.push_back({json_string_value(title), json_string_value(link)}); + else + LOG(LWARNING, ("Failed to parse Product:", jsonStr)); + } + if (config.m_products.empty()) + { + LOG(LWARNING, ("Products list is empty")); + return std::nullopt; + } + return config; +} + +} // namespace products diff --git a/platform/products.hpp b/platform/products.hpp new file mode 100644 index 0000000000..f7829f25d9 --- /dev/null +++ b/platform/products.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include +#include +#include +#include + +namespace products { + +struct ProductsConfig +{ + struct Product + { + private: + std::string m_title; + std::string m_link; + + public: + Product(std::string const & title, std::string const & link) + : m_title(title), m_link(link) + {} + + std::string const & GetTitle() const { return m_title; } + std::string const & GetLink() const { return m_link; } + }; + +private: + std::string m_placePagePrompt; + std::vector m_products; + +public: + std::string const GetPlacePagePrompt() const { return m_placePagePrompt; } + std::vector const & GetProducts() const { return m_products; } + bool HasProducts() const { return !m_products.empty(); } + + static std::optional Parse(std::string const & jsonStr); +}; + +class ProductsSettings +{ +private: + ProductsSettings(); + + std::optional m_productsConfig; + mutable std::mutex m_mutex; + +public: + static ProductsSettings & Instance(); + + void Update(std::string const & jsonStr); + std::optional Get(); +}; + +inline void Update(std::string const & jsonStr) +{ + ProductsSettings::Instance().Update(jsonStr); +} + +inline std::optional GetProductsConfiguration() +{ + return ProductsSettings::Instance().Get(); +} + +} // namespace products diff --git a/platform/qt_location_service.hpp b/platform/qt_location_service.hpp index 66de652ea1..fc534963f5 100644 --- a/platform/qt_location_service.hpp +++ b/platform/qt_location_service.hpp @@ -13,7 +13,7 @@ class QtLocationService : public QObject, public location::LocationService // https://www.freedesktop.org/software/geoclue/docs // /gdbus-org.freedesktop.GeoClue2.Client.html#gdbus-property-org-freedesktop-GeoClue2-Client.Active // But `QGeoPositionInfoSource` doesn't expose that so we have to deduce its state. - bool m_clientIsActive; + bool m_clientIsActive = false; public: explicit QtLocationService(location::LocationObserver &, std::string const &); diff --git a/platform/servers_list.cpp b/platform/servers_list.cpp index 03bf6a2037..7650a82261 100644 --- a/platform/servers_list.cpp +++ b/platform/servers_list.cpp @@ -2,6 +2,7 @@ #include "platform/http_request.hpp" #include "platform/platform.hpp" +#include "platform/settings.hpp" #include "base/logging.hpp" #include "base/assert.hpp" @@ -10,8 +11,13 @@ namespace downloader { + std::optional ParseMetaConfig(std::string const & jsonStr) { + char const kSettings[] = "settings"; + char const kServers[] = "servers"; + char const kProductsConfig[] = "productsConfig"; + MetaConfig outMetaConfig; try { @@ -28,17 +34,26 @@ std::optional ParseMetaConfig(std::string const & jsonStr) // } // } - json_t * settings = json_object_get(root.get(), "settings"); + json_t * settings = json_object_get(root.get(), kSettings); const char * key; const json_t * value; json_object_foreach(settings, key, value) { - const char * valueStr = json_string_value(value); - if (key && value) - outMetaConfig.m_settings[key] = valueStr; + if (key == settings::kDonateUrl || key == settings::kNY) + { + const char * valueStr = json_string_value(value); + if (value) + outMetaConfig.m_settings[key] = valueStr; + } } - servers = json_object_get(root.get(), "servers"); + servers = json_object_get(root.get(), kServers); + + auto const productsConfig = json_object_get(root.get(), kProductsConfig); + if (productsConfig) + outMetaConfig.m_productsConfig = json_dumps(productsConfig, JSON_ENCODE_ANY); + else + LOG(LINFO, ("No ProductsConfig in meta configuration")); } else { diff --git a/platform/servers_list.hpp b/platform/servers_list.hpp index c6f605c4df..7887c02e72 100644 --- a/platform/servers_list.hpp +++ b/platform/servers_list.hpp @@ -14,6 +14,7 @@ struct MetaConfig ServersList m_serversList; using SettingsMap = std::map; SettingsMap m_settings; + std::string m_productsConfig; }; std::optional ParseMetaConfig(std::string const & jsonStr); diff --git a/platform/settings.cpp b/platform/settings.cpp index 6d09c99917..15fd389109 100644 --- a/platform/settings.cpp +++ b/platform/settings.cpp @@ -20,8 +20,12 @@ namespace settings { using namespace std; -char const * kMeasurementUnits = "Units"; -char const * kDeveloperMode = "DeveloperMode"; +std::string_view kMeasurementUnits = "Units"; +std::string_view kMapLanguageCode = "MapLanguageCode"; +std::string_view kDeveloperMode = "DeveloperMode"; +std::string_view kNightMode = "NightMode"; +std::string_view kDonateUrl = "DonateUrl"; +std::string_view kNY = "NY"; StringStorage::StringStorage() : StringStorageBase(GetPlatform().SettingsPathForFile(SETTINGS_FILE_NAME)) {} @@ -424,18 +428,16 @@ void UsageStats::EnterBackground() m_ss.SetValue(m_sessions, ToString(m_sessionsCount)); } -} // namespace settings - -/* -namespace marketing +bool UsageStats::IsLoyalUser() const { -Settings::Settings() : platform::StringStorageBase(GetPlatform().SettingsPathForFile(MARKETING_SETTINGS_FILE_NAME)) {} - -// static -Settings & Settings::Instance() -{ - static Settings instance; - return instance; + #ifdef DEBUG + uint32_t constexpr kMinTotalForegroundTimeout = 30; + uint32_t constexpr kMinSessionsCount = 3; + #else + uint32_t constexpr kMinTotalForegroundTimeout = 60 * 30; // 30 min + uint32_t constexpr kMinSessionsCount = 5; + #endif + return m_sessionsCount >= kMinSessionsCount && m_totalForegroundTime >= kMinTotalForegroundTimeout; } -} // namespace marketing -*/ + +} // namespace settings diff --git a/platform/settings.hpp b/platform/settings.hpp index 9f9b9cd44c..fd45e2b7f8 100644 --- a/platform/settings.hpp +++ b/platform/settings.hpp @@ -9,9 +9,13 @@ namespace settings { /// Metric or Imperial. -extern char const * kMeasurementUnits; - -extern char const * kDeveloperMode; +extern std::string_view kMeasurementUnits; +extern std::string_view kDeveloperMode; +extern std::string_view kMapLanguageCode; +extern std::string_view kNightMode; +// The following two settings are configured externally at the metaserver. +extern std::string_view kDonateUrl; +extern std::string_view kNY; template bool FromString(std::string const & str, T & outValue); @@ -31,22 +35,22 @@ private: /// Retrieve setting /// @return false if setting is absent template -[[nodiscard]] bool Get(std::string const & key, Value & outValue) +[[nodiscard]] bool Get(std::string_view key, Value & outValue) { std::string strVal; return StringStorage::Instance().GetValue(key, strVal) && FromString(strVal, outValue); } template -void TryGet(std::string const & key, Value & outValue) +void TryGet(std::string_view key, Value & outValue) { - bool unused = Get(key, outValue); - UNUSED_VALUE(unused); + bool unused = Get(key, outValue); + UNUSED_VALUE(unused); } /// Automatically saves setting to external file template -void Set(std::string const & key, Value const & value) +void Set(std::string_view key, Value const & value) { StringStorage::Instance().SetValue(key, ToString(value)); } @@ -57,7 +61,7 @@ inline void Update(std::map const & settings) StringStorage::Instance().Update(settings); } -inline void Delete(std::string const & key) { StringStorage::Instance().DeleteKeyAndValue(key); } +inline void Delete(std::string_view key) { StringStorage::Instance().DeleteKeyAndValue(key); } inline void Clear() { StringStorage::Instance().Clear(); } class UsageStats @@ -67,7 +71,7 @@ class UsageStats uint64_t m_totalForegroundTime = 0; uint64_t m_sessionsCount = 0; - std::string m_firstLaunch, m_lastBackground, m_totalForeground, m_sessions; + std::string_view m_firstLaunch, m_lastBackground, m_totalForeground, m_sessions; StringStorage & m_ss; @@ -76,32 +80,8 @@ public: void EnterForeground(); void EnterBackground(); + + bool IsLoyalUser() const; }; } // namespace settings - -/* -namespace marketing -{ -class Settings : public platform::StringStorageBase -{ -public: - template - static void Set(std::string const & key, Value const & value) - { - Instance().SetValue(key, settings::ToString(value)); - } - - template - [[nodiscard]] static bool Get(std::string const & key, Value & outValue) - { - std::string strVal; - return Instance().GetValue(key, strVal) && settings::FromString(strVal, outValue); - } - -private: - static Settings & Instance(); - Settings(); -}; -} // namespace marketing -*/ diff --git a/platform/string_storage_base.cpp b/platform/string_storage_base.cpp index b8e9c793db..b8951e9baf 100644 --- a/platform/string_storage_base.cpp +++ b/platform/string_storage_base.cpp @@ -35,10 +35,13 @@ StringStorageBase::StringStorageBase(std::string const & path) : m_path(path) if (delimPos == std::string::npos) continue; - std::string const key = line.substr(0, delimPos); - std::string const value = line.substr(delimPos + 1); + std::string key = line.substr(0, delimPos); + std::string value = line.substr(delimPos + 1); if (!key.empty() && !value.empty()) - m_values[key] = value; + { + LOG(LINFO, (key, ":", value)); + VERIFY(m_values.emplace(std::move(key), std::move(value)).second, ()); + } } } catch (RootException const & ex) @@ -75,7 +78,7 @@ void StringStorageBase::Clear() Save(); } -bool StringStorageBase::GetValue(std::string const & key, std::string & outValue) const +bool StringStorageBase::GetValue(std::string_view key, std::string & outValue) const { std::lock_guard guard(m_mutex); @@ -87,11 +90,12 @@ bool StringStorageBase::GetValue(std::string const & key, std::string & outValue return true; } -void StringStorageBase::SetValue(std::string const & key, std::string && value) +void StringStorageBase::SetValue(std::string_view key, std::string && value) { std::lock_guard guard(m_mutex); - m_values[key] = std::move(value); + base::EmplaceOrAssign(m_values, key, std::move(value)); + Save(); } @@ -110,7 +114,7 @@ void StringStorageBase::Update(std::map const & values Save(); } -void StringStorageBase::DeleteKeyAndValue(std::string const & key) +void StringStorageBase::DeleteKeyAndValue(std::string_view key) { std::lock_guard guard(m_mutex); diff --git a/platform/string_storage_base.hpp b/platform/string_storage_base.hpp index f0c5153b06..32818d6ea8 100644 --- a/platform/string_storage_base.hpp +++ b/platform/string_storage_base.hpp @@ -12,13 +12,13 @@ public: explicit StringStorageBase(std::string const & path); void Save() const; void Clear(); - bool GetValue(std::string const & key, std::string & outValue) const; - void SetValue(std::string const & key, std::string && value); + bool GetValue(std::string_view key, std::string & outValue) const; + void SetValue(std::string_view key, std::string && value); void Update(std::map const & values); - void DeleteKeyAndValue(std::string const & key); - + void DeleteKeyAndValue(std::string_view key); + private: - using Container = std::map; + using Container = std::map>; Container m_values; mutable std::mutex m_mutex; std::string const m_path; diff --git a/platform/style_utils.hpp b/platform/style_utils.hpp new file mode 100644 index 0000000000..5e845e50a2 --- /dev/null +++ b/platform/style_utils.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace style_utils +{ +enum class NightMode : uint8_t +{ + Off = 0, + On = 1, +}; + +} // namespace style_utils diff --git a/platform/trace.hpp b/platform/trace.hpp new file mode 100644 index 0000000000..a94d59a380 --- /dev/null +++ b/platform/trace.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include "base/macros.hpp" + +#include +#include + +namespace platform +{ +class TraceImpl; + +// API is inspired by https://developer.android.com/ndk/reference/group/tracing +class Trace +{ +public: + static Trace & Instance() noexcept; + + void BeginSection(char const * name) noexcept; + void EndSection() noexcept; + void SetCounter(char const * name, int64_t value) noexcept; + +private: + Trace(); + ~Trace(); + + std::unique_ptr m_impl; + + DISALLOW_COPY_AND_MOVE(Trace); +}; + +class TraceSection +{ +public: + inline TraceSection(char const * section) noexcept + { + Trace::Instance().BeginSection(section); + } + + inline ~TraceSection() noexcept + { + Trace::Instance().EndSection(); + } +}; +} // namespace platform + +#if defined(ENABLE_TRACE) && defined(OMIM_OS_ANDROID) +#define TRACE_SECTION(section) platform::TraceSection ___section(section) +#define TRACE_COUNTER(name, value) platform::Trace::Instance().SetCounter(name, value) +#else +#define TRACE_SECTION(section) static_cast(0) +#define TRACE_COUNTER(name, value) static_cast(0) +#endif diff --git a/platform/trace_android.cpp b/platform/trace_android.cpp new file mode 100644 index 0000000000..6d1b9f6b1e --- /dev/null +++ b/platform/trace_android.cpp @@ -0,0 +1,88 @@ +#include "platform/trace.hpp" + +#include +#include + +namespace platform +{ +// Source: https://developer.android.com/topic/performance/tracing/custom-events-native +typedef void *(*ATrace_beginSection) (char const *); +typedef void *(*ATrace_endSection) (void); +typedef void *(*ATrace_setCounter) (char const *, int64_t); + +class TraceImpl +{ +public: + TraceImpl() + { + m_lib = dlopen("libandroid.so", RTLD_NOW | RTLD_LOCAL); + + // Access the native tracing functions. + if (m_lib != nullptr) + { + // Use dlsym() to prevent crashes on devices running Android 5.1 + // (API level 22) or lower. + m_beginSection = reinterpret_cast(dlsym(m_lib, "ATrace_beginSection")); + m_endSection = reinterpret_cast(dlsym(m_lib, "ATrace_endSection")); + m_setCounter = reinterpret_cast(dlsym(m_lib, "ATrace_setCounter")); + } + } + + ~TraceImpl() + { + if (m_lib != nullptr) + dlclose(m_lib); + } + + void BeginSection(char const * name) noexcept + { + if (m_beginSection != nullptr) + m_beginSection(name); + } + + void EndSection() noexcept + { + if (m_endSection != nullptr) + m_endSection(); + } + + void SetCounter(char const * name, int64_t value) noexcept + { + if (m_setCounter != nullptr) + m_setCounter(name, value); + } + +private: + void * m_lib = nullptr; + ATrace_beginSection m_beginSection = nullptr; + ATrace_endSection m_endSection = nullptr; + ATrace_setCounter m_setCounter = nullptr; +}; + +// static +Trace & Trace::Instance() noexcept { + static Trace instance; + return instance; +} + +Trace::Trace() + : m_impl(std::make_unique()) +{} + +Trace::~Trace() = default; + +void Trace::BeginSection(char const * name) noexcept +{ + m_impl->BeginSection(name); +} + +void Trace::EndSection() noexcept +{ + m_impl->EndSection(); +} + +void Trace::SetCounter(char const * name, int64_t value) noexcept +{ + m_impl->SetCounter(name, value); +} +} // namespace platform diff --git a/private_default.h b/private.h similarity index 62% rename from private_default.h rename to private.h index fe417a1251..d5c7fba067 100644 --- a/private_default.h +++ b/private.h @@ -9,8 +9,9 @@ #define MWM_GEOLOCATION_SERVER "" #define METASERVER_URL "https://meta.omaps.app/maps" #define DIFF_LIST_URL "" -#define DEFAULT_URLS_JSON "[ \"https://cdn.organicmaps.app/\" ]" -#define DEFAULT_CONNECTION_CHECK_IP "140.82.121.4" // For now the IP of cdn.organicmaps.app +#define DEFAULT_URLS_JSON "[ \"https://cdn-de1.organicmaps.app/\",\"https://cdn-us3.organicmaps.app/\",\"https://cdn-nl1.organicmaps.app/\",\"https://cdn-uk1.organicmaps.app/\",\"https://cdn-fi1.organicmaps.app/\",\"https://cdn.organicmaps.app/\" ]" +#define DEFAULT_CONNECTION_CHECK_IP "65.108.198.117" // For now the IP of cdn.organicmaps.app #define TRAFFIC_DATA_BASE_URL "" #define USER_BINDING_PKCS12 "" #define USER_BINDING_PKCS12_PASSWORD "" +#define KAYAK_AFFILIATE_ID "kan_267335" diff --git a/qt/CMakeLists.txt b/qt/CMakeLists.txt index 8513577266..2dc9c6eef0 100644 --- a/qt/CMakeLists.txt +++ b/qt/CMakeLists.txt @@ -239,7 +239,6 @@ if (BUILD_DESIGNER) foreach(BUNDLE ${BUNDLE_FOLDER} ${BUNDLES}) execute_process( COMMAND \"${QT_PATH}/bin/macdeployqt\" \"\${BUNDLE}\" - COMMAND find \"\${BUNDLE}/Contents/MacOS\" -type f -exec python \"${OMIM_ROOT}/tools/macdeployqtfix/macdeployqtfix.py\" -q -nl {} \"${QT_PATH}\" \\; ) endforeach() foreach(BUNDLE ${BUNDLES}) diff --git a/qt/draw_widget.cpp b/qt/draw_widget.cpp index 6ed66b365e..1991e8288f 100644 --- a/qt/draw_widget.cpp +++ b/qt/draw_widget.cpp @@ -78,6 +78,20 @@ void DrawMwmBorder(df::DrapeApi & drapeApi, std::string const & mwmName, kColorCounter = (kColorCounter + 1) % colorList.size(); } } + +#if defined(OMIM_OS_LINUX) +df::TouchEvent::ETouchType qtTouchEventTypeToDfTouchEventType(QEvent::Type qEventType) +{ + switch (qEventType) + { + case QEvent::TouchBegin: return df::TouchEvent::TOUCH_DOWN; + case QEvent::TouchEnd: return df::TouchEvent::TOUCH_UP; + case QEvent::TouchUpdate: return df::TouchEvent::TOUCH_MOVE; + case QEvent::TouchCancel: return df::TouchEvent::TOUCH_CANCEL; + default: return df::TouchEvent::TOUCH_NONE; + } +} +#endif } // namespace DrawWidget::DrawWidget(Framework & framework, std::unique_ptr && screenshotParams, @@ -147,10 +161,7 @@ void DrawWidget::PrepareShutdown() routingManager.SaveRoutePoints(); auto style = m_framework.GetMapStyle(); - if (style == MapStyle::MapStyleVehicleLight) - m_framework.MarkMapStyle(MapStyle::MapStyleDefaultLight); - else if (style == MapStyle::MapStyleVehicleDark) - m_framework.MarkMapStyle(MapStyle::MapStyleDefaultDark); + m_framework.MarkMapStyle(MapStyleIsDark(style) ? MapStyle::MapStyleDefaultDark : MapStyle::MapStyleDefaultLight); } } @@ -195,6 +206,56 @@ void DrawWidget::initializeGL() m_screenshoter->Start(); } +bool DrawWidget::event(QEvent * event) +{ +#if !defined(OMIM_OS_LINUX) + return QOpenGLWidget::event(event); +#else + // TouchScreen + if (auto dfTouchEventType = qtTouchEventTypeToDfTouchEventType(event->type()); + dfTouchEventType != df::TouchEvent::TOUCH_NONE) + { + event->accept(); + QTouchEvent const * qtTouchEvent = dynamic_cast(event); + df::TouchEvent dfTouchEvent; + // The SetTouchType hast to be set even if `qtTouchEvent->points()` is empty + // which theoretically can happen in case of `QEvent::TouchCancel` + dfTouchEvent.SetTouchType(dfTouchEventType); + + int64_t i = 0; + for (auto it = qtTouchEvent->points().cbegin(); + it != qtTouchEvent->points().cend() && i < 2; /* For now drape_frontend can only handle max 2 touches */ + ++it, ++i) + { + df::Touch touch; + touch.m_id = i; + touch.m_location = m2::PointD(L2D(it->position().x()), L2D(it->position().y())); + if (i == 0) + dfTouchEvent.SetFirstTouch(touch); + else + dfTouchEvent.SetSecondTouch(touch); + } + m_framework.TouchEvent(dfTouchEvent); + return true; + } + // TouchPad + else if (event->type() == QEvent::NativeGesture) + { + event->accept(); + auto qNativeGestureEvent = dynamic_cast(event); + if (qNativeGestureEvent->gestureType() == Qt::ZoomNativeGesture) + { + QPointF const pos = qNativeGestureEvent->position(); + double const factor = qNativeGestureEvent->value(); + m_framework.Scale(exp(factor), m2::PointD(L2D(pos.x()), L2D(pos.y())), false); + return true; + } + } + // Everything else + return QOpenGLWidget::event(event); +#endif +} + void DrawWidget::mousePressEvent(QMouseEvent * e) { if (m_screenshotMode) @@ -213,7 +274,7 @@ void DrawWidget::mousePressEvent(QMouseEvent * e) else if (IsAltModifier(e)) SubmitFakeLocationPoint(pt); else - m_framework.TouchEvent(GetTouchEvent(e, df::TouchEvent::TOUCH_DOWN)); + m_framework.TouchEvent(GetDfTouchEventFromQMouseEvent(e, df::TouchEvent::TOUCH_DOWN)); } else if (IsRightButton(e)) { @@ -245,7 +306,7 @@ void DrawWidget::mouseMoveEvent(QMouseEvent * e) if (IsLeftButton(e) && !IsAltModifier(e)) { - m_framework.TouchEvent(GetTouchEvent(e, df::TouchEvent::TOUCH_MOVE)); + m_framework.TouchEvent(GetDfTouchEventFromQMouseEvent(e, df::TouchEvent::TOUCH_MOVE)); e->accept(); } @@ -318,7 +379,7 @@ void DrawWidget::mouseReleaseEvent(QMouseEvent * e) QOpenGLWidget::mouseReleaseEvent(e); if (IsLeftButton(e) && !IsAltModifier(e)) { - m_framework.TouchEvent(GetTouchEvent(e, df::TouchEvent::TOUCH_UP)); + m_framework.TouchEvent(GetDfTouchEventFromQMouseEvent(e, df::TouchEvent::TOUCH_UP)); } else if (m_selectionMode && IsRightButton(e) && m_rubberBand != nullptr && m_rubberBand->isVisible()) @@ -592,11 +653,7 @@ void DrawWidget::FollowRoute() if (routingManager.IsRoutingActive() && !routingManager.IsRoutingFollowing()) { routingManager.FollowRoute(); - auto style = m_framework.GetMapStyle(); - if (style == MapStyle::MapStyleDefaultLight) - SetMapStyle(MapStyle::MapStyleVehicleLight); - else if (style == MapStyle::MapStyleDefaultDark) - SetMapStyle(MapStyle::MapStyleVehicleDark); + SetMapStyleToVehicle(); } } @@ -608,13 +665,7 @@ void DrawWidget::ClearRoute() routingManager.CloseRouting(true /* remove route points */); if (wasActive) - { - auto style = m_framework.GetMapStyle(); - if (style == MapStyle::MapStyleVehicleLight) - SetMapStyle(MapStyle::MapStyleDefaultLight); - else if (style == MapStyle::MapStyleVehicleDark) - SetMapStyle(MapStyle::MapStyleDefaultDark); - } + SetMapStyleToDefault(); m_turnsVisualizer.ClearTurns(m_framework.GetDrapeApi()); } @@ -717,6 +768,24 @@ void DrawWidget::RefreshDrawingRules() SetMapStyle(MapStyleDefaultLight); } +void DrawWidget::SetMapStyleToDefault() +{ + auto const style = m_framework.GetMapStyle(); + SetMapStyle(MapStyleIsDark(style) ? MapStyle::MapStyleDefaultDark : MapStyle::MapStyleDefaultLight); +} + +void DrawWidget::SetMapStyleToVehicle() +{ + auto const style = m_framework.GetMapStyle(); + SetMapStyle(MapStyleIsDark(style) ? MapStyle::MapStyleVehicleDark : MapStyle::MapStyleVehicleLight); +} + +void DrawWidget::SetMapStyleToOutdoors() +{ + auto const style = m_framework.GetMapStyle(); + SetMapStyle(MapStyleIsDark(style) ? MapStyle::MapStyleOutdoorsDark : MapStyle::MapStyleOutdoorsLight); +} + m2::PointD DrawWidget::P2G(m2::PointD const & pt) const { return m_framework.P3dtoG(pt); diff --git a/qt/draw_widget.hpp b/qt/draw_widget.hpp index 63828d5564..05790e64b8 100644 --- a/qt/draw_widget.hpp +++ b/qt/draw_widget.hpp @@ -69,12 +69,19 @@ public: void OnRouteRecommendation(RoutingManager::Recommendation recommendation); void RefreshDrawingRules(); + void SetMapStyleToDefault(); + void SetMapStyleToVehicle(); + void SetMapStyleToOutdoors(); protected: /// @name Overriden from MapWidget. //@{ void initializeGL() override; + // Touch events + bool event(QEvent * event) override; + + // Non-touch events void mousePressEvent(QMouseEvent * e) override; void mouseMoveEvent(QMouseEvent * e) override; void mouseReleaseEvent(QMouseEvent * e) override; diff --git a/qt/mainwindow.cpp b/qt/mainwindow.cpp index aab30b4968..02e31905b2 100644 --- a/qt/mainwindow.cpp +++ b/qt/mainwindow.cpp @@ -121,10 +121,6 @@ MainWindow::MainWindow(Framework & framework, int const height = m_screenshotMode ? static_cast(screenshotParams->m_height) : 0; m_pDrawWidget = new DrawWidget(framework, std::move(screenshotParams), this); - QList gestures; - gestures << Qt::PinchGesture; - m_pDrawWidget->grabGestures(gestures); - setCentralWidget(m_pDrawWidget); if (m_screenshotMode) @@ -286,9 +282,10 @@ void MainWindow::CreateNavigationBar() m_layers = new PopupMenuHolder(this); - m_layers->addAction(QIcon(":/navig64/traffic.png"), tr("Traffic"), - std::bind(&MainWindow::OnLayerEnabled, this, LayerType::TRAFFIC), true); - m_layers->setChecked(LayerType::TRAFFIC, m_pDrawWidget->GetFramework().LoadTrafficEnabled()); + /// @todo Uncomment when we will integrate a traffic provider. + // m_layers->addAction(QIcon(":/navig64/traffic.png"), tr("Traffic"), + // std::bind(&MainWindow::OnLayerEnabled, this, LayerType::TRAFFIC), true); + // m_layers->setChecked(LayerType::TRAFFIC, m_pDrawWidget->GetFramework().LoadTrafficEnabled()); m_layers->addAction(QIcon(":/navig64/subway.png"), tr("Public transport"), std::bind(&MainWindow::OnLayerEnabled, this, LayerType::TRANSIT), true); @@ -298,6 +295,10 @@ void MainWindow::CreateNavigationBar() std::bind(&MainWindow::OnLayerEnabled, this, LayerType::ISOLINES), true); m_layers->setChecked(LayerType::ISOLINES, m_pDrawWidget->GetFramework().LoadIsolinesEnabled()); + m_layers->addAction(QIcon(":/navig64/isolines.png"), tr("Outdoors"), + std::bind(&MainWindow::OnLayerEnabled, this, LayerType::OUTDOORS), true); + m_layers->setChecked(LayerType::OUTDOORS, m_pDrawWidget->GetFramework().LoadOutdoorsEnabled()); + pToolBar->addWidget(m_layers->create()); m_layers->setMainIcon(QIcon(":/navig64/layers.png")); @@ -875,11 +876,11 @@ void MainWindow::SetLayerEnabled(LayerType type, bool enable) auto & frm = m_pDrawWidget->GetFramework(); switch (type) { - case LayerType::TRAFFIC: - /// @todo Uncomment when we will integrate a traffic provider. - // frm.GetTrafficManager().SetEnabled(enable); - // frm.SaveTrafficEnabled(enable); - break; + // @todo Uncomment when we will integrate a traffic provider. + // case LayerType::TRAFFIC: + // frm.GetTrafficManager().SetEnabled(enable); + // frm.SaveTrafficEnabled(enable); + // break; case LayerType::TRANSIT: frm.GetTransitManager().EnableTransitSchemeMode(enable); frm.SaveTransitSchemeEnabled(enable); @@ -888,24 +889,19 @@ void MainWindow::SetLayerEnabled(LayerType type, bool enable) frm.GetIsolinesManager().SetEnabled(enable); frm.SaveIsolinesEnabled(enable); break; - default: - UNREACHABLE(); + case LayerType::OUTDOORS: + frm.SaveOutdoorsEnabled(enable); + if (enable) + m_pDrawWidget->SetMapStyleToOutdoors(); + else + m_pDrawWidget->SetMapStyleToDefault(); break; } } void MainWindow::OnLayerEnabled(LayerType layer) { - for (size_t i = 0; i < LayerType::COUNT; ++i) - { - if (i == layer) - SetLayerEnabled(static_cast(i), m_layers->isChecked(i)); - else - { - m_layers->setChecked(i, false); - SetLayerEnabled(static_cast(i), false); - } - } + SetLayerEnabled(layer, m_layers->isChecked(layer)); } void MainWindow::OnRulerEnabled() diff --git a/qt/mainwindow.hpp b/qt/mainwindow.hpp index 88c1f90092..6ea00a5178 100644 --- a/qt/mainwindow.hpp +++ b/qt/mainwindow.hpp @@ -50,12 +50,11 @@ class MainWindow : public QMainWindow, location::LocationObserver enum LayerType : uint8_t { - TRAFFIC = 0, - TRANSIT, // Metro scheme + /// @todo Uncomment when we will integrate a traffic provider. + // TRAFFIC = 0, + TRANSIT = 0, // Metro scheme ISOLINES, - - // Should be the last - COUNT + OUTDOORS, }; PopupMenuHolder * m_layers = nullptr; PopupMenuHolder * m_routing = nullptr; diff --git a/qt/preferences_dialog.cpp b/qt/preferences_dialog.cpp index 09028f2118..acc0fa8276 100644 --- a/qt/preferences_dialog.cpp +++ b/qt/preferences_dialog.cpp @@ -1,14 +1,20 @@ #include "qt/preferences_dialog.hpp" +#include "indexer/map_style.hpp" +#include "coding/string_utf8_multilang.hpp" #include "map/framework.hpp" #include "platform/measurement_utils.hpp" +#include "platform/preferred_languages.hpp" #include "platform/settings.hpp" +#include "platform/style_utils.hpp" #include #include +#include #include #include +#include #include #include #include @@ -103,6 +109,66 @@ namespace qt }); } + QLabel * mapLanguageLabel = new QLabel("Map Language"); + QComboBox * mapLanguageComboBox = new QComboBox(); + { + // The property maxVisibleItems is ignored for non-editable comboboxes in styles that + // return true for `QStyle::SH_ComboBox_Popup such as the Mac style or the Gtk+ Style. + // So we ensure that it returns false here. + mapLanguageComboBox->setStyleSheet("QComboBox { combobox-popup: 0; }"); + mapLanguageComboBox->setMaxVisibleItems(10); + StringUtf8Multilang::Languages const & supportedLanguages = StringUtf8Multilang::GetSupportedLanguages(/* includeServiceLangs */ false); + QStringList languagesList = QStringList(); + for (auto const & language : supportedLanguages) + languagesList << QString::fromStdString(std::string(language.m_name)); + + mapLanguageComboBox->addItems(languagesList); + std::string const & mapLanguageCode = framework.GetMapLanguageCode(); + int8_t languageIndex = StringUtf8Multilang::GetLangIndex(mapLanguageCode); + if (languageIndex == StringUtf8Multilang::kUnsupportedLanguageCode) + languageIndex = StringUtf8Multilang::kDefaultCode; + + mapLanguageComboBox->setCurrentText(QString::fromStdString(std::string(StringUtf8Multilang::GetLangNameByCode(languageIndex)))); + connect(mapLanguageComboBox, &QComboBox::activated, [&framework, &supportedLanguages](int index) + { + auto const & mapLanguageCode = std::string(supportedLanguages[index].m_code); + framework.SetMapLanguageCode(mapLanguageCode); + }); + } + + QButtonGroup * nightModeGroup = new QButtonGroup(this); + QGroupBox * nightModeRadioBox = new QGroupBox("Night Mode"); + { + using namespace style_utils; + QHBoxLayout * layout = new QHBoxLayout(); + + QRadioButton * radioButton = new QRadioButton("Off"); + layout->addWidget(radioButton); + nightModeGroup->addButton(radioButton, static_cast(NightMode::Off)); + + radioButton = new QRadioButton("On"); + layout->addWidget(radioButton); + nightModeGroup->addButton(radioButton, static_cast(NightMode::On)); + + nightModeRadioBox->setLayout(layout); + + int i; + if (!settings::Get(settings::kNightMode, i)) + { + i = static_cast(MapStyleIsDark(framework.GetMapStyle()) ? NightMode::On : NightMode::Off); + settings::Set(settings::kNightMode, i); + } + nightModeGroup->button(i)->setChecked(true); + + void (QButtonGroup::* buttonClicked)(int) = &QButtonGroup::idClicked; + connect(nightModeGroup, buttonClicked, [&framework](int i) + { + NightMode nightMode = static_cast(i); + settings::Set(settings::kNightMode, i); + framework.SetMapStyle((nightMode == NightMode::Off) ? GetLightMapStyleVariant(framework.GetMapStyle()) : GetDarkMapStyleVariant(framework.GetMapStyle())); + }); + } + #ifdef BUILD_DESIGNER QCheckBox * indexRegenCheckBox = new QCheckBox("Enable auto regeneration of geometry index"); { @@ -134,6 +200,9 @@ namespace qt finalLayout->addWidget(largeFontCheckBox); finalLayout->addWidget(transliterationCheckBox); finalLayout->addWidget(developerModeCheckBox); + finalLayout->addWidget(mapLanguageLabel); + finalLayout->addWidget(mapLanguageComboBox); + finalLayout->addWidget(nightModeRadioBox); #ifdef BUILD_DESIGNER finalLayout->addWidget(indexRegenCheckBox); #endif diff --git a/qt/qt_common/map_widget.cpp b/qt/qt_common/map_widget.cpp index 59102ca1ea..07da1e31f3 100644 --- a/qt/qt_common/map_widget.cpp +++ b/qt/qt_common/map_widget.cpp @@ -15,10 +15,10 @@ #include #include -#include #include #include #include +#include #include #include @@ -50,6 +50,7 @@ MapWidget::MapWidget(Framework & framework, bool isScreenshotMode, QWidget * par VERIFY(connect(m_updateTimer.get(), SIGNAL(timeout()), this, SLOT(update())), ()); m_updateTimer->setSingleShot(false); m_updateTimer->start(1000 / 60); + setAttribute(Qt::WA_AcceptTouchEvents); } MapWidget::~MapWidget() @@ -184,7 +185,7 @@ m2::PointD MapWidget::GetDevicePoint(QMouseEvent * e) const return m2::PointD(L2D(e->position().x()), L2D(e->position().y())); } -df::Touch MapWidget::GetTouch(QMouseEvent * e) const +df::Touch MapWidget::GetDfTouchFromQMouseEvent(QMouseEvent * e) const { df::Touch touch; touch.m_id = 0; @@ -192,11 +193,11 @@ df::Touch MapWidget::GetTouch(QMouseEvent * e) const return touch; } -df::TouchEvent MapWidget::GetTouchEvent(QMouseEvent * e, df::TouchEvent::ETouchType type) const +df::TouchEvent MapWidget::GetDfTouchEventFromQMouseEvent(QMouseEvent * e, df::TouchEvent::ETouchType type) const { df::TouchEvent event; event.SetTouchType(type); - event.SetFirstTouch(GetTouch(e)); + event.SetFirstTouch(GetDfTouchFromQMouseEvent(e)); if (IsCommandModifier(e)) event.SetSecondTouch(GetSymmetrical(event.GetFirstTouch())); @@ -266,6 +267,11 @@ void MapWidget::Build() QVector4D(-1.0, -1.0, 0.0, 0.0), QVector4D(1.0, -1.0, 1.0, 0.0)}; m_vbo->allocate(static_cast(vertices), sizeof(vertices)); + QOpenGLFunctions *f = QOpenGLContext::currentContext()->functions(); + // 0-index of the buffer is linked to "a_position" attribute in vertex shader. + // Introduced in https://github.com/organicmaps/organicmaps/pull/9814 + f->glEnableVertexAttribArray(0); + f->glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, sizeof(QVector4D), nullptr); m_program->release(); m_vao->release(); @@ -419,9 +425,6 @@ void MapWidget::paintGL() int const samplerSizeLocation = m_program->uniformLocation("u_samplerSize"); m_program->setUniformValue(samplerSizeLocation, samplerSize); - m_program->enableAttributeArray("a_position"); - m_program->setAttributeBuffer("a_position", GL_FLOAT, 0, 4, 0); - funcs->glClearColor(0.0, 0.0, 0.0, 1.0); funcs->glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); @@ -468,7 +471,7 @@ void MapWidget::mousePressEvent(QMouseEvent * e) QOpenGLWidget::mousePressEvent(e); if (IsLeftButton(e)) - m_framework.TouchEvent(GetTouchEvent(e, df::TouchEvent::TOUCH_DOWN)); + m_framework.TouchEvent(GetDfTouchEventFromQMouseEvent(e, df::TouchEvent::TOUCH_DOWN)); } void MapWidget::mouseMoveEvent(QMouseEvent * e) @@ -478,7 +481,7 @@ void MapWidget::mouseMoveEvent(QMouseEvent * e) QOpenGLWidget::mouseMoveEvent(e); if (IsLeftButton(e)) - m_framework.TouchEvent(GetTouchEvent(e, df::TouchEvent::TOUCH_MOVE)); + m_framework.TouchEvent(GetDfTouchEventFromQMouseEvent(e, df::TouchEvent::TOUCH_MOVE)); } void MapWidget::mouseReleaseEvent(QMouseEvent * e) @@ -491,7 +494,7 @@ void MapWidget::mouseReleaseEvent(QMouseEvent * e) QOpenGLWidget::mouseReleaseEvent(e); if (IsLeftButton(e)) - m_framework.TouchEvent(GetTouchEvent(e, df::TouchEvent::TOUCH_UP)); + m_framework.TouchEvent(GetDfTouchEventFromQMouseEvent(e, df::TouchEvent::TOUCH_UP)); } void MapWidget::wheelEvent(QWheelEvent * e) @@ -508,35 +511,4 @@ void MapWidget::wheelEvent(QWheelEvent * e) /// @todo Here you can tune the speed of zooming. m_framework.Scale(exp(factor), m2::PointD(L2D(pos.x()), L2D(pos.y())), false); } - -void MapWidget::grabGestures(QList const & gestures) -{ - for (Qt::GestureType gesture : gestures) - grabGesture(gesture); -} - -bool MapWidget::event(QEvent * event) -{ - if (event->type() == QEvent::Gesture) - return gestureEvent(dynamic_cast(event)); - return QOpenGLWidget::event(event); -} - -bool MapWidget::gestureEvent(QGestureEvent const * event) -{ - if (QGesture const * pinch = event->gesture(Qt::PinchGesture)) - pinchTriggered(dynamic_cast(pinch)); - return true; -} - -void MapWidget::pinchTriggered(QPinchGesture const * gesture) -{ - if (gesture->changeFlags() & QPinchGesture::ScaleFactorChanged) - { - auto const factor = gesture->totalScaleFactor(); - if (factor != 1.0) - m_framework.Scale(factor > 1.0 ? Framework::SCALE_MAG : Framework::SCALE_MIN, true); - } -} - } // namespace qt::common diff --git a/qt/qt_common/map_widget.hpp b/qt/qt_common/map_widget.hpp index 01d4ee22ae..304a51aea0 100644 --- a/qt/qt_common/map_widget.hpp +++ b/qt/qt_common/map_widget.hpp @@ -13,8 +13,6 @@ #include class Framework; -class QGestureEvent; -class QPinchGesture; class QMouseEvent; class QWidget; class ScreenBase; @@ -76,8 +74,8 @@ protected: int L2D(int px) const { return px * m_ratio; } m2::PointD GetDevicePoint(QMouseEvent * e) const; - df::Touch GetTouch(QMouseEvent * e) const; - df::TouchEvent GetTouchEvent(QMouseEvent * e, df::TouchEvent::ETouchType type) const; + df::Touch GetDfTouchFromQMouseEvent(QMouseEvent * e) const; + df::TouchEvent GetDfTouchEventFromQMouseEvent(QMouseEvent * e, df::TouchEvent::ETouchType type) const; df::Touch GetSymmetrical(df::Touch const & touch) const; void UpdateScaleControl(); @@ -91,9 +89,6 @@ protected: void paintGL() override; void resizeGL(int width, int height) override; - bool event(QEvent * event) override; - bool gestureEvent(QGestureEvent const * event); - void pinchTriggered(QPinchGesture const * gesture); void mouseDoubleClickEvent(QMouseEvent * e) override; void mousePressEvent(QMouseEvent * e) override; diff --git a/routing/geometry.cpp b/routing/geometry.cpp index 76cae6307f..7cb8aa9e59 100644 --- a/routing/geometry.cpp +++ b/routing/geometry.cpp @@ -53,19 +53,11 @@ class RoadAttrsGetter public: void Load(FilesContainerR const & cont) { - try - { - if (cont.IsExist(CITY_ROADS_FILE_TAG)) - m_cityRoads.Load(cont.GetReader(CITY_ROADS_FILE_TAG)); + if (cont.IsExist(CITY_ROADS_FILE_TAG)) + m_cityRoads.Load(cont.GetReader(CITY_ROADS_FILE_TAG)); - if (cont.IsExist(MAXSPEEDS_FILE_TAG)) - m_maxSpeeds.Load(cont.GetReader(MAXSPEEDS_FILE_TAG)); - } - catch (Reader::Exception const & e) - { - LOG(LERROR, ("File", cont.GetFileName(), "Error while reading", CITY_ROADS_FILE_TAG, "or", - MAXSPEEDS_FILE_TAG, "section.", e.Msg())); - } + if (cont.IsExist(MAXSPEEDS_FILE_TAG)) + m_maxSpeeds.Load(cont.GetReader(MAXSPEEDS_FILE_TAG)); } public: diff --git a/routing/index_graph_loader.cpp b/routing/index_graph_loader.cpp index 79cb2eb7a1..d866a21411 100644 --- a/routing/index_graph_loader.cpp +++ b/routing/index_graph_loader.cpp @@ -133,20 +133,28 @@ IndexGraphLoaderImpl::GraphPtrT IndexGraphLoaderImpl::CreateIndexGraph(NumMwmId MwmSet::MwmHandle const & handle = m_dataSource.GetHandle(numMwmId); MwmValue const * value = handle.GetValue(); - if (!geometry) + try { - auto vehicleModel = m_vehicleModelFactory->GetVehicleModelForCountry(value->GetCountryFileName()); - geometry = make_shared(GeometryLoader::Create(handle, std::move(vehicleModel), m_loadAltitudes)); + base::Timer timer; + + if (!geometry) + { + auto vehicleModel = m_vehicleModelFactory->GetVehicleModelForCountry(value->GetCountryFileName()); + geometry = make_shared(GeometryLoader::Create(handle, std::move(vehicleModel), m_loadAltitudes)); + } + + auto graph = make_unique(geometry, m_estimator, m_avoidRoutingOptions); + graph->SetCurrentTimeGetter(m_currentTimeGetter); + DeserializeIndexGraph(*value, m_vehicleType, *graph); + + LOG(LINFO, (ROUTING_FILE_TAG, "section for", value->GetCountryFileName(), "loaded in", timer.ElapsedSeconds(), "seconds")); + return graph; + } + catch (RootException const & ex) + { + LOG(LERROR, ("Error reading graph for", value->m_file)); + throw; } - - auto graph = make_unique(geometry, m_estimator, m_avoidRoutingOptions); - graph->SetCurrentTimeGetter(m_currentTimeGetter); - - base::Timer timer; - DeserializeIndexGraph(*value, m_vehicleType, *graph); - LOG(LINFO, (ROUTING_FILE_TAG, "section for", value->GetCountryFileName(), "loaded in", timer.ElapsedSeconds(), "seconds")); - - return graph; } IndexGraphLoaderImpl::GeometryPtrT IndexGraphLoaderImpl::CreateGeometry(NumMwmId numMwmId) diff --git a/routing/index_graph_serialization.hpp b/routing/index_graph_serialization.hpp index e51c86231e..04cc65f313 100644 --- a/routing/index_graph_serialization.hpp +++ b/routing/index_graph_serialization.hpp @@ -12,12 +12,9 @@ #include "base/checked_cast.hpp" -#include -#include #include #include #include -#include #include namespace routing @@ -227,7 +224,7 @@ private: if (m_version != kLastVersion) { MYTHROW(CorruptedDataException, - ("Unknown index graph version ", m_version, ", current version ", kLastVersion)); + ("Unknown index graph version ", int(m_version), ", current version ", int(kLastVersion))); } m_numRoads = ReadPrimitiveFromSource(src); diff --git a/routing/routing_helpers.cpp b/routing/routing_helpers.cpp index 22244cd452..78dff21fe5 100644 --- a/routing/routing_helpers.cpp +++ b/routing/routing_helpers.cpp @@ -72,7 +72,7 @@ void ReconstructRoute(DirectionsEngine & engine, IndexRoadGraph const & graph, route.SetGeometry(routeGeometry.begin(), routeGeometry.end()); - LOG(LINFO, (route.DebugPrintTurns())); + LOG(LDEBUG, (route.DebugPrintTurns())); } Segment ConvertEdgeToSegment(NumMwmIds const & numMwmIds, Edge const & edge) diff --git a/routing/routing_integration_tests/bicycle_route_test.cpp b/routing/routing_integration_tests/bicycle_route_test.cpp index 9555f0cb07..fc6c147c31 100644 --- a/routing/routing_integration_tests/bicycle_route_test.cpp +++ b/routing/routing_integration_tests/bicycle_route_test.cpp @@ -305,9 +305,11 @@ UNIT_TEST(IgnoreCycleBarrier_WithoutAccess) UNIT_TEST(AvoidConstruction) { // Will not work when the bridge will be finished. - CalculateRouteAndTestRouteLength(GetVehicleComponents(VehicleType::Bicycle), + TRouteResult const res = CalculateRoute(GetVehicleComponents(VehicleType::Bicycle), mercator::FromLatLon(-27.4724942, 153.030171), {0.0, 0.0}, - mercator::FromLatLon(-27.4706626, 153.035428), 2989.28); + mercator::FromLatLon(-27.4706626, 153.035428)); + TEST_EQUAL(res.second, RouterResultCode::NoError, ()); + TEST_GREATER(res.first->GetTotalDistanceMeters(), 2900, ()); } UNIT_TEST(UK_Canterbury_UseDismount) diff --git a/routing/routing_integration_tests/pedestrian_route_test.cpp b/routing/routing_integration_tests/pedestrian_route_test.cpp index 8970e83d65..0df6ad6a92 100644 --- a/routing/routing_integration_tests/pedestrian_route_test.cpp +++ b/routing/routing_integration_tests/pedestrian_route_test.cpp @@ -123,7 +123,7 @@ UNIT_TEST(HungaryBudapest_AvoidMotorway) integration::CalculateRouteAndTestRouteLength( integration::GetVehicleComponents(VehicleType::Pedestrian), mercator::FromLatLon(47.56566, 19.14942), {0., 0.}, - mercator::FromLatLon(47.593, 19.24018), 10579.2); + mercator::FromLatLon(47.593, 19.24018), 10179.6); } UNIT_TEST(PolandWarshaw_AvoidCycleway) diff --git a/routing/routing_integration_tests/route_test.cpp b/routing/routing_integration_tests/route_test.cpp index 6911b9035f..93c25f4571 100644 --- a/routing/routing_integration_tests/route_test.cpp +++ b/routing/routing_integration_tests/route_test.cpp @@ -856,8 +856,8 @@ UNIT_TEST(Germany_Netherlands_AvoidLoops) TEST(routeResult.first, ()); Route const & route = *routeResult.first; - TestRouteLength(route, 405159); - TestRouteTime(route, 13946); + TestRouteLength(route, 405058); + TestRouteTime(route, 14158); } UNIT_TEST(Germany_Cologne_Croatia_Zagreb) @@ -914,14 +914,18 @@ UNIT_TEST(Russia_Yekaterinburg_NChelny) RoutingOptionSetter optionsGuard(RoutingOptions::Dirty | RoutingOptions::Ferry); // forward CalculateRouteAndTestRouteLength(*components, - start, {0., 0.}, finish, 789014); + start, {0., 0.}, finish, 767702); // backward CalculateRouteAndTestRouteLength(*components, - finish, {0., 0.}, start, 787208); + finish, {0., 0.}, start, 766226); } - // GraphHopper agrees here, OSRM makes a route like above. + // OSRM, GraphHopper uses gravel, Valhalla makes a route like above. + /// @todo Should use tertiary + gravel + villages (46km) here and below instead of primary (86km)? + CalculateRouteAndTestRouteLength(GetVehicleComponents(VehicleType::Car), + FromLatLon(55.9315, 58.202), {0., 0.}, + FromLatLon(55.7555, 57.8348), 45788); // forward CalculateRouteAndTestRouteLength(*components, start, {0., 0.}, finish, 757109); diff --git a/routing/routing_integration_tests/transit_route_test.cpp b/routing/routing_integration_tests/transit_route_test.cpp index ff022d6c00..2961640e90 100644 --- a/routing/routing_integration_tests/transit_route_test.cpp +++ b/routing/routing_integration_tests/transit_route_test.cpp @@ -97,7 +97,7 @@ UNIT_TEST(Transit_Vatikan_NotEnoughGraphDataAtThenEnd) auto const & route = *routeResult.first; integration::CheckSubwayExistence(route); - integration::TestRouteLength(route, 7622.54); + integration::TestRouteLength(route, 7703.56); TEST_LESS(route.GetTotalTimeSec(), 4000, ()); } diff --git a/routing/routing_options.cpp b/routing/routing_options.cpp index c45e2db1a0..11c6e292bd 100644 --- a/routing/routing_options.cpp +++ b/routing/routing_options.cpp @@ -16,7 +16,7 @@ using namespace std; // RoutingOptions ------------------------------------------------------------------------------------- -string const RoutingOptions::kAvoidRoutingOptionSettingsForCar = "avoid_routing_options_car"; +std::string_view constexpr kAvoidRoutingOptionSettingsForCar = "avoid_routing_options_car"; // static RoutingOptions RoutingOptions::LoadCarOptionsFromSettings() diff --git a/routing/routing_options.hpp b/routing/routing_options.hpp index 939d7fff0d..7ae36b522d 100644 --- a/routing/routing_options.hpp +++ b/routing/routing_options.hpp @@ -11,8 +11,6 @@ namespace routing class RoutingOptions { public: - static std::string const kAvoidRoutingOptionSettingsForCar; - enum Road : uint8_t { Usual = 1u << 0, diff --git a/routing/speed_camera_manager.cpp b/routing/speed_camera_manager.cpp index 28d16ab18d..9d772da3a7 100644 --- a/routing/speed_camera_manager.cpp +++ b/routing/speed_camera_manager.cpp @@ -6,7 +6,7 @@ namespace routing { -std::string const SpeedCameraManager::kSpeedCamModeKey = "speed_cam_mode"; +std::string_view constexpr kSpeedCamModeKey = "speed_cam_mode"; SpeedCameraManager::SpeedCameraManager(turns::sound::NotificationManager & notificationManager) : m_notificationManager(notificationManager) diff --git a/routing/speed_camera_manager.hpp b/routing/speed_camera_manager.hpp index bfb1214274..bc1a7b611b 100644 --- a/routing/speed_camera_manager.hpp +++ b/routing/speed_camera_manager.hpp @@ -46,9 +46,7 @@ enum class SpeedCameraManagerMode class SpeedCameraManager { public: - static std::string const kSpeedCamModeKey; - - explicit SpeedCameraManager(turns::sound::NotificationManager & notificationManager); + explicit SpeedCameraManager(turns::sound::NotificationManager & notificationManager); enum class Interval { @@ -150,7 +148,7 @@ private: bool BeepSignalAvailable() const { return m_makeBeepSignal && m_beepSignalCounter < kBeepSignalNumber; } bool VoiceSignalAvailable() const { return m_makeVoiceSignal && m_voiceSignalCounter < kVoiceNotificationNumber; } - + private: SpeedCameraOnRoute m_closestCamera; uint32_t m_beepSignalCounter; diff --git a/routing_common/vehicle_model.hpp b/routing_common/vehicle_model.hpp index 0153e8d316..4394788021 100644 --- a/routing_common/vehicle_model.hpp +++ b/routing_common/vehicle_model.hpp @@ -105,8 +105,9 @@ struct SpeedKMpH bool IsValid() const { return m_weight > 0 && m_eta > 0; } - double m_weight = 0.0; // KMpH - double m_eta = 0.0; // KMpH + double m_weight = 0.0; // KMpH - speed in km/h adjusted for desirability + // cycling on very large road may be fast but speed used for route finding will be treated as much lower + double m_eta = 0.0; // KMpH - actual expected speed in km/h, used to display expected arrival time }; /// \brief Factors which modify weight and ETA speed on feature in case of bad pavement (reduce) diff --git a/search/highlighting.cpp b/search/highlighting.cpp index cd92942c8c..f876b97e13 100644 --- a/search/highlighting.cpp +++ b/search/highlighting.cpp @@ -1,7 +1,7 @@ #include "search/highlighting.hpp" -using namespace std; - +namespace search +{ namespace { // Makes continuous range for tokens and prefix. @@ -42,8 +42,7 @@ public: }; } // namespace -namespace search -{ + void HighlightResult(QueryTokens const & tokens, strings::UniString const & prefix, Result & res) { using Iter = QueryTokens::const_iterator; @@ -51,10 +50,19 @@ void HighlightResult(QueryTokens const & tokens, strings::UniString const & pref CombinedIter beg(tokens.begin(), tokens.end(), prefix.empty() ? nullptr : &prefix); CombinedIter end(tokens.end() /* cur */, tokens.end() /* end */, nullptr); - auto assignHighlightRange = [&](pair const & range) { - res.AddHighlightRange(range); - }; - SearchStringTokensIntersectionRanges(res.GetString(), beg, end, assignHighlightRange); + // Highlight Title + SearchStringTokensIntersectionRanges(res.GetString(), beg, end, + [&](std::pair const & range) + { + res.AddHighlightRange(range); + }); + + // Highlight description. + SearchStringTokensIntersectionRanges(res.GetAddress(), beg, end, + [&](std::pair const & range) + { + res.AddDescHighlightRange(range); + }); } } // namespace search diff --git a/search/intermediate_result.cpp b/search/intermediate_result.cpp index 2c9a7a8219..bd74978fb6 100644 --- a/search/intermediate_result.cpp +++ b/search/intermediate_result.cpp @@ -169,7 +169,7 @@ RankerResult::RankerResult(FeatureType & ft, m2::PointD const & center, m_region.SetParams(fileName, center); - FillDetails(ft, m_details); + FillDetails(ft, m_str, m_details); } RankerResult::RankerResult(FeatureType & ft, std::string const & fileName) @@ -250,7 +250,7 @@ bool RankerResult::RegionInfo::GetCountryId(storage::CountryInfoGetter const & i } // Functions --------------------------------------------------------------------------------------- -void FillDetails(FeatureType & ft, Result::Details & details) +void FillDetails(FeatureType & ft, std::string const & name, Result::Details & details) { if (details.m_isInitialized) return; @@ -261,6 +261,9 @@ void FillDetails(FeatureType & ft, Result::Details & details) if (!brand.empty()) brand = platform::GetLocalizedBrandName(brand); + if (name == brand) + brand.clear(); + /// @todo Avoid temporary string when OpeningHours (boost::spirit) will allow string_view. std::string const openHours(ft.GetMetadata(feature::Metadata::FMD_OPEN_HOURS)); if (!openHours.empty()) diff --git a/search/intermediate_result.hpp b/search/intermediate_result.hpp index d618699933..d68fc4ce63 100644 --- a/search/intermediate_result.hpp +++ b/search/intermediate_result.hpp @@ -192,5 +192,5 @@ private: #endif }; -void FillDetails(FeatureType & ft, Result::Details & meta); +void FillDetails(FeatureType & ft, std::string const & name, Result::Details & details); } // namespace search diff --git a/search/locality_finder.hpp b/search/locality_finder.hpp index c4e5dd46b3..9253518c1f 100644 --- a/search/locality_finder.hpp +++ b/search/locality_finder.hpp @@ -51,7 +51,7 @@ struct LocalityItem return false; feature::NameParamsOut out; - feature::GetReadableName({ m_names, mwmInfo->GetRegionData(), languages::GetCurrentNorm(), + feature::GetReadableName({ m_names, mwmInfo->GetRegionData(), languages::GetCurrentMapLanguage(), false /* allowTranslit */ }, out); name = out.primary; diff --git a/search/query_params.cpp b/search/query_params.cpp index f8938de618..69375f6e4d 100644 --- a/search/query_params.cpp +++ b/search/query_params.cpp @@ -24,9 +24,17 @@ map> const kSynonyms = { {"ne", {"northeast"}}, {"sw", {"southwest"}}, {"se", {"southeast"}}, + + /// @todo Should not duplicate Street synonyms defined in StreetsSynonymsHolder (avoid useless double queries). + /// Remove "street" and "avenue" here, but should update GetNameScore. {"st", {"saint", "street"}}, {"dr", {"doctor"}}, + // widely used in LATAM, like "Ntra Sra Asuncion Zelaya" + {"ntra", {"nuestra"}}, + {"sra", {"senora"}}, + {"sta", {"santa"}}, + {"al", {"allee", "alle"}}, {"ave", {"avenue"}}, /// @todo Should process synonyms with errors like "blvrd" -> "blvd". @@ -203,19 +211,21 @@ void QueryParams::AddSynonyms() { string const ss = ToUtf8(MakeLowerCase(token.GetOriginal())); auto const it = kSynonyms.find(ss); - if (it == kSynonyms.end()) - continue; - - for (auto const & synonym : it->second) - token.AddSynonym(synonym); + if (it != kSynonyms.end()) + { + for (auto const & synonym : it->second) + token.AddSynonym(synonym); + } } if (m_hasPrefix) { string const ss = ToUtf8(MakeLowerCase(m_prefixToken.GetOriginal())); auto const it = kSynonyms.find(ss); if (it != kSynonyms.end()) + { for (auto const & synonym : it->second) m_prefixToken.AddSynonym(synonym); + } } } diff --git a/search/query_saver.cpp b/search/query_saver.cpp index 71d1a0f9a1..e85ea8ff1c 100644 --- a/search/query_saver.cpp +++ b/search/query_saver.cpp @@ -11,16 +11,16 @@ #include "base/logging.hpp" #include "base/string_utils.hpp" -#include -#include #include #include +namespace search +{ using namespace std; namespace { -char constexpr kSettingsKey[] = "UserQueries"; +std::string_view constexpr kSettingsKey = "UserQueries"; using Length = uint16_t; Length constexpr kMaxSuggestionsCount = 50; @@ -69,8 +69,7 @@ private: }; } // namespace -namespace search -{ + QuerySaver::QuerySaver() { Load(); @@ -163,7 +162,7 @@ void QuerySaver::Load() { Deserialize(hexData); } - catch (Reader::SizeException const & /* exception */) + catch (RootException const &) { Clear(); LOG(LWARNING, ("Search history data corrupted! Creating new one.")); diff --git a/search/query_saver.hpp b/search/query_saver.hpp index ae0230300f..9f8900393e 100644 --- a/search/query_saver.hpp +++ b/search/query_saver.hpp @@ -2,8 +2,6 @@ #include #include -#include -#include namespace search { diff --git a/search/ranker.cpp b/search/ranker.cpp index 3d155a87ba..dc2c497b9c 100644 --- a/search/ranker.cpp +++ b/search/ranker.cpp @@ -16,6 +16,8 @@ #include "indexer/road_shields_parser.hpp" #include "indexer/search_string_utils.hpp" +#include "platform/localization.hpp" + #include "coding/string_utf8_multilang.hpp" #include "base/logging.hpp" @@ -471,9 +473,25 @@ private: center = feature::GetCenter(*ft); m_ranker.GetBestMatchName(*ft, name); + // Use brand instead of empty result name. + if (!m_isViewportMode && name.empty()) + { + std::string_view brand = (*ft).GetMetadata(feature::Metadata::FMD_BRAND); + if (!brand.empty()) + name = platform::GetLocalizedBrandName(std::string{ brand }); + } + // Insert exact address (street and house number) instead of empty result name. if (!m_isViewportMode && name.empty()) { + feature::TypesHolder featureTypes(*ft); + featureTypes.SortBySpec(); + auto const bestType = featureTypes.GetBestType(); + auto const addressChecker = ftypes::IsAddressChecker::Instance(); + + if (!addressChecker.IsMatched(bestType)) + return ft; + ReverseGeocoder::Address addr; if (GetExactAddress(*ft, center, addr)) { diff --git a/search/ranking_info.cpp b/search/ranking_info.cpp index fca9d529e0..f8aae65966 100644 --- a/search/ranking_info.cpp +++ b/search/ranking_info.cpp @@ -443,8 +443,16 @@ double RankingInfo::GetErrorsMadePerToken() const NameScore RankingInfo::GetNameScore() const { + // See Pois_Rank test. if (!m_pureCats && Model::IsPoi(m_type) && m_classifType.poi <= PoiType::Attraction) { + // Promote POI's name rank if all tokens were matched with TYPE_SUBPOI/TYPE_COMPLEXPOI only. + for (int i = Model::TYPE_BUILDING; i < Model::TYPE_COUNT; ++i) + { + if (!m_tokenRanges[i].Empty()) + return m_nameScore; + } + // It's better for ranking when POIs would be equal by name score in the next cases: if (m_nameScore == NameScore::FULL_PREFIX) diff --git a/search/ranking_utils.hpp b/search/ranking_utils.hpp index 81adbf0952..069d3f3c4e 100644 --- a/search/ranking_utils.hpp +++ b/search/ranking_utils.hpp @@ -165,7 +165,7 @@ struct NameScores m_errorsMade = ErrorsMade::Min(m_errorsMade, rhs.m_errorsMade); } - bool operator==(NameScores const & rhs) + bool operator==(NameScores const & rhs) const { return m_nameScore == rhs.m_nameScore && m_errorsMade == rhs.m_errorsMade && m_isAltOrOldName == rhs.m_isAltOrOldName && m_matchedLength == rhs.m_matchedLength; diff --git a/search/region_address_getter.cpp b/search/region_address_getter.cpp index 27c905e26e..0107b83459 100644 --- a/search/region_address_getter.cpp +++ b/search/region_address_getter.cpp @@ -11,7 +11,7 @@ RegionAddressGetter::RegionAddressGetter(DataSource const & dataSource, : m_reverseGeocoder(dataSource), m_cityFinder(dataSource), m_infoGetter(infoGetter) { m_nameGetter.LoadCountriesTree(); - m_nameGetter.SetLocale(languages::GetCurrentNorm()); + m_nameGetter.SetLocale(languages::GetCurrentMapLanguage()); } ReverseGeocoder::RegionAddress RegionAddressGetter::GetNearbyRegionAddress( diff --git a/search/result.cpp b/search/result.cpp index 80d8a115b8..14fa477026 100644 --- a/search/result.cpp +++ b/search/result.cpp @@ -178,11 +178,21 @@ void Result::AddHighlightRange(pair const & range) m_hightlightRanges.push_back(range); } +void Result::AddDescHighlightRange(pair const & range) +{ + m_descHightlightRanges.push_back(range); +} + pair const & Result::GetHighlightRange(size_t idx) const { ASSERT(idx < m_hightlightRanges.size(), ()); return m_hightlightRanges[idx]; } +pair const & Result::GetDescHighlightRange(size_t idx) const +{ + ASSERT(idx < m_descHightlightRanges.size(), ()); + return m_descHightlightRanges[idx]; +} void Result::PrependCity(string_view city) { diff --git a/search/result.hpp b/search/result.hpp index 9b2a56b5ac..3e2d3dc969 100644 --- a/search/result.hpp +++ b/search/result.hpp @@ -107,8 +107,13 @@ public: bool IsEqualFeature(Result const & r) const; void AddHighlightRange(std::pair const & range); + void AddDescHighlightRange(const std::pair & range); std::pair const & GetHighlightRange(size_t idx) const; size_t GetHighlightRangesCount() const { return m_hightlightRanges.size(); } + + //returns ranges to hightlight in address + std::pair const & GetDescHighlightRange(size_t idx) const; + size_t GetDescHighlightRangesCount() const { return m_descHightlightRanges.size(); } void PrependCity(std::string_view name); @@ -151,6 +156,7 @@ private: uint32_t m_matchedType = 0; std::string m_suggestionStr; buffer_vector, 4> m_hightlightRanges; + buffer_vector, 4> m_descHightlightRanges; std::shared_ptr m_dbgInfo; // used in debug logs and tests, nullptr in production diff --git a/search/search_integration_tests/smoke_test.cpp b/search/search_integration_tests/smoke_test.cpp index 1eb3b8206a..0143cc1299 100644 --- a/search/search_integration_tests/smoke_test.cpp +++ b/search/search_integration_tests/smoke_test.cpp @@ -185,6 +185,9 @@ UNIT_CLASS_TEST(SmokeTest, TypesSkipperTest) UNIT_CLASS_TEST(SmokeTest, CategoriesTest) { + // Checks all types in categories.txt for their searchability, + // which also depends on point drawability and presence of a name. + auto const & cl = classif(); /// @todo Should rewrite test @@ -231,8 +234,6 @@ UNIT_CLASS_TEST(SmokeTest, CategoriesTest) // No point drawing rules for country scale range. base::StringIL const arrInvisible[] = { - {"man_made", "tower"}, - {"place", "continent"}, {"place", "county"}, {"place", "region"}, @@ -248,14 +249,16 @@ UNIT_CLASS_TEST(SmokeTest, CategoriesTest) {"highway", "motorway_junction"}, {"landuse"}, {"man_made", "chimney"}, - {"man_made", "tower"}, + {"man_made", "flagpole"}, + {"man_made", "mast"}, + {"man_made", "water_tower"}, {"natural"}, {"office"}, {"place"}, {"waterway"}, /// @todo Controversial here. - /// Don't have point drawing rules except text -> type will be removed for Feature with empty name. + /// Don't have point drawing rules (a label only), hence type will be removed for an empty name Feature. {"building", "train_station"}, {"leisure", "track"}, {"natural", "beach"}, @@ -264,12 +267,10 @@ UNIT_CLASS_TEST(SmokeTest, CategoriesTest) for (auto const & tags : arrNoEmptyNames) noEmptyNames.insert(cl.GetTypeByPath(tags)); - uint32_t const commTower = cl.GetTypeByPath({"man_made", "tower", "communication"}); ftypes::TwoLevelPOIChecker isPoi; - auto const isNoEmptyName = [commTower, &isPoi, &noEmptyNames](uint32_t t) + auto const isNoEmptyName = [&isPoi, &noEmptyNames](uint32_t t) { - if (t != commTower) - ftype::TruncValue(t, 2); + ftype::TruncValue(t, 2); if (noEmptyNames.count(t) > 0) return true; diff --git a/search/search_quality/search_quality_tests/real_mwm_tests.cpp b/search/search_quality/search_quality_tests/real_mwm_tests.cpp index 6a5b5e507c..333d6df84d 100644 --- a/search/search_quality/search_quality_tests/real_mwm_tests.cpp +++ b/search/search_quality/search_quality_tests/real_mwm_tests.cpp @@ -1039,11 +1039,9 @@ UNIT_CLASS_TEST(MwmTestsFixture, Streets_Rank) }; /// @todo Streets should be highwer. Now POIs additional rank is greater than Streets rank. - processRequest("Santa Fe ", 20); - /// @todo Prefix search gives POIs (Starbucks) near "Avenida Santa Fe". Some WTFs for street's ranking here: - /// - gives 'Full Prefix' name's rank - /// - gives m_matchedFraction: 0.777778, while 'st' should be counted for streets definitely - processRequest("Santa Fe st ", 12); + processRequest("Santa Fe ", 22); + // Rank looks good here, except the "Santa Fe" train station is on a first place. + processRequest("Santa Fe st ", 2); } { @@ -1104,6 +1102,15 @@ UNIT_CLASS_TEST(MwmTestsFixture, Pois_Rank) // name="Malabia XXX" - should take this name's rank EqualClassifType(Range(results, 0, 1), GetClassifTypes({{"railway", "station", "subway"}})); } + + { + auto request = MakeRequest("Parque Ciudad"); + auto const & results = request->Results(); + TEST_GREATER(results.size(), kPopularPoiResultsCount, ()); + + /// @todo Actually, 2 parks should be the first, but (in test mode only), we have suburb and subway (*Parque*). + TEST_EQUAL(CountClassifType(Range(results, 0, 5), classif().GetTypeByPath({"leisure", "park"})), 2, ()); + } } { @@ -1432,4 +1439,20 @@ UNIT_CLASS_TEST(MwmTestsFixture, CompleteSearch_DistantMWMs) } } +UNIT_CLASS_TEST(MwmTestsFixture, Synonyms) +{ + // Buenos Aires + ms::LatLon const center(-34.6073377, -58.4432843); + SetViewportAndLoadMaps(center); + + { + auto request = MakeRequest("ntra sra de asuncion zelaya"); + auto const & results = request->Results(); + TEST_GREATER(results.size(), kPopularPoiResultsCount, ()); + + TEST_EQUAL(results[0].GetFeatureType(), classif().GetTypeByPath({"landuse", "residential"}), ()); + TEST_EQUAL(results[0].GetString(), "Barrio Nuestra Señora de la Asunción", ()); + } +} + } // namespace real_mwm_tests diff --git a/search/types_skipper.cpp b/search/types_skipper.cpp index 96de34d35e..d0686fc366 100644 --- a/search/types_skipper.cpp +++ b/search/types_skipper.cpp @@ -15,18 +15,23 @@ TypesSkipper::TypesSkipper() { Classificator const & c = classif(); + /// @todo(pastk): why "office-*" are skipped? + /// also could be unnamed (yet) place-hamlet/isolated_dwelling? + /// and natural-water-pond/lake? + // POIs like natural-spring, waterway-waterfall, highway-bus_stop are saved from skipping by the TwoLevelPOIChecker(). StringIL const arrSkipEmptyName1[] = { {"area:highway"}, {"building"}, {"highway"}, {"landuse"}, {"natural"}, {"office"}, {"place"}, {"waterway"}, }; for (auto const & e : arrSkipEmptyName1) m_skipIfEmptyName[0].push_back(c.GetTypeByPath(e)); - // Test for exact type (man_made-tower-communication is not). - StringIL const arrSkipEmptyNameExact[] = { + StringIL const arrSkipEmptyName2[] = { {"man_made", "chimney"}, - {"man_made", "tower"}, + {"man_made", "flagpole"}, + {"man_made", "mast"}, + {"man_made", "water_tower"}, }; - for (auto const & e : arrSkipEmptyNameExact) + for (auto const & e : arrSkipEmptyName2) m_skipIfEmptyName[1].push_back(c.GetTypeByPath(e)); m_skipAlways[0].push_back(c.GetTypeByPath({"isoline"})); @@ -46,6 +51,7 @@ void TypesSkipper::SkipEmptyNameTypes(feature::TypesHolder & types) const if (m_isPoi(type)) return false; + ftype::TruncValue(type, 2); if (HasType(m_skipIfEmptyName[1], type)) return true; diff --git a/search/types_skipper.hpp b/search/types_skipper.hpp index b80b24b1b3..50de6fff48 100644 --- a/search/types_skipper.hpp +++ b/search/types_skipper.hpp @@ -8,18 +8,20 @@ namespace search { -// Skips some feature's types when feature name is empty. +// Helper functions to determine if a feature should be indexed for search. class TypesSkipper { public: TypesSkipper(); + // Removes types which shouldn't be searchable in case there is no feature name. void SkipEmptyNameTypes(feature::TypesHolder & types) const; // Always skip feature for search index even it has name and other useful types. // Useful for mixed features, sponsored objects. bool SkipAlways(feature::TypesHolder const & types) const; + // Skip "entrance" only features if they have no name or a number ref only. bool SkipSpecialNames(feature::TypesHolder const & types, std::string_view defName) const; private: diff --git a/storage/map_files_downloader.cpp b/storage/map_files_downloader.cpp index 9c29fdd11a..9dd98843e4 100644 --- a/storage/map_files_downloader.cpp +++ b/storage/map_files_downloader.cpp @@ -7,6 +7,8 @@ #include "platform/platform.hpp" #include "platform/servers_list.hpp" #include "platform/settings.hpp" +#include "platform/products.hpp" +#include "platform/locale.hpp" #include "coding/url.hpp" @@ -49,7 +51,7 @@ void MapFilesDownloader::RunMetaConfigAsync(std::function && callback) { m_serversList = metaConfig.m_serversList; settings::Update(metaConfig.m_settings); - + products::Update(metaConfig.m_productsConfig); callback(); // Reset flag to invoke servers list downloading next time if current request has failed. @@ -149,6 +151,12 @@ std::vector MapFilesDownloader::MakeUrlList(std::string const & rel return urls; } +std::string GetAcceptLanguage() +{ + auto const locale = platform::GetCurrentLocale(); + return locale.m_language + "-" + locale.m_country; +} + // static MetaConfig MapFilesDownloader::LoadMetaConfig() { @@ -160,7 +168,8 @@ MetaConfig MapFilesDownloader::LoadMetaConfig() { platform::HttpClient request(metaServerUrl); request.SetRawHeader("X-OM-DataVersion", std::to_string(m_dataVersion)); - request.SetRawHeader("X-OM-AppVersion", GetPlatform().Version()); + request.SetRawHeader("X-OM-AppVersion", pl.Version()); + request.SetRawHeader("Accept-Language", GetAcceptLanguage()); request.SetTimeout(10.0); // timeout in seconds request.RunHttpRequest(httpResult); } diff --git a/storage/storage.cpp b/storage/storage.cpp index 6c8d12a895..7c25ffa9de 100644 --- a/storage/storage.cpp +++ b/storage/storage.cpp @@ -1267,12 +1267,9 @@ bool Storage::IsNodeDownloaded(CountryId const & countryId) const { CHECK_THREAD_CHECKER(m_threadChecker, ()); - for (auto const & localeMap : m_localFiles) - { - if (countryId == localeMap.first) - return true; - } - return false; + auto const it = m_localFiles.find(countryId); + /// @todo IDK what is the logic here, but other functions also check on empty list. + return (it != m_localFiles.end() && !it->second.empty()); } bool Storage::HasLatestVersion(CountryId const & countryId) const @@ -1283,7 +1280,7 @@ bool Storage::HasLatestVersion(CountryId const & countryId) const bool Storage::IsAllowedToEditVersion(CountryId const & countryId) const { auto const status = CountryStatusEx(countryId); - switch (status) + switch (status) { case Status::OnDisk: return true; case Status::OnDiskOutOfDate: @@ -1292,7 +1289,7 @@ bool Storage::IsAllowedToEditVersion(CountryId const & countryId) const ASSERT(localFile, ("Local file shouldn't be nullptr.")); auto const currentVersionTime = base::YYMMDDToSecondsSinceEpoch(static_cast(m_currentVersion)); auto const localVersionTime = base::YYMMDDToSecondsSinceEpoch(static_cast(localFile->GetVersion())); - return currentVersionTime - localVersionTime < kMaxSecondsTillLastVersionUpdate && + return currentVersionTime - localVersionTime < kMaxSecondsTillLastVersionUpdate && base::SecondsSinceEpoch() - localVersionTime < kMaxSecondsTillNoEdits; } default: return false; @@ -1350,10 +1347,11 @@ void Storage::DeleteNode(CountryId const & countryId) if (!node) return; - auto deleteAction = [this](CountryTree::Node const & descendantNode) { - bool onDisk = m_localFiles.find(descendantNode.Value().Name()) != m_localFiles.end(); + auto const deleteAction = [this](CountryTree::Node const & descendantNode) + { + bool const onDisk = m_localFiles.find(descendantNode.Value().Name()) != m_localFiles.end(); if (descendantNode.ChildrenCount() == 0 && onDisk) - this->DeleteCountry(descendantNode.Value().Name(), MapFileType::Map); + DeleteCountry(descendantNode.Value().Name(), MapFileType::Map); }; node->ForEachInSubtree(deleteAction); } @@ -1784,7 +1782,8 @@ Progress Storage::CalculateProgress(CountriesVec const & descendants) const void Storage::UpdateNode(CountryId const & countryId) { - ForEachInSubtree(countryId, [this](CountryId const & descendantId, bool groupNode) { + ForEachInSubtree(countryId, [this](CountryId const & descendantId, bool groupNode) + { if (!groupNode && m_localFiles.find(descendantId) != m_localFiles.end()) DownloadNode(descendantId, true /* isUpdate */); }); diff --git a/tools/QtCreator.MapsWithMe.xml b/tools/QtCreator.MapsWithMe.xml deleted file mode 100644 index ef542b991f..0000000000 --- a/tools/QtCreator.MapsWithMe.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - CodeStyleData - - false - false - false - false - false - false - false - false - true - false - true - false - true - true - true - true - false - false - false - 2 - true - false - 1 - true - 2 - - - - DisplayName - MapsWithMe - - diff --git a/tools/android/formatter/OrganicMapsCppEclipse.xml b/tools/android/formatter/OrganicMapsCppEclipse.xml deleted file mode 100644 index 8924a27a84..0000000000 --- a/tools/android/formatter/OrganicMapsCppEclipse.xml +++ /dev/null @@ -1,166 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tools/android/formatter/OrganicMapsJavaEclipse.xml b/tools/android/formatter/OrganicMapsJavaEclipse.xml deleted file mode 100644 index b1d3074da4..0000000000 --- a/tools/android/formatter/OrganicMapsJavaEclipse.xml +++ /dev/null @@ -1,291 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tools/check_memleak_includes.pl b/tools/check_memleak_includes.pl deleted file mode 100644 index 46207533bc..0000000000 --- a/tools/check_memleak_includes.pl +++ /dev/null @@ -1,106 +0,0 @@ -#!/usr/bin/perl -w - -# modules -use strict; -use File::Find; - -my $ROOT = ".."; -my @EXCLUDE = ("3party", "base", "std", "out", "tools", "testing"); -my @SOURCE_EXT = (".cpp"); -my @HEADER_EXT = (".hpp", ".h"); - -my $START_MD = "start_mem_debug.hpp"; -my $STOP_MD = "stop_mem_debug.hpp"; -my $INCLUDE_KEYWORD = "#include"; - -####################### -sub analyze_header { - # search for start inlude, in header file after it should be no more includes! - # and stop include should be the last at the end - my $res = index($_[1], $START_MD); - if ($res == -1) { - print "ERROR: No memory leak detector in file $_[0]\n"; - } - else { - my $res2 = index($_[1], $STOP_MD, $res); - if ($res2 == -1) { - print "ERROR: Last line in header file $_[0] should contain #include \"$STOP_MD\"\n"; - } - else - { - print "$_[0] is OK\n"; - } - } -} - -################### -sub analyze_source { - # search for start inlude, in source file it should be only one and after it no more includes! - my $res = index($_[1], $START_MD); - if ($res == -1) { - print "ERROR: No memory leak detector in file $_[0]\n"; - } - else { - my $res2 = index($_[1], $INCLUDE_KEYWORD, $res); - if ($res2 != -1) { - print "ERROR: #include \"$START_MD\" should be the last include in $_[0]\n"; - } - else - { - print "$_[0] is OK\n"; - } - } -} - -################################################################### -sub process_file { - my $fullPath = $File::Find::name; - my $f = $_; - - # ignore tests directories - unless ($fullPath =~ /_tests\//) - { - my $isSource = 0; - foreach my $ext (@SOURCE_EXT) { - $isSource = 1 if ($f =~ /$ext$/); - } - my $isHeader = 0; - foreach my $ext (@HEADER_EXT) { - $isHeader = 1 if ($f =~ /$ext$/); - } - - if ($isSource or $isHeader) { - open(INFILE, "<$f") or die("ERROR: can't open input file $fullPath\n"); - binmode(INFILE); - my @fileAttribs = stat(INFILE); - read(INFILE, my $buffer, $fileAttribs[7]); - close(INFILE); - analyze_source($fullPath, $buffer) if $isSource; - analyze_header($fullPath, $buffer) if $isHeader; - } - } -} - - -####################################### -# ENTY POINT -####################################### - -print("Scanning sources for correct memory leak detector includes\n"); -my @raw_dirs = <$ROOT/*>; -my @dirs; - -# filter out excluded directories -foreach my $f (@raw_dirs) { - my $good = 1; - foreach my $excl (@EXCLUDE) { - if (-f $f or $f =~ /$excl/) { - $good = 0; - } - } - push(@dirs, $f) if $good; -} - -# set array print delimeter -print "Directories for checking:\n@dirs\n"; -find(\&process_file, @dirs); diff --git a/tools/kothic b/tools/kothic index 2796db7ae3..6e17463575 160000 --- a/tools/kothic +++ b/tools/kothic @@ -1 +1 @@ -Subproject commit 2796db7ae3c3f3c00ae07880dc2d8dfc8edd776b +Subproject commit 6e1746357549ca4b09ee1560e9bf25e3fcce20ea diff --git a/tools/macdeployqtfix b/tools/macdeployqtfix deleted file mode 160000 index c2fa2720f6..0000000000 --- a/tools/macdeployqtfix +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c2fa2720f627236d031819c7d6ba33ee6cedbf13 diff --git a/tools/null-image.py b/tools/null-image.py deleted file mode 100755 index 2140fa9033..0000000000 --- a/tools/null-image.py +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env python3 -# -# Replace an image with an empty placeholder of the same size. -# - -import sys -from PIL import Image - -for img in sys.argv[1:]: - print('Processing', img) - orig = Image.open(img) - width, height = orig.size - new = Image.new("RGBA", (width, height), (255, 255, 255, 0)) - new.save(img, orig.format) diff --git a/tools/python/data/essential/setup.py b/tools/python/data/essential/setup.py index 8521d1792c..bdbdd6820d 100755 --- a/tools/python/data/essential/setup.py +++ b/tools/python/data/essential/setup.py @@ -29,7 +29,6 @@ setup( "mapcss-mapping.csv", "mixed_nodes.txt", "mixed_tags.txt", - "mwm_names_en.txt", "old_vs_new.csv", "patterns.txt", "replaced_tags.txt", diff --git a/tools/python/google_maps_bookmarks.py b/tools/python/google_maps_bookmarks.py index 44b962546d..f3859e24cb 100755 --- a/tools/python/google_maps_bookmarks.py +++ b/tools/python/google_maps_bookmarks.py @@ -2,97 +2,230 @@ import csv import json -import os.path +import argparse +import mimetypes import traceback import urllib.error import urllib.parse import urllib.request import xml.etree.ElementTree as ET -from os import R_OK - +from os import path, access, R_OK, linesep +from io import StringIO +from datetime import datetime class GoogleMapsConverter: - - def __init__(self): - print("Follow these steps to export your saved places from Google Maps and convert them to a KML-File") + def __init__(self, input_file=None, output_format=None, bookmark_list_name=None, api_key=None): + print("Follow these steps to export your saved places from Google Maps and convert them to a GPX or KML File") print() print("1. Create an API key for Google Places API following this guide") - print() - print("https://developers.google.com/maps/documentation/places/web-service/get-api-key") - print() + print(" https://developers.google.com/maps/documentation/places/web-service/get-api-key") print("2. Go to https://takeout.google.com/ and sign in with your Google account") print("3. Select 'Saved' and 'Maps (My Places)' and create an export") - print("4. Unzip the export and look for csv files in the folder Takeout/Saved/") + print("4. Download and unzip the export") + print ("5a. Look for CSV files (e.g. for lists) in the folder Takeout/Saved") + print ("5b. Look for GeoJSON files (e.g. for Saved Places) in the folder Takeout/Maps") print() - while True: - self.csv_file = input("Insert path to csv file: ") + + if input_file is None: + self.get_input_file() + else: + self.input_file = input_file + if not path.isfile(self.input_file): + raise FileNotFoundError(f"Couldn't find {self.input_file}") + if not access(self.input_file, R_OK): + raise PermissionError(f"Couldn't read {self.input_file}") - if not self.csv_file: - print("Please provide a csv file" + os.linesep) - continue - elif not os.path.isfile(self.csv_file): - print(f"Couldn't find {self.csv_file}" + os.linesep) - continue - elif not os.access(self.csv_file, R_OK): - print(f"Couldn't read {self.csv_file}" + os.linesep) - continue - else: - break + if output_format is None: + self.get_output_format() + else: + self.output_format = output_format - while True: - self.api_key = input("API key: ") - if not self.api_key: - print("Please provide an API key" + os.linesep) - continue - else: - break + if bookmark_list_name is None: + self.get_bookmark_list_name() + else: + self.bookmark_list_name = bookmark_list_name + self.output_file = self.bookmark_list_name + "." + self.output_format - while True: - bookmark_list_name = input("Bookmark list name: ") - if not bookmark_list_name: - print("Please provide a name" + os.linesep) - continue - else: - self.kml_file = bookmark_list_name + ".kml" - break - print() + if api_key is None: + self.get_api_key() + else: + self.api_key = api_key + self.places = [] - def parse_csv(self): - with open(self.csv_file, newline='') as file: - row_count = sum(1 for _ in file) - file.seek(0) - csvreader = csv.reader(file, delimiter=',') - next(csvreader) # skip header - for idx, row in enumerate(csvreader): - name = row[0] - description = row[1] - url = row[2] - print(f"\rProgress: {idx + 1}/{row_count - 1} Parsing {name}...", end='') - try: - if url.startswith("https://www.google.com/maps/search/"): - coordinates = url.split('/')[-1].split(',') - coordinates.reverse() - coordinates = ','.join(coordinates) - elif url.startswith('https://www.google.com/maps/place/'): - ftid = url.split('!1s')[-1] - params = {'key': self.api_key, 'fields': 'geometry', 'ftid': ftid} - places_url = "https://maps.googleapis.com/maps/api/place/details/json?" \ - + urllib.parse.urlencode(params) - try: - data = get_json(places_url) - location = data['result']['geometry']['location'] - coordinates = ','.join([str(location['lng']), str(location['lat'])]) - except (urllib.error.URLError, KeyError): - print(f"Couldn't extract coordinates from Googe Maps. Skipping {name}") - continue - else: - print(f"Couldn't parse url. Skipping {name}") - continue + def get_input_file(self): + while True: + self.input_file = input("Path to the file: ") + if not path.isfile(self.input_file): + print(f"Couldn't find {self.input_file}") + continue + if not access(self.input_file, R_OK): + print(f"Couldn't read {self.input_file}") + continue + break + + def get_output_format(self): + while True: + self.output_format = input("Output format (kml or gpx): ").lower() + if self.output_format not in ['kml', 'gpx']: + print("Please provide a valid output format" + linesep) + continue + else: + break + + def get_bookmark_list_name(self): + while True: + self.bookmark_list_name = input("Bookmark list name: ") + if not self.bookmark_list_name: + print("Please provide a name" + linesep) + continue + else: + self.output_file = self.bookmark_list_name + "." + self.output_format + break + + def get_api_key(self): + while True: + if self.api_key: + break + self.api_key = input("API key: ") + if not self.api_key: + print("Please provide an API key" + linesep) + continue + else: + break + + def convert_timestamp(self, timestamp): + if timestamp.endswith('Z'): + timestamp = timestamp[:-1] + date = datetime.fromisoformat(timestamp) + return date.strftime('%Y-%m-%d %H:%M:%S') + + def get_json(self, url): + max_attempts = 3 + for retry in range(max_attempts): + try: + response = urllib.request.urlopen(url) + return json.load(response) + except urllib.error.URLError: + print(f"Couldn't connect to Google Maps. Retrying... ({retry + 1}/{max_attempts})") + if retry < max_attempts - 1: + continue + else: + raise - self.places.append({'name': name, 'description': description, 'coordinates': coordinates}) - except Exception: - print(f"Couldn't parse {name}: {traceback.format_exc()}") + def get_name_and_coordinates_from_google_api(self, api_key, q=None, cid=None): + url = None + if q: + params = {'query': q, 'key': api_key} + url = f"https://maps.googleapis.com/maps/api/place/textsearch/json?{urllib.parse.urlencode(params)}" + elif cid: + params = {'cid': cid, 'fields': 'geometry,name', 'key': api_key} + url= f"https://maps.googleapis.com/maps/api/place/details/json?{urllib.parse.urlencode(params)}" + else: + return None + + result = self.get_json(url) + if result['status'] == 'OK': + place = result.get('results', [result.get('result')])[0] + location = place['geometry']['location'] + name = place['name'] + return {'name': name, 'coordinates': [str(location['lat']), str(location['lng'])]} + else: + print(f'{result.get("status", "")}: {result.get("error_message", "")}') + return None + + def process_geojson_features(self, content): + try: + geojson = json.loads(content) + except json.JSONDecodeError: + raise ValueError(f"The file {self.input_file} is not a valid JSON file.") + for feature in geojson['features']: + geometry = feature['geometry'] + coordinates = geometry['coordinates'] + properties = feature['properties'] + google_maps_url = properties.get('google_maps_url', '') + location = properties.get('location', {}) + name = None + + # Check for "null island" coordinates [0, 0] + # These are a common artifact of Google Maps exports + # See https://github.com/organicmaps/organicmaps/pull/8721 + if coordinates == [0, 0]: + parsed_url = urllib.parse.urlparse(google_maps_url) + query_params = urllib.parse.parse_qs(parsed_url.query) + # Google Maps URLs can contain either a query string parameter 'q', 'cid' + q = query_params.get('q', [None])[0] + cid = query_params.get('cid', [None])[0] + # Sometimes the 'q' parameter is a comma-separated lat long pair + if q and ',' in q and all(part.replace('.', '', 1).replace('-', '', 1).isdigit() for part in q.split(',')): + coordinates = q.split(',') + else: + result = self.get_name_and_coordinates_from_google_api(self.api_key, q=q, cid=cid) + if result: + coordinates = result['coordinates'] + if 'name' in result: + name = result['name'] + else: + print(f"Couldn't extract coordinates from Google Maps. Skipping {q or cid}") + + coord_string = ', '.join(map(str, coordinates)) if coordinates else None + # If name was not retrieved from the Google Maps API, then use the name from the location object, + # with a fallback to the address, and finally to the coordinates + if not name: + name = location.get('name') or location.get('address') or coord_string + + description = "" + if 'address' in properties: + description += f"Address: {location['address']}
    " + if 'date' in properties: + description += f"Date bookmarked: {self.convert_timestamp(properties['date'])}
    " + if 'Comment' in properties: + description += f"Comment: {properties['Comment']}
    " + if google_maps_url: + description += f"Google Maps URL: {google_maps_url}
    " + + place = { + 'name': name, + 'description': description + } + if coordinates: + place['coordinates'] = ','.join(map(str, coordinates)) + else: + place['coordinates'] = '0,0' + self.places.append(place) + + def process_csv_features(self, content): + csvreader = csv.reader(StringIO(content), delimiter=',') + next(csvreader) # skip header + for idx, row in enumerate(csvreader): + name = row[0] + description = row[1] + url = row[2] + print(f"\rProgress: {idx + 1} Parsing {name}...", end='') + try: + if url.startswith("https://www.google.com/maps/search/"): + coordinates = url.split('/')[-1].split(',') + coordinates.reverse() + coordinates = ','.join(coordinates) + elif url.startswith('https://www.google.com/maps/place/'): + ftid = url.split('!1s')[-1] + params = {'key': self.api_key, 'fields': 'geometry', 'ftid': ftid} + places_url = "https://maps.googleapis.com/maps/api/place/details/json?" \ + + urllib.parse.urlencode(params) + try: + data = self.get_json(places_url) + location = data['result']['geometry']['location'] + coordinates = ','.join([str(location['lng']), str(location['lat'])]) + except (urllib.error.URLError, KeyError): + print(f"Couldn't extract coordinates from Google Maps. Skipping {name}") + continue + else: + print(f"Couldn't parse url. Skipping {name}") + continue + + self.places.append({'name': name, 'description': description, 'coordinates': coordinates}) + except Exception: + print(f"Couldn't parse {name}: {traceback.format_exc()}") def write_kml(self): root = ET.Element("kml") @@ -104,26 +237,50 @@ class GoogleMapsConverter: point = ET.SubElement(placemark, "Point") ET.SubElement(point, "coordinates").text = place['coordinates'] tree = ET.ElementTree(root) - tree.write(self.kml_file) - print() - print() - print("Exported Google Saved Places to " + os.path.abspath(self.kml_file)) + tree.write(self.output_file) + def write_gpx(self): + gpx = ET.Element("gpx", version="1.1", creator="GoogleMapsConverter") + for place in self.places: + wpt = ET.SubElement(gpx, "wpt", lat=place['coordinates'].split(',')[1], lon=place['coordinates'].split(',')[0]) + ET.SubElement(wpt, "name").text = place['name'] + ET.SubElement(wpt, "desc").text = place['description'] + tree = ET.ElementTree(gpx) + tree.write(self.output_file) -def get_json(url): - max_attempts = 3 - for retry in range(max_attempts): - try: - response = urllib.request.urlopen(url) - return json.load(response) - except urllib.error.URLError: - print(f"Couldn't connect to Google Maps. Retrying... ({retry + 1}/{max_attempts})") - if retry < max_attempts - 1: - continue + def convert(self): + with open(self.input_file, 'r') as file: + content = file.read().strip() + if not content: + raise ValueError(f"The file {self.input_file} is empty or not a valid JSON file.") + + mime_type, _ = mimetypes.guess_type(self.input_file) + if mime_type == 'application/geo+json' or mime_type == 'application/json': + self.process_geojson_features(content) + elif mime_type == 'text/csv': + self.process_csv_features(content) else: - raise + raise ValueError(f"Unsupported file format: {self.input_file}") + + # Write to output file in the desired format, KML or GPX + if self.output_format == 'kml': + self.write_kml() + elif self.output_format == 'gpx': + self.write_gpx() + print("Exported Google Saved Places to " + path.abspath(self.output_file)) +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Convert Google Maps saved places to KML or GPX.") + parser.add_argument('--input', help="Path to the file") + parser.add_argument('--format', choices=['kml', 'gpx'], default='gpx', help="Output format: 'kml' or 'gpx'") + parser.add_argument('--bookmark_list_name', help="Name of the bookmark list") + parser.add_argument('--api_key', help="API key for Google Places API") + args = parser.parse_args() -converter = GoogleMapsConverter() -converter.parse_csv() -converter.write_kml() + converter = GoogleMapsConverter( + input_file=args.input, + output_format=args.format, + bookmark_list_name=args.bookmark_list_name, + api_key=args.api_key + ) + converter.convert() \ No newline at end of file diff --git a/tools/python/maps_generator/README.md b/tools/python/maps_generator/README.md index cf78b7e914..732c953fa0 100644 --- a/tools/python/maps_generator/README.md +++ b/tools/python/maps_generator/README.md @@ -27,6 +27,7 @@ The app version can be found in the "About" section of Organic Maps app. ```sh ./tools/unix/build_omim.sh -r generator_tool ./tools/unix/build_omim.sh -r world_roads_builder_tool +./tools/unix/build_omim.sh -r mwm_diff_tool ``` 3. Go to the `maps_generator` directory: diff --git a/tools/python/maps_generator/generator/diffs.py b/tools/python/maps_generator/generator/diffs.py index 9b9df202b0..1d9efab754 100644 --- a/tools/python/maps_generator/generator/diffs.py +++ b/tools/python/maps_generator/generator/diffs.py @@ -18,9 +18,7 @@ class Status: def calculate_diff(params): - new, old, out = params["new"], params["old"], params["out"] - - diff_size = 0 + diff_tool, new, old, out = params["tool"], params["new"], params["old"], params["out"] if not new.exists(): return Status.NO_NEW_VERSION, params @@ -32,7 +30,7 @@ def calculate_diff(params): if out.exists(): status = Status.NOTHING_TO_DO else: - res = subprocess.run("mwm_diff_tool " + old.as_posix() + " " + new.as_posix() + " " + out.as_posix()) + res = subprocess.run([diff_tool.as_posix(), "make", old, new, out]) if res.returncode != 0: return Status.INTERNAL_ERROR, params @@ -52,7 +50,6 @@ def calculate_diff(params): def mwm_diff_calculation(data_dir, logger, depth): data = list(data_dir.get_mwms())[:depth] - results = map(calculate_diff, data) for status, params in results: if Status.is_error(status): @@ -61,7 +58,8 @@ def mwm_diff_calculation(data_dir, logger, depth): class DataDir(object): - def __init__(self, mwm_name, new_version_dir, old_version_root_dir): + def __init__(self, diff_tool, mwm_name, new_version_dir, old_version_root_dir): + self.diff_tool_path = Path(diff_tool) self.mwm_name = mwm_name self.diff_name = self.mwm_name + ".mwmdiff" @@ -80,6 +78,7 @@ class DataDir(object): diff_dir = Path(self.new_version_dir, old_version_dir.name) diff_dir.mkdir(exist_ok=True) yield { + "tool": self.diff_tool_path, "new": self.new_version_path, "old": Path(old_version_dir, self.mwm_name), "out": Path(diff_dir, self.diff_name) diff --git a/tools/python/maps_generator/generator/env.py b/tools/python/maps_generator/generator/env.py index 477074e9ac..b02545e99b 100644 --- a/tools/python/maps_generator/generator/env.py +++ b/tools/python/maps_generator/generator/env.py @@ -420,6 +420,7 @@ class Env: self.gen_tool = self.setup_generator_tool() if WORLD_NAME in self.countries: self.world_roads_builder_tool = self.setup_world_roads_builder_tool() + self.diff_tool = self.setup_mwm_diff_tool() logger.info(f"Build name is {self.build_name}.") logger.info(f"Build path is {self.build_path}.") @@ -531,6 +532,12 @@ class Env: logger.info(f"world_roads_builder_tool found - {world_roads_builder_tool_path}") return world_roads_builder_tool_path + @staticmethod + def setup_mwm_diff_tool() -> AnyStr: + logger.info(f"Check mwm_diff_tool. Looking for it in {settings.BUILD_PATH} ...") + mwm_diff_tool_path = find_executable(settings.BUILD_PATH, "mwm_diff_tool") + logger.info(f"mwm_diff_tool found - {mwm_diff_tool_path}") + return mwm_diff_tool_path @staticmethod def setup_osm_tools() -> Dict[AnyStr, AnyStr]: diff --git a/tools/python/maps_generator/generator/settings.py b/tools/python/maps_generator/generator/settings.py index d8bcdbf38e..6d861f6dda 100644 --- a/tools/python/maps_generator/generator/settings.py +++ b/tools/python/maps_generator/generator/settings.py @@ -97,7 +97,7 @@ NODE_STORAGE = "mem" if total_virtual_memory() / 10 ** 9 >= 64 else "map" # Stages section: NEED_PLANET_UPDATE = False THREADS_COUNT_FEATURES_STAGE = multiprocessing.cpu_count() -DATA_ARCHIVE_DIR = USER_RESOURCE_PATH +DATA_ARCHIVE_DIR = "" DIFF_VERSION_DEPTH = 2 # Logging section: @@ -235,11 +235,11 @@ def init(default_settings_path: AnyStr): global THREADS_COUNT_FEATURES_STAGE NEED_PLANET_UPDATE = cfg.get_opt("Stages", "NEED_PLANET_UPDATE", NEED_PLANET_UPDATE) DATA_ARCHIVE_DIR = cfg.get_opt_path( - "Generator tool", "DATA_ARCHIVE_DIR", DATA_ARCHIVE_DIR - ) - DIFF_VERSION_DEPTH = cfg.get_opt( - "Generator tool", "DIFF_VERSION_DEPTH", DIFF_VERSION_DEPTH + "Stages", "DATA_ARCHIVE_DIR", DATA_ARCHIVE_DIR ) + DIFF_VERSION_DEPTH = int(cfg.get_opt( + "Stages", "DIFF_VERSION_DEPTH", DIFF_VERSION_DEPTH + )) threads_count = int( cfg.get_opt( diff --git a/tools/python/maps_generator/generator/stages_declaration.py b/tools/python/maps_generator/generator/stages_declaration.py index 35ee8ee699..a455b87352 100644 --- a/tools/python/maps_generator/generator/stages_declaration.py +++ b/tools/python/maps_generator/generator/stages_declaration.py @@ -314,13 +314,13 @@ class StageRoutingTransit(Stage): @country_stage -@production_only class StageMwmDiffs(Stage): def apply(self, env: Env, country, logger, **kwargs): data_dir = diffs.DataDir( - mwm_name=env.build_name, - new_version_dir=env.build_path, - old_version_root_dir=settings.DATA_ARCHIVE_DIR, + diff_tool = env.diff_tool, + mwm_name = f"{country}.mwm", + new_version_dir = env.paths.mwm_path, + old_version_root_dir = settings.DATA_ARCHIVE_DIR, ) diffs.mwm_diff_calculation(data_dir, logger, depth=settings.DIFF_VERSION_DEPTH) diff --git a/tools/python/maps_generator/var/etc/map_generator.ini.default b/tools/python/maps_generator/var/etc/map_generator.ini.default index 4f6f169026..3af13c82d5 100644 --- a/tools/python/maps_generator/var/etc/map_generator.ini.default +++ b/tools/python/maps_generator/var/etc/map_generator.ini.default @@ -87,7 +87,8 @@ NEED_BUILD_WORLD_ROADS: false # Set to 1 to update the entire OSM planet file (as taken from "planet.openstreetmap.org") # via an osmupdate tool before the generation. Not for use with partial planet extracts. NEED_PLANET_UPDATE: 0 -# If you want to calculate diffs you need to specify where the old maps are. +# If you want to calculate diffs you need to specify where the old maps are, +# e.g. ${Main:MAIN_OUT_PATH}/2021_03_16__09_00_00/ DATA_ARCHIVE_DIR: ${Generator tool:USER_RESOURCE_PATH} # How many versions in the archive to use for diff calculation: DIFF_VERSION_DEPTH: 2 diff --git a/tools/unix/generate_localizations.sh b/tools/unix/generate_localizations.sh index d33276a851..4b6c97f05a 100755 --- a/tools/unix/generate_localizations.sh +++ b/tools/unix/generate_localizations.sh @@ -1,5 +1,5 @@ #!/bin/bash -set -euxo pipefail +set -euo pipefail # Use ruby from brew on Mac OS X, because system ruby is outdated/broken/will be removed in future releases. case $OSTYPE in @@ -42,15 +42,6 @@ if [ ! -f "$TWINE_PATH/$TWINE_GEM" ]; then ) fi -# Validate and format/sort strings files. -STRINGS_UTILS="$OMIM_PATH/tools/python/strings_utils.py" -"$STRINGS_UTILS" --validate --output -"$STRINGS_UTILS" --types-strings --validate --output - -# Check for unused strings. -CLEAN_STRINGS="$OMIM_PATH/tools/python/clean_strings_txt.py" -"$CLEAN_STRINGS" --validate - # Generate android/iphone/jquery localization files from strings files. TWINE="$(gem contents --show-install-dir twine)/bin/twine" if [[ $TWINE == *".om/bin/twine" ]]; then @@ -63,6 +54,18 @@ fi OMIM_DATA="$OMIM_PATH/data" STRINGS_PATH="$OMIM_DATA/strings" +# Validate and format/sort strings files. +STRINGS_UTILS="$OMIM_PATH/tools/python/strings_utils.py" +"$STRINGS_UTILS" --validate --output +"$STRINGS_UTILS" --types-strings --validate --output +"$STRINGS_UTILS" "$STRINGS_PATH/sound.txt" --validate --output +"$STRINGS_UTILS" "$OMIM_DATA/countries_names.txt" --validate --output +"$STRINGS_UTILS" "$OMIM_PATH/iphone/plist.txt" --validate --output + +# Check for unused strings. +CLEAN_STRINGS="$OMIM_PATH/tools/python/clean_strings_txt.py" +"$CLEAN_STRINGS" --validate + MERGED_FILE="$(mktemp)" cat "$STRINGS_PATH"/{strings,types_strings}.txt> "$MERGED_FILE" diff --git a/tools/unix/version.sh b/tools/unix/version.sh index 05bb6c3347..aaa57abb65 100755 --- a/tools/unix/version.sh +++ b/tools/unix/version.sh @@ -31,7 +31,9 @@ function ios_version { } function ios_build { - echo "$COUNT" + MINOR=$((16#${GIT_HASH:0:4})) + PATCH=$((16#${GIT_HASH:4:4})) + echo "$COUNT.$MINOR.$PATCH" } function android_name { @@ -80,10 +82,11 @@ Where format is one of the following arguments (shows current values): EOF } +init + if [ -z ${1:-} ] || [[ ! $(type -t "$1") == function ]]; then usage exit 1 else - init "$1" fi diff --git a/tools/upload_to_dropbox.sh b/tools/upload_to_dropbox.sh deleted file mode 100755 index 19973d7e69..0000000000 --- a/tools/upload_to_dropbox.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash - -if [ "$#" -lt 3 ]; then - echo "Illegal number of parameters" - echo "The four arguments to this script are:" - echo " 1) build mode (debug/release), use the \${BUILD_MODE} from Jenkins environment vars" - echo " 2) the path to the root of the workspace, use \${WORKSPACE} from Jenkins env. vars" - echo " 3) the auth key to your dropbox. You can generate it in your dropbox" - echo " 4) the target folder (android, drape or anything else, defaults to testing)" - exit 0 # Exiting with a non-error code in order not to break the rest of the build -fi - -BUILD_MODE=$1 -FOLDER=$2 -KEY=$3 -TARGET="${4-testing}" - -rm -f $FOLDER/omim/android/app/build/outputs/apk/android-symbols.zip - -zip -r $FOLDER/omim/android/app/build/outputs/apk/android-symbols.zip $FOLDER/omim/android/obj - -# Upload the files to DropBox: -# Later this ugly bit will be replaced by a new and shiny python script -# 1) Delete the files from dropbox first -for s in $(curl -H "Authorization: Bearer $KEY" "https://api.dropboxapi.com/1/metadata/auto/$TARGET/$BUILD_MODE" -X GET | python -m json.tool | grep "path" | grep "/$TARGET/" | cut -d ":" -f 2 | sed "s/\",//" | sed "s/\"//"); -do - curl -H "Authorization: Bearer $KEY" "https://api.dropbox.com/1/fileops/delete" -X POST --data "root=auto&path=$s" -done - -cd $FOLDER/omim/android/app/build/outputs/apk/ - -# 2) Upload the new ones now -for s in $(ls | grep "android" | grep -v "unaligned"); -do - curl -H "Authorization: Bearer $KEY" https://api-content.dropbox.com/1/files_put/auto/$TARGET/$BUILD_MODE/ -T $s; -done diff --git a/tools/user_code_coverage.py b/tools/user_code_coverage.py deleted file mode 100644 index 1ccdc5ea0f..0000000000 --- a/tools/user_code_coverage.py +++ /dev/null @@ -1,36 +0,0 @@ -import os -import json -import sys - -if len(sys.argv) < 3: - print "USAGE: " + sys.argv[0] + " [username] [htmlfile]" - exit() - -USERNAME = sys.argv[1] - -HTMLFILE = sys.argv[1] - -if __name__ == "__main__": - os.system('git log --pretty="%H" --author="'+USERNAME+'" | while read commit_hash; do git show --oneline --name-only $commit_hash | tail -n+2; done | sort | uniq > /tmp/wrote.files') - files = {} - for f in open('/tmp/wrote.files'): - f = f.strip() - if os.path.exists(f): - os.system("git blame -w "+f+" > /tmp/wrote.blame") - stat = {'total': 0, 'unclean': 0} - for line in open('/tmp/wrote.blame'): - stat['total'] += 1 - if USERNAME in line: - stat['unclean'] += 1 - files[f] = stat - html = open(HTMLFILE, 'w') - print >> html, "" - keys = files.keys() - keys.sort(key = lambda a: 1. * files[a]['unclean'] / max(files[a]['total'],0.01)) - keys.sort(key = lambda a: files[a]['unclean']) - keys.reverse() - print >> html, "" - for k in keys: - v = files[k] - print >> html, ""%(k,v['unclean'], v['total'],v['unclean'], v['total'] ) - print >> html, "" \ No newline at end of file diff --git a/xcode/fastlane/Fastfile b/xcode/fastlane/Fastfile index 22aaad363e..2c6593657a 100644 --- a/xcode/fastlane/Fastfile +++ b/xcode/fastlane/Fastfile @@ -35,6 +35,16 @@ platform :ios do keychain_name: ENV['MATCH_KEYCHAIN_NAME'], keychain_password: ENV['MATCH_KEYCHAIN_PASSWORD'] ) + import_certificate( + certificate_path: 'keys/Apple/AppleWWDRCAG7.cer', + keychain_name: ENV['MATCH_KEYCHAIN_NAME'], + keychain_password: ENV['MATCH_KEYCHAIN_PASSWORD'] + ) + import_certificate( + certificate_path: 'keys/Apple/AppleWWDRCAG8.cer', + keychain_name: ENV['MATCH_KEYCHAIN_NAME'], + keychain_password: ENV['MATCH_KEYCHAIN_PASSWORD'] + ) # Organic Maps certificates. import_certificate( certificate_path: 'keys/CertificatesDev.p12', @@ -51,22 +61,42 @@ platform :ios do get_provisioning_profile( api_key_path: 'keys/appstore.json', app_identifier: 'app.organicmaps', - provisioning_name: 'CarPlay Release', + provisioning_name: 'App Development (Fastlane)', ignore_profiles_with_different_name: true, readonly: true, development: true, skip_install: false, - filename: 'keys/CarPlay_Release.mobileprovision' + filename: 'keys/App_Development.mobileprovision' ) get_provisioning_profile( api_key_path: 'keys/appstore.json', app_identifier: 'app.organicmaps', - provisioning_name: 'CarPlay AppStore', + provisioning_name: 'App Distribution (Fastlane)', ignore_profiles_with_different_name: true, adhoc: false, readonly: true, skip_install: false, - filename: 'keys/CarPlay_AppStore.mobileprovision' + filename: 'keys/App_Distribution.mobileprovision' + ) + get_provisioning_profile( + api_key_path: 'keys/appstore.json', + app_identifier: 'app.organicmaps.widgetextension', + provisioning_name: 'WidgetExtension Development (Fastlane)', + ignore_profiles_with_different_name: true, + readonly: true, + development: true, + skip_install: false, + filename: 'keys/WidgetExtension_Development.mobileprovision' + ) + get_provisioning_profile( + api_key_path: 'keys/appstore.json', + app_identifier: 'app.organicmaps.widgetextension', + provisioning_name: 'WidgetExtension Distribution (Fastlane)', + ignore_profiles_with_different_name: true, + adhoc: false, + readonly: true, + skip_install: false, + filename: 'keys/WidgetExtension_Distribution.mobileprovision' ) end end @@ -85,6 +115,24 @@ platform :ios do prepare generate_version generate_testflight_changelog + update_code_signing_settings( + path: "../iphone/Maps/Maps.xcodeproj", + targets: ["OMaps"], + build_configurations: "Release", + use_automatic_signing: false, + code_sign_identity: "Apple Distribution", + team_id: "9Z6432XD7L", + profile_name: "App Distribution (Fastlane)", + ) + update_code_signing_settings( + path: "../iphone/Maps/Maps.xcodeproj", + targets: ["OMapsWidgetExtension"], + build_configurations: "Release", + use_automatic_signing: false, + code_sign_identity: "Apple Distribution", + team_id: "9Z6432XD7L", + profile_name: "WidgetExtension Distribution (Fastlane)", + ) build_ios_app( workspace: 'omim.xcworkspace', scheme: 'OMaps', @@ -96,7 +144,8 @@ platform :ios do export_method: 'app-store', export_options: { provisioningProfiles: { - 'app.organicmaps' => 'CarPlay AppStore' + 'app.organicmaps' => 'App Distribution (Fastlane)', + 'app.organicmaps.widgetextension' => 'WidgetExtension Distribution (Fastlane)', } }, skip_profile_detection: false, diff --git a/xcode/keys/Apple/AppleWWDRCAG2.cer b/xcode/keys/Apple/AppleWWDRCAG2.cer new file mode 100644 index 0000000000..b77e1e9eb6 Binary files /dev/null and b/xcode/keys/Apple/AppleWWDRCAG2.cer differ diff --git a/xcode/keys/Apple/AppleWWDRCAG3.cer b/xcode/keys/Apple/AppleWWDRCAG3.cer new file mode 100644 index 0000000000..32f96f81dd Binary files /dev/null and b/xcode/keys/Apple/AppleWWDRCAG3.cer differ diff --git a/xcode/keys/Apple/AppleWWDRCAG4.cer b/xcode/keys/Apple/AppleWWDRCAG4.cer new file mode 100644 index 0000000000..b9f0bf298d Binary files /dev/null and b/xcode/keys/Apple/AppleWWDRCAG4.cer differ diff --git a/xcode/keys/Apple/AppleWWDRCAG5.cer b/xcode/keys/Apple/AppleWWDRCAG5.cer new file mode 100644 index 0000000000..8b564c7680 Binary files /dev/null and b/xcode/keys/Apple/AppleWWDRCAG5.cer differ diff --git a/xcode/keys/Apple/AppleWWDRCAG6.cer b/xcode/keys/Apple/AppleWWDRCAG6.cer new file mode 100644 index 0000000000..424a70bd3b Binary files /dev/null and b/xcode/keys/Apple/AppleWWDRCAG6.cer differ diff --git a/xcode/keys/Apple/AppleWWDRCAG7.cer b/xcode/keys/Apple/AppleWWDRCAG7.cer new file mode 100644 index 0000000000..df350fd357 Binary files /dev/null and b/xcode/keys/Apple/AppleWWDRCAG7.cer differ diff --git a/xcode/keys/Apple/AppleWWDRCAG8.cer b/xcode/keys/Apple/AppleWWDRCAG8.cer new file mode 100644 index 0000000000..2899edb9a1 Binary files /dev/null and b/xcode/keys/Apple/AppleWWDRCAG8.cer differ diff --git a/xcode/map/map.xcodeproj/project.pbxproj b/xcode/map/map.xcodeproj/project.pbxproj index 97d510969c..f5810fc704 100644 --- a/xcode/map/map.xcodeproj/project.pbxproj +++ b/xcode/map/map.xcodeproj/project.pbxproj @@ -91,6 +91,7 @@ BBD9E2C71EE9D01900DF189A /* routing_mark.hpp in Headers */ = {isa = PBXBuildFile; fileRef = BBD9E2C51EE9D01900DF189A /* routing_mark.hpp */; }; BBFC7E3A202D29C000531BE7 /* user_mark_layer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = BBFC7E38202D29BF00531BE7 /* user_mark_layer.cpp */; }; BBFC7E3B202D29C000531BE7 /* user_mark_layer.hpp in Headers */ = {isa = PBXBuildFile; fileRef = BBFC7E39202D29BF00531BE7 /* user_mark_layer.hpp */; }; + ED49D74C2CEF3D69004AF27E /* elevation_info_tests.cpp in Sources */ = {isa = PBXBuildFile; fileRef = ED49D74B2CEF3CE3004AF27E /* elevation_info_tests.cpp */; }; F6B283031C1B03320081957A /* gps_track_collection.cpp in Sources */ = {isa = PBXBuildFile; fileRef = F6B282FB1C1B03320081957A /* gps_track_collection.cpp */; }; F6B283041C1B03320081957A /* gps_track_collection.hpp in Headers */ = {isa = PBXBuildFile; fileRef = F6B282FC1C1B03320081957A /* gps_track_collection.hpp */; }; F6B283051C1B03320081957A /* gps_track_filter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = F6B282FD1C1B03320081957A /* gps_track_filter.cpp */; }; @@ -238,6 +239,7 @@ BBD9E2C51EE9D01900DF189A /* routing_mark.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = routing_mark.hpp; sourceTree = ""; }; BBFC7E38202D29BF00531BE7 /* user_mark_layer.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = user_mark_layer.cpp; sourceTree = ""; }; BBFC7E39202D29BF00531BE7 /* user_mark_layer.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = user_mark_layer.hpp; sourceTree = ""; }; + ED49D74B2CEF3CE3004AF27E /* elevation_info_tests.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = elevation_info_tests.cpp; sourceTree = ""; }; F6B282FB1C1B03320081957A /* gps_track_collection.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = gps_track_collection.cpp; sourceTree = ""; }; F6B282FC1C1B03320081957A /* gps_track_collection.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = gps_track_collection.hpp; sourceTree = ""; }; F6B282FD1C1B03320081957A /* gps_track_filter.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = gps_track_filter.cpp; sourceTree = ""; }; @@ -352,6 +354,7 @@ 674A29EE1B26FD5F001A525C /* testingmain.cpp */, BB421D6A1E8C0026005BFA4D /* transliteration_test.cpp */, 674A2A351B27011A001A525C /* working_time_tests.cpp */, + ED49D74B2CEF3CE3004AF27E /* elevation_info_tests.cpp */, ); name = map_tests; path = ../../map/map_tests; @@ -643,6 +646,7 @@ buildActionMask = 2147483647; files = ( 671ED38E20D403B300D4317E /* chart_generator_tests.cpp in Sources */, + ED49D74C2CEF3D69004AF27E /* elevation_info_tests.cpp in Sources */, 67F183761BD5045700AB1840 /* bookmarks_test.cpp in Sources */, FAA8387626BB3C17002E54C6 /* extrapolator_tests.cpp in Sources */, BB421D6C1E8C0031005BFA4D /* transliteration_test.cpp in Sources */, diff --git a/xcode/platform/platform.xcodeproj/project.pbxproj b/xcode/platform/platform.xcodeproj/project.pbxproj index 1ee9d96c60..d59e90274d 100644 --- a/xcode/platform/platform.xcodeproj/project.pbxproj +++ b/xcode/platform/platform.xcodeproj/project.pbxproj @@ -114,6 +114,14 @@ D5B191CF2386C7E4009CD0D6 /* http_uploader_background.hpp in Headers */ = {isa = PBXBuildFile; fileRef = D5B191CE2386C7E4009CD0D6 /* http_uploader_background.hpp */; }; EB60B4DC204C130300E4953B /* network_policy_ios.mm in Sources */ = {isa = PBXBuildFile; fileRef = EB60B4DB204C130300E4953B /* network_policy_ios.mm */; }; EB60B4DE204C175700E4953B /* network_policy_ios.h in Headers */ = {isa = PBXBuildFile; fileRef = EB60B4DD204C175700E4953B /* network_policy_ios.h */; }; + ED49D7402CEE438E004AF27E /* products.cpp in Sources */ = {isa = PBXBuildFile; fileRef = ED49D73F2CEE438E004AF27E /* products.cpp */; }; + ED49D7412CEE438E004AF27E /* products.hpp in Headers */ = {isa = PBXBuildFile; fileRef = ED49D73E2CEE438E004AF27E /* products.hpp */; }; + ED49D7442CEE43A4004AF27E /* meta_config_tests.cpp in Sources */ = {isa = PBXBuildFile; fileRef = ED49D7422CEE43A4004AF27E /* meta_config_tests.cpp */; }; + ED49D7452CEE43A4004AF27E /* products_tests.cpp in Sources */ = {isa = PBXBuildFile; fileRef = ED49D7432CEE43A4004AF27E /* products_tests.cpp */; }; + ED965B252CD8F72E0049E39E /* duration.hpp in Headers */ = {isa = PBXBuildFile; fileRef = ED965B242CD8F72A0049E39E /* duration.hpp */; }; + ED965B272CD8F7810049E39E /* duration.cpp in Sources */ = {isa = PBXBuildFile; fileRef = ED965B262CD8F77D0049E39E /* duration.cpp */; }; + ED965B472CDA52DB0049E39E /* duration_tests.cpp in Sources */ = {isa = PBXBuildFile; fileRef = ED965B462CDA4EC00049E39E /* duration_tests.cpp */; }; + ED965B482CDA575B0049E39E /* duration.cpp in Sources */ = {isa = PBXBuildFile; fileRef = ED965B262CD8F77D0049E39E /* duration.cpp */; }; F6DF73581EC9EAE700D8BA0B /* string_storage_base.cpp in Sources */ = {isa = PBXBuildFile; fileRef = F6DF73561EC9EAE700D8BA0B /* string_storage_base.cpp */; }; F6DF73591EC9EAE700D8BA0B /* string_storage_base.cpp in Sources */ = {isa = PBXBuildFile; fileRef = F6DF73561EC9EAE700D8BA0B /* string_storage_base.cpp */; }; F6DF735A1EC9EAE700D8BA0B /* string_storage_base.cpp in Sources */ = {isa = PBXBuildFile; fileRef = F6DF73561EC9EAE700D8BA0B /* string_storage_base.cpp */; }; @@ -252,6 +260,13 @@ D5B191CE2386C7E4009CD0D6 /* http_uploader_background.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = http_uploader_background.hpp; sourceTree = ""; }; EB60B4DB204C130300E4953B /* network_policy_ios.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = network_policy_ios.mm; sourceTree = ""; }; EB60B4DD204C175700E4953B /* network_policy_ios.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = network_policy_ios.h; sourceTree = ""; }; + ED49D73E2CEE438E004AF27E /* products.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = products.hpp; sourceTree = ""; }; + ED49D73F2CEE438E004AF27E /* products.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = products.cpp; sourceTree = ""; }; + ED49D7422CEE43A4004AF27E /* meta_config_tests.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = meta_config_tests.cpp; sourceTree = ""; }; + ED49D7432CEE43A4004AF27E /* products_tests.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = products_tests.cpp; sourceTree = ""; }; + ED965B242CD8F72A0049E39E /* duration.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = duration.hpp; sourceTree = ""; }; + ED965B262CD8F77D0049E39E /* duration.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = duration.cpp; sourceTree = ""; }; + ED965B462CDA4EC00049E39E /* duration_tests.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = duration_tests.cpp; sourceTree = ""; }; F6DF73561EC9EAE700D8BA0B /* string_storage_base.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = string_storage_base.cpp; sourceTree = ""; }; F6DF73571EC9EAE700D8BA0B /* string_storage_base.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = string_storage_base.hpp; sourceTree = ""; }; FAA8389026BB48E9002E54C6 /* libcppjansson.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libcppjansson.a; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -310,6 +325,9 @@ 675340DE1C58C405002CF0D9 /* platform_tests */ = { isa = PBXGroup; children = ( + ED49D7422CEE43A4004AF27E /* meta_config_tests.cpp */, + ED49D7432CEE43A4004AF27E /* products_tests.cpp */, + ED965B462CDA4EC00049E39E /* duration_tests.cpp */, 168EFCC12A30EB7400F71EE8 /* distance_tests.cpp */, 675341001C58C4C9002CF0D9 /* apk_test.cpp */, 675341011C58C4C9002CF0D9 /* country_file_tests.cpp */, @@ -374,6 +392,8 @@ 6753437A1A3F5CF500A0A8C3 /* platform */ = { isa = PBXGroup; children = ( + ED965B242CD8F72A0049E39E /* duration.hpp */, + ED965B262CD8F77D0049E39E /* duration.cpp */, 1669C83E2A30DCD200530AD1 /* distance.cpp */, 1669C83F2A30DCD200530AD1 /* distance.hpp */, 675343861A3F5D5900A0A8C3 /* apple_location_service.mm */, @@ -456,6 +476,8 @@ 675343A71A3F5D5A00A0A8C3 /* servers_list.hpp */, 675343A81A3F5D5A00A0A8C3 /* settings.cpp */, 675343A91A3F5D5A00A0A8C3 /* settings.hpp */, + ED49D73E2CEE438E004AF27E /* products.hpp */, + ED49D73F2CEE438E004AF27E /* products.cpp */, 34C624BB1DABCCD100510300 /* socket_apple.mm */, 34C624BC1DABCCD100510300 /* socket.hpp */, F6DF73561EC9EAE700D8BA0B /* string_storage_base.cpp */, @@ -516,6 +538,7 @@ 34C624BE1DABCCD100510300 /* socket.hpp in Headers */, D50B2296238591570056820A /* http_payload.hpp in Headers */, 675343C11A3F5D5A00A0A8C3 /* location_service.hpp in Headers */, + ED965B252CD8F72E0049E39E /* duration.hpp in Headers */, 67AB92DD1B7B3D7300AB5194 /* mwm_version.hpp in Headers */, 675343CA1A3F5D5A00A0A8C3 /* platform_unix_impl.hpp in Headers */, 675343D21A3F5D5A00A0A8C3 /* servers_list.hpp in Headers */, @@ -533,6 +556,7 @@ 675343CD1A3F5D5A00A0A8C3 /* platform.hpp in Headers */, 6741250F1B4C00CC00A3E828 /* local_country_file.hpp in Headers */, 3D15ACE2214A707900F725D5 /* localization.hpp in Headers */, + ED49D7412CEE438E004AF27E /* products.hpp in Headers */, D5B191CF2386C7E4009CD0D6 /* http_uploader_background.hpp in Headers */, 1669C8422A30DCD200530AD1 /* distance.hpp in Headers */, 3DE8B98F1DEC3115000E6083 /* network_policy.hpp in Headers */, @@ -685,8 +709,10 @@ 6741250A1B4C00CC00A3E828 /* country_file.cpp in Sources */, 4564FA7F2094978D0043CCFB /* remote_file.cpp in Sources */, 674125081B4C00CC00A3E828 /* country_defines.cpp in Sources */, + ED965B272CD8F7810049E39E /* duration.cpp in Sources */, EB60B4DC204C130300E4953B /* network_policy_ios.mm in Sources */, 6741250E1B4C00CC00A3E828 /* local_country_file.cpp in Sources */, + ED49D7402CEE438E004AF27E /* products.cpp in Sources */, 670E8C761BB318AB00094197 /* platform_ios.mm in Sources */, 67A2526A1BB40E100063F8A8 /* platform_qt.cpp in Sources */, 56EB1EDE1C6B6E6C0022D831 /* mwm_traits.cpp in Sources */, @@ -725,8 +751,11 @@ buildActionMask = 2147483647; files = ( 6783389E1C6DE59200FD6263 /* platform_test.cpp in Sources */, + ED49D7442CEE43A4004AF27E /* meta_config_tests.cpp in Sources */, + ED49D7452CEE43A4004AF27E /* products_tests.cpp in Sources */, 678338961C6DE59200FD6263 /* apk_test.cpp in Sources */, 678338991C6DE59200FD6263 /* jansson_test.cpp in Sources */, + ED965B482CDA575B0049E39E /* duration.cpp in Sources */, 6783389C1C6DE59200FD6263 /* location_test.cpp in Sources */, 678338981C6DE59200FD6263 /* get_text_by_id_tests.cpp in Sources */, 6783389A1C6DE59200FD6263 /* language_test.cpp in Sources */, @@ -734,6 +763,7 @@ 678338971C6DE59200FD6263 /* country_file_tests.cpp in Sources */, 678338951C6DE59200FD6263 /* testingmain.cpp in Sources */, 1669C8412A30DCD200530AD1 /* distance.cpp in Sources */, + ED965B472CDA52DB0049E39E /* duration_tests.cpp in Sources */, 44CAB5F62A1F926800819330 /* utm_mgrs_utils_tests.cpp in Sources */, F6DF735A1EC9EAE700D8BA0B /* string_storage_base.cpp in Sources */, 168EFCC22A30EB7400F71EE8 /* distance_tests.cpp in Sources */,
    Filenamedirty LOCLOCmeter
    %s%s%s