From e38a50bf95292c701e99c4412a64ea6bc1da9d61 Mon Sep 17 00:00:00 2001 From: Hitesh Kumar Saini Date: Fri, 7 Jun 2024 16:10:44 +0530 Subject: [PATCH 01/10] Added `org.envirocar.map` Module (#1003) * Raise Java version for GitHub actions * Fix switch statement * feat: org.envirocar.map module * feat: Point, Marker.Builder & Polyline.Builder * build(deps): Kotlin 1.9.0 * feat: MapController interface * feat: CameraUpdate & CameraUpdateFactory * build: specify applicationIdSuffix for debug * refactor: clean Marker & Polyline classes * build: use JavaVersion.VERSION_17 * build: fix MAPBOX_DOWNLOADS_TOKEN access * refactor: MapView, MapProvider, MapController to match requirements * feat: MapboxMapController & MapboxMapProvider * feat: addMarker & removeMarker support in MapboxMapController * feat: Polyline borderWidth & borderColor * feat: MapboxMapController addPolyline removePolyline * feat: MapController::runWhenReady The method allows to run the specified lambda block once CompletableDeferred is completed. The order of execution is preserved. * fix: MapboxMapController: internal access modifier & utilize runWhenReady * feat: expose style in MapboxMapProvider * feat: MapController::notifyCameraUpdate animation * fix: MapboxMapController display polyline below markers * chore: gitignore .DS_Store * chore: remove .DS_Store * chore: update MapController docstring * chore: replace also w/ let --------- Co-authored-by: Sebastian Drost --- .github/workflows/android_ci.yml | 4 +- .github/workflows/build.yml | 4 +- .gitignore | 1 + .../src/org/envirocar/obd/OBDSimulator.java | 9 +- build.gradle | 7 +- gradle/libs.versions.toml | 6 + org.envirocar.algorithm/build.gradle | 4 +- org.envirocar.app/build.gradle | 6 +- org.envirocar.core/build.gradle | 4 +- org.envirocar.map/.gitignore | 1 + org.envirocar.map/build.gradle.kts | 42 +++ org.envirocar.map/consumer-rules.pro | 0 org.envirocar.map/proguard-rules.pro | 21 ++ .../src/main/AndroidManifest.xml | 9 + .../java/org/envirocar/map/MapController.kt | 131 +++++++++ .../java/org/envirocar/map/MapProvider.kt | 32 +++ .../main/java/org/envirocar/map/MapView.kt | 68 +++++ .../org/envirocar/map/camera/CameraUpdate.kt | 68 +++++ .../map/camera/CameraUpdateFactory.kt | 63 +++++ .../java/org/envirocar/map/model/Animation.kt | 31 ++ .../java/org/envirocar/map/model/Marker.kt | 48 ++++ .../java/org/envirocar/map/model/Point.kt | 14 + .../java/org/envirocar/map/model/Polyline.kt | 78 +++++ .../provider/mapbox/MapboxMapController.kt | 266 ++++++++++++++++++ .../map/provider/mapbox/MapboxMapProvider.kt | 53 ++++ .../res/drawable-hdpi/marker_icon_default.png | Bin 0 -> 1520 bytes .../res/drawable-mdpi/marker_icon_default.png | Bin 0 -> 1010 bytes .../drawable-xhdpi/marker_icon_default.png | Bin 0 -> 1995 bytes .../drawable-xxhdpi/marker_icon_default.png | Bin 0 -> 2998 bytes .../drawable-xxxhdpi/marker_icon_default.png | Bin 0 -> 4006 bytes .../src/main/res/values/developer-config.xml | 4 + org.envirocar.obd/build.gradle | 4 +- org.envirocar.remote/build.gradle | 4 +- org.envirocar.storage/build.gradle | 4 +- settings.gradle | 10 +- 35 files changed, 966 insertions(+), 30 deletions(-) create mode 100644 org.envirocar.map/.gitignore create mode 100644 org.envirocar.map/build.gradle.kts create mode 100644 org.envirocar.map/consumer-rules.pro create mode 100644 org.envirocar.map/proguard-rules.pro create mode 100644 org.envirocar.map/src/main/AndroidManifest.xml create mode 100644 org.envirocar.map/src/main/java/org/envirocar/map/MapController.kt create mode 100644 org.envirocar.map/src/main/java/org/envirocar/map/MapProvider.kt create mode 100644 org.envirocar.map/src/main/java/org/envirocar/map/MapView.kt create mode 100644 org.envirocar.map/src/main/java/org/envirocar/map/camera/CameraUpdate.kt create mode 100644 org.envirocar.map/src/main/java/org/envirocar/map/camera/CameraUpdateFactory.kt create mode 100644 org.envirocar.map/src/main/java/org/envirocar/map/model/Animation.kt create mode 100644 org.envirocar.map/src/main/java/org/envirocar/map/model/Marker.kt create mode 100644 org.envirocar.map/src/main/java/org/envirocar/map/model/Point.kt create mode 100644 org.envirocar.map/src/main/java/org/envirocar/map/model/Polyline.kt create mode 100644 org.envirocar.map/src/main/java/org/envirocar/map/provider/mapbox/MapboxMapController.kt create mode 100644 org.envirocar.map/src/main/java/org/envirocar/map/provider/mapbox/MapboxMapProvider.kt create mode 100644 org.envirocar.map/src/main/res/drawable-hdpi/marker_icon_default.png create mode 100644 org.envirocar.map/src/main/res/drawable-mdpi/marker_icon_default.png create mode 100644 org.envirocar.map/src/main/res/drawable-xhdpi/marker_icon_default.png create mode 100644 org.envirocar.map/src/main/res/drawable-xxhdpi/marker_icon_default.png create mode 100644 org.envirocar.map/src/main/res/drawable-xxxhdpi/marker_icon_default.png create mode 100644 org.envirocar.map/src/main/res/values/developer-config.xml diff --git a/.github/workflows/android_ci.yml b/.github/workflows/android_ci.yml index a131ca268..17e6dcfa0 100644 --- a/.github/workflows/android_ci.yml +++ b/.github/workflows/android_ci.yml @@ -15,10 +15,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: set up JDK 11 + - name: set up JDK 17 uses: actions/setup-java@v2 with: - java-version: '11' + java-version: '17' distribution: 'adopt' cache: gradle diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bb169d836..cadb33ab3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,10 +15,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: set up JDK 11 + - name: set up JDK 17 uses: actions/setup-java@v2 with: - java-version: '11' + java-version: '17' distribution: 'adopt' cache: gradle diff --git a/.gitignore b/.gitignore index 44a8232cc..d9b7e2c63 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ local.properties gradle-release.properties lcs-keystore */.settings/ +**/.DS_Store diff --git a/android-obd-simulator/src/org/envirocar/obd/OBDSimulator.java b/android-obd-simulator/src/org/envirocar/obd/OBDSimulator.java index 0a39c127b..320b65e8b 100644 --- a/android-obd-simulator/src/org/envirocar/obd/OBDSimulator.java +++ b/android-obd-simulator/src/org/envirocar/obd/OBDSimulator.java @@ -278,11 +278,10 @@ public boolean onCreateOptionsMenu(Menu menu) { @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.discoverable: - // Ensure this device is discoverable by others - ensureDiscoverable(); - return true; + if (item.getItemId() == R.id.discoverable) { + // Ensure this device is discoverable by others + ensureDiscoverable(); + return true; } return false; } diff --git a/build.gradle b/build.gradle index 4e44fcb95..b8a9cb72b 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,8 @@ plugins { - alias libs.plugins.android.application apply false - alias libs.plugins.android.library apply false - alias libs.plugins.license.plugin + alias(libs.plugins.android.application) apply false + alias(libs.plugins.android.library) apply false + alias(libs.plugins.jetbrains.kotlin.android) apply false + alias(libs.plugins.license.plugin) } allprojects { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e88ab3e80..b75db3335 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,11 +13,14 @@ coreTesting = "1.1.1" dagger = "2.51.1" easypermissions = "3.0.0" envirocarAidl = "d67cfa49f3" +espressoCore = "3.5.1" fabprogresscircle = "1.01" gson = "2.10.1" hellochartsLibrary = "1.5.8" jsr305 = "3.0.2" junit = "4.13.2" +junitVersion = "1.1.5" +kotlin = "1.9.0" legacySupportV4 = "1.0.0" licensePlugin = "0.16.1" lifecycleCompiler = "2.7.0" @@ -58,6 +61,8 @@ androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "a androidx-cardview = { module = "androidx.cardview:cardview", version.ref = "cardview" } androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" } androidx-core = { module = "androidx.core:core", version.ref = "androidxCore" } +androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } +androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-legacy-support-v4 = { module = "androidx.legacy:legacy-support-v4", version.ref = "legacySupportV4" } androidx-lifecycle-compiler = { module = "androidx.lifecycle:lifecycle-runtime", version.ref = "lifecycleCompiler" } androidx-lifecycle-extensions = { module = "androidx.lifecycle:lifecycle-extensions", version.ref = "lifecycleExtensions" } @@ -107,4 +112,5 @@ transitionseverywhere = { module = "com.andkulikov:transitionseverywhere", versi [plugins] android-application = { id = "com.android.application", version.ref = "agp" } android-library = { id = "com.android.library", version.ref = "agp" } +jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } license-plugin = { id = "com.github.hierynomus.license", version.ref = "licensePlugin" } diff --git a/org.envirocar.algorithm/build.gradle b/org.envirocar.algorithm/build.gradle index b45d101c4..a8521a3a1 100644 --- a/org.envirocar.algorithm/build.gradle +++ b/org.envirocar.algorithm/build.gradle @@ -19,8 +19,8 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } } diff --git a/org.envirocar.app/build.gradle b/org.envirocar.app/build.gradle index f23280db8..005a73744 100644 --- a/org.envirocar.app/build.gradle +++ b/org.envirocar.app/build.gradle @@ -41,12 +41,14 @@ android { minifyEnabled false proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" signingConfig signingConfigs.debug + + applicationIdSuffix ".debug" } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } sourceSets { diff --git a/org.envirocar.core/build.gradle b/org.envirocar.core/build.gradle index ab03a7eb2..2fec3813a 100644 --- a/org.envirocar.core/build.gradle +++ b/org.envirocar.core/build.gradle @@ -12,8 +12,8 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } } diff --git a/org.envirocar.map/.gitignore b/org.envirocar.map/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/org.envirocar.map/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/org.envirocar.map/build.gradle.kts b/org.envirocar.map/build.gradle.kts new file mode 100644 index 000000000..ea72344b6 --- /dev/null +++ b/org.envirocar.map/build.gradle.kts @@ -0,0 +1,42 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.jetbrains.kotlin.android) +} + +android { + namespace = "org.envirocar.map" + compileSdk = 34 + + defaultConfig { + minSdk = 21 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } +} + +dependencies { + + implementation(libs.androidx.appcompat) + implementation(libs.material) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + + // Provider: Mapbox + implementation("com.mapbox.maps:android:11.4.0") +} diff --git a/org.envirocar.map/consumer-rules.pro b/org.envirocar.map/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/org.envirocar.map/proguard-rules.pro b/org.envirocar.map/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/org.envirocar.map/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/org.envirocar.map/src/main/AndroidManifest.xml b/org.envirocar.map/src/main/AndroidManifest.xml new file mode 100644 index 000000000..d6fda84e2 --- /dev/null +++ b/org.envirocar.map/src/main/AndroidManifest.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/MapController.kt b/org.envirocar.map/src/main/java/org/envirocar/map/MapController.kt new file mode 100644 index 000000000..0fe92379c --- /dev/null +++ b/org.envirocar.map/src/main/java/org/envirocar/map/MapController.kt @@ -0,0 +1,131 @@ +package org.envirocar.map + +import androidx.annotation.CallSuper +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import org.envirocar.map.camera.CameraUpdate +import org.envirocar.map.camera.CameraUpdateFactory +import org.envirocar.map.model.Animation +import org.envirocar.map.model.Marker +import org.envirocar.map.model.Point +import org.envirocar.map.model.Polyline + +/** + * [MapController] + * --------------- + * [MapController] provides various methods to interact with the visible [MapView]. + * + * ``` + * override fun onCreate(savedInstanceState: Bundle?) { + * super.onCreate(savedInstanceState) + * val view: MapView = findViewById(R.id.mapView) + * val controller: MapController = mapView.getController(...) + * /* Interact with the [MapView] using the [MapController]. */ + * } + * ``` + * + * @see MapView + * @see CameraUpdate + * @see CameraUpdateFactory + * @see Animation + * @see Marker + * @see Point + * @see Polyline + */ +abstract class MapController { + private var jobLock = Any() + private var queueLock = Any() + private var job: Job? = null + private val queue = mutableListOf<() -> Unit>() + private val scope = CoroutineScope(Dispatchers.Main) + internal val readyCompletableDeferred: CompletableDeferred = CompletableDeferred() + + /** Sets the minimum zoom level. */ + @CallSuper + open fun setMinZoom(minZoom: Float) { + assert(minZoom in CAMERA_ZOOM_MIN..CAMERA_ZOOM_MAX) { + "Minimum zoom level must be between $CAMERA_ZOOM_MIN and $CAMERA_ZOOM_MAX." + } + } + + /** Sets the maximum zoom level. */ + @CallSuper + open fun setMaxZoom(maxZoom: Float) { + assert(maxZoom in CAMERA_ZOOM_MIN..CAMERA_ZOOM_MAX) { + "Maximum zoom level must be between $CAMERA_ZOOM_MIN and $CAMERA_ZOOM_MAX." + } + } + + /** Notifies the [MapView] about a [CameraUpdate]. */ + @CallSuper + open fun notifyCameraUpdate(cameraUpdate: CameraUpdate, animation: Animation? = null) { + with(cameraUpdate) { + when (this) { + is CameraUpdate.Companion.CameraUpdateBearing -> { + assert(bearing in CAMERA_BEARING_MIN..CAMERA_BEARING_MAX) { + "Bearing must be between $CAMERA_BEARING_MIN and $CAMERA_BEARING_MAX." + } + } + + is CameraUpdate.Companion.CameraUpdateTilt -> { + assert(tilt in CAMERA_TILT_MIN..CAMERA_TILT_MAX) { + "Tilt must be between $CAMERA_TILT_MIN and $CAMERA_TILT_MAX." + } + } + + is CameraUpdate.Companion.CameraUpdateZoom -> { + assert(zoom in CAMERA_ZOOM_MIN..CAMERA_ZOOM_MAX) { + "Zoom must be between $CAMERA_ZOOM_MIN and $CAMERA_ZOOM_MAX." + } + } + + else -> { + /* NO/OP */ + } + } + } + } + + /** Adds a [Marker] to the [MapView]. */ + abstract fun addMarker(marker: Marker) + + /** Adds a [Polyline] to the [MapView]. */ + abstract fun addPolyline(polyline: Polyline) + + /** Removes a [Marker] from the [MapView]. */ + abstract fun removeMarker(marker: Marker) + + /** Removes a [Polyline] from the [MapView]. */ + abstract fun removePolyline(polyline: Polyline) + + /** + * Executes the specified [block] once [readyCompletableDeferred] is completed. + * The order of execution for subsequent calls is preserved. + */ + internal fun runWhenReady(block: () -> Unit) { + synchronized(jobLock) { + synchronized(queueLock) { queue.add(block) } + if (job == null) { + job = scope.launch { + readyCompletableDeferred.await() + while (synchronized(queueLock) { queue.isNotEmpty() }) { + synchronized(queueLock) { queue.removeFirst().invoke() } + } + synchronized(jobLock) { job = null } + } + } + } + } + + companion object { + internal const val CAMERA_BEARING_MIN = 0.0F + internal const val CAMERA_BEARING_MAX = 360.0F + internal const val CAMERA_TILT_MIN = 0.0F + internal const val CAMERA_TILT_MAX = 60.0F + internal const val CAMERA_ZOOM_MIN = 0.0F + internal const val CAMERA_ZOOM_MAX = 22.0F + } +} diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/MapProvider.kt b/org.envirocar.map/src/main/java/org/envirocar/map/MapProvider.kt new file mode 100644 index 000000000..8647c53ec --- /dev/null +++ b/org.envirocar.map/src/main/java/org/envirocar/map/MapProvider.kt @@ -0,0 +1,32 @@ +package org.envirocar.map + +import android.content.Context +import android.view.View + +/** + * [MapProvider] + * ------------- + * [MapProvider] allows to initialize a [MapView] with a specific provider. + * Currently available providers are: + * * `MapboxMapProvider` + * * `MapLibreMapProvider` + * * `OsmDroidMapProvider` + * * `GoogleMapProvider` + * + * Each provider may require additional setup during compilation. + * Please refer to the module's documentation for more information. + */ +interface MapProvider { + + /** + * Returns the underlying [View] implementation for the specified map provider. + * A fresh instance of the [View] is created if it doesn't exist. + */ + fun getView(context: Context): View + + /** + * Returns the underlying [MapController] implementation for the specified map provider. + * The [getView] method must be called before this method. + */ + fun getController(): MapController +} diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/MapView.kt b/org.envirocar.map/src/main/java/org/envirocar/map/MapView.kt new file mode 100644 index 000000000..4b2a4261b --- /dev/null +++ b/org.envirocar.map/src/main/java/org/envirocar/map/MapView.kt @@ -0,0 +1,68 @@ +package org.envirocar.map + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.FrameLayout +import org.envirocar.map.camera.CameraUpdateFactory +import org.envirocar.map.model.Point + +/** + * [MapView] + * --------- + * The [MapView] may be used to display a map inside the view hierarchy. + */ +class MapView : FrameLayout { + + constructor(context: Context) : super(context) + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) + + private lateinit var instance: MapProvider + + /** + * Initializes the instance with the specified [MapProvider]. + * + * @param mapProvider The [MapProvider] to use for the [MapView] instance. + * @return The [MapController] associated with the [MapView] instance. + */ + fun getController(mapProvider: MapProvider): MapController { + if (!::instance.isInitialized) { + instance = mapProvider + addView( + instance.getView(context).apply { + visibility = View.INVISIBLE + layoutParams = LayoutParams( + LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT + ) + } + ) + + // Restore default camera state independent of the provider. + with(instance.getController()) { + listOf( + CameraUpdateFactory.newCameraUpdateBasedOnPoint(Point(CAMERA_POINT_LATITUDE_DEFAULT, CAMERA_POINT_LONGITUDE_DEFAULT)), + CameraUpdateFactory.newCameraUpdateBearing(CAMERA_BEARING_DEFAULT), + CameraUpdateFactory.newCameraUpdateTilt(CAMERA_TILT_DEFAULT), + CameraUpdateFactory.newCameraUpdateZoom(CAMERA_ZOOM_DEFAULT) + ).forEach { + notifyCameraUpdate(it) + } + } + + } else if (instance != mapProvider) { + error("MapView is already initialized with a different MapProvider.") + } + return instance.getController() + } + + companion object { + internal const val CAMERA_POINT_LATITUDE_DEFAULT = 52.5163 + internal const val CAMERA_POINT_LONGITUDE_DEFAULT = 13.3777 + internal const val CAMERA_BEARING_DEFAULT = 0.0F + internal const val CAMERA_TILT_DEFAULT = 0.0F + internal const val CAMERA_ZOOM_DEFAULT = 15.0F + } +} diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/camera/CameraUpdate.kt b/org.envirocar.map/src/main/java/org/envirocar/map/camera/CameraUpdate.kt new file mode 100644 index 000000000..82ff934a9 --- /dev/null +++ b/org.envirocar.map/src/main/java/org/envirocar/map/camera/CameraUpdate.kt @@ -0,0 +1,68 @@ +package org.envirocar.map.camera + +import org.envirocar.map.model.Point + +/** + * [CameraUpdate] + * -------------- + * [CameraUpdate] is a marker interface for all camera updates. + * + */ +sealed interface CameraUpdate { + companion object { + + /** + * [CameraUpdateBasedOnPoint] + * -------------------------- + * Camera update to transform the camera so that the point is centered on screen. + * + * @param point The geographical point. + */ + internal data class CameraUpdateBasedOnPoint( + val point: Point + ) : CameraUpdate + + /** + * [CameraUpdateBasedOnBounds] + * --------------------------- + * Camera update to transform the camera so that bounds specified by the points are centered on + * screen at the greatest possible zoom level. The padding may be used to specify additional + * padding on each side of the bounds. + * + * @param points The geographical points specifying the bounds. + * @param padding The padding in pixels. + */ + internal data class CameraUpdateBasedOnBounds( + val points: List, + val padding: Float + ) : CameraUpdate + + /** + * [CameraUpdateBearing] + * --------------------- + * Camera update to transform the camera so that the bearing is set to the specified value. + */ + internal data class CameraUpdateBearing( + val bearing: Float + ) : CameraUpdate + + /** + * [CameraUpdateTilt] + * ------------------ + * Camera update to transform the camera so that the tilt is set to the specified value. + */ + internal data class CameraUpdateTilt( + val tilt: Float + ) : CameraUpdate + + /** + * [CameraUpdateZoom] + * ------------------ + * Camera update to transform the camera so that the zoom is set to the specified value. + */ + internal data class CameraUpdateZoom( + val zoom: Float + ) : CameraUpdate + + } +} diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/camera/CameraUpdateFactory.kt b/org.envirocar.map/src/main/java/org/envirocar/map/camera/CameraUpdateFactory.kt new file mode 100644 index 000000000..cfd66c008 --- /dev/null +++ b/org.envirocar.map/src/main/java/org/envirocar/map/camera/CameraUpdateFactory.kt @@ -0,0 +1,63 @@ +package org.envirocar.map.camera + +import org.envirocar.map.model.Point + +/** + * [CameraUpdateFactory] + * ---------------------- + * [CameraUpdateFactory] is a factory class for creating [CameraUpdate]s. + */ +object CameraUpdateFactory { + + /** + * Creates a [CameraUpdate] to transform the camera so that the point is centered on screen. + * + * @param point The geographical point. + */ + fun newCameraUpdateBasedOnPoint(point: Point): CameraUpdate { + return CameraUpdate.Companion.CameraUpdateBasedOnPoint(point) + } + + /** + * Creates a [CameraUpdate] to transform the camera so that bounds specified by the points + * are centered on screen at the greatest possible zoom level. The padding may be used to + * specify additional padding on each side of the bounds. + * + * @param points The geographical points specifying the bounds. + * @param padding The padding in pixels. + */ + fun newCameraUpdateBasedOnBounds(points: List, padding: Float): CameraUpdate { + return CameraUpdate.Companion.CameraUpdateBasedOnBounds(points, padding) + } + + /** + * Creates a [CameraUpdate] to transform the camera so that the bearing is set to the specified value. + * Minimum bearing value is 0 and maximum bearing value is 360. + * + * @param bearing The bearing of the camera. + */ + fun newCameraUpdateBearing(bearing: Float): CameraUpdate { + return CameraUpdate.Companion.CameraUpdateBearing(bearing) + } + + /** + * Creates a [CameraUpdate] to transform the camera so that the tilt is set to the specified value. + * Minimum tilt value is 0 and maximum tilt value is 60. + * + * @param tilt The tilt of the camera. + */ + fun newCameraUpdateTilt(tilt: Float): CameraUpdate { + return CameraUpdate.Companion.CameraUpdateTilt(tilt) + } + + /** + * Creates a [CameraUpdate] to transform the camera so that the zoom is set to the specified value. + * Minimum zoom value is 0 and maximum zoom value is 22. + * + * @param zoom The zoom level of the camera. + */ + fun newCameraUpdateZoom(zoom: Float): CameraUpdate { + return CameraUpdate.Companion.CameraUpdateZoom(zoom) + } + +} diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/model/Animation.kt b/org.envirocar.map/src/main/java/org/envirocar/map/model/Animation.kt new file mode 100644 index 000000000..ef23da13d --- /dev/null +++ b/org.envirocar.map/src/main/java/org/envirocar/map/model/Animation.kt @@ -0,0 +1,31 @@ +package org.envirocar.map.model + +/** + * [Animation] + * ----------- + * [Animation] may be used specify a new animation. + * Utilize the [Animation.Builder] to create a new instance. + * + * @property duration The duration of the animation (in milliseconds). + */ +class Animation private constructor( + val duration: Long +) { + class Builder { + private var duration: Long = DEFAULT_DURATION + + /** Sets duration of the animation (in milliseconds). */ + fun withDuration(value: Long) = apply { duration = value } + + /** Builds the animation. */ + fun build(): Animation { + return Animation( + duration + ) + } + + companion object { + private const val DEFAULT_DURATION = 1000L + } + } +} diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/model/Marker.kt b/org.envirocar.map/src/main/java/org/envirocar/map/model/Marker.kt new file mode 100644 index 000000000..ca3847d55 --- /dev/null +++ b/org.envirocar.map/src/main/java/org/envirocar/map/model/Marker.kt @@ -0,0 +1,48 @@ +package org.envirocar.map.model + +import androidx.annotation.DrawableRes + +/** + * [Marker] + * --------- + * [Marker] may be used to indicate a specific location on the map. + * Utilize the [Marker.Builder] to create a new instance. + * + * @property id The unique identifier. + * @property point The geographical point. + * @property title The title of the marker. + * @property drawable The drawable of the marker. + */ +class Marker private constructor( + val id: Int, + val point: Point, + val title: String?, + @DrawableRes val drawable: Int? +) { + class Builder(private val point: Point) { + private var title: String? = null + @DrawableRes + private var drawable: Int? = null + + /** Sets the title of the marker. */ + fun withTitle(value: String) = apply { title = value } + + /** Sets the drawable of the marker. */ + fun withDrawable(@DrawableRes value: Int) = apply { drawable = value } + + /** Builds the marker. */ + fun build(): Marker { + return Marker( + count++, + point, + title, + drawable + ) + } + } + + companion object { + @Volatile + private var count = 0 + } +} diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/model/Point.kt b/org.envirocar.map/src/main/java/org/envirocar/map/model/Point.kt new file mode 100644 index 000000000..c3b06246d --- /dev/null +++ b/org.envirocar.map/src/main/java/org/envirocar/map/model/Point.kt @@ -0,0 +1,14 @@ +package org.envirocar.map.model + +/** + * [Point] + * -------- + * [Point] represents a geographical point with latitude and longitude. + * + * @property latitude The latitude of the point. + * @property longitude The longitude of the point. + */ +data class Point( + val latitude: Double, + val longitude: Double +) diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/model/Polyline.kt b/org.envirocar.map/src/main/java/org/envirocar/map/model/Polyline.kt new file mode 100644 index 000000000..797dcd939 --- /dev/null +++ b/org.envirocar.map/src/main/java/org/envirocar/map/model/Polyline.kt @@ -0,0 +1,78 @@ +package org.envirocar.map.model + +import androidx.annotation.ColorInt + +/** + * [Polyline] + * ---------- + * [Polyline] represents a line on the map. + * Utilize [Polyline.Builder] to create a new [Polyline]. + * + * @property id The unique identifier. + * @property points The list of geographical points that make up the polyline. + * @property color The color for displaying a single color polyline. + * @property colors The list of colors for displaying a gradient polyline. + */ +class Polyline private constructor( + val id: Int, + val points: List, + val width: Float, + @ColorInt val color: Int, + val borderWidth: Float, + @ColorInt val borderColor: Int, + val colors: List? +) { + class Builder(private val points: List) { + private var width: Float = DEFAULT_WIDTH + @ColorInt + private var color: Int = DEFAULT_COLOR + private var borderWidth: Float = DEFAULT_BORDER_WIDTH + @ColorInt + private var borderColor: Int = DEFAULT_BORDER_COLOR + private var colors: List? = null + + /** Sets the width of the polyline. */ + fun withWidth(value: Float) = apply { width = value } + + /** Sets the color of the polyline, for displaying a single color polyline. */ + fun withColor(@ColorInt value: Int) = apply { color = value } + + /** Sets the width of the border of the polyline. */ + fun withBorderWidth(value: Float) = apply { borderWidth = value } + + /** Sets the color of the border of the polyline. */ + fun withBorderColor(@ColorInt value: Int) = apply { borderColor = value } + + /** Sets the list of colors of the polyline, for displaying a gradient polyline. */ + fun withColors(value: List) = apply { colors = value } + + /** Builds the polyline. */ + fun build(): Polyline { + assert(points.isNotEmpty()) { + "Polyline must have at least one point." + } + assert(colors == null || colors?.size == points.size) { + "Polyline must have same number of colors as points for a gradient." + } + return Polyline( + count++, + points, + width, + color, + borderWidth, + borderColor, + colors + ) + } + } + + companion object { + @Volatile + private var count = 0 + + private const val DEFAULT_WIDTH = 2.0F + private const val DEFAULT_COLOR = 0xFF000000.toInt() + private const val DEFAULT_BORDER_WIDTH = 0.0F + private const val DEFAULT_BORDER_COLOR = 0xFFFFFFFF.toInt() + } +} diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/provider/mapbox/MapboxMapController.kt b/org.envirocar.map/src/main/java/org/envirocar/map/provider/mapbox/MapboxMapController.kt new file mode 100644 index 000000000..36e76ad31 --- /dev/null +++ b/org.envirocar.map/src/main/java/org/envirocar/map/provider/mapbox/MapboxMapController.kt @@ -0,0 +1,266 @@ +package org.envirocar.map.provider.mapbox + +import android.view.View +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.graphics.drawable.toBitmap +import com.mapbox.geojson.Feature +import com.mapbox.geojson.LineString +import com.mapbox.maps.CameraBoundsOptions +import com.mapbox.maps.CameraOptions +import com.mapbox.maps.EdgeInsets +import com.mapbox.maps.MapView +import com.mapbox.maps.extension.style.expressions.dsl.generated.interpolate +import com.mapbox.maps.extension.style.layers.addLayerBelow +import com.mapbox.maps.extension.style.layers.generated.lineLayer +import com.mapbox.maps.extension.style.layers.properties.generated.LineCap +import com.mapbox.maps.extension.style.layers.properties.generated.LineJoin +import com.mapbox.maps.extension.style.sources.addSource +import com.mapbox.maps.extension.style.sources.generated.geoJsonSource +import com.mapbox.maps.plugin.animation.MapAnimationOptions +import com.mapbox.maps.plugin.animation.easeTo +import com.mapbox.maps.plugin.annotation.AnnotationConfig +import com.mapbox.maps.plugin.annotation.annotations +import com.mapbox.maps.plugin.annotation.generated.PointAnnotation +import com.mapbox.maps.plugin.annotation.generated.PointAnnotationOptions +import com.mapbox.maps.plugin.annotation.generated.createPointAnnotationManager +import com.mapbox.maps.plugin.attribution.attribution +import com.mapbox.maps.plugin.compass.compass +import com.mapbox.maps.plugin.logo.logo +import com.mapbox.maps.plugin.scalebar.scalebar +import org.envirocar.map.MapController +import org.envirocar.map.R +import org.envirocar.map.camera.CameraUpdate +import org.envirocar.map.model.Animation +import org.envirocar.map.model.Marker +import org.envirocar.map.model.Point +import org.envirocar.map.model.Polyline + +/** + * [MapboxMapController] + * --------------------- + * [Mapbox](https://www.mapbox.com) based implementation for [MapController]. + */ +internal class MapboxMapController(private val viewInstance: MapView) : MapController() { + private val markers = mutableMapOf() + private val polylines = mutableSetOf() + + // https://docs.mapbox.com/android/maps/guides/annotations/annotations/ + // https://docs.mapbox.com/android/maps/examples/line-gradient/ + // PolylineAnnotationManager API is not sufficient to create polylines with gradients. + private val pointAnnotationManager = viewInstance.annotations.createPointAnnotationManager( + AnnotationConfig(layerId = MAPBOX_MARKER_LAYER_ID) + ) + + init { + // Disable attribution, compass, logo & scalebar. + viewInstance.attribution.enabled = false + viewInstance.compass.enabled = false + viewInstance.logo.enabled = false + viewInstance.scalebar.enabled = false + + // Once the map style is loaded, make the view visible & mark this instance as ready. + if (viewInstance.mapboxMap.style != null) { + viewInstance.visibility = View.VISIBLE + readyCompletableDeferred.complete(Unit) + } else { + viewInstance.mapboxMap.subscribeStyleLoaded { + viewInstance.visibility = View.VISIBLE + readyCompletableDeferred.complete(Unit) + } + } + } + + override fun setMinZoom(minZoom: Float) = runWhenReady { + super.setMinZoom(minZoom) + viewInstance.mapboxMap.setBounds( + CameraBoundsOptions.Builder() + .minZoom(minZoom.toMapboxZoom()) + .build() + ) + } + + override fun setMaxZoom(maxZoom: Float) = runWhenReady { + super.setMaxZoom(maxZoom) + viewInstance.mapboxMap.setBounds( + CameraBoundsOptions.Builder() + .maxZoom(maxZoom.toMapboxZoom()) + .build() + ) + } + + override fun notifyCameraUpdate(cameraUpdate: CameraUpdate, animation: Animation?) = runWhenReady { + super.notifyCameraUpdate(cameraUpdate, animation) + + fun setOrEaseCamera(cameraOptions: CameraOptions) { + if (animation == null) { + viewInstance.mapboxMap.setCamera(cameraOptions) + } else { + viewInstance.mapboxMap.easeTo( + cameraOptions, + MapAnimationOptions.Builder() + .duration(animation.duration) + .build() + ) + } + } + + with(cameraUpdate) { + when (this) { + is CameraUpdate.Companion.CameraUpdateBasedOnBounds -> { + viewInstance.mapboxMap.cameraForCoordinates( + points.map { it.toMapboxPoint() }, + CameraOptions.Builder() + .padding(padding.toDouble().let { EdgeInsets(it, it, it, it) }) + .build(), + null, + null, + null + ) { + setOrEaseCamera(it) + } + } + + is CameraUpdate.Companion.CameraUpdateBasedOnPoint -> { + setOrEaseCamera( + CameraOptions.Builder() + .center(point.toMapboxPoint()) + .build() + ) + } + + is CameraUpdate.Companion.CameraUpdateBearing -> { + setOrEaseCamera( + CameraOptions.Builder() + .bearing(bearing.toMapboxBearing()) + .build() + ) + } + + is CameraUpdate.Companion.CameraUpdateTilt -> { + // Mapbox SDK refers to tilt as pitch. + setOrEaseCamera( + CameraOptions.Builder() + .pitch(tilt.toMapboxTilt()) + .build() + ) + } + + is CameraUpdate.Companion.CameraUpdateZoom -> { + setOrEaseCamera( + CameraOptions.Builder() + .zoom(zoom.toMapboxZoom()) + .build() + ) + } + } + } + } + + override fun addMarker(marker: Marker) = runWhenReady { + if (markers.contains(marker.id)) { + error("Marker with ID ${marker.id} already exists.") + } + var options = PointAnnotationOptions() + marker.point.let { + options = options.withPoint(it.toMapboxPoint()) + } + marker.title?.let { + options = options.withTextField(it) + } + // Mapbox does not include a default marker icon. + (marker.drawable ?: R.drawable.marker_icon_default).let { + options = options.withIconImage( + AppCompatResources.getDrawable(viewInstance.context, it)!!.toBitmap() + ) + } + markers[marker.id] = pointAnnotationManager.create(options) + } + + override fun addPolyline(polyline: Polyline) = runWhenReady { + if (polylines.contains(polyline.id)) { + error("Polyline with ID ${polyline.id} already exists.") + } + polylines.add(polyline.id) + viewInstance.mapboxMap.style?.addSource( + geoJsonSource(MAPBOX_POLYLINE_SOURCE_ID + polyline.id) { + feature(Feature.fromGeometry(LineString.fromLngLats(polyline.points.map { it.toMapboxPoint() }))) + lineMetrics(true) + } + ) + viewInstance.mapboxMap.style?.addLayerBelow( + lineLayer( + MAPBOX_POLYLINE_LAYER_ID + polyline.id, + MAPBOX_POLYLINE_SOURCE_ID + polyline.id + ) { + lineCap(LineCap.ROUND) + lineJoin(LineJoin.ROUND) + lineWidth(polyline.width.toDouble()) + lineColor(polyline.color) + lineBorderWidth(polyline.borderWidth.toDouble()) + lineBorderColor(polyline.borderColor) + polyline.colors?.let { + lineGradient( + interpolate { + linear() + lineProgress() + for (i in polyline.points.indices) { + stop { + literal((i + 1).toDouble() / polyline.points.size.toDouble()) + color(it[i]) + } + } + } + ) + } + }, + MAPBOX_MARKER_LAYER_ID + ) + } + + override fun removeMarker(marker: Marker) = runWhenReady { + if (!markers.contains(marker.id)) { + error("Marker with ID ${marker.id} does not exist.") + } + markers.remove(marker.id)?.also { pointAnnotationManager.delete(it) } + } + + override fun removePolyline(polyline: Polyline) = runWhenReady { + if (!polylines.contains(polyline.id)) { + error("Polyline with ID ${polyline.id} does not exist.") + } + polylines.remove(polyline.id) + viewInstance.mapboxMap.style?.removeStyleSource(MAPBOX_POLYLINE_SOURCE_ID + polyline.id) + viewInstance.mapboxMap.style?.removeStyleLayer(MAPBOX_POLYLINE_LAYER_ID + polyline.id) + } + + private fun Point.toMapboxPoint() = com.mapbox.geojson.Point.fromLngLat(longitude, latitude) + + private fun Float.toMapboxBearing() = this + .times(MAPBOX_CAMERA_BEARING_MAX - MAPBOX_CAMERA_BEARING_MIN) + .div(CAMERA_BEARING_MAX - CAMERA_BEARING_MIN) + .toDouble() + + private fun Float.toMapboxTilt() = this + .times(MAPBOX_CAMERA_TILT_MAX - MAPBOX_CAMERA_TILT_MIN) + .div(CAMERA_TILT_MAX - CAMERA_TILT_MIN) + .toDouble() + + private fun Float.toMapboxZoom() = this + .times(MAPBOX_CAMERA_ZOOM_MAX - MAPBOX_CAMERA_ZOOM_MIN) + .div(CAMERA_ZOOM_MAX - CAMERA_ZOOM_MIN) + .toDouble() + + + companion object { + internal const val MAPBOX_CAMERA_BEARING_MIN = 0.0F + internal const val MAPBOX_CAMERA_BEARING_MAX = 360.0F + internal const val MAPBOX_CAMERA_TILT_MIN = 0.0F + internal const val MAPBOX_CAMERA_TILT_MAX = 60.0F + internal const val MAPBOX_CAMERA_ZOOM_MIN = 0.0F + internal const val MAPBOX_CAMERA_ZOOM_MAX = 22.0F + + internal const val MAPBOX_MARKER_LAYER_ID = "marker-layer" + internal const val MAPBOX_POLYLINE_LAYER_ID = "polyline-layer-" + internal const val MAPBOX_POLYLINE_SOURCE_ID = "polyline-source-" + } +} diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/provider/mapbox/MapboxMapProvider.kt b/org.envirocar.map/src/main/java/org/envirocar/map/provider/mapbox/MapboxMapProvider.kt new file mode 100644 index 000000000..929fae9bc --- /dev/null +++ b/org.envirocar.map/src/main/java/org/envirocar/map/provider/mapbox/MapboxMapProvider.kt @@ -0,0 +1,53 @@ +package org.envirocar.map.provider.mapbox + +import android.content.Context +import com.mapbox.maps.MapView +import org.envirocar.map.MapController +import org.envirocar.map.MapProvider + +/** + * [MapboxMapProvider] + * ------------------- + * [Mapbox](https://www.mapbox.com) based implementation for [MapProvider]. + * + * Following options are available to be configured: + * + * @param style + * The style for the map to be loaded from a specified URI or from a JSON represented as [String]. + * The URI can be one of the following forms: + * * `mapbox://` + * * `http://` + * * `https://` + * * `asset://` + * * `file://` + * The default style is `mapbox://styles/mapbox/streets-v12`. + */ +class MapboxMapProvider( + private val style: String = DEFAULT_STYLE +) : MapProvider { + private lateinit var viewInstance: MapView + private lateinit var controllerInstance: MapboxMapController + + override fun getView(context: Context): MapView { + if (!::viewInstance.isInitialized) { + viewInstance = MapView(context).apply { + mapboxMap.loadStyle(style) + } + } + return viewInstance + } + + override fun getController(): MapController { + if (!::viewInstance.isInitialized) { + error("MapboxMapProvider is not initialized.") + } + if (!::controllerInstance.isInitialized) { + controllerInstance = MapboxMapController(viewInstance) + } + return controllerInstance + } + + companion object { + private const val DEFAULT_STYLE = "mapbox://styles/mapbox/streets-v12" + } +} diff --git a/org.envirocar.map/src/main/res/drawable-hdpi/marker_icon_default.png b/org.envirocar.map/src/main/res/drawable-hdpi/marker_icon_default.png new file mode 100644 index 0000000000000000000000000000000000000000..8b0af4c6ab8c454be95b4765a3898fa5d67d0067 GIT binary patch literal 1520 zcmVWN}=HIdwYsq>HX6Vp1j}WG``&$)bn-*ROo1!`-_!`FN*rcxAr@`RFn z73$*>((8at2l}aidJi-<(#wGW`Q2_^FXBr}G#ri6*z9a>6aYVG8qCmMcv&aQ^ydEk zhQMMSxH&#fv1C&9p5dnq!8s_r1oT-1xbS`(+_76wXrJFt$#fbc(Ed(URok_dmCM_} z48^MbVyB=Q@V^*7isyC#MR@dL)xI7K(oPFD&U=y}Q{DVc3cy|&jcyFCw*8o8G=I3d(0OVrBuhiF5M?-^VTi6DJ zSNci*dB)DJckxXd;#5yJCMW3*uh8bsPC9b_Jbe-ls|ELXhIgo=C&6d~6GPpV?NObs zry~ms^y=~B^xnmbwD<7g|2*M)Ji|NGL0xUHw-_q;EewI)++uixhYnHK*|VB;@D6oQ zS8uMt0u$i&8ds%en#rNwleRh?BXSYA8r zf?Nh6nIFg6^J33diI0pJZw{hPu8k3RXj)OUho-8EJQYqFyl{cmIB{qR?@$MI(MC&x zHKc;$$tB(1z0n+?g^39oIB}vV9MA9$bx>D#RI;og2PbY{W^<#v-YrvYb@85hT`ux) zxi>aO!6g=|Rp{HTt@J#fP_#6|9UPgN!R=Qzk+c`g>`Dle zyPTn>3c{s@4+;mleZb(rkheekY+`r%+2xSBkG*!#aFc+r%_Y{>vTJ}nH`4|i$xnCg za;UB2FnA=l{QCI;O~hiR!bNsERqs&DKq1%$1FWrZ;vNj}B{KN zET2;B_OynHY79NI7q=D{L#z3sJAo00Bf*}3gfO}O;DH*eYP^cKkZE9)`$ zWRI8+U4g{B-*=t_030000m{G8B)KoXJFa>pBy66(wY{N1{V$IeF+19SHcT z6u!wx>Kh-Ywd`huNn7=Ohk*=HT|M7$glc?V+76YwcTrt+HGNN~8&5eL(u>;+Am2oF`sBU=Mo2*bi}Ud?l#qhEy>oiS&DPrAXu9LANYbpjS$iC z=2g_qjQ8{N)H5?fi~IJ`oYP6~Mn>poE=RRmlXReI@Ly?%6|^ zBN6h~*Kc*13179f5wY$#>x=+L|K#LFq}&)Vy%DK>Tv(vf?d^FoH4N>kHsZ`C`lHvnlCvw70rk7d}-2OGCW*D&88mdA?;f;6Xy?E;tH z<$FW1S<9of9pD3BaOhnoX71%{sJB>@r2rrJf+J6;q4oXrG?_=J6krP<_!{Ssv?x)_ z&c2C>9TRG{otiJ1BLr^7mzJWRcaS=4U<)7fNg+d&_z1OrnEqBWb?9INTg``xMM$?R zE8i+Vvkyz9-ZMH%uz{^vmmjQfIP9pZsyfKaP(lz!gPF_;!Db7XZ=!)%j7Am~#-6*~ zw-mcwVjqzuttd(^Psn6W+>tEqtrKovcjt1Say1`+ObS+3SR9mZD$x`##@|9n5*! zI5shs>|fmc@Qvnci0*J56TGa@y7niKS;nWPrZz4AzO)gNH1U>54<|%nqID{xj55k7 gql_~COZ*aG0OGQs<%kp{B>(^b07*qoM6N<$f?LhmRsaA1 literal 0 HcmV?d00001 diff --git a/org.envirocar.map/src/main/res/drawable-xhdpi/marker_icon_default.png b/org.envirocar.map/src/main/res/drawable-xhdpi/marker_icon_default.png new file mode 100644 index 0000000000000000000000000000000000000000..d05c82bfe28d9e4d87f8cce2b74a6b39b4805c1f GIT binary patch literal 1995 zcmV;+2Q>JJP)v@uO-+Qf8KOssBPh>J=a zQEaH95(bcmj(~&1LuWce|L<}R9Ol8jceqd&_e&1<&b{aH&42#a`3DDOh;NFD>iE*g zZ7(-Jx4MKbo!mm)+PJlTHa^~&$vO^`q^kJx3Ac~ARhe9?kK0e&T9|B~r39H6By^nH z=ki&OD~l>iim5z5pS)|<(0abAc=YTUO+J54!+aeW9itbqf7REgxivG<(6R|Kxdy)c zCbp5v3JPdjSvge}7nu^@z}Pr-jSSPs#DshVatE2@)r<);xf4Qinw0j|)+R~HEFjso z2M;vq3P6%4OcP{sKMH-L+I8!wzPg&+E*DvWNGwL}fdD;t{8&DMUN)ub>(V0!sqL(& zAeYlgRsnNuEGnXW-j)9GaXhNq@9|V!nwhzvOOVOI5WbM)HkFprN}(t(FOlQ~lWb4B z#lpAU!hL;B4Q(weqt(DaBO}z_+bcirRDEXRzF}*rs<>p;$N^XjYpNWQ2#T$(gMWLc zb`z~OuqM_92f-ytg2(5!*1WY&Mj~`S6rzq`P;0oyGc&-NSX<=+WC`iPV*T)SW{hHF zYKm^Ks~F;IDr{gQ*v&4gjITz3wL2a@gpc%t6YFA&CMvcb?ECsRDl4hf=QA|fAFSBd zhK48{jcT7?-M5dn95_H5_v|5$-%q1GJ=(i_PoGjG9;XU}^Ew<38ssrB1-9t#9U=h; z;K@hz^@g?=RS^?Hasu$MiySjGV&^ zLG4_iWVq?AD>qlWc3-}%`8lINUyM=bSkf35xP+YIcu3<@`pWC&Z@)q42b{1`$i5is zH6THSOXmW?nQ3D#gJUaz2+`Bps@orZWHgaubqUTaCfKGOTF1r=4MyNc*aCh(a)hp* zJehL4@eJ?KM}|71NrS?r%^}XbT6F$!*Dme!W;9ulAy4{7=}KS_Qs>OXVX1$=c67*( zg$|K!;oRG3HqEy>-AYLRQ7$={C7$Cn)z~#WY>FTjexJV-!WSZhbi!~YuvV{0mjZx; z!igk7X!|K7ia1!CJ)DEc<_yTBI@Ajn6B3-}9JQ#CwBY{cz`*|&HI%hmWZg!MBxf|e znwT);w5Ub*+WsT-krQvk>AWF0$SXSo>8Vu%E z{N8E939IlyT9oe^99j{#gE=v`z>#$I>a-a($_{+FisJXRo*v5yVop(5zbe+sDCKXG z2GbJ^T1F0YDTdLM@tH(5nD*X)C0mU-FxOIsk+#B`4xEmm>RN}A#CzMeEsNl#t}ZFP zPA0e0s#D$kRwG}_6c}rCYfTXpHDk@HZ}kuGp=Qc7IgG(r#cDe(BUoKs?f4fk(_8OMz0hW8Y1^+AVB?m z5Sk?lw|c!zA*U1T7V+FA++v|nXvP#lCYGxZb1j^rossf*yuUCWe>WHoQ%Rne3UWP5 zlEZIw_*F?etz~C_c|9lRe!}{GUJGrSiAHq^LTY@e;g*wX!MoYneOseZ-E_Lxoa=8!A`P?gc=40T zNvc`9HtDA_lya|hcT@PqbBa0~_rJ~0KWe}pAjybI(y5Sx7%Ge^{e`@|PoYy|Oi*s2 z3OvI*^g&+}_EaIbBw2B}p3y8i{kvS@dv5npnSrbR5%)4fMj!MwLC__*BoXwRbsZNy zo{pd^>+88t*sCRTh2r<1Q0_rX%wjtOUh>0a^PNy z>E*y!AQ0$Fl;H4krDzdig3DqfVWo`0lV@Vh7Gr>=H<>b$z=Ok+qk?9Y0hyE>$c-%- ziv>xX*z-h;Tn(To-OJ>jnM(OZY=&2NuMjcp)^MWKOrJ?aaw(h3f~~|EIK;%3|G!%) z$ohnidW2+i4QwAIg$yOkq{3vwh7B7wY}l}2!-fqTHf-3iVZ(+E8#Zj%uwlc74I4IW d=-@vA1^|>xdY&#X)Q$iE002ovPDHLkV1g93zViS8 literal 0 HcmV?d00001 diff --git a/org.envirocar.map/src/main/res/drawable-xxhdpi/marker_icon_default.png b/org.envirocar.map/src/main/res/drawable-xxhdpi/marker_icon_default.png new file mode 100644 index 0000000000000000000000000000000000000000..703b172c154d584d8c46e9cec018410bd2fc7075 GIT binary patch literal 2998 zcmbuB_ct4k_r|FjrCO?}P+B8++q4=4rA85I#0a5CtcV)5M}3P3wO6Q-)K)cW)7mQb zY>b$-OU;%dsE_~Qd(XM|InO!I{rUAo8yaXcGXfZ?sHm8Ao@*FiDfntj80fBUGgF-K zm2i1$T6mkdJ9_)sc{xz2A>HjA9_YB)IXM_R*dYTv`y8%5m@ITO)S&(|+i+_%w1*vk z(Lk<)1)cgZOpC~T_>DkmemJBC2%$)&qNco*)yoA%G29ar#svNeOZ{C3V7epxFL9{t`uYI!~4 zH}YO?BU`CP#SC~}+N59*s#vm9SRbBw!fS3YWXQc5iY+->IZG%mmRn04*pTTnqtqnK z9D|IWP#vt00$T+4{zQrNw&?7X%W^@a>W-uQFO%;8^Q6aK;>I1zD&otnTeF4ojPkoX z?N%~k$!Eu8v}Dh?-yXp2_oT$i^fk*p;Bq*-&|uF7HR^2W`71XUJMPR(`~xTU2Vi-* zu4Fd0!d||-$(3)UkXv+B3z7^QW7_)|o0WVrNli+{WY&jzg4h8B~W>}Y;jG{l?U313-3+oHE` z6l)cH1q+lVcTWd9{BfLw#5;>h>ksdAF>#U(+@ihPC1Dx+uCom_7J{8P#-r7pS@TNA zDOoO@(Cv}2gB%KFl8wCW*%J{hPW+Yd?RaAoxHwlEjiI5gCo!B{pjZ0S#Fd^nIm7vP z1jaAwZ?KU=<}Ns!_Xm)~Mx#2aYP9n!d7hc=aQfwa8-8Vt0x1jLF0ckzY}qE`prPSa zIpa{~!}O6Fw!VVx4jI+G0+}4+5sAVIfiScn3sTBFnk)SpGCVdhO<-6-{MC4Hg+%OZ zdP#m-9CO(W>n%E1^Bb5a9p`<&*h8|&wP}v=M zh3lsJfoj zB}&Mnx&*J)XfD};hi6`fq?OOs#b^=&h#=qNnaA>P)^Bnv1SC7KCtHaS7FPIW-uhhp z^G*sUHiJbV6n%x@!({QOR_`BFL9Z5DgX*UbORlgzrNC4S+W+NM3OW8$&Z&bwLv3lCv0~H%oBaUv*vTKUlx<7&?JF%9!+JRW zoc#z3Pl<@<@4UfzXxIDG$BLj)_dccxC(u{_oVHqeNr zkMKKrf4J4<0(&fxW+{E-Puo=Ky=i$AW(ewzfdDXG!iim)gEcm4yROT9sKUpc-PwxZ zvClVIq5jlT6xr|lm+WBSSdS{e+3!?oV=(nM;#E|y6>HUrQBQ~N)^1P=J;OBG#d<@8 zcqp70lhUZq-&t<;1x08(GyF?pXr!gEL|>=)T0SrYaYZn>FIFgJ&ru8RUF!{8haX~G z$FQ(P-&_yb>cZU4Rh6JA&z=%!ujOvg3bS^BCv!zwT}*3^`qCq4!IBQ=$9va~+@G69 zM!()br*I%W%c&^dDFm@>x{Gs+*Ot*BWi$=eV6q*fi-Ayj@ya;jqv4JwS=iwOZ^Bnu zCyzTT<`w>jf7Cg-^Pz)718zpSEEuYc^|Sf;GCUC}qz4X*iFnggggIQ#VCa4`O5W6A zz1??4lZ999#71%tF`m2FK7E6YU1JULvJTYxHc^RQ=$sVJv|b$5eQyTbGJVg{n2dMi zvjIu9I!2YrOs;9~372Ux-D?@*4Lu7Im6a zFB+VXsTgg;vPEX4mzak2WlnY(Ire#yvQXs@lN4;%a+k*eTdAl>HJ^=$)s{nojSAPo z&NvA=l!O^fb{h6DSdTd6uJ2^yY7DGfXAqO+GfNlFTYj@A0Iq6Le3MgdCWlz= z8h=6#Ph0({?vW4;G7QZW;P?q34|z76rpm`xso0sQGaT@~7CtU-_pg9){p|@!|82YL zQw||V8*i)jJRg_VnP!$4d(*(Wr%8p2{r&!*Feke`npH5weUdSatjH|&mvXpMv~*<< zJCR7P64Tpi_i*q10C^UUXT<@3$9mWj;9qKy5uWaSMiL-q1>j(gn%xWP`iT#*#x6hb z{W!d&(pfsX1@ep)BAFiSbzrY4AU(%)oT~Q(4m9KFT#oK9y4dXQe=omVnBkQ^uYzVX z(^pDsg|Sw~1^uuw&?m=55~O#$g520D(X*-6tnzX@UV&F;Zmm|TCt|4iuPv9o+u9{cGDK#?L!(f+68;lKnT)mGojo(ghgCNl>ko~3FTa@YM2`KH|yE) z0G+3VV!K4M1oEg6S`ovTBF#(HZTC7od zdASDwyjk)B&~KJ07SWVGS-;hP1Q_J*_|b+ZWCh8zOV9?0L%6>_odBJFYeIJD44noG zgvMxvqCDp$BiA%2k=re%_BV<G9xB%_1Xux3p6yH=89bjT?ZOEDZnH5eZ>=1KE}e;57+;c5sE}( zXA5e10Uh0Zh^n^Khh)YiofkO%IpauffLh;Qcm+m1(KiX!$2D3ua(uifFV8D(9w^FI zG0pLRDPup?K!Qi0_@&Ua3-%c8ci&hWmmo)iuKIGQoOG)H70*GOeX?THDd>v*Kfw zG6m7wK6>Ok)v*9!=4w%^Q)%HHtFmz;Oom{!`F&Hcp~BO5-7$_6tuJIodbZ;*2Gxgb zo?th0l@;~J2huwb7P6+(J*&3BC|L<%d0iXK9b4 YkEChNJ2NU=6>%yZO#_Vzb=$E20b+i|v;Y7A literal 0 HcmV?d00001 diff --git a/org.envirocar.map/src/main/res/drawable-xxxhdpi/marker_icon_default.png b/org.envirocar.map/src/main/res/drawable-xxxhdpi/marker_icon_default.png new file mode 100644 index 0000000000000000000000000000000000000000..8331ffef71de3726f45963f5d013c472693c5fff GIT binary patch literal 4006 zcmb_<@p~ygxmX_|2l#Px}7&1aei!@_|lo$v&QhMZwAqcX8 zlp`d5bl2ni{tM6Z;oRpsU(R(to$K6jMh04pU`{Xq0ASS7R)7AFjs9Wo3h3WWpPHZi zN4EksECWq^+yX#(Re8o2$b`9z^8zHZ-0m$MP zHBk=VX(f563+ax7qVW*RV1RbAXtMmi47&$NVQAJ`OU5ZM4?M zq)E%s!Lg&>-ktHD2#~{Jo@fmR*fx{@PiM*DDbrhgsDW#ZS;w- z=fyA9M+vT_Iss_IW6)cCRKN1a(I&o-5Md!NDKPagojb9~^G`U_b~SW>VKwp)pUiY1 z#3W&B)gNy*QkT!jWvQVN19sj-%E@7U#Kl^i7z)9>dcmQGde3H7GjzJ*O!%%uMJZA2{+T`$IPhb$bT<}kG9xtU^0QsZ zpHRcAXW>d%34JV|3^8Ki&FpPxv8m7HTM=f3x_Bq+Z{w4_UqFU z7AA2p?U;MJ!9csldr03iU4@RiBm&eJ#q4lFaF}>%2%W3_1;IznwHz@h-=|+4fekwrX?q4+Bj=c==&Xy$G$6e}Q8~yX zrM;82^&TzW{zls{4%eUBC~?o>=bY&e)rHe|%p>F(*ArL9TVLyL@gKJBJvX<`GT<`K z%zC_g4VdO5H&Uau`MpRlAZ-F-m|Q2R#IyC8#Wg?^o5uM*jPq`bqlEw-&%uEGadi%qkC9|}EafppKAo*y(=!uJ*{DyV>B@6z zlFtTsOhE$di?2d6C+@$Qc{T7zMX#aEOU?e0TNpzHkUc20FK4?Zm6q6VWD;4kNLn>W zGFGTwpKM`&_z~5Isnh0+$Z;|8-y^xh+KRogh>gL~htC*sVqrZT9JRr#=AJQpV_U-Z zNHax9x`N*soZPg_4#XJ8f(#_YS9sgk zm=|4#j~)^_b#TJB^j6(9CIlEMIW8 z7;R9Y_{MQ*n$A0`m*JpfDVb zFP)TtKC!4IBRJS}TVx}Ie(miI8*8^OGBT4c9xoLoCSWNq?d76Kxg(H1QpLSs7mvQo zOTWt-sj>cbc+M@rw|lBueCz)Ot1sKqx)we#*|N3#TwP;Oz0jYhrH`qWtD0_uVU{;w zqP}JI^`%m?wdL6q#>`1uQqIXa(~N2xuZ>s_V`jfL(QZgxHSH z6TTZpbA?}0Ev+m&t!;^6deA}+TWe^*OFV_BTIof$-UqRt+ET^0wAGcIbahdhfFD*) zoz^ZqGC)u5t8YO<3Y%hsOZiv63BsENXMF)|bdr-IujQ3n{{x7gK;A#j>x4ay0=*+@nZ~pl0tDg)}mZU^^&ChIwDCz)6{Q-eIZYi~D0uz6I;_ z0}6uoPcqFiaN%qaR8rUgx$(|4Ae0vmpr|kokc4%Lo`fgtzs6xs>$D z`$u5H*l}e3LC;p?3FSw>79Q_PWAr2I+7I_KU?I!Yw(lbWR+HbQ;ww=LI9;OFJOU&# z?5+$wC1Sb6|57+%T6}CW;&in%Y|e@f%vasp2EbNvcZZUdUjjvzI-#vpam#}X&ix%~ z7i=%vgxvGLIem8`#T&)UeN1dxliq1q^TFhvWYVB6EjOK3`%;3~ehL_x6p4AH z#hu_h9W5*@V7FOGonH+uyaMmAtZX)f9hcj(fBJkp83cp%rwQmM$ul7@9Hws2+d4rn z@VU9auXi}2>tRA|ACEK8y?+KYsNO~;fITqBW|+~JGjqp7pFS0*j*o{v-_+=B%`Y$v zzZZWMu}0D1$T~1jiGJD^Uf*=m(7iCyv$-w*aCZ$%*J#QT!OhJXas88@c-~I+mPye_ z-{JMRA*Y^7D_y9BowHdH%hbq7Av>?Zw33imq697V>(^ge!`lY5M%?y_%seO>4gh~& zC;R#FvtE6Bs<3NVaAuHZasTG-NJdSwFgBz8d?x4h&+&6nd*jrt z_QIIM3Y5bxHzi&wf}0oW!}Tq152G`JYPxTc>|<@!*t^Qy`^PVMs3!?=s&>>8bxy<7 zc8D+=mpH}GLu=qTMsfo|K>Q`JuLFqa0Mtum^(om}-CApG?o_RuX`I*|$f`Y3%a}cV zYBIZN>Ki? zJ31wCFF^02*?H$Z6-GA-f`Zzjd}?O!`FPtMwJ=TPG9K4rCVjuF$W~3sK6{!VyzVk{ zDxbF(Z2t@!nEk#pgx_;3H0O4@&h;_RIXCRq^Bw!3{hT<*#m&X-9Ysj|D=Tn&=$1Vq zFG1S44z*WfUbR(;vOS&KBXF0p@SQP!(&mPb2fTmo zN_Dc=JoT^6XbVQ!i|BbiPOgH~bAe!V8RkjXEXA%wgvRw)QtQW3e{{@&+|daCfu`MW z;{5l|hg%*Mq95k+_a;3@H;Whw-1<0aF^QZv_bXJP(J5J{pFVN}W)u*Fy8OrjKSPdx zi$e19_A1aZDuwYGC_^GDBlLGO5R?2FMFVtikR};|uXmj@ zaaO8k(Y@0l#^DOygGDU|GJXXh894KM!!?AiT!CBJUFs5L z^A8j}D72EseINnJ#OSBVrCQVXRYv;yG=Wufm-B5uCwMi}VOJ>re-TCN0V~(DsU1eoKcJC zK4`ml9qJPZe&J-&kjxsl;7PbqSBAkJCJZjKe*MGYLNdIMohvH{Sp)Y4inR#DTzl7 zdA6qfv*hZQq&BD{C{qP_g|% + + YOUR_PUBLIC_MAPBOX_ACCESS_TOKEN + diff --git a/org.envirocar.obd/build.gradle b/org.envirocar.obd/build.gradle index 383e36217..3d7a7c6b7 100644 --- a/org.envirocar.obd/build.gradle +++ b/org.envirocar.obd/build.gradle @@ -12,8 +12,8 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } buildTypes { diff --git a/org.envirocar.remote/build.gradle b/org.envirocar.remote/build.gradle index 05703e761..057609e3c 100644 --- a/org.envirocar.remote/build.gradle +++ b/org.envirocar.remote/build.gradle @@ -12,8 +12,8 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } buildTypes { diff --git a/org.envirocar.storage/build.gradle b/org.envirocar.storage/build.gradle index 3f26bf61e..7112e8aef 100644 --- a/org.envirocar.storage/build.gradle +++ b/org.envirocar.storage/build.gradle @@ -12,8 +12,8 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } } diff --git a/settings.gradle b/settings.gradle index 4aa110415..55c11514a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -12,7 +12,7 @@ pluginManagement { } credentials { username = "mapbox" - password = rootProject.properties["MAPBOX_DOWNLOADS_TOKEN"] ?: "" + password = providers.gradleProperty("MAPBOX_DOWNLOADS_TOKEN").get() } } } @@ -31,11 +31,8 @@ dependencyResolutionManagement { basic(BasicAuthentication) } credentials { - // Do not change the username below. - // This should always be `mapbox` (not your username). username = "mapbox" - // Use the secret token you stored in gradle.properties as the password - password = rootProject.properties["MAPBOX_DOWNLOADS_TOKEN"] ?: "" + password = providers.gradleProperty("MAPBOX_DOWNLOADS_TOKEN").get() } } } @@ -45,8 +42,9 @@ rootProject.name = "enviroCar" include ":org.envirocar.app" include ":org.envirocar.core" -include ":org.envirocar.remote" +include ":org.envirocar.map" include ":org.envirocar.obd" +include ":org.envirocar.remote" include ":org.envirocar.storage" include ":org.envirocar.algorithm" include ":android-obd-simulator" From c3c4ce2edb27dc99ee6e605e8540e03ca0ad7254 Mon Sep 17 00:00:00 2001 From: Hitesh Kumar Saini Date: Fri, 5 Jul 2024 11:02:56 +0530 Subject: [PATCH 02/10] Refactor track details components to use `org.envirocar.map` module (#1005) * build: depend on org.envirocar.map * fix: use mapbox_access_token from map module * fix: comment out incompatible API [WIP] * fix: remove mapbox initialization * Rename .java to .kt * refactor: migrate /tracklist to org.envirocar.map module * feat: default methods in Animation, Marker & Polyline * feat: TrackMapFactory * feat: AbstractTrackListCardAdapter MapView * fix: Marker.default * fix: AbstractTrackListCardAdapter MapView initialization * feat: MapController clearMarkers/clearPolylines & make counter Long * fix: AbstractTrackListCardAdapter avoid duplicate polylines * refactor: use getters in TrackMapFactory * refactor: migrate /trackdetails to org.envirocar.map module * fix: remove redundant IllegalStateException from MapView::getController https://github.com/enviroCar/enviroCar-app/pull/1005#pullrequestreview-2144947935 --- android-obd-simulator/build.gradle | 2 +- org.envirocar.algorithm/build.gradle | 2 +- org.envirocar.app/build.gradle | 15 +- .../res/layout/activity_map_expanded.xml | 4 +- .../layout/activity_track_details_layout.xml | 2 +- .../fragment_tracklist_cardlayout_content.xml | 28 +- ...> fragment_tracklist_cardlayout_local.xml} | 0 .../fragment_tracklist_cardlayout_remote.xml | 6 +- org.envirocar.app/res/values-de/strings.xml | 2 + org.envirocar.app/res/values/strings.xml | 11 +- .../org/envirocar/app/BaseApplication.java | 4 - .../injection/modules/MainActivityModule.java | 9 - .../envirocar/app/views/BaseMainActivity.java | 3 - .../recordingscreen/TrackMapFragment.java | 104 ++--- .../trackdetails/MapExpandedActivity.java | 249 +++------- .../trackdetails/TrackDetailsActivity.java | 140 +----- .../app/views/trackdetails/TrackMapFactory.kt | 97 ++++ .../AbstractTrackListCardAdapter.java | 433 ------------------ .../tracklist/AbstractTrackListCardAdapter.kt | 208 +++++++++ .../tracklist/OnTrackInteractionCallback.java | 57 --- .../tracklist/OnTrackInteractionCallback.kt | 13 + .../tracklist/TrackListLocalCardAdapter.java | 83 ---- .../tracklist/TrackListLocalCardAdapter.kt | 24 + .../tracklist/TrackListLocalCardFragment.java | 21 +- .../tracklist/TrackListRemoteCardAdapter.java | 126 ----- .../tracklist/TrackListRemoteCardAdapter.kt | 24 + .../TrackListRemoteCardFragment.java | 51 +-- org.envirocar.core/build.gradle | 2 +- .../java/org/envirocar/map/MapController.kt | 6 + .../main/java/org/envirocar/map/MapView.kt | 7 +- .../map/camera/CameraUpdateFactory.kt | 5 + .../java/org/envirocar/map/model/Animation.kt | 4 + .../java/org/envirocar/map/model/Marker.kt | 8 +- .../java/org/envirocar/map/model/Polyline.kt | 8 +- .../provider/mapbox/MapboxMapController.kt | 17 +- org.envirocar.obd/build.gradle | 2 +- org.envirocar.remote/build.gradle | 2 +- org.envirocar.storage/build.gradle | 2 +- settings.gradle | 2 +- 39 files changed, 615 insertions(+), 1168 deletions(-) rename org.envirocar.app/res/layout/{fragment_tracklist_cardlayout.xml => fragment_tracklist_cardlayout_local.xml} (100%) create mode 100644 org.envirocar.app/src/org/envirocar/app/views/trackdetails/TrackMapFactory.kt delete mode 100644 org.envirocar.app/src/org/envirocar/app/views/tracklist/AbstractTrackListCardAdapter.java create mode 100644 org.envirocar.app/src/org/envirocar/app/views/tracklist/AbstractTrackListCardAdapter.kt delete mode 100644 org.envirocar.app/src/org/envirocar/app/views/tracklist/OnTrackInteractionCallback.java create mode 100644 org.envirocar.app/src/org/envirocar/app/views/tracklist/OnTrackInteractionCallback.kt delete mode 100644 org.envirocar.app/src/org/envirocar/app/views/tracklist/TrackListLocalCardAdapter.java create mode 100644 org.envirocar.app/src/org/envirocar/app/views/tracklist/TrackListLocalCardAdapter.kt delete mode 100644 org.envirocar.app/src/org/envirocar/app/views/tracklist/TrackListRemoteCardAdapter.java create mode 100644 org.envirocar.app/src/org/envirocar/app/views/tracklist/TrackListRemoteCardAdapter.kt diff --git a/android-obd-simulator/build.gradle b/android-obd-simulator/build.gradle index 2db3399d7..281f0414d 100644 --- a/android-obd-simulator/build.gradle +++ b/android-obd-simulator/build.gradle @@ -1,5 +1,5 @@ plugins { - alias libs.plugins.android.application + alias(libs.plugins.android.application) } android { diff --git a/org.envirocar.algorithm/build.gradle b/org.envirocar.algorithm/build.gradle index a8521a3a1..f6c9a8109 100644 --- a/org.envirocar.algorithm/build.gradle +++ b/org.envirocar.algorithm/build.gradle @@ -1,5 +1,5 @@ plugins { - alias libs.plugins.android.library + alias(libs.plugins.android.library) } android { diff --git a/org.envirocar.app/build.gradle b/org.envirocar.app/build.gradle index 005a73744..2a4492878 100644 --- a/org.envirocar.app/build.gradle +++ b/org.envirocar.app/build.gradle @@ -1,5 +1,6 @@ plugins { - alias libs.plugins.android.application + alias(libs.plugins.android.application) + alias(libs.plugins.jetbrains.kotlin.android) } android { @@ -144,9 +145,8 @@ dependencies { implementation libs.circleimageview implementation libs.easypermissions - // MapBox SDK. - - implementation libs.mapbox.android.sdk + // TODO(alexmercerind): Remove after migration to org.envirocar.map. + implementation(libs.mapbox.android.sdk) // Testing. @@ -172,11 +172,12 @@ dependencies { // Modules. + api project(path: ":org.envirocar.algorithm") api project(path: ":org.envirocar.core") - api project(path: ":org.envirocar.remote") + api project(path: ":org.envirocar.map") api project(path: ":org.envirocar.obd") + api project(path: ":org.envirocar.remote") api project(path: ":org.envirocar.storage") - api project(path: ":org.envirocar.algorithm") implementation libs.envirocar.aidl } @@ -189,4 +190,6 @@ configurations.configureEach { } } } + // TODO(alexmercerind): Remove after migration to org.envirocar.map. + exclude module: "mapbox-android-core" } diff --git a/org.envirocar.app/res/layout/activity_map_expanded.xml b/org.envirocar.app/res/layout/activity_map_expanded.xml index ddce17ee4..f306c3f06 100644 --- a/org.envirocar.app/res/layout/activity_map_expanded.xml +++ b/org.envirocar.app/res/layout/activity_map_expanded.xml @@ -73,7 +73,7 @@ app:layout_constraintRight_toRightOf="parent" app:pressedTranslationZ="12dp" /> - - + - - - + android:orientation="vertical"> - + android:clickable="false" + android:transitionName="transition_track_details" /> + android:elevation="4dp" /> @@ -70,14 +66,14 @@ android:gravity="center" android:text="0:00" android:textColor="@color/blue_light_cario" - android:textSize="24sp"/> + android:textSize="24sp" /> + android:textSize="10sp" /> + android:textSize="24sp" /> + android:textSize="10sp" /> diff --git a/org.envirocar.app/res/layout/fragment_tracklist_cardlayout.xml b/org.envirocar.app/res/layout/fragment_tracklist_cardlayout_local.xml similarity index 100% rename from org.envirocar.app/res/layout/fragment_tracklist_cardlayout.xml rename to org.envirocar.app/res/layout/fragment_tracklist_cardlayout_local.xml diff --git a/org.envirocar.app/res/layout/fragment_tracklist_cardlayout_remote.xml b/org.envirocar.app/res/layout/fragment_tracklist_cardlayout_remote.xml index e625edaa2..bdbfe1e0d 100644 --- a/org.envirocar.app/res/layout/fragment_tracklist_cardlayout_remote.xml +++ b/org.envirocar.app/res/layout/fragment_tracklist_cardlayout_remote.xml @@ -70,7 +70,7 @@ android:layout_below="@id/fragment_tracklist_cardlayout_toolbar"> Attributauswahl + + Track wird hochgeladen diff --git a/org.envirocar.app/res/values/strings.xml b/org.envirocar.app/res/values/strings.xml index 3f170db98..b76156aa6 100644 --- a/org.envirocar.app/res/values/strings.xml +++ b/org.envirocar.app/res/values/strings.xml @@ -185,7 +185,9 @@ - Attribut Selection + Attribute Selection + + Track upload active @@ -197,11 +199,4 @@ @string/item_campaign_profile_default @string/item_campaign_profile_dvfo - - - - - public_access_token - Track wird hochgeladen - diff --git a/org.envirocar.app/src/org/envirocar/app/BaseApplication.java b/org.envirocar.app/src/org/envirocar/app/BaseApplication.java index ec5700d44..8b7ddd4e5 100644 --- a/org.envirocar.app/src/org/envirocar/app/BaseApplication.java +++ b/org.envirocar.app/src/org/envirocar/app/BaseApplication.java @@ -26,8 +26,6 @@ import android.content.IntentFilter; import android.os.Build; -import com.mapbox.mapboxsdk.Mapbox; - import org.acra.ACRA; import org.acra.BuildConfig; import org.acra.annotation.AcraCore; @@ -94,8 +92,6 @@ public void onCreate() { // hack Logger.addFileHandlerLocation(getFilesDir().getAbsolutePath()); - Mapbox.getInstance(this, ""); - baseApplicationComponent = DaggerBaseApplicationComponent .builder() diff --git a/org.envirocar.app/src/org/envirocar/app/injection/modules/MainActivityModule.java b/org.envirocar.app/src/org/envirocar/app/injection/modules/MainActivityModule.java index 5eca50baa..8e68d2158 100644 --- a/org.envirocar.app/src/org/envirocar/app/injection/modules/MainActivityModule.java +++ b/org.envirocar.app/src/org/envirocar/app/injection/modules/MainActivityModule.java @@ -22,8 +22,6 @@ import android.app.Activity; import android.content.Context; -import com.mapbox.mapboxsdk.Mapbox; - import org.envirocar.app.injection.scopes.PerActivity; import org.envirocar.app.views.dashboard.DashboardFragment; import org.envirocar.app.views.others.OthersFragment; @@ -81,11 +79,4 @@ public TrackListPagerFragment provideTrackListPagerFragment(){ public OthersFragment provideOthersFragment(){ return new OthersFragment(); } - - @Provides - @PerActivity - public Mapbox provideMapBox() { - return Mapbox.getInstance(mActivity, ""); - } - } diff --git a/org.envirocar.app/src/org/envirocar/app/views/BaseMainActivity.java b/org.envirocar.app/src/org/envirocar/app/views/BaseMainActivity.java index 772b7bc31..e3eab58e3 100644 --- a/org.envirocar.app/src/org/envirocar/app/views/BaseMainActivity.java +++ b/org.envirocar.app/src/org/envirocar/app/views/BaseMainActivity.java @@ -36,7 +36,6 @@ import com.google.android.material.bottomnavigation.BottomNavigationView; import com.google.android.material.snackbar.Snackbar; -import com.mapbox.mapboxsdk.Mapbox; import com.squareup.otto.Subscribe; import org.envirocar.app.BaseApplicationComponent; @@ -108,8 +107,6 @@ public class BaseMainActivity extends BaseInjectorActivity { @Inject protected OthersFragment othersFragment; @Inject - protected Mapbox mapbox; - @Inject protected AgreementManager agreementManager; @Inject diff --git a/org.envirocar.app/src/org/envirocar/app/views/recordingscreen/TrackMapFragment.java b/org.envirocar.app/src/org/envirocar/app/views/recordingscreen/TrackMapFragment.java index b8aeb7cdd..4d539024e 100644 --- a/org.envirocar.app/src/org/envirocar/app/views/recordingscreen/TrackMapFragment.java +++ b/org.envirocar.app/src/org/envirocar/app/views/recordingscreen/TrackMapFragment.java @@ -32,9 +32,9 @@ import androidx.annotation.Nullable; import com.google.android.material.floatingactionbutton.FloatingActionButton; -import com.mapbox.android.core.location.LocationEngineRequest; -import com.mapbox.android.core.permissions.PermissionsListener; -import com.mapbox.android.core.permissions.PermissionsManager; +//import com.mapbox.android.core.location.LocationEngineRequest; +//import com.mapbox.android.core.permissions.PermissionsListener; +//import com.mapbox.android.core.permissions.PermissionsManager; import com.mapbox.geojson.Feature; import com.mapbox.geojson.FeatureCollection; import com.mapbox.geojson.LineString; @@ -73,12 +73,12 @@ /** * @author dewall */ -public class TrackMapFragment extends BaseInjectorFragment implements PermissionsListener { +public class TrackMapFragment extends BaseInjectorFragment /* implements PermissionsListener */ { private static final Logger LOG = Logger.getLogger(TrackMapFragment.class); private FragmentTrackMapBinding binding; - private PermissionsManager permissionsManager; + // private PermissionsManager permissionsManager; private MapboxMap mapboxMap; private Style mapStyle; private LocationComponent locationComponent; @@ -296,59 +296,59 @@ protected void injectDependencies(BaseApplicationComponent baseApplicationCompon @SuppressWarnings( {"MissingPermission"}) private void enableLocationComponent(@NonNull Style loadedMapStyle) { // Check if permissions are enabled and if not request - if (PermissionsManager.areLocationPermissionsGranted(getContext())) { - - // Get an instance of the component - locationComponent = mapboxMap.getLocationComponent(); - - // Activate with options - locationComponent.activateLocationComponent( - LocationComponentActivationOptions - .builder(getContext(), loadedMapStyle) - .useDefaultLocationEngine(true) - .locationEngineRequest(new LocationEngineRequest.Builder(750) - .setFastestInterval(750) - .setPriority(LocationEngineRequest.PRIORITY_HIGH_ACCURACY) - .build()) - .build()); - - // Enable to make component visible - locationComponent.setLocationComponentEnabled(true); - - // Set the component's camera mode - locationComponent.setCameraMode(CameraMode.TRACKING); - - // Set the component's render mode - locationComponent.setRenderMode(RenderMode.COMPASS); - } else { - permissionsManager = new PermissionsManager(this); - permissionsManager.requestLocationPermissions(getActivity()); - } +// if (PermissionsManager.areLocationPermissionsGranted(getContext())) { +// +// // Get an instance of the component +// locationComponent = mapboxMap.getLocationComponent(); +// +// // Activate with options +// locationComponent.activateLocationComponent( +// LocationComponentActivationOptions +// .builder(getContext(), loadedMapStyle) +// .useDefaultLocationEngine(true) +// .locationEngineRequest(new LocationEngineRequest.Builder(750) +// .setFastestInterval(750) +// .setPriority(LocationEngineRequest.PRIORITY_HIGH_ACCURACY) +// .build()) +// .build()); +// +// // Enable to make component visible +// locationComponent.setLocationComponentEnabled(true); +// +// // Set the component's camera mode +// locationComponent.setCameraMode(CameraMode.TRACKING); +// +// // Set the component's render mode +// locationComponent.setRenderMode(RenderMode.COMPASS); +// } else { +// permissionsManager = new PermissionsManager(this); +// permissionsManager.requestLocationPermissions(getActivity()); +// } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - permissionsManager.onRequestPermissionsResult(requestCode, permissions, grantResults); - } - - @Override - public void onExplanationNeeded(List permissionsToExplain) { - Toast.makeText(getContext(), getContext().getString(R.string.notification_location_access), Toast.LENGTH_SHORT).show(); +// permissionsManager.onRequestPermissionsResult(requestCode, permissions, grantResults); } - @Override - public void onPermissionResult(boolean granted) { - if (granted) { - mapboxMap.getStyle(new Style.OnStyleLoaded() { - @Override - public void onStyleLoaded(@NonNull Style style) { - enableLocationComponent(style); - } - }); - } else { - Toast.makeText(getContext(), getContext().getString(R.string.notification_location_access_not_granted), Toast.LENGTH_LONG).show(); - } - } +// @Override +// public void onExplanationNeeded(List permissionsToExplain) { +// Toast.makeText(getContext(), getContext().getString(R.string.notification_location_access), Toast.LENGTH_SHORT).show(); +// } +// +// @Override +// public void onPermissionResult(boolean granted) { +// if (granted) { +// mapboxMap.getStyle(new Style.OnStyleLoaded() { +// @Override +// public void onStyleLoaded(@NonNull Style style) { +// enableLocationComponent(style); +// } +// }); +// } else { +// Toast.makeText(getContext(), getContext().getString(R.string.notification_location_access_not_granted), Toast.LENGTH_LONG).show(); +// } +// } @Override @SuppressWarnings( {"MissingPermission"}) diff --git a/org.envirocar.app/src/org/envirocar/app/views/trackdetails/MapExpandedActivity.java b/org.envirocar.app/src/org/envirocar/app/views/trackdetails/MapExpandedActivity.java index a9950d927..b6ed108af 100644 --- a/org.envirocar.app/src/org/envirocar/app/views/trackdetails/MapExpandedActivity.java +++ b/org.envirocar.app/src/org/envirocar/app/views/trackdetails/MapExpandedActivity.java @@ -22,31 +22,18 @@ import android.app.Activity; import android.app.AlertDialog; import android.content.Intent; -import android.graphics.BitmapFactory; import android.os.Bundle; import android.view.Gravity; import android.view.View; import android.widget.ImageView; import android.widget.TextView; -import androidx.annotation.NonNull; import androidx.cardview.widget.CardView; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.transition.ChangeBounds; import androidx.transition.TransitionManager; import com.google.android.material.floatingactionbutton.FloatingActionButton; -import com.mapbox.geojson.Feature; -import com.mapbox.geojson.Point; -import com.mapbox.mapboxsdk.camera.CameraUpdateFactory; -import com.mapbox.mapboxsdk.geometry.LatLngBounds; -import com.mapbox.mapboxsdk.maps.MapView; -import com.mapbox.mapboxsdk.maps.MapboxMap; -import com.mapbox.mapboxsdk.maps.OnMapReadyCallback; -import com.mapbox.mapboxsdk.maps.Style; -import com.mapbox.mapboxsdk.style.layers.PropertyFactory; -import com.mapbox.mapboxsdk.style.layers.SymbolLayer; -import com.mapbox.mapboxsdk.style.sources.GeoJsonSource; import org.envirocar.app.R; import org.envirocar.app.databinding.ActivityMapExpandedBinding; @@ -56,10 +43,16 @@ import org.envirocar.core.entity.Track; import org.envirocar.core.logging.Logger; import org.envirocar.core.EnviroCarDB; +import org.envirocar.map.MapController; +import org.envirocar.map.MapView; +import org.envirocar.map.model.Animation; +import org.envirocar.map.model.Polyline; +import org.envirocar.map.provider.mapbox.MapboxMapProvider; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import javax.inject.Inject; @@ -91,10 +84,9 @@ public class MapExpandedActivity extends BaseInjectorActivity { protected TextView legendEnd; protected TextView legendName; - protected MapboxMap mapboxMapExpanded; - protected TrackMapLayer trackMapOverlay; + private Polyline mPolyline; + private MapController mMapController; private Track track; - private Style style; private List options = new ArrayList<>(); private List spinnerStrings = new ArrayList<>(); private boolean mIsCentredOnTrack; @@ -130,12 +122,11 @@ protected void onCreate(Bundle savedInstanceState) { legendEnd = binding.legendEnd; legendName = binding.legendUnit; + // TODO(alexmercerind): Switch to camera API in feature/location-indicator. mMapViewExpanded.setOnTouchListener((v, event) -> onTouchMapView()); mCentreFab.setOnClickListener(v -> onClickFollowFab()); mVisualiseFab.setOnClickListener(v -> onClickVisualiseFab()); - mMapViewExpanded.onCreate(savedInstanceState); - // Get the track to show. int trackID = getIntent().getIntExtra(EXTRA_TRACKID, -1); Track.TrackId trackid = new Track.TrackId(trackID); @@ -144,8 +135,6 @@ protected void onCreate(Bundle savedInstanceState) { .blockingFirst(); this.track = track; - trackMapOverlay = new TrackMapLayer(track); - options = track.getSupportedProperties(); for (Measurement.PropertyKey propertyKey : options) { spinnerStrings.add(getString(propertyKey.getStringResource())); @@ -169,12 +158,16 @@ protected boolean onTouchMapView() { } protected void onClickFollowFab() { - final LatLngBounds viewBbox = trackMapOverlay.getViewBoundingBox(); if (!mIsCentredOnTrack) { mIsCentredOnTrack = true; TransitionManager.beginDelayedTransition(mMapViewExpandedContainer, new androidx.transition.Slide(Gravity.RIGHT)); mCentreFab.hide(); - mapboxMapExpanded.easeCamera(CameraUpdateFactory.newLatLngBounds(viewBbox, 50), 2500); + if (mMapController != null) { + mMapController.notifyCameraUpdate( + Objects.requireNonNull(new TrackMapFactory(track).getCameraUpdateBasedOnBounds()), + new Animation.Builder().withDuration(2500).build() + ); + } } } @@ -185,17 +178,32 @@ protected void onClickVisualiseFab() { b.setItems(spinnerStrings.toArray(new String[0]), (dialogInterface, i) -> { dialogInterface.dismiss(); LOG.info("Choice: " + i); - makeMapChanges(i); + addPolyline(i); }); b.show(); } - private void makeMapChanges(int choice) { - final LatLngBounds viewBbox = trackMapOverlay.getViewBoundingBox(); - if (mapboxMapExpanded != null) { - LOG.info("Choice: " + choice); + private void addPolyline(int choice) { + if (mMapController != null) { + + final TrackMapFactory factory = new TrackMapFactory(track); + + if (mPolyline != null) { + mMapController.removePolyline(mPolyline); + } + if (!spinnerStrings.get(choice).equalsIgnoreCase("None")) { + + final Measurement.PropertyKey key = options.get(choice); + + // Display gradient polyline. + + mPolyline = factory.getGradientPolyline(key); + mMapController.addPolyline(Objects.requireNonNull(mPolyline)); + + // Set legend values. + if (legendCard.getVisibility() != View.VISIBLE) { TransitionManager.beginDelayedTransition(legendCard, new androidx.transition.Slide(Gravity.LEFT)); legendCard.setVisibility(View.VISIBLE); @@ -203,170 +211,53 @@ private void makeMapChanges(int choice) { TransitionManager.beginDelayedTransition(legendCard, new ChangeBounds()); } - mapboxMapExpanded.getStyle(new Style.OnStyleLoaded() { - @Override - public void onStyleLoaded(@NonNull Style style) { - //Remove current gradient layer - style.removeLayer(TrackMapLayer.GRADIENT_LAYER); - style.removeSource(TrackMapLayer.GRADIENT_SOURCE); - - //Add new gradient layer based on choice of data - style.addSource(trackMapOverlay.getGradientGeoJSONSource()); - style.addLayerBelow(trackMapOverlay.getGradientLineLayer(options.get(choice)), "marker-layer1"); - - //Set legend values - try { - legendStart.setText(DECIMAL_FORMATTER.format(trackMapOverlay.getGradMin())); - legendEnd.setText(DECIMAL_FORMATTER.format(trackMapOverlay.getGradMax())); - Float mid = (trackMapOverlay.getGradMin() + trackMapOverlay.getGradMax()) / 2; - legendMid.setText(DECIMAL_FORMATTER.format(mid)); - legendName.setText(options.get(choice).getStringResource()); - } catch (Exception e){ - LOG.error("Error while formatting legend.", e); - } - } - }); + try { + legendStart.setText(DECIMAL_FORMATTER.format(factory.getGradientMinValue(key))); + legendEnd.setText(DECIMAL_FORMATTER.format(factory.getGradientMaxValue(key))); + Float mid = (Objects.requireNonNull(factory.getGradientMinValue(key)) + Objects.requireNonNull(factory.getGradientMaxValue(key))) / 2; + legendMid.setText(DECIMAL_FORMATTER.format(mid)); + legendName.setText(options.get(choice).getStringResource()); + } catch (Exception e){ + LOG.error("Error while formatting legend.", e); + } + } else { - //None gradient chosen. So remove the gradient layers + + // Display polyline. + + mPolyline = factory.getPolyline(); + mMapController.addPolyline(Objects.requireNonNull(mPolyline)); + TransitionManager.beginDelayedTransition(legendCard, new androidx.transition.Slide(Gravity.LEFT)); legendCard.setVisibility(GONE); - mapboxMapExpanded.getStyle(new Style.OnStyleLoaded() { - @Override - public void onStyleLoaded(@NonNull Style style) { - style.removeLayer(TrackMapLayer.GRADIENT_LAYER); - style.removeSource(TrackMapLayer.GRADIENT_SOURCE); - } - }); - } - mapboxMapExpanded.easeCamera(CameraUpdateFactory.newLatLngBounds(viewBbox, 50)); - } - } - - @Override - public void onBackPressed() { - super.onBackPressed(); - } - private void initMapView() { - final LatLngBounds viewBbox = trackMapOverlay.getViewBoundingBox(); - mMapViewExpanded.getMapAsync(new OnMapReadyCallback() { - @Override - public void onMapReady(@NonNull MapboxMap mapboxMap1) { - mapboxMap1.getUiSettings().setLogoEnabled(false); - mapboxMap1.getUiSettings().setAttributionEnabled(false); - mapboxMap1.setStyle(new Style.Builder().fromUrl("https://api.maptiler.com/maps/basic/style.json?key=YJCrA2NeKXX45f8pOV6c "), new Style.OnStyleLoaded() { - @Override - public void onStyleLoaded(@NonNull Style style) { - MapExpandedActivity.this.style = style; - //Set normal source and line layer - style.addSource(trackMapOverlay.getGeoJsonSource()); - style.addLayer(trackMapOverlay.getLineLayer()); - - mapboxMap1.moveCamera(CameraUpdateFactory.newLatLngBounds(viewBbox, 50)); - setUpStartStopIcons(style); - - if (options.contains(Measurement.PropertyKey.SPEED)) { - makeMapChanges(options.indexOf(Measurement.PropertyKey.SPEED)); - } else { - makeMapChanges(options.indexOf(Measurement.PropertyKey.GPS_SPEED)); - } - } - }); - mapboxMapExpanded = mapboxMap1; - mapboxMapExpanded.setMaxZoomPreference(18); - mapboxMapExpanded.setMinZoomPreference(1); } - }); - } - - private void setUpStartStopIcons(@NonNull Style loadedMapStyle) { - int size = track.getMeasurements().size(); - if (size >= 2) { - //Set Source with start and stop marker - Double lng = track.getMeasurements().get(0).getLongitude(); - Double lat = track.getMeasurements().get(0).getLatitude(); - GeoJsonSource geoJsonSource = new GeoJsonSource("marker-source1", Feature.fromGeometry( - Point.fromLngLat(lng, lat))); - loadedMapStyle.addSource(geoJsonSource); - - lng = track.getMeasurements().get(size - 1).getLongitude(); - lat = track.getMeasurements().get(size - 1).getLatitude(); - geoJsonSource = new GeoJsonSource("marker-source2", Feature.fromGeometry( - Point.fromLngLat(lng, lat))); - loadedMapStyle.addSource(geoJsonSource); - - //Set symbol layer to set the icons to be displayed at the start and stop - loadedMapStyle.addImage("start-marker", - BitmapFactory.decodeResource( - this.getResources(), R.drawable.start_marker)); - SymbolLayer symbolLayer = new SymbolLayer("marker-layer1", "marker-source1"); - symbolLayer.withProperties( - PropertyFactory.iconImage("start-marker"), - PropertyFactory.iconAllowOverlap(true), - PropertyFactory.iconIgnorePlacement(true) + mMapController.notifyCameraUpdate( + Objects.requireNonNull(factory.getCameraUpdateBasedOnBounds()), + null ); - loadedMapStyle.addLayer(symbolLayer); - - loadedMapStyle.addImage("stop-marker", - BitmapFactory.decodeResource( - this.getResources(), R.drawable.stop_marker)); - symbolLayer = new SymbolLayer("marker-layer2", "marker-source2"); - symbolLayer.withProperties( - PropertyFactory.iconImage("stop-marker"), - PropertyFactory.iconAllowOverlap(true), - PropertyFactory.iconIgnorePlacement(true) - ); - loadedMapStyle.addLayerAbove(symbolLayer, "marker-layer1"); } } - @Override - public void onStart() { - super.onStart(); - mMapViewExpanded.onStart(); - } - - @Override - public void onResume() { - super.onResume(); - supportStartPostponedEnterTransition(); - mMapViewExpanded.onResume(); - } - - @Override - public void onPause() { - super.onPause(); - mMapViewExpanded.onPause(); - } + private void initMapView() { + // TODO(alexmercerind): Retrieve currently selected provider from a common repository. + if (mMapController != null) { + return; + } + mMapController = mMapViewExpanded.getController(new MapboxMapProvider()); - @Override - public void onStop() { - super.onStop(); - mMapViewExpanded.onStop(); - } + final TrackMapFactory factory = new TrackMapFactory(track); - @Override - public void onLowMemory() { - super.onLowMemory(); - mMapViewExpanded.onLowMemory(); - } + mMapController.setMinZoom(factory.getMinZoom()); + mMapController.setMaxZoom(factory.getMaxZoom()); + mMapController.addMarker(Objects.requireNonNull(factory.getStartMarker())); + mMapController.addMarker(Objects.requireNonNull(factory.getStopMarker())); + mMapController.notifyCameraUpdate(Objects.requireNonNull(factory.getCameraUpdateBasedOnBounds()), null); - @Override - protected void onDestroy() { - super.onDestroy(); - if (style != null) { - style.removeLayer(MapLayer.LAYER_NAME); - style.removeLayer("marker-layer1"); - style.removeLayer("marker-layer2"); + if (options.contains(Measurement.PropertyKey.SPEED)) { + addPolyline(options.indexOf(Measurement.PropertyKey.SPEED)); + } else { + addPolyline(options.indexOf(Measurement.PropertyKey.GPS_SPEED)); } - if (mMapViewExpanded != null) - mMapViewExpanded.onDestroy(); } - - @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - mMapViewExpanded.onSaveInstanceState(outState); - } - } diff --git a/org.envirocar.app/src/org/envirocar/app/views/trackdetails/TrackDetailsActivity.java b/org.envirocar.app/src/org/envirocar/app/views/trackdetails/TrackDetailsActivity.java index 09cac328a..12141bc33 100644 --- a/org.envirocar.app/src/org/envirocar/app/views/trackdetails/TrackDetailsActivity.java +++ b/org.envirocar.app/src/org/envirocar/app/views/trackdetails/TrackDetailsActivity.java @@ -20,7 +20,6 @@ import android.app.Activity; import android.content.Intent; -import android.graphics.BitmapFactory; import android.graphics.Color; import android.os.Build; import android.os.Bundle; @@ -32,23 +31,12 @@ import android.widget.RelativeLayout; import android.widget.TextView; -import androidx.annotation.NonNull; import androidx.appcompat.widget.Toolbar; import androidx.core.widget.NestedScrollView; import com.google.android.material.appbar.AppBarLayout; import com.google.android.material.appbar.CollapsingToolbarLayout; import com.google.android.material.floatingactionbutton.FloatingActionButton; -import com.mapbox.geojson.Feature; -import com.mapbox.geojson.Point; -import com.mapbox.mapboxsdk.camera.CameraUpdateFactory; -import com.mapbox.mapboxsdk.geometry.LatLngBounds; -import com.mapbox.mapboxsdk.maps.MapView; -import com.mapbox.mapboxsdk.maps.MapboxMap; -import com.mapbox.mapboxsdk.maps.Style; -import com.mapbox.mapboxsdk.style.layers.PropertyFactory; -import com.mapbox.mapboxsdk.style.layers.SymbolLayer; -import com.mapbox.mapboxsdk.style.sources.GeoJsonSource; import org.envirocar.app.BaseApplicationComponent; import org.envirocar.app.R; @@ -65,6 +53,9 @@ import org.envirocar.core.logging.Logger; import org.envirocar.core.trackprocessing.statistics.TrackStatisticsProvider; import org.envirocar.core.utils.CarUtils; +import org.envirocar.map.MapController; +import org.envirocar.map.MapView; +import org.envirocar.map.provider.mapbox.MapboxMapProvider; import java.text.DateFormat; import java.text.DecimalFormat; @@ -73,6 +64,7 @@ import java.util.Date; import java.util.List; import java.util.Locale; +import java.util.Objects; import java.util.TimeZone; import javax.inject.Inject; @@ -136,9 +128,7 @@ public static void navigate(Activity activity, View transition, int trackID) { protected TextView stoptimeValue; private Track track; - TrackMapLayer trackMapOverlay; - protected MapboxMap mapboxMap; - protected Style mapStyle; + private MapController mMapController; @Override protected void injectDependencies(BaseApplicationComponent baseApplicationComponent) { @@ -180,7 +170,6 @@ protected void onCreate(Bundle savedInstanceState) { stoptimeLayout = binding.activityTrackDetailsAttributes.activityTrackDetailsStoptimeContainer; stoptimeValue = binding.activityTrackDetailsAttributes.activityTrackDetailsStoptimeValue; - mMapView.onCreate(savedInstanceState); supportPostponeEnterTransition(); // Set the toolbar as default actionbar. @@ -196,8 +185,6 @@ protected void onCreate(Bundle savedInstanceState) { .blockingFirst(); this.track = track; - this.trackMapOverlay = new TrackMapLayer(track); - String itemTitle = track.getName(); CollapsingToolbarLayout collapsingToolbarLayout = findViewById(R.id.collapsing_toolbar); collapsingToolbarLayout.setTitle(itemTitle); @@ -207,7 +194,6 @@ protected void onCreate(Bundle savedInstanceState) { TextView title = findViewById(R.id.title); title.setText(itemTitle); - // Initialize the mapview and the trackpath initMapView(); initViewValues(track); @@ -259,62 +245,20 @@ private void initActivityTransition() { * Initializes the MapView, its base layers and settings. */ private void initMapView() { - final LatLngBounds viewBbox = trackMapOverlay.getViewBoundingBox(); - mMapView.getMapAsync(tep -> { - tep.getUiSettings().setLogoEnabled(false); - tep.getUiSettings().setAttributionEnabled(false); - tep.setStyle(new Style.Builder().fromUrl("https://api.maptiler.com/maps/basic/style.json?key=YJCrA2NeKXX45f8pOV6c "), style -> { - mapStyle = style; - style.addSource(trackMapOverlay.getGeoJsonSource()); - style.addLayer(trackMapOverlay.getLineLayer()); - tep.moveCamera(CameraUpdateFactory.newLatLngBounds(viewBbox, 50)); - setUpStartStopIcons(style); - }); - mapboxMap = tep; - mapboxMap.setMaxZoomPreference(trackMapOverlay.getMaxZoom()); - mapboxMap.setMinZoomPreference(trackMapOverlay.getMinZoom()); - }); - } - - private void setUpStartStopIcons(@NonNull Style loadedMapStyle) { - int size = track.getMeasurements().size(); - if (size >= 2) { - //Set Source with start and stop marker - Double lng = track.getMeasurements().get(0).getLongitude(); - Double lat = track.getMeasurements().get(0).getLatitude(); - GeoJsonSource geoJsonSource = new GeoJsonSource("marker-source1", Feature.fromGeometry( - Point.fromLngLat(lng, lat))); - loadedMapStyle.addSource(geoJsonSource); - - lng = track.getMeasurements().get(size - 1).getLongitude(); - lat = track.getMeasurements().get(size - 1).getLatitude(); - geoJsonSource = new GeoJsonSource("marker-source2", Feature.fromGeometry( - Point.fromLngLat(lng, lat))); - loadedMapStyle.addSource(geoJsonSource); - - //Set symbol layer to set the icons to be displayed at the start and stop - loadedMapStyle.addImage("start-marker", - BitmapFactory.decodeResource( - this.getResources(), R.drawable.start_marker)); - SymbolLayer symbolLayer = new SymbolLayer("marker-layer1", "marker-source1"); - symbolLayer.withProperties( - PropertyFactory.iconImage("start-marker"), - PropertyFactory.iconAllowOverlap(true), - PropertyFactory.iconIgnorePlacement(true) - ); - loadedMapStyle.addLayer(symbolLayer); - - loadedMapStyle.addImage("stop-marker", - BitmapFactory.decodeResource( - this.getResources(), R.drawable.stop_marker)); - symbolLayer = new SymbolLayer("marker-layer2", "marker-source2"); - symbolLayer.withProperties( - PropertyFactory.iconImage("stop-marker"), - PropertyFactory.iconAllowOverlap(true), - PropertyFactory.iconIgnorePlacement(true) - ); - loadedMapStyle.addLayerAbove(symbolLayer, "marker-layer1"); + // TODO(alexmercerind): Retrieve currently selected provider from a common repository. + if (mMapController != null) { + return; } + mMapController = mMapView.getController(new MapboxMapProvider()); + + final TrackMapFactory factory = new TrackMapFactory(track); + + mMapController.setMinZoom(factory.getMinZoom()); + mMapController.setMaxZoom(factory.getMaxZoom()); + mMapController.addPolyline(Objects.requireNonNull(factory.getPolyline())); + mMapController.addMarker(Objects.requireNonNull(factory.getStartMarker())); + mMapController.addMarker(Objects.requireNonNull(factory.getStopMarker())); + mMapController.notifyCameraUpdate(Objects.requireNonNull(factory.getCameraUpdateBasedOnBounds()), null); } private void initViewValues(Track track) { @@ -437,57 +381,9 @@ private void initViewValues(Track track) { } } - @Override - public void onStart() { - super.onStart(); - mMapView.onStart(); - } - @Override public void onResume() { super.onResume(); supportStartPostponedEnterTransition(); - mMapView.onResume(); - } - - @Override - public void onPause() { - super.onPause(); - mMapView.onPause(); - } - - @Override - public void onStop() { - super.onStop(); - mMapView.onStop(); - } - - @Override - public void onLowMemory() { - super.onLowMemory(); - mMapView.onLowMemory(); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - if (mapStyle != null) { - mapStyle.removeLayer(MapLayer.LAYER_NAME); - mapStyle.removeLayer("marker-layer1"); - mapStyle.removeLayer("marker-layer2"); - } else { - LOG.info("Style was null."); - } - if (mMapView != null) { - mMapView.onDestroy(); - } else { - LOG.info("mMapView was null."); - } - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - mMapView.onSaveInstanceState(outState); } } diff --git a/org.envirocar.app/src/org/envirocar/app/views/trackdetails/TrackMapFactory.kt b/org.envirocar.app/src/org/envirocar/app/views/trackdetails/TrackMapFactory.kt new file mode 100644 index 000000000..0d2aa3355 --- /dev/null +++ b/org.envirocar.app/src/org/envirocar/app/views/trackdetails/TrackMapFactory.kt @@ -0,0 +1,97 @@ +package org.envirocar.app.views.trackdetails + +import android.animation.ArgbEvaluator +import org.envirocar.app.R +import org.envirocar.core.entity.Measurement +import org.envirocar.core.entity.Track +import org.envirocar.map.camera.CameraUpdateFactory +import org.envirocar.map.model.Marker +import org.envirocar.map.model.Point +import org.envirocar.map.model.Polyline +import kotlin.math.max + +class TrackMapFactory(private val track: Track) { + + private val measurements get() = when { + track.measurements == null -> null + track.measurements.isEmpty() -> null + else -> track.measurements + } + + private val bounds = measurements?.run { + val latitudeMin = minOf { it.latitude } + val latitudeMax = maxOf { it.latitude } + val longitudeMin = minOf { it.longitude } + val longitudeMax = maxOf { it.longitude } + val latitudeRatio = max((latitudeMax - latitudeMin) / 10.0, 0.01) + val longitudeRatio = max((longitudeMax - longitudeMin) / 10.0, 0.01) + listOf( + Point(latitudeMin - latitudeRatio, longitudeMin - longitudeRatio), + Point(latitudeMax + latitudeRatio, longitudeMax + longitudeRatio) + ) + } + + val cameraUpdateBasedOnBounds get() = bounds?.let { CameraUpdateFactory.newCameraUpdateBasedOnBounds(it, 50.0F) } + + val startMarker get() = measurements?.run { + Marker.Builder(Point(first().latitude, first().longitude)) + .withDrawable(R.drawable.start_marker) + .build() + } + + val stopMarker get() = measurements?.run { + Marker.Builder(Point(last().latitude, last().longitude)) + .withDrawable(R.drawable.stop_marker) + .build() + } + + val polyline get() = measurements?.run { + Polyline.Builder(map { Point(it.latitude, it.longitude) }) + .withWidth(POLYLINE_WIDTH) + .withColor(POLYLINE_COLOR) + .build() + } + + val minZoom get() = 1.0F + + val maxZoom get() = 18.0F + + fun getGradientMinValue(key: Measurement.PropertyKey) = measurements?.run { + val values = map { if (it.hasProperty(key)) it.getProperty(key).toFloat() else 0.0F } + if (key == Measurement.PropertyKey.SPEED) 0.0F else values.min() + } + + fun getGradientMaxValue(key: Measurement.PropertyKey) = measurements?.run { + val values = map { if (it.hasProperty(key)) it.getProperty(key).toFloat() else 0.0F } + values.max() + } + + fun getGradientPolyline(key: Measurement.PropertyKey) = measurements?.run { + when { + size > 2 -> { + val values = map { if (it.hasProperty(key)) it.getProperty(key).toFloat() else 0.0F } + val min = if (key == Measurement.PropertyKey.SPEED) 0.0F else values.min() + val max = values.max() + val evaluator = ArgbEvaluator() + val colors = values.map { evaluator.evaluate(it / max, POLYLINE_COLORS_MIN, POLYLINE_COLORS_MAX) as Int } + Polyline.Builder(map { Point(it.latitude, it.longitude) }) + .withWidth(POLYLINE_WIDTH) + .withColors(colors) + .build() + } + else -> { + Polyline.Builder(map { Point(it.latitude, it.longitude) }) + .withWidth(POLYLINE_WIDTH) + .withColor(POLYLINE_COLOR) + .build() + } + } + } + + companion object { + private const val POLYLINE_WIDTH = 4.0F + private const val POLYLINE_COLOR = 0xFF0065A0.toInt() + private const val POLYLINE_COLORS_MIN = 0xFF00FF00.toInt() + private const val POLYLINE_COLORS_MAX = 0xFFFF0000.toInt() + } +} diff --git a/org.envirocar.app/src/org/envirocar/app/views/tracklist/AbstractTrackListCardAdapter.java b/org.envirocar.app/src/org/envirocar/app/views/tracklist/AbstractTrackListCardAdapter.java deleted file mode 100644 index 3fbb75672..000000000 --- a/org.envirocar.app/src/org/envirocar/app/views/tracklist/AbstractTrackListCardAdapter.java +++ /dev/null @@ -1,433 +0,0 @@ -/** - * Copyright (C) 2013 - 2021 the enviroCar community - * - * This file is part of the enviroCar app. - * - * The enviroCar app is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * The enviroCar app is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with the enviroCar app. If not, see http://www.gnu.org/licenses/. - */ -package org.envirocar.app.views.tracklist; - -import android.os.AsyncTask; -import android.view.View; -import android.widget.ImageButton; -import android.widget.LinearLayout; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.appcompat.widget.Toolbar; -import androidx.recyclerview.widget.RecyclerView; - -import com.github.jorgecastilloprz.FABProgressCircle; -import com.google.android.material.floatingactionbutton.FloatingActionButton; -import com.mapbox.mapboxsdk.camera.CameraUpdateFactory; -import com.mapbox.mapboxsdk.geometry.LatLngBounds; -import com.mapbox.mapboxsdk.maps.MapView; -import com.mapbox.mapboxsdk.maps.MapboxMap; -import com.mapbox.mapboxsdk.maps.OnMapReadyCallback; -import com.mapbox.mapboxsdk.maps.Style; - -import org.envirocar.app.R; -import org.envirocar.app.databinding.FragmentTracklistCardlayoutBinding; -import org.envirocar.app.databinding.FragmentTracklistCardlayoutRemoteBinding; -import org.envirocar.app.views.trackdetails.TrackMapLayer; -import org.envirocar.core.entity.Track; -import org.envirocar.core.logging.Logger; - -import java.text.DateFormat; -import java.text.DecimalFormat; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.List; -import java.util.Locale; -import java.util.TimeZone; - -import io.reactivex.Scheduler; -import io.reactivex.android.schedulers.AndroidSchedulers; - -/** - * TODO JavaDoc - * - * @author dewall - */ -public abstract class AbstractTrackListCardAdapter extends RecyclerView.Adapter { - private static final Logger LOG = Logger.getLogger(AbstractTrackListCardAdapter.class); - - protected static final DecimalFormat DECIMAL_FORMATTER_TWO = new DecimalFormat("#.##"); - protected static final DateFormat DATE_FORMAT = DateFormat.getDateTimeInstance(); - protected static final DateFormat UTC_DATE_FORMATTER = new SimpleDateFormat("HH:mm:ss", Locale - .ENGLISH); - - static { - UTC_DATE_FORMATTER.setTimeZone(TimeZone.getTimeZone("UTC")); - } - - protected final List mTrackDataset; - protected Scheduler.Worker mMainThreadWorker = AndroidSchedulers.mainThread().createWorker(); - protected final OnTrackInteractionCallback mTrackInteractionCallback; - - /** - * Constructor. - * - * @param tracks the list of tracks to show cards for. - */ - public AbstractTrackListCardAdapter(List tracks, final OnTrackInteractionCallback - callback) { - this.mTrackDataset = tracks; - this.mTrackInteractionCallback = callback; - } - - @Override - public int getItemCount() { - return mTrackDataset.size(); - } - - @Override - public long getItemId(int position) { - return mTrackDataset.get(position).getTrackID().getId(); - } - - /** - * Adds a track to the dataset. - * - * @param track the track to insert. - */ - public void addItem(Track track) { - mTrackDataset.add(track); - int pos = mTrackDataset.indexOf(track); - notifyItemInserted(pos); - } - - /** - * Removes a track from the dataset. - * - * @param track the track to remove. - */ - public void removeItem(Track track) { - if (mTrackDataset.contains(track)) { - mTrackDataset.remove(track); - int pos = mTrackDataset.indexOf(track); - notifyItemRemoved(pos); - } - } - - - protected void bindLocalTrackViewHolder(TrackCardViewHolder holder, Track track) { - holder.getDistance().setText("..."); - holder.getDuration().setText("..."); - LOG.info("bindLocalTrackViewHolder()"); - // First, load the track from the dataset - holder.getTitleTextView().setText(track.getName()); - - // Initialize the mapView. - initMapView(holder, track); - - // Set all the view parameters. - new AsyncTask() { - @Override - protected Void doInBackground(Void... params) { - // Set the duration text. - try { - String date = UTC_DATE_FORMATTER.format(new Date( - track.getDuration())); - mMainThreadWorker.schedule(() -> holder.getDuration().setText(date)); - - // Set the tracklength parameter. - - double distanceOfTrack = track.getLength(); - String tracklength = String.format("%s km", DECIMAL_FORMATTER_TWO.format( - distanceOfTrack)); - mMainThreadWorker.schedule(() -> holder.getDistance().setText(tracklength)); - - } catch (Exception e) { - LOG.warn(e.getMessage(), e); - mMainThreadWorker.schedule(() -> { - holder.getDistance().setText("0 km"); - holder.getDuration().setText("0:00"); - }); - } - - return null; - } - }.execute(); - - // if the menu is not already inflated, then.. - if (!holder.getToolbar().getMenu().hasVisibleItems()) { - // Inflate the menu and set an appropriate OnMenuItemClickListener. - holder.getToolbar().inflateMenu(R.menu.menu_tracklist_cardlayout); - if (track.isRemoteTrack()) { - holder.getToolbar().getMenu().removeItem(R.id.menu_tracklist_cardlayout_item_upload); - } - } - - holder.getToolbar().setOnMenuItemClickListener(item -> { - LOG.info("Item clicked for track " + track.getTrackID()); - - if (item.getItemId() == R.id.menu_tracklist_cardlayout_item_details) { - mTrackInteractionCallback.onTrackDetailsClicked(track, holder.getMapView()); - } else if (item.getItemId() == R.id.menu_tracklist_cardlayout_item_delete) { - mTrackInteractionCallback.onDeleteTrackClicked(track); - } else if (item.getItemId() == R.id.menu_tracklist_cardlayout_item_share) { - mTrackInteractionCallback.onShareTrackClicked(track); - } else if (item.getItemId() == R.id.menu_tracklist_cardlayout_item_upload) { - mTrackInteractionCallback.onUploadTrackClicked(track); - } - - return false; - }); - - // Initialize the OnClickListener for the invisible button that is overlaid - // over the map view. - holder.getInvisMapButton().setOnClickListener(v -> { - LOG.info("Clicked on the map. Navigate to the details activity"); - mTrackInteractionCallback.onTrackDetailsClicked(track, holder.getMapView()); - }); - - holder.getCardViewLayout().setOnLongClickListener(view -> { - mTrackInteractionCallback.onLongPressedTrack(track); - return true; - }); - - holder.getInvisMapButton().setOnLongClickListener(view -> { - mTrackInteractionCallback.onLongPressedTrack(track); - return true; - }); - } - - - /** - * Initializes the MapView, its base layers and settings. - */ - protected void initMapView(TrackCardViewHolder holder, Track track) { - // First, clear the overlays in the MapView. - LOG.info("initMapView()"); - TrackMapLayer trackMapOverlay = new TrackMapLayer(track); - final LatLngBounds viewBbox = trackMapOverlay.getViewBoundingBox(); - holder.getMapView().addOnDidFailLoadingMapListener(holder.failLoadingMapListener); - holder.getMapView().getMapAsync(new OnMapReadyCallback() { - @Override - public void onMapReady(@NonNull MapboxMap tep) { - LOG.info("onMapReady()"); - tep.getUiSettings().setLogoEnabled(false); - tep.getUiSettings().setAttributionEnabled(false); - tep.setStyle(new Style.Builder().fromUrl("https://api.maptiler.com/maps/basic/style.json?key=YJCrA2NeKXX45f8pOV6c "), new Style.OnStyleLoaded() { - @Override - public void onStyleLoaded(@NonNull Style style) { - LOG.info("onStyleLoaded()"); - style.addSource(trackMapOverlay.getGeoJsonSource()); - style.addLayer(trackMapOverlay.getLineLayer()); - tep.moveCamera(CameraUpdateFactory.newLatLngBounds(viewBbox, 50)); - } - }); - - } - }); - } - - @Override - public void onViewAttachedToWindow(@NonNull E holder) { - super.onViewAttachedToWindow(holder); - holder.getMapView().onStart(); - holder.getMapView().onResume(); - } - - @Override - public void onViewDetachedFromWindow(@NonNull E holder) { - super.onViewDetachedFromWindow(holder); - holder.getMapView().onPause(); - holder.getMapView().onStop(); - } - -// private void initRouteCoordinates(Track track) { -// // Create a list to store our line coordinates. -// routeCoordinates.clear(); -// List temp = track.getMeasurements(); -// for (Measurement measurement : temp) { -// routeCoordinates.add(Point.fromLngLat(measurement.getLongitude(), measurement.getLatitude())); -// } -// -// latLngs.clear(); -// for (int i = 0; i < routeCoordinates.size(); ++i) { -// latLngs.add(new LatLng(routeCoordinates.get(i).latitude(), routeCoordinates.get(i).longitude())); -// } -// -// if (latLngs.size() == 1) { -// LatLng latLng = latLngs.get(0); -// mViewBoundingBox = LatLngBounds.from( -// latLng.getLatitude() + 0.01, -// latLng.getLongitude() + 0.01, -// latLng.getLatitude() - 0.01, -// latLng.getLongitude() - 0.01); -// } else { -// mTrackBoundingBox = new LatLngBounds.Builder() -// .includes(latLngs) -// .build(); -// -// double latRatio = Math.max(mTrackBoundingBox.getLatitudeSpan() / 10.0, 0.01); -// double lngRatio = Math.max(mTrackBoundingBox.getLongitudeSpan() / 10.0, 0.01); -// -// // The view bounding box of the pathoverlay -// mViewBoundingBox = LatLngBounds.from( -// mTrackBoundingBox.getLatNorth() + latRatio, -// mTrackBoundingBox.getLonEast() + lngRatio, -// mTrackBoundingBox.getLatSouth() - latRatio, -// mTrackBoundingBox.getLonWest() - lngRatio); -// } -// -// } - - /** - * - */ - static abstract class TrackCardViewHolder extends RecyclerView.ViewHolder { - protected MapView.OnDidFailLoadingMapListener failLoadingMapListener; - - abstract Toolbar getToolbar(); - abstract TextView getTitleTextView(); - abstract View getContentView(); - abstract TextView getDistance(); - abstract TextView getDuration(); - abstract MapView getMapView(); - abstract ImageButton getInvisMapButton(); - abstract LinearLayout getCardViewLayout(); - - public TrackCardViewHolder(View itemView) { - super(itemView); - failLoadingMapListener = new MapView.OnDidFailLoadingMapListener() { - @Override - public void onDidFailLoadingMap(String errorMessage) { - LOG.info("Map loading failed : " + errorMessage); - } - }; - } - } - - /** - * Default view holder for standard local and not uploaded tracks. - */ - static class LocalTrackCardViewHolder extends TrackCardViewHolder { - FragmentTracklistCardlayoutBinding binding; - - @Override - Toolbar getToolbar() { - return binding.fragmentTracklistCardlayoutToolbar; - } - - @Override - TextView getTitleTextView() { - return binding.fragmentTracklistCardlayoutToolbarTitle; - } - - @Override - View getContentView() { - return binding.fragmentTracklistCardlayoutContent.fragmentTracklistCardlayoutContent; - } - - @Override - TextView getDistance() { - return binding.fragmentTracklistCardlayoutContent.trackDetailsAttributesHeaderDistance; - } - - @Override - TextView getDuration() { - return binding.fragmentTracklistCardlayoutContent.trackDetailsAttributesHeaderDuration; - } - - @Override - MapView getMapView() { - return binding.fragmentTracklistCardlayoutContent.fragmentTracklistCardlayoutMap; - } - - @Override - ImageButton getInvisMapButton() { - return binding.fragmentTracklistCardlayoutContent.fragmentTracklistCardlayoutInvisMapbutton; - } - - @Override - LinearLayout getCardViewLayout() { - return binding.fragmentLayoutCardView; - } - - public LocalTrackCardViewHolder(FragmentTracklistCardlayoutBinding binding) { - super(binding.getRoot()); - this.binding = binding; - } - } - - /** - * Remote track view holder that only contains the views that can be filled with information - * of a remote track list. (i.e. users/{getUserStatistic}/tracks) - */ - static class RemoteTrackCardViewHolder extends TrackCardViewHolder { - FragmentTracklistCardlayoutRemoteBinding binding; - - @Override - Toolbar getToolbar() { - return binding.fragmentTracklistCardlayoutToolbar; - } - - @Override - TextView getTitleTextView() { - return binding.fragmentTracklistCardlayoutToolbarTitle; - } - - @Override - View getContentView() { - return binding.fragmentTracklistCardlayoutContent.fragmentTracklistCardlayoutContent; - } - - @Override - TextView getDistance() { - return binding.fragmentTracklistCardlayoutContent.trackDetailsAttributesHeaderDistance; - } - - @Override - TextView getDuration() { - return binding.fragmentTracklistCardlayoutContent.trackDetailsAttributesHeaderDuration; - } - - @Override - MapView getMapView() { - return binding.fragmentTracklistCardlayoutContent.fragmentTracklistCardlayoutMap; - } - - @Override - ImageButton getInvisMapButton() { - return binding.fragmentTracklistCardlayoutContent.fragmentTracklistCardlayoutInvisMapbutton; - } - - @Override - LinearLayout getCardViewLayout() { - return binding.fragmentLayoutCardView; - } - - FABProgressCircle getProgressCircle() { - return binding.fragmentTracklistCardlayoutRemoteProgresscircle; - } - - FloatingActionButton getDownloadButton() { - return binding.fragmentTracklistCardlayoutRemoteDownloadfab; - } - - TextView getDownloadNotification() { - return binding.fragmentTracklistCardlayoutDownloadingNotification; - } - - public RemoteTrackCardViewHolder(FragmentTracklistCardlayoutRemoteBinding binding) { - super(binding.getRoot()); - this.binding = binding; - } - } -} diff --git a/org.envirocar.app/src/org/envirocar/app/views/tracklist/AbstractTrackListCardAdapter.kt b/org.envirocar.app/src/org/envirocar/app/views/tracklist/AbstractTrackListCardAdapter.kt new file mode 100644 index 000000000..35297375e --- /dev/null +++ b/org.envirocar.app/src/org/envirocar/app/views/tracklist/AbstractTrackListCardAdapter.kt @@ -0,0 +1,208 @@ +package org.envirocar.app.views.tracklist + +import android.view.View +import android.widget.ImageButton +import android.widget.LinearLayout +import android.widget.TextView +import androidx.appcompat.widget.Toolbar +import androidx.recyclerview.widget.RecyclerView +import org.envirocar.app.R +import org.envirocar.app.databinding.FragmentTracklistCardlayoutLocalBinding +import org.envirocar.app.databinding.FragmentTracklistCardlayoutRemoteBinding +import org.envirocar.app.views.trackdetails.TrackMapFactory +import org.envirocar.core.entity.Track +import org.envirocar.core.logging.Logger +import org.envirocar.map.MapController +import org.envirocar.map.MapView +import org.envirocar.map.provider.mapbox.MapboxMapProvider +import java.text.DecimalFormat +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.TimeZone + +abstract class AbstractTrackListCardAdapter( + private val tracks: MutableList, + private val callback: OnTrackInteractionCallback +) : RecyclerView.Adapter() { + private val mapControllers = mutableMapOf() + + sealed class TrackCardViewHolder(view: View) : RecyclerView.ViewHolder(view) { + abstract val toolbar: Toolbar + abstract val titleTextView: TextView + abstract val contentView: View + abstract val distance: TextView + abstract val duration: TextView + abstract val mapView: MapView + abstract val invisibleMapButton: ImageButton + abstract val cardViewLayout: LinearLayout + } + + class LocalTrackCardViewHolder(binding: FragmentTracklistCardlayoutLocalBinding) : + TrackCardViewHolder(binding.root) { + override val toolbar = binding.fragmentTracklistCardlayoutToolbar + override val titleTextView = binding.fragmentTracklistCardlayoutToolbarTitle + override val contentView = binding.fragmentTracklistCardlayoutContent.root + override val distance = binding.fragmentTracklistCardlayoutContent.trackDetailsAttributesHeaderDistance + override val duration = binding.fragmentTracklistCardlayoutContent.trackDetailsAttributesHeaderDuration + override val mapView = binding.fragmentTracklistCardlayoutContent.fragmentTracklistCardlayoutMap + override val invisibleMapButton = binding.fragmentTracklistCardlayoutContent.fragmentTracklistCardlayoutInvisibleMapbutton + override val cardViewLayout = binding.fragmentLayoutCardView + + } + + class RemoteTrackCardViewHolder(binding: FragmentTracklistCardlayoutRemoteBinding) : + TrackCardViewHolder(binding.root) { + override val toolbar = binding.fragmentTracklistCardlayoutToolbar + override val titleTextView = binding.fragmentTracklistCardlayoutToolbarTitle + override val contentView = binding.fragmentTracklistCardlayoutContent.root + override val distance = binding.fragmentTracklistCardlayoutContent.trackDetailsAttributesHeaderDistance + override val duration = binding.fragmentTracklistCardlayoutContent.trackDetailsAttributesHeaderDuration + override val mapView = binding.fragmentTracklistCardlayoutContent.fragmentTracklistCardlayoutMap + override val invisibleMapButton = binding.fragmentTracklistCardlayoutContent.fragmentTracklistCardlayoutInvisibleMapbutton + override val cardViewLayout = binding.fragmentLayoutCardView + val progressCircle = binding.fragmentTracklistCardlayoutRemoteProgressCircle + val downloadFab = binding.fragmentTracklistCardlayoutRemoteDownloadFab + val downloadingNotification = binding.fragmentTracklistCardlayoutRemoteDownloadingNotification + } + + fun addTrack(track: Track) { + tracks.add(track) + // [MapController] will be created when the view is bound. + notifyItemInserted(tracks.indexOf(track)) + } + + fun removeTrack(track: Track) { + val index = tracks.indexOf(track) + tracks.remove(track) + mapControllers.remove(track.id) + notifyItemRemoved(index) + } + + fun clearTracks() { + tracks.clear() + mapControllers.clear() + notifyItemRangeRemoved(0, itemCount) + } + + override fun getItemCount() = tracks.size + + override fun onBindViewHolder(holder: E, position: Int) { + LOG.info("onBindViewHolder()") + bindTrackCardViewHolder(holder, tracks[position]) + } + + fun bindTrackCardViewHolder(holder: E, track: Track) { + LOG.info("bindTrackCardViewHolder()") + + holder.titleTextView.text = track.name + + holder.distance.text = "..." + holder.duration.text = "..." + + setupMapView(holder.mapView, track) + + try { + val distance = String.format("%s km", DECIMAL_FORMAT.format(track.length)) + val duration = DATE_FORMAT.format(Date(track.duration)) + holder.distance.text = distance + holder.duration.text = duration + } catch(e: Exception) { + LOG.warn(e.message, e) + holder.distance.text = "0 km" + holder.duration.text = "0:00" + } + + if (!holder.toolbar.menu.hasVisibleItems()) { + holder.toolbar.inflateMenu(R.menu.menu_tracklist_cardlayout) + } + holder.toolbar.setOnMenuItemClickListener { item -> + LOG.info("${item.itemId} clicked for track ${track.trackID}.") + when (item.itemId) { + R.id.menu_tracklist_cardlayout_item_details -> callback.onTrackDetailsClicked(track, holder.mapView) + R.id.menu_tracklist_cardlayout_item_delete -> callback.onDeleteTrackClicked(track) + R.id.menu_tracklist_cardlayout_item_share -> callback.onShareTrackClicked(track) + R.id.menu_tracklist_cardlayout_item_upload -> callback.onUploadTrackClicked(track) + } + false + } + + holder.invisibleMapButton.setOnClickListener { + callback.onTrackDetailsClicked(track, holder.mapView) + } + holder.invisibleMapButton.setOnLongClickListener { + callback.onLongPressedTrack(track) + true + } + holder.cardViewLayout.setOnLongClickListener { + callback.onLongPressedTrack(track) + true + } + + when (holder) { + is LocalTrackCardViewHolder -> { + /* NO/OP */ + } + is RemoteTrackCardViewHolder -> { + holder.toolbar.menu.removeItem(R.id.menu_tracklist_cardlayout_item_upload) + when (track.downloadState) { + Track.DownloadState.REMOTE -> { + holder.contentView.visibility = View.GONE + holder.progressCircle.visibility = View.VISIBLE + holder.downloadFab.show() + holder.downloadFab.setOnClickListener { + holder.downloadFab.setOnClickListener(null) + callback.onDownloadTrackClicked(track, holder) + } + holder.downloadingNotification.visibility = View.GONE + } + Track.DownloadState.DOWNLOADING -> { + holder.contentView.visibility = View.GONE + holder.progressCircle.visibility = View.VISIBLE + holder.progressCircle.post { holder.progressCircle.show() } + holder.downloadFab.show() + holder.downloadFab.setOnClickListener(null) + holder.downloadingNotification.visibility = View.VISIBLE + } + Track.DownloadState.DOWNLOADED -> { + holder.contentView.visibility = View.VISIBLE + holder.progressCircle.visibility = View.GONE + holder.downloadFab.hide() + holder.downloadFab.setOnClickListener(null) + holder.downloadingNotification.visibility = View.GONE + } + null -> { /* NO/OP */ } + } + } + else -> error("Unknown [TrackCardViewHolder] instance.") + } + } + + private fun setupMapView(view: MapView, track: Track) { + LOG.info("setupMapView()") + // TODO(alexmercerind): Retrieve currently selected provider from a common repository. + mapControllers + .getOrPut(track.id) { view.getController(MapboxMapProvider()) } + .run { + val factory = TrackMapFactory(track) + factory.cameraUpdateBasedOnBounds?.let { notifyCameraUpdate(it) } + factory.polyline?.let { + clearPolylines() + addPolyline(it) + } + } + } + + val Track.id : String get() = when { + isLocalTrack -> trackID.id.toString() + isRemoteTrack -> remoteID.toString() + else -> error("Unknown track type.") + } + + companion object { + private val LOG = Logger.getLogger(AbstractTrackListCardAdapter::class.java) + private val DECIMAL_FORMAT = DecimalFormat("#.##") + private val DATE_FORMAT = SimpleDateFormat("HH:mm:ss", Locale.ENGLISH) + .apply { timeZone = TimeZone.getTimeZone("UTC") } + } +} diff --git a/org.envirocar.app/src/org/envirocar/app/views/tracklist/OnTrackInteractionCallback.java b/org.envirocar.app/src/org/envirocar/app/views/tracklist/OnTrackInteractionCallback.java deleted file mode 100644 index 90db4d331..000000000 --- a/org.envirocar.app/src/org/envirocar/app/views/tracklist/OnTrackInteractionCallback.java +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright (C) 2013 - 2021 the enviroCar community - * - * This file is part of the enviroCar app. - * - * The enviroCar app is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * The enviroCar app is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with the enviroCar app. If not, see http://www.gnu.org/licenses/. - */ -package org.envirocar.app.views.tracklist; - -import android.view.View; - -import org.envirocar.core.entity.Track; - - -/** - * @author dewall - */ -interface OnTrackInteractionCallback { - - /** - * @param track the track to show the details for. - */ - void onTrackDetailsClicked(Track track, View transitionView); - - /** - * @param track the track to delete. - */ - void onDeleteTrackClicked(Track track); - - /** - * @param track the track to upload. - */ - void onUploadTrackClicked(Track track); - - /** - * @param track the track to export. - */ - void onShareTrackClicked(Track track); - - /** - * @param track the track to download. - */ - void onDownloadTrackClicked(Track track, AbstractTrackListCardAdapter.TrackCardViewHolder holder); - - void onLongPressedTrack(Track track); -} diff --git a/org.envirocar.app/src/org/envirocar/app/views/tracklist/OnTrackInteractionCallback.kt b/org.envirocar.app/src/org/envirocar/app/views/tracklist/OnTrackInteractionCallback.kt new file mode 100644 index 000000000..f647e2216 --- /dev/null +++ b/org.envirocar.app/src/org/envirocar/app/views/tracklist/OnTrackInteractionCallback.kt @@ -0,0 +1,13 @@ +package org.envirocar.app.views.tracklist + +import android.view.View +import org.envirocar.core.entity.Track + +interface OnTrackInteractionCallback { + fun onTrackDetailsClicked(track: Track, transitionView: View) + fun onDeleteTrackClicked(track: Track) + fun onUploadTrackClicked(track: Track) + fun onShareTrackClicked(track: Track) + fun onDownloadTrackClicked(track: Track, holder: AbstractTrackListCardAdapter.TrackCardViewHolder) + fun onLongPressedTrack(track: Track) +} diff --git a/org.envirocar.app/src/org/envirocar/app/views/tracklist/TrackListLocalCardAdapter.java b/org.envirocar.app/src/org/envirocar/app/views/tracklist/TrackListLocalCardAdapter.java deleted file mode 100644 index a281a311f..000000000 --- a/org.envirocar.app/src/org/envirocar/app/views/tracklist/TrackListLocalCardAdapter.java +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Copyright (C) 2013 - 2021 the enviroCar community - *

- * This file is part of the enviroCar app. - *

- * The enviroCar app is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - *

- * The enviroCar app is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - *

- * You should have received a copy of the GNU General Public License along - * with the enviroCar app. If not, see http://www.gnu.org/licenses/. - */ -package org.envirocar.app.views.tracklist; - -import android.view.LayoutInflater; -import android.view.ViewGroup; - -import com.mapbox.mapboxsdk.maps.MapView; - -import org.envirocar.app.databinding.FragmentTracklistCardlayoutBinding; -import org.envirocar.core.entity.Track; -import org.envirocar.core.logging.Logger; - -import java.util.ArrayList; -import java.util.List; - -/** - * @author dewall - */ -public class TrackListLocalCardAdapter extends AbstractTrackListCardAdapter< - AbstractTrackListCardAdapter.LocalTrackCardViewHolder> { - private static final Logger LOGGER = Logger.getLogger(TrackListLocalCardAdapter.class); - - /** - * Constructor. - * - * @param tracks the list of tracks to show cards for. - * @param callback - */ - public TrackListLocalCardAdapter(List tracks, OnTrackInteractionCallback callback) { - super(tracks, callback); - } - - protected List mapViews = new ArrayList<>(); - - @Override - public TrackListLocalCardAdapter.LocalTrackCardViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - final FragmentTracklistCardlayoutBinding binding = FragmentTracklistCardlayoutBinding.inflate( - LayoutInflater.from(parent.getContext()), - parent, - false - ); - LocalTrackCardViewHolder holder = new LocalTrackCardViewHolder(binding); - mapViews.add(holder.getMapView()); - return holder; - } - - @Override - public void onBindViewHolder(final LocalTrackCardViewHolder holder, int position) { - bindLocalTrackViewHolder(holder, mTrackDataset.get(position)); - } - - public void onLowMemory() { - for (MapView mapView : mapViews) { - mapView.onLowMemory(); - } - } - - public void onDestroy() { - for (MapView mapView : mapViews) { - mapView.onPause(); - mapView.onStop(); - mapView.onDestroy(); - } - } - -} diff --git a/org.envirocar.app/src/org/envirocar/app/views/tracklist/TrackListLocalCardAdapter.kt b/org.envirocar.app/src/org/envirocar/app/views/tracklist/TrackListLocalCardAdapter.kt new file mode 100644 index 000000000..be9bed661 --- /dev/null +++ b/org.envirocar.app/src/org/envirocar/app/views/tracklist/TrackListLocalCardAdapter.kt @@ -0,0 +1,24 @@ +package org.envirocar.app.views.tracklist + +import android.view.LayoutInflater +import android.view.ViewGroup +import org.envirocar.app.databinding.FragmentTracklistCardlayoutLocalBinding +import org.envirocar.core.entity.Track + +class TrackListLocalCardAdapter( + tracks: MutableList, + callback: OnTrackInteractionCallback +) : AbstractTrackListCardAdapter( + tracks, + callback +) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LocalTrackCardViewHolder { + val binding = FragmentTracklistCardlayoutLocalBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + return LocalTrackCardViewHolder(binding) + } +} diff --git a/org.envirocar.app/src/org/envirocar/app/views/tracklist/TrackListLocalCardFragment.java b/org.envirocar.app/src/org/envirocar/app/views/tracklist/TrackListLocalCardFragment.java index eb70e2922..8047763a7 100644 --- a/org.envirocar.app/src/org/envirocar/app/views/tracklist/TrackListLocalCardFragment.java +++ b/org.envirocar.app/src/org/envirocar/app/views/tracklist/TrackListLocalCardFragment.java @@ -108,21 +108,8 @@ public void onResume() { loadDataset(); } - @Override - public void onLowMemory() { - super.onLowMemory(); - mRecyclerViewAdapter.onLowMemory(); - - } - - @Override - public void onDestroy() { - super.onDestroy(); - mRecyclerViewAdapter.onDestroy(); - } - protected void onUploadTracksFABClicked() { - new MaterialAlertDialogBuilder(getContext(), R.style.MaterialDialog) + new MaterialAlertDialogBuilder(requireContext(), R.style.MaterialDialog) .setTitle(R.string.track_list_upload_all_tracks_title) .setMessage(R.string.track_list_upload_all_tracks_content) .setIcon(R.drawable.ic_cloud_upload_white_24dp) @@ -218,7 +205,7 @@ public void onTrackChunkUploadEndEvent(TrackchunkEndUploadedEvent event) { LOG.info("Received TrackchunkEndUploadedEvent for %s", event.getTrack().getName()); this.getActivity().runOnUiThread(() -> { - mRecyclerViewAdapter.removeItem(event.getTrack()); + mRecyclerViewAdapter.removeTrack(event.getTrack()); if(mTrackList.isEmpty()){ showNoTracksInfo(); } @@ -411,7 +398,7 @@ protected void onStart() { @Override public void onNext(Track track) { // Update the lists. - mRecyclerViewAdapter.removeItem(track); + mRecyclerViewAdapter.removeTrack(track); if (onTrackUploadedListener != null) onTrackUploadedListener.onTrackUploaded(track); @@ -546,7 +533,7 @@ public void onNext(UploadAllTracks.Result result) { if (result.isSuccessful()) { numberOfSuccesses++; // Update the lists. - mRecyclerViewAdapter.removeItem(result.getTrack()); + mRecyclerViewAdapter.removeTrack(result.getTrack()); if (onTrackUploadedListener != null) onTrackUploadedListener.onTrackUploaded(result.getTrack()); diff --git a/org.envirocar.app/src/org/envirocar/app/views/tracklist/TrackListRemoteCardAdapter.java b/org.envirocar.app/src/org/envirocar/app/views/tracklist/TrackListRemoteCardAdapter.java deleted file mode 100644 index b29055ae6..000000000 --- a/org.envirocar.app/src/org/envirocar/app/views/tracklist/TrackListRemoteCardAdapter.java +++ /dev/null @@ -1,126 +0,0 @@ -/** - * Copyright (C) 2013 - 2021 the enviroCar community - * - * This file is part of the enviroCar app. - * - * The enviroCar app is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * The enviroCar app is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with the enviroCar app. If not, see http://www.gnu.org/licenses/. - */ -package org.envirocar.app.views.tracklist; - -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import com.mapbox.mapboxsdk.maps.MapView; - -import org.envirocar.app.databinding.FragmentTracklistCardlayoutRemoteBinding; -import org.envirocar.core.entity.Track; -import org.envirocar.core.logging.Logger; - -import java.util.ArrayList; -import java.util.List; - -/** - * @author dewall - */ -public class TrackListRemoteCardAdapter extends AbstractTrackListCardAdapter< - AbstractTrackListCardAdapter.RemoteTrackCardViewHolder> { - private static final Logger LOG = Logger.getLogger(TrackListRemoteCardAdapter.class); - - /** - * Constructor. - * - * @param tracks the list of tracks to show cards for. - * @param callback - */ - public TrackListRemoteCardAdapter(List tracks, OnTrackInteractionCallback callback) { - super(tracks, callback); - } - - protected List mapViews = new ArrayList<>(); - - @Override - public RemoteTrackCardViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - final FragmentTracklistCardlayoutRemoteBinding binding = FragmentTracklistCardlayoutRemoteBinding.inflate( - LayoutInflater.from(parent.getContext()), - parent, - false - ); - RemoteTrackCardViewHolder holder = new RemoteTrackCardViewHolder(binding); - mapViews.add(holder.getMapView()); - return holder; - } - - @Override - public void onBindViewHolder(RemoteTrackCardViewHolder holder, int position) { - LOG.info("onBindViewHolder()"); - - final Track remoteTrack = mTrackDataset.get(position); - - // Reset the most important settings of the views. - holder.getTitleTextView().setText(remoteTrack.getName()); - holder.getDownloadButton().setOnClickListener(null); - holder.getMapView().onCreate(null); - holder.getMapView().removeOnDidFailLoadingMapListener(holder.failLoadingMapListener); - holder.getToolbar().getMenu().clear(); - // Depending on the tracks state - switch (remoteTrack.getDownloadState()) { - case REMOTE: - holder.getContentView().setVisibility(View.GONE); - holder.getProgressCircle().setVisibility(View.VISIBLE); - - // Workaround: Sometimes the inner arcview can be null when set visible - holder.getProgressCircle().post(() -> { - //holder.mProgressCircle.hide(); - }); - holder.getDownloadButton().show(); - holder.getDownloadButton().setOnClickListener(v -> { - holder.getDownloadButton().setOnClickListener(null); - mTrackInteractionCallback.onDownloadTrackClicked(remoteTrack, holder); - }); - holder.getDownloadNotification().setVisibility(View.GONE); - break; - case DOWNLOADING: - holder.getContentView().setVisibility(View.GONE); - holder.getProgressCircle().setVisibility(View.VISIBLE); - holder.getProgressCircle().post(() -> holder.getProgressCircle().show()); - holder.getDownloadButton().show(); - holder.getDownloadNotification().setVisibility(View.VISIBLE); - break; - case DOWNLOADED: - holder.getContentView().setVisibility(View.VISIBLE); - holder.getProgressCircle().setVisibility(View.GONE); - holder.getDownloadNotification().setVisibility(View.GONE); - bindLocalTrackViewHolder(holder, remoteTrack); - break; - } - - //holder.mMapView.postInvalidate(); - } - - public void onLowMemory() { - for (MapView mapView : mapViews) { - mapView.onLowMemory(); - } - } - - public void onDestroy() { - for (MapView mapView : mapViews) { - mapView.onPause(); - mapView.onStop(); - mapView.onDestroy(); - } - } - -} diff --git a/org.envirocar.app/src/org/envirocar/app/views/tracklist/TrackListRemoteCardAdapter.kt b/org.envirocar.app/src/org/envirocar/app/views/tracklist/TrackListRemoteCardAdapter.kt new file mode 100644 index 000000000..e6b137f3d --- /dev/null +++ b/org.envirocar.app/src/org/envirocar/app/views/tracklist/TrackListRemoteCardAdapter.kt @@ -0,0 +1,24 @@ +package org.envirocar.app.views.tracklist + +import android.view.LayoutInflater +import android.view.ViewGroup +import org.envirocar.app.databinding.FragmentTracklistCardlayoutRemoteBinding +import org.envirocar.core.entity.Track + +class TrackListRemoteCardAdapter( + tracks: MutableList, + callback: OnTrackInteractionCallback +) : AbstractTrackListCardAdapter( + tracks, + callback +) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RemoteTrackCardViewHolder { + val binding = FragmentTracklistCardlayoutRemoteBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + return RemoteTrackCardViewHolder(binding) + } +} diff --git a/org.envirocar.app/src/org/envirocar/app/views/tracklist/TrackListRemoteCardFragment.java b/org.envirocar.app/src/org/envirocar/app/views/tracklist/TrackListRemoteCardFragment.java index b0c0232bd..1ca35a653 100644 --- a/org.envirocar.app/src/org/envirocar/app/views/tracklist/TrackListRemoteCardFragment.java +++ b/org.envirocar.app/src/org/envirocar/app/views/tracklist/TrackListRemoteCardFragment.java @@ -83,19 +83,6 @@ public void onResume() { loadDataset(); } - @Override - public void onLowMemory() { - super.onLowMemory(); - mRecyclerViewAdapter.onLowMemory(); - - } - - @Override - public void onDestroy() { - super.onDestroy(); - mRecyclerViewAdapter.onDestroy(); - } - @Override public void onDestroyView() { LOG.info("onDestroyView()"); @@ -173,8 +160,7 @@ protected void loadDataset() { public void onReceiveNewUserSettingsEvent(NewUserSettingsEvent event) { if (!event.mIsLoggedIn) { mMainThreadWorker.schedule(() -> { - mRecyclerViewAdapter.mTrackDataset.clear(); - mRecyclerViewAdapter.notifyDataSetChanged(); + mRecyclerViewAdapter.clearTracks(); tracksLoaded = false; }); } @@ -184,8 +170,7 @@ public void onReceiveNewUserSettingsEvent(NewUserSettingsEvent event) { public void onReceiveTrackchunkEndUploadedEvent(TrackchunkEndUploadedEvent event) { LOG.info("Received TrackchunkEndUploadedEvent"); mMainThreadWorker.schedule(() -> { - mRecyclerViewAdapter.mTrackDataset.clear(); - mRecyclerViewAdapter.notifyDataSetChanged(); + mRecyclerViewAdapter.clearTracks(); tracksLoaded = false; }); } @@ -196,8 +181,11 @@ private void onDownloadTrackClickedInner(final Track track, AbstractTrackListCar (AbstractTrackListCardAdapter.RemoteTrackCardViewHolder) viewHolder; // Show the downloading text notification. - ECAnimationUtils.animateShowView(getActivity(), holder.getDownloadNotification(), - R.anim.fade_in); + ECAnimationUtils.animateShowView( + getActivity(), + holder.getDownloadingNotification(), + R.anim.fade_in + ); holder.getProgressCircle().show(); track.setDownloadState(Track.DownloadState.DOWNLOADING); @@ -212,14 +200,21 @@ public void onComplete() { holder.getProgressCircle().attachListener(() -> { // When the visualization is finished, then Init the // content view including its mapview and track details. - mRecyclerViewAdapter.bindLocalTrackViewHolder(holder, track); + mRecyclerViewAdapter.bindTrackCardViewHolder(holder, track); // and hide the download button - ECAnimationUtils.animateHideView(getActivity(), R.anim.fade_out, - holder.getProgressCircle(), holder.getDownloadButton(), holder - .getDownloadNotification()); - ECAnimationUtils.animateShowView(getActivity(), holder.getContentView(), R - .anim.fade_in); + ECAnimationUtils.animateHideView( + getActivity(), + R.anim.fade_out, + holder.getProgressCircle(), + holder.getDownloadFab(), + holder.getDownloadingNotification() + ); + ECAnimationUtils.animateShowView( + getActivity(), + holder.getContentView(), + R.anim.fade_in + ); }); } @@ -229,8 +224,7 @@ public void onError(Throwable e) { showSnackbar(R.string.track_list_communication_error); holder.getProgressCircle().hide(); track.setDownloadState(Track.DownloadState.DOWNLOADING); - holder.getDownloadNotification().setText( - R.string.track_list_error_while_downloading); + holder.getDownloadingNotification().setText(R.string.track_list_error_while_downloading); } @Override @@ -427,8 +421,7 @@ private void updateInfoBackground(){ R.string.track_list_bg_not_logged_in_sub); mProgressView.setVisibility(View.INVISIBLE); mRecyclerView.setVisibility(View.GONE); - mRecyclerViewAdapter.mTrackDataset.clear(); - mRecyclerViewAdapter.notifyDataSetChanged(); + mRecyclerViewAdapter.clearTracks(); } else { mRecyclerView.setVisibility(View.VISIBLE); diff --git a/org.envirocar.core/build.gradle b/org.envirocar.core/build.gradle index 2fec3813a..718c9fd67 100644 --- a/org.envirocar.core/build.gradle +++ b/org.envirocar.core/build.gradle @@ -1,5 +1,5 @@ plugins { - alias libs.plugins.android.library + alias(libs.plugins.android.library) } android { diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/MapController.kt b/org.envirocar.map/src/main/java/org/envirocar/map/MapController.kt index 0fe92379c..23a9bc086 100644 --- a/org.envirocar.map/src/main/java/org/envirocar/map/MapController.kt +++ b/org.envirocar.map/src/main/java/org/envirocar/map/MapController.kt @@ -101,6 +101,12 @@ abstract class MapController { /** Removes a [Polyline] from the [MapView]. */ abstract fun removePolyline(polyline: Polyline) + /** Removes all [Marker]s from the [MapView]. */ + abstract fun clearMarkers() + + /** Removes all [Polyline]s from the [MapView]. */ + abstract fun clearPolylines() + /** * Executes the specified [block] once [readyCompletableDeferred] is completed. * The order of execution for subsequent calls is preserved. diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/MapView.kt b/org.envirocar.map/src/main/java/org/envirocar/map/MapView.kt index 4b2a4261b..54af739cd 100644 --- a/org.envirocar.map/src/main/java/org/envirocar/map/MapView.kt +++ b/org.envirocar.map/src/main/java/org/envirocar/map/MapView.kt @@ -19,6 +19,7 @@ class MapView : FrameLayout { constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) + private val lock = Any() private lateinit var instance: MapProvider /** @@ -27,7 +28,7 @@ class MapView : FrameLayout { * @param mapProvider The [MapProvider] to use for the [MapView] instance. * @return The [MapController] associated with the [MapView] instance. */ - fun getController(mapProvider: MapProvider): MapController { + fun getController(mapProvider: MapProvider): MapController = synchronized(lock) { if (!::instance.isInitialized) { instance = mapProvider addView( @@ -39,7 +40,6 @@ class MapView : FrameLayout { ) } ) - // Restore default camera state independent of the provider. with(instance.getController()) { listOf( @@ -51,9 +51,6 @@ class MapView : FrameLayout { notifyCameraUpdate(it) } } - - } else if (instance != mapProvider) { - error("MapView is already initialized with a different MapProvider.") } return instance.getController() } diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/camera/CameraUpdateFactory.kt b/org.envirocar.map/src/main/java/org/envirocar/map/camera/CameraUpdateFactory.kt index cfd66c008..de14ee110 100644 --- a/org.envirocar.map/src/main/java/org/envirocar/map/camera/CameraUpdateFactory.kt +++ b/org.envirocar.map/src/main/java/org/envirocar/map/camera/CameraUpdateFactory.kt @@ -14,6 +14,7 @@ object CameraUpdateFactory { * * @param point The geographical point. */ + @JvmStatic fun newCameraUpdateBasedOnPoint(point: Point): CameraUpdate { return CameraUpdate.Companion.CameraUpdateBasedOnPoint(point) } @@ -26,6 +27,7 @@ object CameraUpdateFactory { * @param points The geographical points specifying the bounds. * @param padding The padding in pixels. */ + @JvmStatic fun newCameraUpdateBasedOnBounds(points: List, padding: Float): CameraUpdate { return CameraUpdate.Companion.CameraUpdateBasedOnBounds(points, padding) } @@ -36,6 +38,7 @@ object CameraUpdateFactory { * * @param bearing The bearing of the camera. */ + @JvmStatic fun newCameraUpdateBearing(bearing: Float): CameraUpdate { return CameraUpdate.Companion.CameraUpdateBearing(bearing) } @@ -46,6 +49,7 @@ object CameraUpdateFactory { * * @param tilt The tilt of the camera. */ + @JvmStatic fun newCameraUpdateTilt(tilt: Float): CameraUpdate { return CameraUpdate.Companion.CameraUpdateTilt(tilt) } @@ -56,6 +60,7 @@ object CameraUpdateFactory { * * @param zoom The zoom level of the camera. */ + @JvmStatic fun newCameraUpdateZoom(zoom: Float): CameraUpdate { return CameraUpdate.Companion.CameraUpdateZoom(zoom) } diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/model/Animation.kt b/org.envirocar.map/src/main/java/org/envirocar/map/model/Animation.kt index ef23da13d..4948b8a79 100644 --- a/org.envirocar.map/src/main/java/org/envirocar/map/model/Animation.kt +++ b/org.envirocar.map/src/main/java/org/envirocar/map/model/Animation.kt @@ -25,6 +25,10 @@ class Animation private constructor( } companion object { + + /** Creates an [Animation] with default style. */ + fun default() = Builder().build() + private const val DEFAULT_DURATION = 1000L } } diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/model/Marker.kt b/org.envirocar.map/src/main/java/org/envirocar/map/model/Marker.kt index ca3847d55..7ee35c72d 100644 --- a/org.envirocar.map/src/main/java/org/envirocar/map/model/Marker.kt +++ b/org.envirocar.map/src/main/java/org/envirocar/map/model/Marker.kt @@ -14,7 +14,7 @@ import androidx.annotation.DrawableRes * @property drawable The drawable of the marker. */ class Marker private constructor( - val id: Int, + val id: Long, val point: Point, val title: String?, @DrawableRes val drawable: Int? @@ -42,7 +42,11 @@ class Marker private constructor( } companion object { + + /** Creates a [Marker] with default style. */ + fun default(point: Point) = Builder(point).build() + @Volatile - private var count = 0 + private var count = 0L } } diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/model/Polyline.kt b/org.envirocar.map/src/main/java/org/envirocar/map/model/Polyline.kt index 797dcd939..9e939cda3 100644 --- a/org.envirocar.map/src/main/java/org/envirocar/map/model/Polyline.kt +++ b/org.envirocar.map/src/main/java/org/envirocar/map/model/Polyline.kt @@ -14,7 +14,7 @@ import androidx.annotation.ColorInt * @property colors The list of colors for displaying a gradient polyline. */ class Polyline private constructor( - val id: Int, + val id: Long, val points: List, val width: Float, @ColorInt val color: Int, @@ -67,8 +67,12 @@ class Polyline private constructor( } companion object { + + /** Creates a [Polyline] with default style. */ + fun default(points: List) = Builder(points).build() + @Volatile - private var count = 0 + private var count = 0L private const val DEFAULT_WIDTH = 2.0F private const val DEFAULT_COLOR = 0xFF000000.toInt() diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/provider/mapbox/MapboxMapController.kt b/org.envirocar.map/src/main/java/org/envirocar/map/provider/mapbox/MapboxMapController.kt index 36e76ad31..62c44a452 100644 --- a/org.envirocar.map/src/main/java/org/envirocar/map/provider/mapbox/MapboxMapController.kt +++ b/org.envirocar.map/src/main/java/org/envirocar/map/provider/mapbox/MapboxMapController.kt @@ -41,8 +41,8 @@ import org.envirocar.map.model.Polyline * [Mapbox](https://www.mapbox.com) based implementation for [MapController]. */ internal class MapboxMapController(private val viewInstance: MapView) : MapController() { - private val markers = mutableMapOf() - private val polylines = mutableSetOf() + private val markers = mutableMapOf() + private val polylines = mutableSetOf() // https://docs.mapbox.com/android/maps/guides/annotations/annotations/ // https://docs.mapbox.com/android/maps/examples/line-gradient/ @@ -233,6 +233,19 @@ internal class MapboxMapController(private val viewInstance: MapView) : MapContr viewInstance.mapboxMap.style?.removeStyleLayer(MAPBOX_POLYLINE_LAYER_ID + polyline.id) } + override fun clearMarkers() { + markers.values.forEach { pointAnnotationManager.delete(it) } + markers.clear() + } + + override fun clearPolylines() { + polylines.forEach { + viewInstance.mapboxMap.style?.removeStyleSource(MAPBOX_POLYLINE_SOURCE_ID + it) + viewInstance.mapboxMap.style?.removeStyleLayer(MAPBOX_POLYLINE_LAYER_ID + it) + } + polylines.clear() + } + private fun Point.toMapboxPoint() = com.mapbox.geojson.Point.fromLngLat(longitude, latitude) private fun Float.toMapboxBearing() = this diff --git a/org.envirocar.obd/build.gradle b/org.envirocar.obd/build.gradle index 3d7a7c6b7..9c38dc300 100644 --- a/org.envirocar.obd/build.gradle +++ b/org.envirocar.obd/build.gradle @@ -1,5 +1,5 @@ plugins { - alias libs.plugins.android.library + alias(libs.plugins.android.library) } android { diff --git a/org.envirocar.remote/build.gradle b/org.envirocar.remote/build.gradle index 057609e3c..6d54819dc 100644 --- a/org.envirocar.remote/build.gradle +++ b/org.envirocar.remote/build.gradle @@ -1,5 +1,5 @@ plugins { - alias libs.plugins.android.library + alias(libs.plugins.android.library) } android { diff --git a/org.envirocar.storage/build.gradle b/org.envirocar.storage/build.gradle index 7112e8aef..ad96de5e0 100644 --- a/org.envirocar.storage/build.gradle +++ b/org.envirocar.storage/build.gradle @@ -1,5 +1,5 @@ plugins { - alias libs.plugins.android.library + alias(libs.plugins.android.library) } android { diff --git a/settings.gradle b/settings.gradle index 55c11514a..dfc9c2cf4 100644 --- a/settings.gradle +++ b/settings.gradle @@ -40,11 +40,11 @@ dependencyResolutionManagement { rootProject.name = "enviroCar" +include ":org.envirocar.algorithm" include ":org.envirocar.app" include ":org.envirocar.core" include ":org.envirocar.map" include ":org.envirocar.obd" include ":org.envirocar.remote" include ":org.envirocar.storage" -include ":org.envirocar.algorithm" include ":android-obd-simulator" From e4b138da42224aee66a623d85c14abd797a6476b Mon Sep 17 00:00:00 2001 From: Sebastian Drost Date: Fri, 5 Jul 2024 07:36:46 +0200 Subject: [PATCH 03/10] Fix track centering in MapExpandedActivity --- .../src/main/java/org/envirocar/map/MapView.kt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/MapView.kt b/org.envirocar.map/src/main/java/org/envirocar/map/MapView.kt index 54af739cd..054a5788e 100644 --- a/org.envirocar.map/src/main/java/org/envirocar/map/MapView.kt +++ b/org.envirocar.map/src/main/java/org/envirocar/map/MapView.kt @@ -2,11 +2,14 @@ package org.envirocar.map import android.content.Context import android.util.AttributeSet +import android.view.KeyEvent +import android.view.MotionEvent import android.view.View import android.widget.FrameLayout import org.envirocar.map.camera.CameraUpdateFactory import org.envirocar.map.model.Point + /** * [MapView] * --------- @@ -21,6 +24,18 @@ class MapView : FrameLayout { private val lock = Any() private lateinit var instance: MapProvider + private lateinit var listener: OnTouchListener + + override fun dispatchTouchEvent(event: MotionEvent): Boolean { + if (event.action == MotionEvent.ACTION_UP) { + if (listener != null) listener.onTouch(this, event) + } + return super.dispatchTouchEvent(event) + } + + override fun setOnTouchListener(listener: OnTouchListener?) { + this.listener = listener!! + } /** * Initializes the instance with the specified [MapProvider]. From 3c3864bc1babfbfe5cb4de79059eaa16bc3ec0fe Mon Sep 17 00:00:00 2001 From: Hitesh Kumar Saini Date: Mon, 29 Jul 2024 11:42:23 +0530 Subject: [PATCH 04/10] Refactor recording screen components to use `org.envirocar.map` module (#1007) * feat: mark model classes as open * feat: CameraState * feat: location drawables * feat: MapController addPolygon removePolygon * feat: LocationAccuracyPolygon, LocationBearingMarker & LocationPointMarker * feat: LocationIndicator * feat: Marker.Builder.withBitmap * refactor: bitmap in LocationBearingMarker & LocationPointMarker * fix: LocationIndicator bearing w/ camera bearing & enabled flag * refactor: introduce BaseLocationIndicator * build: declare uses-feature in AndroidManifest.xml * feat: CameraUpdateBasedOnPointAndBearing * chore: set default LocationIndicatorCameraMode.Follow animation duration * feat: bearing from SensorManager in LocationIndicator * chore: remove outdated TODO * build(deps): unify mapbox dependency to version catalog * refactor: make CoroutineScope private in (Base)LocationProvider * refactor: migrate TrackMapFragment to map module & remove mapbox usage from app module --- gradle/libs.versions.toml | 4 +- org.envirocar.app/build.gradle | 5 - .../res/layout/fragment_track_map.xml | 2 +- .../app/events/TrackPathOverlayEvent.java | 17 +- .../provider/RecordingDetailsProvider.java | 13 +- .../logbook/LogbookAddFuelingFragment.java | 4 +- .../recordingscreen/TrackMapFragment.java | 322 ++++-------------- .../trackdetails/MapExpandedActivity.java | 1 - .../app/views/trackdetails/MapLayer.java | 104 ------ .../app/views/trackdetails/TrackMapLayer.java | 288 ---------------- .../envirocar/app/views/utils/MapUtils.java | 58 ---- org.envirocar.map/build.gradle.kts | 4 +- .../src/main/AndroidManifest.xml | 8 +- .../java/org/envirocar/map/MapController.kt | 18 +- .../org/envirocar/map/camera/CameraState.kt | 23 ++ .../org/envirocar/map/camera/CameraUpdate.kt | 14 + .../map/camera/CameraUpdateFactory.kt | 12 + .../map/camera/MutableCameraState.kt | 16 + .../map/location/BaseLocationIndicator.kt | 161 +++++++++ .../map/location/LocationIndicator.kt | 114 +++++++ .../location/LocationIndicatorCameraMode.kt | 20 ++ .../annotation/LocationAccuracyPolygon.kt | 52 +++ .../annotation/LocationBearingMarker.kt | 44 +++ .../annotation/LocationPointMarker.kt | 44 +++ .../java/org/envirocar/map/model/Animation.kt | 2 +- .../java/org/envirocar/map/model/Marker.kt | 36 +- .../java/org/envirocar/map/model/Polygon.kt | 50 +++ .../java/org/envirocar/map/model/Polyline.kt | 14 +- .../provider/mapbox/MapboxMapController.kt | 91 ++++- .../main/res/drawable/location_bearing.xml | 9 + .../src/main/res/drawable/location_point.xml | 11 + 31 files changed, 814 insertions(+), 747 deletions(-) delete mode 100644 org.envirocar.app/src/org/envirocar/app/views/trackdetails/MapLayer.java delete mode 100644 org.envirocar.app/src/org/envirocar/app/views/trackdetails/TrackMapLayer.java delete mode 100644 org.envirocar.app/src/org/envirocar/app/views/utils/MapUtils.java create mode 100644 org.envirocar.map/src/main/java/org/envirocar/map/camera/CameraState.kt create mode 100644 org.envirocar.map/src/main/java/org/envirocar/map/camera/MutableCameraState.kt create mode 100644 org.envirocar.map/src/main/java/org/envirocar/map/location/BaseLocationIndicator.kt create mode 100644 org.envirocar.map/src/main/java/org/envirocar/map/location/LocationIndicator.kt create mode 100644 org.envirocar.map/src/main/java/org/envirocar/map/location/LocationIndicatorCameraMode.kt create mode 100644 org.envirocar.map/src/main/java/org/envirocar/map/location/annotation/LocationAccuracyPolygon.kt create mode 100644 org.envirocar.map/src/main/java/org/envirocar/map/location/annotation/LocationBearingMarker.kt create mode 100644 org.envirocar.map/src/main/java/org/envirocar/map/location/annotation/LocationPointMarker.kt create mode 100644 org.envirocar.map/src/main/java/org/envirocar/map/model/Polygon.kt create mode 100644 org.envirocar.map/src/main/res/drawable/location_bearing.xml create mode 100644 org.envirocar.map/src/main/res/drawable/location_point.xml diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b75db3335..8b64280a4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -26,7 +26,7 @@ licensePlugin = "0.16.1" lifecycleCompiler = "2.7.0" lifecycleExtensions = "2.2.0" lifecycleRuntime = "2.7.0" -mapboxAndroidSdk = "9.2.1" +mapboxMapsAndroid = "11.4.0" material = "1.12.0" mockitoCore = "5.7.0" opencsv = "4.6" @@ -88,7 +88,7 @@ gson = { module = "com.google.code.gson:gson", version.ref = "gson" } hellocharts-library = { module = "com.github.lecho:hellocharts-library", version.ref = "hellochartsLibrary" } jsr305 = { module = "com.google.code.findbugs:jsr305", version.ref = "jsr305" } junit = { module = "junit:junit", version.ref = "junit" } -mapbox-android-sdk = { module = "com.mapbox.mapboxsdk:mapbox-android-sdk", version.ref = "mapboxAndroidSdk" } +mapbox-maps-android = { module = "com.mapbox.maps:android", version.ref = "mapboxMapsAndroid" } material = { module = "com.google.android.material:material", version.ref = "material" } material-dialogs-core = { module = "com.afollestad.material-dialogs:core", version.ref = "afollestadCore" } mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockitoCore" } diff --git a/org.envirocar.app/build.gradle b/org.envirocar.app/build.gradle index 2a4492878..8ee9934ad 100644 --- a/org.envirocar.app/build.gradle +++ b/org.envirocar.app/build.gradle @@ -145,9 +145,6 @@ dependencies { implementation libs.circleimageview implementation libs.easypermissions - // TODO(alexmercerind): Remove after migration to org.envirocar.map. - implementation(libs.mapbox.android.sdk) - // Testing. testImplementation libs.junit @@ -190,6 +187,4 @@ configurations.configureEach { } } } - // TODO(alexmercerind): Remove after migration to org.envirocar.map. - exclude module: "mapbox-android-core" } diff --git a/org.envirocar.app/res/layout/fragment_track_map.xml b/org.envirocar.app/res/layout/fragment_track_map.xml index a50726990..2793fdc72 100644 --- a/org.envirocar.app/res/layout/fragment_track_map.xml +++ b/org.envirocar.app/res/layout/fragment_track_map.xml @@ -25,7 +25,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - diff --git a/org.envirocar.app/src/org/envirocar/app/events/TrackPathOverlayEvent.java b/org.envirocar.app/src/org/envirocar/app/events/TrackPathOverlayEvent.java index cf89c8938..c3cc9bbf6 100644 --- a/org.envirocar.app/src/org/envirocar/app/events/TrackPathOverlayEvent.java +++ b/org.envirocar.app/src/org/envirocar/app/events/TrackPathOverlayEvent.java @@ -18,26 +18,23 @@ */ package org.envirocar.app.events; -import org.envirocar.app.views.trackdetails.MapLayer; +import org.envirocar.map.model.Point; + +import java.util.List; /** * @author dewall */ public class TrackPathOverlayEvent { - public final MapLayer mTrackOverlay; + public final List points; /** * Constructor. * - * @param mTrackOverlay + * @param points The list of points recorded as part of the track. */ - public TrackPathOverlayEvent(MapLayer mTrackOverlay) { - this.mTrackOverlay = mTrackOverlay; - } - - @Override - public String toString() { - return super.toString(); + public TrackPathOverlayEvent(List points) { + this.points = points; } } diff --git a/org.envirocar.app/src/org/envirocar/app/recording/provider/RecordingDetailsProvider.java b/org.envirocar.app/src/org/envirocar/app/recording/provider/RecordingDetailsProvider.java index 8c45433a3..5045413d4 100644 --- a/org.envirocar.app/src/org/envirocar/app/recording/provider/RecordingDetailsProvider.java +++ b/org.envirocar.app/src/org/envirocar/app/recording/provider/RecordingDetailsProvider.java @@ -36,10 +36,12 @@ import org.envirocar.app.events.TrackPathOverlayEvent; import org.envirocar.app.recording.RecordingService; import org.envirocar.app.recording.RecordingState; -import org.envirocar.app.views.trackdetails.MapLayer; import org.envirocar.core.entity.Measurement; import org.envirocar.core.events.recording.RecordingNewMeasurementEvent; import org.envirocar.core.logging.Logger; +import org.envirocar.map.model.Point; + +import java.util.ArrayList; import io.reactivex.Scheduler; import io.reactivex.android.schedulers.AndroidSchedulers; @@ -53,7 +55,7 @@ public class RecordingDetailsProvider implements LifecycleObserver { private final Scheduler.Worker mMainThreadWorker = AndroidSchedulers.mainThread().createWorker(); - private MapLayer mTrackMapOverlay = new MapLayer(); + private final ArrayList mPoints = new ArrayList<>(); private int mNumMeasurements; private double mDistanceValue; @@ -125,7 +127,7 @@ public GPSSpeedChangeEvent produceGPSSpeedEvent() { @Produce public TrackPathOverlayEvent provideTrackPathOverlay() { - return new TrackPathOverlayEvent(mTrackMapOverlay); + return new TrackPathOverlayEvent(mPoints); } @Produce @@ -148,7 +150,8 @@ public StartingTimeEvent provideStartingTime() { private void updatePathOverlay(Measurement measurement) { mMainThreadWorker.schedule(() -> { LOG.info("Map being updated with new points: " + measurement.getLatitude() + measurement.getLongitude()); - mTrackMapOverlay.addPoint(measurement.getLatitude(), measurement.getLongitude()); + mPoints.add(new Point(measurement.getLatitude(), measurement.getLongitude())); + eventBus.post(new TrackPathOverlayEvent(mPoints)); }); } @@ -204,7 +207,7 @@ private void updateAverageSpeed(Measurement measurement) { public void clear() { mMainThreadWorker.schedule(() -> { - mTrackMapOverlay.clearPath(); + mPoints.clear(); mNumMeasurements = 0; mDistanceValue = 0; mTotalSpeed = 0; diff --git a/org.envirocar.app/src/org/envirocar/app/views/logbook/LogbookAddFuelingFragment.java b/org.envirocar.app/src/org/envirocar/app/views/logbook/LogbookAddFuelingFragment.java index 3c803bdcd..0f9b65044 100644 --- a/org.envirocar.app/src/org/envirocar/app/views/logbook/LogbookAddFuelingFragment.java +++ b/org.envirocar.app/src/org/envirocar/app/views/logbook/LogbookAddFuelingFragment.java @@ -18,8 +18,6 @@ */ package org.envirocar.app.views.logbook; -import static com.mapbox.mapboxsdk.Mapbox.getApplicationContext; - import android.app.Activity; import android.os.Bundle; import android.text.InputFilter; @@ -217,7 +215,7 @@ private void onClickAddFueling() { addFuelingTotalCostText.setError(null); addFuelingVolumeText.setError(null); - if (!new ContextInternetAccessProvider(getApplicationContext()).isConnected()){ + if (!new ContextInternetAccessProvider(requireActivity().getApplicationContext()).isConnected()){ closeThisFragment(); showSnackbarInfo(R.string.error_not_connected_to_network); } diff --git a/org.envirocar.app/src/org/envirocar/app/views/recordingscreen/TrackMapFragment.java b/org.envirocar.app/src/org/envirocar/app/views/recordingscreen/TrackMapFragment.java index 4d539024e..1549b623b 100644 --- a/org.envirocar.app/src/org/envirocar/app/views/recordingscreen/TrackMapFragment.java +++ b/org.envirocar.app/src/org/envirocar/app/views/recordingscreen/TrackMapFragment.java @@ -1,26 +1,27 @@ /** * Copyright (C) 2013 - 2021 the enviroCar community - * + *

* This file is part of the enviroCar app. - * + *

* The enviroCar app is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + *

* The enviroCar app is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General * Public License for more details. - * + *

* You should have received a copy of the GNU General Public License along * with the enviroCar app. If not, see http://www.gnu.org/licenses/. */ package org.envirocar.app.views.recordingscreen; +import android.Manifest; +import android.content.pm.PackageManager; import android.graphics.Color; import android.os.Bundle; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -28,30 +29,10 @@ import android.view.animation.AnimationUtils; import android.widget.Toast; -import androidx.annotation.NonNull; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.Nullable; import com.google.android.material.floatingactionbutton.FloatingActionButton; -//import com.mapbox.android.core.location.LocationEngineRequest; -//import com.mapbox.android.core.permissions.PermissionsListener; -//import com.mapbox.android.core.permissions.PermissionsManager; -import com.mapbox.geojson.Feature; -import com.mapbox.geojson.FeatureCollection; -import com.mapbox.geojson.LineString; -import com.mapbox.geojson.Point; -import com.mapbox.mapboxsdk.camera.CameraUpdateFactory; -import com.mapbox.mapboxsdk.location.LocationComponent; -import com.mapbox.mapboxsdk.location.LocationComponentActivationOptions; -import com.mapbox.mapboxsdk.location.OnLocationCameraTransitionListener; -import com.mapbox.mapboxsdk.location.modes.CameraMode; -import com.mapbox.mapboxsdk.location.modes.RenderMode; -import com.mapbox.mapboxsdk.maps.MapView; -import com.mapbox.mapboxsdk.maps.MapboxMap; -import com.mapbox.mapboxsdk.maps.OnMapReadyCallback; -import com.mapbox.mapboxsdk.maps.Style; -import com.mapbox.mapboxsdk.style.layers.LineLayer; -import com.mapbox.mapboxsdk.style.layers.PropertyFactory; -import com.mapbox.mapboxsdk.style.sources.GeoJsonSource; import com.squareup.otto.Subscribe; import org.envirocar.app.BaseApplicationComponent; @@ -61,11 +42,14 @@ import org.envirocar.app.injection.BaseInjectorFragment; import org.envirocar.app.injection.components.MainActivityComponent; import org.envirocar.app.injection.modules.MainActivityModule; -import org.envirocar.app.views.trackdetails.MapLayer; import org.envirocar.core.logging.Logger; - -import java.util.ArrayList; -import java.util.List; +import org.envirocar.map.MapController; +import org.envirocar.map.MapView; +import org.envirocar.map.camera.CameraUpdateFactory; +import org.envirocar.map.location.LocationIndicator; +import org.envirocar.map.location.LocationIndicatorCameraMode; +import org.envirocar.map.model.Polyline; +import org.envirocar.map.provider.mapbox.MapboxMapProvider; import io.reactivex.Scheduler; import io.reactivex.android.schedulers.AndroidSchedulers; @@ -73,30 +57,26 @@ /** * @author dewall */ -public class TrackMapFragment extends BaseInjectorFragment /* implements PermissionsListener */ { +public class TrackMapFragment extends BaseInjectorFragment { private static final Logger LOG = Logger.getLogger(TrackMapFragment.class); private FragmentTrackMapBinding binding; - // private PermissionsManager permissionsManager; - private MapboxMap mapboxMap; - private Style mapStyle; - private LocationComponent locationComponent; protected MapView mMapView; protected FloatingActionButton mFollowFab; - private MapLayer mPathOverlay; - private List points = new ArrayList<>(); + private MapController mMapController; + private LocationIndicator mLocationIndicator; + + private Polyline mCurrentPolyline; - private final Scheduler.Worker mMainThreadWorker = AndroidSchedulers.mainThread() - .createWorker(); + private final Scheduler.Worker mMainThreadWorker = AndroidSchedulers.mainThread().createWorker(); private boolean mIsFollowingLocation; @Nullable @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle - savedInstanceState) { + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { LOG.info("onCreateView()"); binding = FragmentTrackMapBinding.inflate(inflater, container, false); @@ -108,41 +88,16 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle mMapView.setOnTouchListener((v, event) -> onTouchMapView()); mFollowFab.setOnClickListener(v -> onClickFollowFab()); - // Init the map view - mMapView.onCreate(savedInstanceState); + // TODO(alexmercerind): Retrieve currently selected provider from a common repository. + mMapController = mMapView.getController(new MapboxMapProvider()); + mMapController.setMinZoom(16.0F); + mMapController.notifyCameraUpdate(CameraUpdateFactory.newCameraUpdateZoom(16.0F), null); + + mLocationIndicator = null; + mCurrentPolyline = null; + + enableLocationIndicator(); - mMapView.getMapAsync(new OnMapReadyCallback() { - @Override - public void onMapReady(@NonNull MapboxMap mapbox) { - mapboxMap = mapbox; - mapbox.getUiSettings().setLogoEnabled(false); - mapbox.getUiSettings().setAttributionEnabled(false); - mapbox.setMinZoomPreference(18); - mapbox.setStyle(new Style.Builder().fromUrl("https://api.maptiler.com/maps/basic/style.json?key=YJCrA2NeKXX45f8pOV6c "), - new Style.OnStyleLoaded() { - @Override - public void onStyleLoaded(@NonNull Style style) { - enableLocationComponent(style); - mapStyle = style; - // If the mPathOverlay has already been set, then add the overlay to the mapview. - //if (mPathOverlay != null) { - // mapStyle.addSource(mPathOverlay.getGeoJsonSource()); - // mapStyle.addLayer(mPathOverlay.getLineLayer()); - //} - GeoJsonSource geoJsonSource = new GeoJsonSource("source-id", FeatureCollection.fromFeatures(new Feature[] {Feature.fromGeometry( - LineString.fromLngLats(points) - )})); - style.addSource(geoJsonSource); - - LineLayer lineLayer = new LineLayer("linelayer", "source-id").withSourceLayer("source-id").withProperties( - PropertyFactory.lineColor(Color.BLUE), - PropertyFactory.lineWidth(4f) - ); - style.addLayer(lineLayer); - } - }); - } - }); mIsFollowingLocation = true; mFollowFab.setVisibility(View.INVISIBLE); @@ -155,37 +110,9 @@ public void onDestroyView() { binding = null; } - private void setCameraTrackingMode(@CameraMode.Mode int mode) { - locationComponent.setCameraMode(mode, new OnLocationCameraTransitionListener() { - @Override - public void onLocationCameraTransitionFinished(@CameraMode.Mode int cameraMode) { - if (mode != CameraMode.NONE) { - locationComponent.zoomWhileTracking(15, 750, new MapboxMap.CancelableCallback() { - @Override - public void onCancel() { - // No impl - } - - @Override - public void onFinish() { - locationComponent.tiltWhileTracking(45); - } - }); - } else { - mapboxMap.easeCamera(CameraUpdateFactory.tiltTo(0)); - } - } - - @Override - public void onLocationCameraTransitionCanceled(@CameraMode.Mode int cameraMode) { - // No impl - } - }); - } - protected boolean onTouchMapView() { if (mIsFollowingLocation) { - setCameraTrackingMode(CameraMode.NONE); + mLocationIndicator.setCameraMode(LocationIndicatorCameraMode.None.INSTANCE); mIsFollowingLocation = false; // show the floating action button that can enable the follow location mode. @@ -196,7 +123,7 @@ protected boolean onTouchMapView() { protected void onClickFollowFab() { if (!mIsFollowingLocation) { - setCameraTrackingMode(CameraMode.TRACKING_GPS); + mLocationIndicator.setCameraMode(new LocationIndicatorCameraMode.Follow()); mIsFollowingLocation = true; hideFollowFAB(); @@ -208,8 +135,7 @@ protected void onClickFollowFab() { */ private void showFollowFAB() { // load the translate animation. - Animation slideLeft = AnimationUtils.loadAnimation(getActivity(), - R.anim.translate_slide_in_right); + Animation slideLeft = AnimationUtils.loadAnimation(getActivity(), R.anim.translate_slide_in_right); // and start it on the fab. mFollowFab.startAnimation(slideLeft); @@ -221,8 +147,7 @@ private void showFollowFAB() { */ private void hideFollowFAB() { // load the translate animation. - Animation slideRight = AnimationUtils.loadAnimation(getActivity(), - R.anim.translate_slide_out_right); + Animation slideRight = AnimationUtils.loadAnimation(getActivity(), R.anim.translate_slide_out_right); // set a listener that makes the button invisible when the animation has finished. slideRight.setAnimationListener(new Animation.AnimationListener() { @@ -249,154 +174,55 @@ public void onAnimationRepeat(Animation animation) { @Subscribe public void onReceivePathOverlayEvent(TrackPathOverlayEvent event) { mMainThreadWorker.schedule(() -> { - mPathOverlay = event.mTrackOverlay; - points = mPathOverlay.getPoints(); - - if (mMapView != null) { - mMapView.getMapAsync( - new OnMapReadyCallback() { - @Override - public void onMapReady(@NonNull MapboxMap mapboxMap) { - TrackMapFragment.this.mapboxMap = mapboxMap; - mapboxMap.getStyle(new Style.OnStyleLoaded() { - @Override - public void onStyleLoaded(@NonNull Style style) { - style.removeLayer("linelayer"); - if(style.removeSource("source-id")) - { - Log.i("Info","removeSource successfull"); - GeoJsonSource geoJsonSource = new GeoJsonSource("source-id", FeatureCollection.fromFeatures(new Feature[] {Feature.fromGeometry( - LineString.fromLngLats(points) - )})); - style.addSource(geoJsonSource); - LineLayer lineLayer = new LineLayer("linelayer", "source-id").withSourceLayer("source-id").withProperties( - PropertyFactory.lineColor(Color.BLUE), - PropertyFactory.lineWidth(4f) - ); - style.addLayer(lineLayer); - } else{ - Log.i("Info","removeSource failed"); - } - - } - }); - } - } - ); + if (event.points.isEmpty()) { + return; + } + if (mCurrentPolyline != null) { + mMapController.removePolyline(mCurrentPolyline); } + mCurrentPolyline = new Polyline.Builder(event.points) + .withColor(Color.BLUE) + .withWidth(4.0F) + .build(); + mMapController.addPolyline(mCurrentPolyline); }); } @Override protected void injectDependencies(BaseApplicationComponent baseApplicationComponent) { - MainActivityComponent mainActivityComponent = baseApplicationComponent.plus(new MainActivityModule(getActivity())); + MainActivityComponent mainActivityComponent = baseApplicationComponent.plus(new MainActivityModule(getActivity())); mainActivityComponent.inject(this); } - @SuppressWarnings( {"MissingPermission"}) - private void enableLocationComponent(@NonNull Style loadedMapStyle) { - // Check if permissions are enabled and if not request -// if (PermissionsManager.areLocationPermissionsGranted(getContext())) { -// -// // Get an instance of the component -// locationComponent = mapboxMap.getLocationComponent(); -// -// // Activate with options -// locationComponent.activateLocationComponent( -// LocationComponentActivationOptions -// .builder(getContext(), loadedMapStyle) -// .useDefaultLocationEngine(true) -// .locationEngineRequest(new LocationEngineRequest.Builder(750) -// .setFastestInterval(750) -// .setPriority(LocationEngineRequest.PRIORITY_HIGH_ACCURACY) -// .build()) -// .build()); -// -// // Enable to make component visible -// locationComponent.setLocationComponentEnabled(true); -// -// // Set the component's camera mode -// locationComponent.setCameraMode(CameraMode.TRACKING); -// -// // Set the component's render mode -// locationComponent.setRenderMode(RenderMode.COMPASS); -// } else { -// permissionsManager = new PermissionsManager(this); -// permissionsManager.requestLocationPermissions(getActivity()); -// } - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { -// permissionsManager.onRequestPermissionsResult(requestCode, permissions, grantResults); - } - -// @Override -// public void onExplanationNeeded(List permissionsToExplain) { -// Toast.makeText(getContext(), getContext().getString(R.string.notification_location_access), Toast.LENGTH_SHORT).show(); -// } -// -// @Override -// public void onPermissionResult(boolean granted) { -// if (granted) { -// mapboxMap.getStyle(new Style.OnStyleLoaded() { -// @Override -// public void onStyleLoaded(@NonNull Style style) { -// enableLocationComponent(style); -// } -// }); -// } else { -// Toast.makeText(getContext(), getContext().getString(R.string.notification_location_access_not_granted), Toast.LENGTH_LONG).show(); -// } -// } - - @Override - @SuppressWarnings( {"MissingPermission"}) - public void onStart() { - super.onStart(); - mMapView.onStart(); - } - - @Override - public void onResume() { - super.onResume(); - onClickFollowFab(); - mMapView.onResume(); - } - - @Override - public void onPause() { - super.onPause(); - mMapView.onPause(); - } - - @Override - public void onStop() { - super.onStop(); - mMapView.onStop(); - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - mMapView.onSaveInstanceState(outState); - } - - @Override - public void onDestroy() { - super.onDestroy(); - if(mMapView!=null){ - LOG.info("mMapView is not null. onDestroy() called."); - mMapView.onDestroy(); - } else{ - LOG.info("mMapView is null. onDestroy() wasn't called."); + private void enableLocationIndicator() { + if (mLocationIndicator != null) return; + if ( + requireContext().checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED && + requireContext().checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED + ) { + mLocationIndicator = new LocationIndicator(mMapController, requireContext()); + mLocationIndicator.setCameraMode(new LocationIndicatorCameraMode.Follow()); + mLocationIndicator.enable(); + } else { + final var launcher = registerForActivityResult( + new ActivityResultContracts.RequestMultiplePermissions(), + result -> { + for (final Boolean value : result.values()) { + if (!value) { + Toast.makeText( + requireContext(), + requireContext().getString(R.string.notification_location_access_not_granted), + Toast.LENGTH_LONG + ).show(); + return; + } + } + enableLocationIndicator(); + }); + launcher.launch(new String[]{ + android.Manifest.permission.ACCESS_FINE_LOCATION, + android.Manifest.permission.ACCESS_COARSE_LOCATION + }); } - - } - - @Override - public void onLowMemory() { - super.onLowMemory(); - mMapView.onLowMemory(); } } diff --git a/org.envirocar.app/src/org/envirocar/app/views/trackdetails/MapExpandedActivity.java b/org.envirocar.app/src/org/envirocar/app/views/trackdetails/MapExpandedActivity.java index b6ed108af..0c79c7d6b 100644 --- a/org.envirocar.app/src/org/envirocar/app/views/trackdetails/MapExpandedActivity.java +++ b/org.envirocar.app/src/org/envirocar/app/views/trackdetails/MapExpandedActivity.java @@ -122,7 +122,6 @@ protected void onCreate(Bundle savedInstanceState) { legendEnd = binding.legendEnd; legendName = binding.legendUnit; - // TODO(alexmercerind): Switch to camera API in feature/location-indicator. mMapViewExpanded.setOnTouchListener((v, event) -> onTouchMapView()); mCentreFab.setOnClickListener(v -> onClickFollowFab()); mVisualiseFab.setOnClickListener(v -> onClickVisualiseFab()); diff --git a/org.envirocar.app/src/org/envirocar/app/views/trackdetails/MapLayer.java b/org.envirocar.app/src/org/envirocar/app/views/trackdetails/MapLayer.java deleted file mode 100644 index 67f9ddbb3..000000000 --- a/org.envirocar.app/src/org/envirocar/app/views/trackdetails/MapLayer.java +++ /dev/null @@ -1,104 +0,0 @@ -/** - * Copyright (C) 2013 - 2021 the enviroCar community - * - * This file is part of the enviroCar app. - * - * The enviroCar app is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * The enviroCar app is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with the enviroCar app. If not, see http://www.gnu.org/licenses/. - */ -package org.envirocar.app.views.trackdetails; - -import android.graphics.Color; - -import com.mapbox.geojson.Feature; -import com.mapbox.geojson.FeatureCollection; -import com.mapbox.geojson.LineString; -import com.mapbox.geojson.Point; -import com.mapbox.mapboxsdk.geometry.LatLng; -import com.mapbox.mapboxsdk.style.layers.LineLayer; -import com.mapbox.mapboxsdk.style.layers.Property; -import com.mapbox.mapboxsdk.style.layers.PropertyFactory; -import com.mapbox.mapboxsdk.style.layers.PropertyValue; -import com.mapbox.mapboxsdk.style.sources.GeoJsonSource; - -import org.envirocar.core.logging.Logger; - -import java.util.ArrayList; - -public class MapLayer { - private static final Logger LOG = Logger.getLogger(MapLayer.class); - - - public static final String SOURCE_NAME = "base-source"; - public static final String LAYER_NAME = "base-layer"; - - protected LineLayer lineLayer; - protected GeoJsonSource geoJsonSource; - protected ArrayList mPoints = new ArrayList<>(); - protected ArrayList latLngs = new ArrayList<>(); - protected Float maxZoom, minZoom; - - public MapLayer(){ - maxZoom = 18f; - minZoom = 1f; - } - - public void addPoint(double aLatitude, double aLongitude) { - mPoints.add(Point.fromLngLat(aLongitude,aLatitude)); - latLngs.add(new LatLng(aLatitude, aLongitude)); - } - - public void clearPath(){ - mPoints.clear(); - latLngs.clear(); - } - - public LineLayer getLineLayer() { - setLineLayer(); - return lineLayer; - } - - public void setGeoJsonSource() { - this.geoJsonSource = new GeoJsonSource(SOURCE_NAME, FeatureCollection.fromFeatures(new Feature[] {Feature.fromGeometry( - LineString.fromLngLats(mPoints) - )})); - } - - public void setLineLayer() { - lineLayer = new LineLayer(LAYER_NAME, SOURCE_NAME).withSourceLayer(SOURCE_NAME).withProperties( - PropertyFactory.lineColor(Color.parseColor("#0065A0")), - PropertyFactory.lineWidth(4f), - PropertyFactory.lineCap(Property.LINE_CAP_ROUND)); - } - - public void changeLineProperties(PropertyValue properties){ - lineLayer.setProperties(properties); - } - - public GeoJsonSource getGeoJsonSource() { - setGeoJsonSource(); - return geoJsonSource; - } - - public ArrayList getPoints() { - return mPoints; - } - - public Float getMaxZoom() { - return maxZoom; - } - - public Float getMinZoom() { - return minZoom; - } -} diff --git a/org.envirocar.app/src/org/envirocar/app/views/trackdetails/TrackMapLayer.java b/org.envirocar.app/src/org/envirocar/app/views/trackdetails/TrackMapLayer.java deleted file mode 100644 index beac57a1c..000000000 --- a/org.envirocar.app/src/org/envirocar/app/views/trackdetails/TrackMapLayer.java +++ /dev/null @@ -1,288 +0,0 @@ -/** - * Copyright (C) 2013 - 2021 the enviroCar community - * - * This file is part of the enviroCar app. - * - * The enviroCar app is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * The enviroCar app is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with the enviroCar app. If not, see http://www.gnu.org/licenses/. - */ -package org.envirocar.app.views.trackdetails; - -import android.animation.ArgbEvaluator; -import android.graphics.Color; - -import com.mapbox.geojson.BoundingBox; -import com.mapbox.geojson.Feature; -import com.mapbox.geojson.FeatureCollection; -import com.mapbox.geojson.LineString; -import com.mapbox.mapboxsdk.geometry.LatLng; -import com.mapbox.mapboxsdk.geometry.LatLngBounds; -import com.mapbox.mapboxsdk.style.expressions.Expression; -import com.mapbox.mapboxsdk.style.layers.LineLayer; -import com.mapbox.mapboxsdk.style.layers.Property; -import com.mapbox.mapboxsdk.style.layers.PropertyFactory; -import com.mapbox.mapboxsdk.style.sources.GeoJsonOptions; -import com.mapbox.mapboxsdk.style.sources.GeoJsonSource; - -import org.envirocar.core.entity.Measurement; -import org.envirocar.core.entity.Track; -import org.envirocar.core.logging.Logger; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import static com.mapbox.mapboxsdk.style.expressions.Expression.interpolate; -import static com.mapbox.mapboxsdk.style.expressions.Expression.lineProgress; -import static com.mapbox.mapboxsdk.style.expressions.Expression.linear; -import static com.mapbox.mapboxsdk.style.expressions.Expression.rgb; -import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.lineCap; -import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.lineColor; -import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.lineGradient; -import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.lineJoin; -import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.lineWidth; - -/** - * @author dewall - */ -public class TrackMapLayer extends MapLayer{ - private static final Logger LOG = Logger.getLogger(TrackMapLayer.class); - - public static final String GRADIENT_LAYER = "gradient-layer"; - public static final String GRADIENT_SOURCE = "source-layer"; - - private Float gradMax, gradMin; - private final Track mTrack; - private List measurementList = new ArrayList<>(); - private Boolean hasNoMeasurements; - protected LatLngBounds mTrackBoundingBox; - protected LatLngBounds mViewBoundingBox; - protected LatLngBounds mScrollableLimitBox; - - /** - * Constructor. - * - * @param track the track to create a overlay for. - */ - public TrackMapLayer(Track track) { - super(); - mTrack = track; - if(mTrack.getMeasurements() != null) - { - measurementList = mTrack.getMeasurements(); - hasNoMeasurements = false; - } - else - hasNoMeasurements = true; - - initPath(); - } - - /** - * Initializes the track path and the bounding boxes required by the mapviews. - */ - private void initPath() { - if(!hasNoMeasurements) - { - // For each measurement value add the longitude and latitude coordinates as a new - // mappoint to the point list. In addition, try to find out the maximum and minimum - // lon/lat coordinates for the zoom value of the mapview. - for (Measurement measurement : measurementList) { - double latitude = measurement.getLatitude(); - double longitude = measurement.getLongitude(); - - if(latitude == 0.0 || longitude == 0.0) { - LOG.warn("An coordinate was 0.0"); - continue; - } - addPoint(latitude, longitude); - } - - //If there is only one point added, create another dummy point that is close to - // the first point. If there are no points, add 2 dummy points - if(mPoints.size() == 0){ - hasNoMeasurements = true; - addPoint(7.635147738274369, 51.96057578167202); - addPoint(7.635078051137631, 51.96024289279303); - } - - } else { - addPoint(7.635147738274369, 51.96057578167202); - addPoint(7.635078051137631, 51.96024289279303); - } - setGeoJsonSource(); - setBoundingBoxes(); - } - - protected void setBoundingBoxes(){ - if(mPoints.size() == 1){ - LatLng latLng = latLngs.get(0); - mViewBoundingBox = LatLngBounds.from( - latLng.getLatitude() + 0.01, - latLng.getLongitude() + 0.01, - latLng.getLatitude() - 0.01, - latLng.getLongitude() - 0.01); - } else { - mTrackBoundingBox = new LatLngBounds.Builder() - .includes(latLngs) - .build(); - - double latRatio = Math.max(mTrackBoundingBox.getLatitudeSpan() / 10.0, 0.01); - double lngRatio = Math.max(mTrackBoundingBox.getLongitudeSpan() / 10.0, 0.01); - // The view bounding box of the pathoverlay - mViewBoundingBox = LatLngBounds.from( - mTrackBoundingBox.getLatNorth() + latRatio, - mTrackBoundingBox.getLonEast() + lngRatio, - mTrackBoundingBox.getLatSouth() - latRatio, - mTrackBoundingBox.getLonWest() - lngRatio); - - // The bounding box that limits the scrolling of the mapview. - mScrollableLimitBox = LatLngBounds.from( - mTrackBoundingBox.getLatNorth() + 0.05, - mTrackBoundingBox.getLonEast() + 0.05, - mTrackBoundingBox.getLatSouth() - 0.05, - mTrackBoundingBox.getLonWest() - 0.05); - } - - } - - /** - * Gets the {@link BoundingBox} of the track. - * - * @return the BoundingBox of the track. - */ - public LatLngBounds getTrackBoundingBox() { - return mTrackBoundingBox; - } - - /** - * Gets the view {@link BoundingBox} of the track, which is a slightly buffered bounding box - * for zoom purposes of the track. - * - * @return the BoundingBox of the track. - */ - public LatLngBounds getViewBoundingBox() { - return mViewBoundingBox; - } - - /** - * Gets the {@link BoundingBox} that is used as a scrollable limit of the track in the mapview. - * - * @return the BoundingBox of the track. - */ - public LatLngBounds getScrollableLimitBox() { - return mScrollableLimitBox; - } - - public LineLayer getGradientLineLayer(Measurement.PropertyKey propertyKey){ - - if(!hasNoMeasurements) - { - float size = (float)measurementList.size(), i= 0f; - if(size>2) - { - List propertyValues = new ArrayList<>(); - for(Measurement measurement : measurementList){ - if(measurement.hasProperty(propertyKey)) - propertyValues.add(measurement.getProperty(propertyKey)); - else { - propertyValues.add((double) 0); - LOG.info("Measurement doesnt have " + propertyKey.toString()); - } - } - - Double min ; - if(propertyKey.equals(Measurement.PropertyKey.SPEED)) - min = (double) 0; - else - min = Collections.min(propertyValues); - - Double max = Collections.max(propertyValues); - gradMax = max.floatValue(); - gradMin = min.floatValue(); - - //Set the start and end colors for the map legend - int startColor = Color.parseColor("#00FF00"); - int endColor = Color.parseColor("#FF0000"); - ArgbEvaluator evaluator = new ArgbEvaluator(); - List stops = new ArrayList<>(); - - for(Double value : propertyValues){ - //Calculate the color that each point on the line should be and add it to - // the list of stops - Double fraction = value / max; - Float stop = i / size; - Integer temp = (Integer) evaluator.evaluate(fraction.floatValue(), startColor, endColor); - stops.add(Expression.stop(stop, rgb(Color.red(temp), Color.green(temp), Color.blue(temp)))); - i++; - } - return new LineLayer(GRADIENT_LAYER, GRADIENT_SOURCE).withProperties( - lineCap(Property.LINE_CAP_ROUND), - lineJoin(Property.LINE_JOIN_ROUND), - lineWidth(4f), - lineGradient(interpolate( - linear(), lineProgress(), - stops.toArray(new Expression.Stop[0]) - ))); - - } else { - return new LineLayer(GRADIENT_LAYER, GRADIENT_SOURCE).withProperties( - lineCap(Property.LINE_CAP_ROUND), - lineJoin(Property.LINE_JOIN_ROUND), - lineWidth(4f), - lineColor(Color.parseColor("#0065A0"))); - } - } else { - // Line has no points, so return a transparent linestring - return new LineLayer(GRADIENT_LAYER, GRADIENT_SOURCE).withProperties( - lineCap(Property.LINE_CAP_ROUND), - lineJoin(Property.LINE_JOIN_ROUND), - lineWidth(4f), - lineColor(Color.TRANSPARENT)); - } - } - - public GeoJsonSource getGradientGeoJSONSource(){ - return new GeoJsonSource(GRADIENT_SOURCE, FeatureCollection.fromFeatures(new Feature[] {Feature.fromGeometry( - LineString.fromLngLats(mPoints) - )}), new GeoJsonOptions().withLineMetrics(true)); - } - - public Float getGradMax() { - return gradMax; - } - - public Float getGradMin() { - return gradMin; - } - - @Override - public void setLineLayer() { - if(!hasNoMeasurements) - lineLayer = new LineLayer(LAYER_NAME, SOURCE_NAME).withSourceLayer(SOURCE_NAME).withProperties( - PropertyFactory.lineColor(Color.parseColor("#0065A0")), - PropertyFactory.lineWidth(3f), - PropertyFactory.lineCap(Property.LINE_CAP_ROUND)); - else - lineLayer = new LineLayer(LAYER_NAME, SOURCE_NAME).withSourceLayer(SOURCE_NAME).withProperties( - PropertyFactory.lineColor(Color.TRANSPARENT), - PropertyFactory.lineWidth(3f), - PropertyFactory.lineCap(Property.LINE_CAP_ROUND)); - } - - @Override - public LineLayer getLineLayer() { - this.setLineLayer(); - return lineLayer; - } -} diff --git a/org.envirocar.app/src/org/envirocar/app/views/utils/MapUtils.java b/org.envirocar.app/src/org/envirocar/app/views/utils/MapUtils.java deleted file mode 100644 index a684d62e6..000000000 --- a/org.envirocar.app/src/org/envirocar/app/views/utils/MapUtils.java +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Copyright (C) 2013 - 2021 the enviroCar community - * - * This file is part of the enviroCar app. - * - * The enviroCar app is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * The enviroCar app is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with the enviroCar app. If not, see http://www.gnu.org/licenses/. - */ -package org.envirocar.app.views.utils; - -import com.mapbox.mapboxsdk.style.sources.TileSet; - -import org.envirocar.app.views.trackdetails.TrackMapLayer; -import org.envirocar.core.entity.Track; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * @author dewall - */ -public class MapUtils { - - private static Map TRACKID_TO_OVERLAY_CACHE = new ConcurrentHashMap<>(); - private static TileSet OSM_TILE_LAYER; - - public static TileSet getOSMTileLayer() { - if (OSM_TILE_LAYER == null) { - OSM_TILE_LAYER = new TileSet("openstreetmap", "http://tile" + - ".openstreetmap.org/{z}/{x}/{y}.png"); - OSM_TILE_LAYER.setName("OpenStreetMap"); - OSM_TILE_LAYER.setAttribution("OpenStreetMap Contributors"); - OSM_TILE_LAYER.setMaxZoom(18); - OSM_TILE_LAYER.setMinZoom(1); - } - return OSM_TILE_LAYER; - } - - public static TrackMapLayer createTrackPathOverlay(Track track){ - if(TRACKID_TO_OVERLAY_CACHE.containsKey(track.getTrackID().getId())){ - return TRACKID_TO_OVERLAY_CACHE.get(track.getTrackID().getId()); - } - - TrackMapLayer overlay = new TrackMapLayer(track); - TRACKID_TO_OVERLAY_CACHE.put(track.getTrackID().getId(), overlay); - return overlay; - } -} diff --git a/org.envirocar.map/build.gradle.kts b/org.envirocar.map/build.gradle.kts index ea72344b6..8b900f356 100644 --- a/org.envirocar.map/build.gradle.kts +++ b/org.envirocar.map/build.gradle.kts @@ -8,7 +8,7 @@ android { compileSdk = 34 defaultConfig { - minSdk = 21 + minSdk = 23 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") @@ -38,5 +38,5 @@ dependencies { androidTestImplementation(libs.androidx.espresso.core) // Provider: Mapbox - implementation("com.mapbox.maps:android:11.4.0") + implementation(libs.mapbox.maps.android) } diff --git a/org.envirocar.map/src/main/AndroidManifest.xml b/org.envirocar.map/src/main/AndroidManifest.xml index d6fda84e2..eca9c8b95 100644 --- a/org.envirocar.map/src/main/AndroidManifest.xml +++ b/org.envirocar.map/src/main/AndroidManifest.xml @@ -1,9 +1,13 @@ - - + + + + + + diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/MapController.kt b/org.envirocar.map/src/main/java/org/envirocar/map/MapController.kt index 23a9bc086..9feda1ad4 100644 --- a/org.envirocar.map/src/main/java/org/envirocar/map/MapController.kt +++ b/org.envirocar.map/src/main/java/org/envirocar/map/MapController.kt @@ -5,12 +5,15 @@ import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch +import org.envirocar.map.camera.CameraState import org.envirocar.map.camera.CameraUpdate import org.envirocar.map.camera.CameraUpdateFactory import org.envirocar.map.model.Animation import org.envirocar.map.model.Marker import org.envirocar.map.model.Point +import org.envirocar.map.model.Polygon import org.envirocar.map.model.Polyline /** @@ -33,6 +36,7 @@ import org.envirocar.map.model.Polyline * @see Animation * @see Marker * @see Point + * @see Polygon * @see Polyline */ abstract class MapController { @@ -40,9 +44,12 @@ abstract class MapController { private var queueLock = Any() private var job: Job? = null private val queue = mutableListOf<() -> Unit>() - private val scope = CoroutineScope(Dispatchers.Main) + internal val scope = CoroutineScope(Dispatchers.Main) internal val readyCompletableDeferred: CompletableDeferred = CompletableDeferred() + /** [CameraState] provides access to various camera attributes as [StateFlow]. */ + abstract val camera: CameraState + /** Sets the minimum zoom level. */ @CallSuper open fun setMinZoom(minZoom: Float) { @@ -95,18 +102,27 @@ abstract class MapController { /** Adds a [Polyline] to the [MapView]. */ abstract fun addPolyline(polyline: Polyline) + /** Adds a [Polygon] from the [MapView]. */ + abstract fun addPolygon(polygon: Polygon) + /** Removes a [Marker] from the [MapView]. */ abstract fun removeMarker(marker: Marker) /** Removes a [Polyline] from the [MapView]. */ abstract fun removePolyline(polyline: Polyline) + /** Removes a [Polygon] from the [MapView]. */ + abstract fun removePolygon(polygon: Polygon) + /** Removes all [Marker]s from the [MapView]. */ abstract fun clearMarkers() /** Removes all [Polyline]s from the [MapView]. */ abstract fun clearPolylines() + /** Removes all [Polygon]s from the [MapView]. */ + abstract fun clearPolygons() + /** * Executes the specified [block] once [readyCompletableDeferred] is completed. * The order of execution for subsequent calls is preserved. diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/camera/CameraState.kt b/org.envirocar.map/src/main/java/org/envirocar/map/camera/CameraState.kt new file mode 100644 index 000000000..a5a2eb372 --- /dev/null +++ b/org.envirocar.map/src/main/java/org/envirocar/map/camera/CameraState.kt @@ -0,0 +1,23 @@ +package org.envirocar.map.camera + +import kotlinx.coroutines.flow.StateFlow +import org.envirocar.map.model.Point + +/** + * [CameraState] + * ------------- + * [CameraState] provides access to various camera attributes as [StateFlow]. + */ +interface CameraState { + /** Position. */ + val position: StateFlow + + /** Bearing. */ + val bearing: StateFlow + + /** Tilt. */ + val tilt: StateFlow + + /** Zoom. */ + val zoom: StateFlow +} diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/camera/CameraUpdate.kt b/org.envirocar.map/src/main/java/org/envirocar/map/camera/CameraUpdate.kt index 82ff934a9..9b1549d42 100644 --- a/org.envirocar.map/src/main/java/org/envirocar/map/camera/CameraUpdate.kt +++ b/org.envirocar.map/src/main/java/org/envirocar/map/camera/CameraUpdate.kt @@ -37,6 +37,20 @@ sealed interface CameraUpdate { val padding: Float ) : CameraUpdate + /** + * [CameraUpdateBasedOnPointAndBearing] + * -------------------------- + * Camera update to transform the camera so that the point is centered on screen & the + * bearing is set to the specified value. + * + * @param point The geographical point. + * @param bearing The bearing of the camera. + */ + internal data class CameraUpdateBasedOnPointAndBearing( + val point: Point, + val bearing: Float + ) : CameraUpdate + /** * [CameraUpdateBearing] * --------------------- diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/camera/CameraUpdateFactory.kt b/org.envirocar.map/src/main/java/org/envirocar/map/camera/CameraUpdateFactory.kt index de14ee110..7727c0902 100644 --- a/org.envirocar.map/src/main/java/org/envirocar/map/camera/CameraUpdateFactory.kt +++ b/org.envirocar.map/src/main/java/org/envirocar/map/camera/CameraUpdateFactory.kt @@ -32,6 +32,18 @@ object CameraUpdateFactory { return CameraUpdate.Companion.CameraUpdateBasedOnBounds(points, padding) } + /** + * Creates a [CameraUpdate] to transform the camera so that the point is centered on screen & the + * bearing is set to the specified value. + * + * @param point The geographical point. + * @param bearing The bearing of the camera. + */ + @JvmStatic + fun newCameraUpdateBasedOnPointAndBearing(point: Point, bearing: Float): CameraUpdate { + return CameraUpdate.Companion.CameraUpdateBasedOnPointAndBearing(point, bearing) + } + /** * Creates a [CameraUpdate] to transform the camera so that the bearing is set to the specified value. * Minimum bearing value is 0 and maximum bearing value is 360. diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/camera/MutableCameraState.kt b/org.envirocar.map/src/main/java/org/envirocar/map/camera/MutableCameraState.kt new file mode 100644 index 000000000..a4dbff66d --- /dev/null +++ b/org.envirocar.map/src/main/java/org/envirocar/map/camera/MutableCameraState.kt @@ -0,0 +1,16 @@ +package org.envirocar.map.camera + +import kotlinx.coroutines.flow.MutableStateFlow +import org.envirocar.map.MapView.Companion.CAMERA_BEARING_DEFAULT +import org.envirocar.map.MapView.Companion.CAMERA_POINT_LATITUDE_DEFAULT +import org.envirocar.map.MapView.Companion.CAMERA_POINT_LONGITUDE_DEFAULT +import org.envirocar.map.MapView.Companion.CAMERA_TILT_DEFAULT +import org.envirocar.map.MapView.Companion.CAMERA_ZOOM_DEFAULT +import org.envirocar.map.model.Point + +internal class MutableCameraState: CameraState { + override val position = MutableStateFlow(Point(CAMERA_POINT_LATITUDE_DEFAULT, CAMERA_POINT_LONGITUDE_DEFAULT)) + override val bearing = MutableStateFlow(CAMERA_BEARING_DEFAULT) + override val tilt = MutableStateFlow(CAMERA_TILT_DEFAULT) + override val zoom = MutableStateFlow(CAMERA_ZOOM_DEFAULT) +} diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/location/BaseLocationIndicator.kt b/org.envirocar.map/src/main/java/org/envirocar/map/location/BaseLocationIndicator.kt new file mode 100644 index 000000000..00bdb2734 --- /dev/null +++ b/org.envirocar.map/src/main/java/org/envirocar/map/location/BaseLocationIndicator.kt @@ -0,0 +1,161 @@ +package org.envirocar.map.location + +import android.content.Context +import android.location.Location +import androidx.annotation.CallSuper +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import org.envirocar.map.MapController +import org.envirocar.map.MapView +import org.envirocar.map.camera.CameraUpdateFactory +import org.envirocar.map.location.annotation.LocationAccuracyPolygon +import org.envirocar.map.location.annotation.LocationBearingMarker +import org.envirocar.map.location.annotation.LocationPointMarker +import org.envirocar.map.model.Marker +import org.envirocar.map.model.Point +import org.envirocar.map.model.Polygon + +/** + * [BaseLocationIndicator] + * ----------------------- + * [BaseLocationIndicator] allows to display the provided location on the [MapView]. The current + * user location may be supplied manually using the [notifyLocation] method of the instance. The + * constructor takes existing [MapController] (bound to a [MapView]) reference as a parameter. + */ +open class BaseLocationIndicator( + private val controller: MapController, + private val context: Context +) { + internal var location: Location? = null + + private val markers = mutableListOf() + private val polygons = mutableListOf() + + private val lock = Any() + private val scope = CoroutineScope(Dispatchers.Main) + + private var enabled = false + private var locationIndicatorCameraMode: LocationIndicatorCameraMode = LocationIndicatorCameraMode.None + private var followCameraDebounceJob: Job? = null + + /** + * Enables the location indicator. + */ + @CallSuper + open fun enable() { + if (enabled) { + error("LocationIndicator is already enabled.") + } + enabled = true + location?.let { notifyLocation(it) } + } + + /** + * Disables the location indicator. + */ + @CallSuper + open fun disable() { + if (!enabled) { + error("LocationIndicator is already disabled.") + } + enabled = false + location = null + clearMarkers() + clearPolygons() + } + + /** + * Sets the camera mode. + */ + fun setCameraMode(value: LocationIndicatorCameraMode) { + locationIndicatorCameraMode = value + location?.let { followCameraIfRequired(it) } + } + + /** + * Notifies about the current user location to update the [MapView]. + */ + fun notifyLocation(value: Location) = synchronized(lock) { + location = value + if (enabled) { + + clearMarkers() + clearPolygons() + + markers.add(LocationPointMarker(value.toPoint(), context)) + if (value.hasBearing()) { + markers.add( + LocationBearingMarker( + value.toPoint(), + value.bearing - controller.camera.bearing.value, + context + ) + ) + } + if (value.hasAccuracy()) { + polygons.add( + LocationAccuracyPolygon( + value.toPoint(), + value.accuracy + ) + ) + } + + markers.forEach { marker -> controller.addMarker(marker) } + polygons.forEach { polygon -> controller.addPolygon(polygon) } + + followCameraIfRequired(value) + } + } + + private fun followCameraIfRequired(location: Location) { + locationIndicatorCameraMode.let { value -> + if (value is LocationIndicatorCameraMode.Follow) { + if (followCameraDebounceJob?.isActive != true) { + followCameraDebounceJob = scope.launch { + val cameraUpdate = if (location.hasBearing()) { + CameraUpdateFactory.newCameraUpdateBasedOnPointAndBearing( + location.toPoint(), + location.bearing, + ) + } else { + CameraUpdateFactory.newCameraUpdateBasedOnPoint( + location.toPoint() + ) + } + + controller.notifyCameraUpdate( + cameraUpdate, + animation = value.animation + ) + delay(value.animation.duration) + } + } + } + } + } + + private fun clearMarkers() { + markers.forEach { controller.removeMarker(it) } + markers.clear() + } + + private fun clearPolygons() { + polygons.forEach { controller.removePolygon(it) } + polygons.clear() + } + + init { + controller.camera.bearing + .onEach { synchronized(lock) { location?.let { notifyLocation(it) } } } + .launchIn(scope) + } + + private fun Location.toPoint() = Point(latitude, longitude) + +} diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/location/LocationIndicator.kt b/org.envirocar.map/src/main/java/org/envirocar/map/location/LocationIndicator.kt new file mode 100644 index 000000000..eae0b989a --- /dev/null +++ b/org.envirocar.map/src/main/java/org/envirocar/map/location/LocationIndicator.kt @@ -0,0 +1,114 @@ +package org.envirocar.map.location + +import android.Manifest +import android.content.Context +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import android.location.Location +import android.location.LocationListener +import android.location.LocationManager +import androidx.annotation.RequiresPermission +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import org.envirocar.map.MapController +import org.envirocar.map.MapView + + +/** + * [LocationIndicator] + * ------------------- + * [LocationIndicator] allows to display the current location on the [MapView]. + * + * The class uses its own internal [LocationManager] & [SensorManager]. + * + * The constructor takes existing [MapController] (bound to a [MapView]) reference as a parameter. + * + * Following permissions should be granted to the application before instantiating this class: + * * [Manifest.permission.ACCESS_FINE_LOCATION] + * * [Manifest.permission.ACCESS_COARSE_LOCATION] + */ +class LocationIndicator( + private val controller: MapController, + private val context: Context +) : BaseLocationIndicator(controller, context), LocationListener, SensorEventListener { + private val lock = Any() + + private var locationManager: LocationManager? = null + + private var sensorManager: SensorManager? = null + private var sensorManagerAzimuth: Float? = null + private var sensorManagerRotationMatrix = FloatArray(9) + private var sensorManagerOrientation = FloatArray(3) + private var accelerometerSensorEvent: SensorEvent? = null + private var magneticFieldSensorEvent: SensorEvent? = null + + /** + * Enables the location indicator. + */ + @RequiresPermission(allOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION]) + override fun enable() { + locationManager = context.getSystemService(LocationManager::class.java) + sensorManager = context.getSystemService(SensorManager::class.java) + locationManager?.requestLocationUpdates( + LocationManager.GPS_PROVIDER, + 500L, + 0F, + this@LocationIndicator + ) + sensorManager?.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)?.let { + sensorManager?.registerListener( + this, + it, + SensorManager.SENSOR_DELAY_NORMAL + ) + } + sensorManager?.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)?.let { + sensorManager?.registerListener( + this, + it, + SensorManager.SENSOR_DELAY_NORMAL + ) + } + location = locationManager?.getLastKnownLocation(LocationManager.GPS_PROVIDER) + super.enable() + } + + /** + * Disables the location indicator. + */ + override fun disable() { + locationManager?.removeUpdates(this) + sensorManager?.unregisterListener(this) + locationManager = null + sensorManager = null + super.disable() + } + + override fun onLocationChanged(value: Location) = synchronized(lock) { + // Set the bearing from the [Sensor.TYPE_MAGNETIC_FIELD] in the [Location], if available. + value.bearing = sensorManagerAzimuth ?: value.bearing + + notifyLocation(value) + } + + override fun onSensorChanged(event: SensorEvent?) = synchronized(lock) { + if (event?.sensor?.type == Sensor.TYPE_ACCELEROMETER) accelerometerSensorEvent = event + if (event?.sensor?.type == Sensor.TYPE_MAGNETIC_FIELD) magneticFieldSensorEvent = event + if (accelerometerSensorEvent != null && magneticFieldSensorEvent != null) { + SensorManager.getRotationMatrix( + sensorManagerRotationMatrix, + null, + accelerometerSensorEvent?.values, + magneticFieldSensorEvent?.values + ) + SensorManager.getOrientation(sensorManagerRotationMatrix, sensorManagerOrientation) + sensorManagerAzimuth = (Math.toDegrees(sensorManagerOrientation[0].toDouble()) + 360).toFloat() % 360 + accelerometerSensorEvent = null + magneticFieldSensorEvent = null + } + } + + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {} +} diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/location/LocationIndicatorCameraMode.kt b/org.envirocar.map/src/main/java/org/envirocar/map/location/LocationIndicatorCameraMode.kt new file mode 100644 index 000000000..bc92853f4 --- /dev/null +++ b/org.envirocar.map/src/main/java/org/envirocar/map/location/LocationIndicatorCameraMode.kt @@ -0,0 +1,20 @@ +package org.envirocar.map.location + +import org.envirocar.map.model.Animation + +/** + * [LocationIndicatorCameraMode] + * ----------------------------- + * [LocationIndicatorCameraMode] allows to specify the camera mode for the [LocationIndicator]. + */ +sealed class LocationIndicatorCameraMode { + /** + * Camera does not follow the current location. + */ + data object None : LocationIndicatorCameraMode() + + /** + * Camera updates to the current location. + */ + data class Follow(val animation: Animation = Animation.Builder().withDuration(200L).build()) : LocationIndicatorCameraMode() +} diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/location/annotation/LocationAccuracyPolygon.kt b/org.envirocar.map/src/main/java/org/envirocar/map/location/annotation/LocationAccuracyPolygon.kt new file mode 100644 index 000000000..2a6d3bfbd --- /dev/null +++ b/org.envirocar.map/src/main/java/org/envirocar/map/location/annotation/LocationAccuracyPolygon.kt @@ -0,0 +1,52 @@ +package org.envirocar.map.location.annotation + +import androidx.annotation.ColorInt +import org.envirocar.map.model.Point +import org.envirocar.map.model.Polygon +import kotlin.math.PI +import kotlin.math.asin +import kotlin.math.atan2 +import kotlin.math.cos +import kotlin.math.sin + +/** + * [LocationAccuracyPolygon] + * ------------------------- + * The [Polygon] used to display the current location accuracy in form of a circle. + */ +internal class LocationAccuracyPolygon(point: Point, radius: Float) : + Polygon(ID, calculatePolygonPoints(point, radius), COLOR) { + + companion object { + private fun calculatePolygonPoints(point: Point, radius: Float): List { + val points = (radius * 32.0).toInt() + val srcLatitude = point.latitude.toRadians() + val srcLongitude = point.longitude.toRadians() + val distance = radius / EARTH_RADIUS + return MutableList(points) { + val bearing = it * 2.0 * PI / points + val dstLatitude = asin( + sin(srcLatitude) * cos(distance) + + cos(srcLatitude) * sin(distance) * cos(bearing) + ) + val dstLongitude = srcLongitude + atan2( + sin(bearing) * sin(distance) * cos(srcLatitude), + cos(distance) - sin(srcLatitude) * sin(dstLatitude) + ) + val x = dstLatitude.toDegrees() + val y = dstLongitude.toDegrees() + Point(x, y) + }.apply { firstOrNull()?.let { add(it) } } + } + + private fun Double.toDegrees() = (this * 180.0 / PI) + private fun Double.toRadians() = (this * PI / 180.0) + + private const val EARTH_RADIUS = 6371008.8 + + private const val ID = -0xAL + + @ColorInt + private const val COLOR = 0x144384F4 + } +} diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/location/annotation/LocationBearingMarker.kt b/org.envirocar.map/src/main/java/org/envirocar/map/location/annotation/LocationBearingMarker.kt new file mode 100644 index 000000000..2e0a7d37f --- /dev/null +++ b/org.envirocar.map/src/main/java/org/envirocar/map/location/annotation/LocationBearingMarker.kt @@ -0,0 +1,44 @@ +package org.envirocar.map.location.annotation + +import android.content.Context +import android.graphics.Bitmap +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.graphics.drawable.toBitmap +import org.envirocar.map.R +import org.envirocar.map.model.Marker +import org.envirocar.map.model.Point + +/** + * [LocationBearingMarker] + * ------------------------- + * The [Marker] used to display the current location's bearing. + */ +internal class LocationBearingMarker( + point: Point, + bearing: Float, + context: Context +) : Marker( + ID, + point, + TITLE, + DRAWABLE, + bitmap ?: synchronized(lock) { + bitmap ?: AppCompatResources.getDrawable(context, R.drawable.location_bearing)!!.toBitmap() + .also { bitmap = it } + }, + SCALE, + bearing +) { + + companion object { + private val lock = Any() + + @Volatile + private var bitmap: Bitmap? = null + + private const val ID = -0xBL + private val TITLE = null + private val DRAWABLE = null + private const val SCALE = 0.2F + } +} diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/location/annotation/LocationPointMarker.kt b/org.envirocar.map/src/main/java/org/envirocar/map/location/annotation/LocationPointMarker.kt new file mode 100644 index 000000000..aaf09ff57 --- /dev/null +++ b/org.envirocar.map/src/main/java/org/envirocar/map/location/annotation/LocationPointMarker.kt @@ -0,0 +1,44 @@ +package org.envirocar.map.location.annotation + +import android.content.Context +import android.graphics.Bitmap +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.graphics.drawable.toBitmap +import org.envirocar.map.R +import org.envirocar.map.model.Marker +import org.envirocar.map.model.Point + +/** + * [LocationPointMarker] + * ------------------------- + * The [Marker] used to display the current location. + */ +internal class LocationPointMarker( + point: Point, + context: Context +) : Marker( + ID, + point, + TITLE, + DRAWABLE, + bitmap ?: synchronized(lock) { + bitmap ?: AppCompatResources.getDrawable(context, R.drawable.location_point)!!.toBitmap() + .also { bitmap = it } + }, + SCALE, + ROTATION +) { + + companion object { + private val lock = Any() + + @Volatile + private var bitmap: Bitmap? = null + + private const val ID = -0xCL + private val TITLE = null + private val DRAWABLE = null + private const val SCALE = 0.18F + private const val ROTATION = 0.0F + } +} diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/model/Animation.kt b/org.envirocar.map/src/main/java/org/envirocar/map/model/Animation.kt index 4948b8a79..639d8cbf7 100644 --- a/org.envirocar.map/src/main/java/org/envirocar/map/model/Animation.kt +++ b/org.envirocar.map/src/main/java/org/envirocar/map/model/Animation.kt @@ -8,7 +8,7 @@ package org.envirocar.map.model * * @property duration The duration of the animation (in milliseconds). */ -class Animation private constructor( +open class Animation internal constructor( val duration: Long ) { class Builder { diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/model/Marker.kt b/org.envirocar.map/src/main/java/org/envirocar/map/model/Marker.kt index 7ee35c72d..22d8e8b05 100644 --- a/org.envirocar.map/src/main/java/org/envirocar/map/model/Marker.kt +++ b/org.envirocar.map/src/main/java/org/envirocar/map/model/Marker.kt @@ -1,6 +1,8 @@ package org.envirocar.map.model +import android.graphics.Bitmap import androidx.annotation.DrawableRes +import org.envirocar.map.R /** * [Marker] @@ -12,17 +14,26 @@ import androidx.annotation.DrawableRes * @property point The geographical point. * @property title The title of the marker. * @property drawable The drawable of the marker. + * @property bitmap The bitmap of the marker. + * @property scale The scale of the marker. + * @property rotation The rotation of the marker. */ -class Marker private constructor( +open class Marker internal constructor( val id: Long, val point: Point, val title: String?, - @DrawableRes val drawable: Int? + @DrawableRes val drawable: Int?, + val bitmap: Bitmap?, + val scale: Float, + val rotation: Float ) { class Builder(private val point: Point) { private var title: String? = null @DrawableRes - private var drawable: Int? = null + private var drawable: Int? = DEFAULT_DRAWABLE + private var bitmap: Bitmap? = null + private var scale: Float = 1.0F + private var rotation: Float = 0.0F /** Sets the title of the marker. */ fun withTitle(value: String) = apply { title = value } @@ -30,13 +41,28 @@ class Marker private constructor( /** Sets the drawable of the marker. */ fun withDrawable(@DrawableRes value: Int) = apply { drawable = value } + /** Sets the bitmap of the marker. */ + fun withBitmap(value: Bitmap) = apply { bitmap = value } + + /** Sets the scale of the marker. */ + fun withScale(value: Float) = apply { scale = value } + + /** Sets the rotation of the marker. */ + fun withRotation(value: Float) = apply { rotation = value } + /** Builds the marker. */ fun build(): Marker { + assert(drawable == null || bitmap == null) { + "Marker must have either drawable or bitmap." + } return Marker( count++, point, title, - drawable + drawable, + bitmap, + scale, + rotation ) } } @@ -48,5 +74,7 @@ class Marker private constructor( @Volatile private var count = 0L + + private val DEFAULT_DRAWABLE = R.drawable.marker_icon_default } } diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/model/Polygon.kt b/org.envirocar.map/src/main/java/org/envirocar/map/model/Polygon.kt new file mode 100644 index 000000000..8bf7e9194 --- /dev/null +++ b/org.envirocar.map/src/main/java/org/envirocar/map/model/Polygon.kt @@ -0,0 +1,50 @@ +package org.envirocar.map.model + +import androidx.annotation.ColorInt + +/** + * [Polygon] + * ---------- + * [Polygon] may be used to draw a polygon on the map. + * Utilize [Polygon.Builder] to create a new [Polygon]. + * + * @property id The unique identifier. + * @property points The list of geographical points that make up the polygon. + * @property color The fill color for the polygon. + */ +open class Polygon internal constructor( + val id: Long, + val points: List, + @ColorInt val color: Int +) { + class Builder(private val points: List) { + @ColorInt + private var color: Int = DEFAULT_COLOR + + /** Sets the fill color for the polygon. */ + fun withColor(@ColorInt value: Int) = apply { color = value } + + /** Builds the polygon. */ + fun build(): Polygon { + assert(points.isNotEmpty()) { + "Polygon must have at least one point." + } + return Polygon( + count++, + points, + color + ) + } + } + + companion object { + + /** Creates a [Polygon] with default style. */ + fun default(points: List) = Builder(points).build() + + @Volatile + private var count = 0L + + private const val DEFAULT_COLOR = 0xFF000000.toInt() + } +} diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/model/Polyline.kt b/org.envirocar.map/src/main/java/org/envirocar/map/model/Polyline.kt index 9e939cda3..92de74270 100644 --- a/org.envirocar.map/src/main/java/org/envirocar/map/model/Polyline.kt +++ b/org.envirocar.map/src/main/java/org/envirocar/map/model/Polyline.kt @@ -5,15 +5,17 @@ import androidx.annotation.ColorInt /** * [Polyline] * ---------- - * [Polyline] represents a line on the map. + * [Polyline] may be used to draw a polyline on the map. * Utilize [Polyline.Builder] to create a new [Polyline]. * - * @property id The unique identifier. - * @property points The list of geographical points that make up the polyline. - * @property color The color for displaying a single color polyline. - * @property colors The list of colors for displaying a gradient polyline. + * @property id The unique identifier. + * @property points The list of geographical points that make up the polyline. + * @property color The color for displaying a single color polyline. + * @property borderWidth The border width of the polyline. + * @property borderColor The border color of the polyline. + * @property colors The list of colors for displaying a gradient polyline. */ -class Polyline private constructor( +open class Polyline internal constructor( val id: Long, val points: List, val width: Float, diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/provider/mapbox/MapboxMapController.kt b/org.envirocar.map/src/main/java/org/envirocar/map/provider/mapbox/MapboxMapController.kt index 62c44a452..e42111e2d 100644 --- a/org.envirocar.map/src/main/java/org/envirocar/map/provider/mapbox/MapboxMapController.kt +++ b/org.envirocar.map/src/main/java/org/envirocar/map/provider/mapbox/MapboxMapController.kt @@ -3,12 +3,14 @@ package org.envirocar.map.provider.mapbox import android.view.View import androidx.appcompat.content.res.AppCompatResources import androidx.core.graphics.drawable.toBitmap +import com.google.gson.JsonPrimitive import com.mapbox.geojson.Feature import com.mapbox.geojson.LineString import com.mapbox.maps.CameraBoundsOptions import com.mapbox.maps.CameraOptions import com.mapbox.maps.EdgeInsets import com.mapbox.maps.MapView +import com.mapbox.maps.coroutine.cameraChangedEvents import com.mapbox.maps.extension.style.expressions.dsl.generated.interpolate import com.mapbox.maps.extension.style.layers.addLayerBelow import com.mapbox.maps.extension.style.layers.generated.lineLayer @@ -22,17 +24,23 @@ import com.mapbox.maps.plugin.annotation.AnnotationConfig import com.mapbox.maps.plugin.annotation.annotations import com.mapbox.maps.plugin.annotation.generated.PointAnnotation import com.mapbox.maps.plugin.annotation.generated.PointAnnotationOptions +import com.mapbox.maps.plugin.annotation.generated.PolygonAnnotation +import com.mapbox.maps.plugin.annotation.generated.PolygonAnnotationOptions import com.mapbox.maps.plugin.annotation.generated.createPointAnnotationManager +import com.mapbox.maps.plugin.annotation.generated.createPolygonAnnotationManager import com.mapbox.maps.plugin.attribution.attribution import com.mapbox.maps.plugin.compass.compass import com.mapbox.maps.plugin.logo.logo import com.mapbox.maps.plugin.scalebar.scalebar +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.envirocar.map.MapController -import org.envirocar.map.R import org.envirocar.map.camera.CameraUpdate +import org.envirocar.map.camera.MutableCameraState import org.envirocar.map.model.Animation import org.envirocar.map.model.Marker import org.envirocar.map.model.Point +import org.envirocar.map.model.Polygon import org.envirocar.map.model.Polyline /** @@ -41,14 +49,24 @@ import org.envirocar.map.model.Polyline * [Mapbox](https://www.mapbox.com) based implementation for [MapController]. */ internal class MapboxMapController(private val viewInstance: MapView) : MapController() { + override val camera = MutableCameraState() private val markers = mutableMapOf() + private val polygons = mutableMapOf() private val polylines = mutableSetOf() // https://docs.mapbox.com/android/maps/guides/annotations/annotations/ // https://docs.mapbox.com/android/maps/examples/line-gradient/ // PolylineAnnotationManager API is not sufficient to create polylines with gradients. private val pointAnnotationManager = viewInstance.annotations.createPointAnnotationManager( - AnnotationConfig(layerId = MAPBOX_MARKER_LAYER_ID) + AnnotationConfig( + layerId = MAPBOX_MARKER_LAYER_ID, + ) + ) + private val polygonAnnotationManager = viewInstance.annotations.createPolygonAnnotationManager( + AnnotationConfig( + layerId = MAPBOX_POLYGON_LAYER_ID, + belowLayerId = MAPBOX_MARKER_LAYER_ID + ) ) init { @@ -68,6 +86,16 @@ internal class MapboxMapController(private val viewInstance: MapView) : MapContr readyCompletableDeferred.complete(Unit) } } + + // Forward the camera events from Mapbox to CameraState in MapController. + viewInstance.mapboxMap.cameraChangedEvents + .onEach { + camera.position.emit(it.cameraState.center.toPoint()) + camera.bearing.emit(it.cameraState.bearing.toFloat()) + camera.tilt.emit(it.cameraState.pitch.toFloat()) + camera.zoom.emit(it.cameraState.zoom.toFloat()) + } + .launchIn(scope) } override fun setMinZoom(minZoom: Float) = runWhenReady { @@ -128,6 +156,15 @@ internal class MapboxMapController(private val viewInstance: MapView) : MapContr ) } + is CameraUpdate.Companion.CameraUpdateBasedOnPointAndBearing -> { + setOrEaseCamera( + CameraOptions.Builder() + .center(point.toMapboxPoint()) + .bearing(bearing.toMapboxBearing()) + .build() + ) + } + is CameraUpdate.Companion.CameraUpdateBearing -> { setOrEaseCamera( CameraOptions.Builder() @@ -167,12 +204,20 @@ internal class MapboxMapController(private val viewInstance: MapView) : MapContr marker.title?.let { options = options.withTextField(it) } - // Mapbox does not include a default marker icon. - (marker.drawable ?: R.drawable.marker_icon_default).let { + marker.drawable?.let { options = options.withIconImage( AppCompatResources.getDrawable(viewInstance.context, it)!!.toBitmap() ) } + marker.bitmap?.let { + options = options.withIconImage(it) + } + marker.scale.let { + options = options.withIconSize(it.toDouble()) + } + marker.rotation.let { + options = options.withIconRotate(it.toDouble()) + } markers[marker.id] = pointAnnotationManager.create(options) } @@ -217,6 +262,25 @@ internal class MapboxMapController(private val viewInstance: MapView) : MapContr ) } + override fun addPolygon(polygon: Polygon) = runWhenReady { + if (polygons.contains(polygon.id)) { + error("Polygon with ID ${polygon.id} already exists.") + } + var options = PolygonAnnotationOptions() + .withData(JsonPrimitive("polygon-${polygon.id}")) + polygon.points.let { + options = options.withPoints( + listOf( + it.map { point -> point.toMapboxPoint() } + ) + ) + } + polygon.color.let { + options = options.withFillColor(it) + } + polygons[polygon.id] = polygonAnnotationManager.create(options) + } + override fun removeMarker(marker: Marker) = runWhenReady { if (!markers.contains(marker.id)) { error("Marker with ID ${marker.id} does not exist.") @@ -233,12 +297,19 @@ internal class MapboxMapController(private val viewInstance: MapView) : MapContr viewInstance.mapboxMap.style?.removeStyleLayer(MAPBOX_POLYLINE_LAYER_ID + polyline.id) } - override fun clearMarkers() { + override fun removePolygon(polygon: Polygon) = runWhenReady { + if (!polygons.contains(polygon.id)) { + error("Polygon with ID ${polygon.id} does not exist.") + } + polygons.remove(polygon.id)?.also { polygonAnnotationManager.delete(it) } + } + + override fun clearMarkers() = runWhenReady { markers.values.forEach { pointAnnotationManager.delete(it) } markers.clear() } - override fun clearPolylines() { + override fun clearPolylines() = runWhenReady { polylines.forEach { viewInstance.mapboxMap.style?.removeStyleSource(MAPBOX_POLYLINE_SOURCE_ID + it) viewInstance.mapboxMap.style?.removeStyleLayer(MAPBOX_POLYLINE_LAYER_ID + it) @@ -246,8 +317,15 @@ internal class MapboxMapController(private val viewInstance: MapView) : MapContr polylines.clear() } + override fun clearPolygons() = runWhenReady { + polygons.values.forEach { polygonAnnotationManager.delete(it) } + polygons.clear() + } + private fun Point.toMapboxPoint() = com.mapbox.geojson.Point.fromLngLat(longitude, latitude) + private fun com.mapbox.geojson.Point.toPoint() = Point(longitude(), latitude()) + private fun Float.toMapboxBearing() = this .times(MAPBOX_CAMERA_BEARING_MAX - MAPBOX_CAMERA_BEARING_MIN) .div(CAMERA_BEARING_MAX - CAMERA_BEARING_MIN) @@ -273,6 +351,7 @@ internal class MapboxMapController(private val viewInstance: MapView) : MapContr internal const val MAPBOX_CAMERA_ZOOM_MAX = 22.0F internal const val MAPBOX_MARKER_LAYER_ID = "marker-layer" + internal const val MAPBOX_POLYGON_LAYER_ID = "polygon-layer" internal const val MAPBOX_POLYLINE_LAYER_ID = "polyline-layer-" internal const val MAPBOX_POLYLINE_SOURCE_ID = "polyline-source-" } diff --git a/org.envirocar.map/src/main/res/drawable/location_bearing.xml b/org.envirocar.map/src/main/res/drawable/location_bearing.xml new file mode 100644 index 000000000..9c5df8c7a --- /dev/null +++ b/org.envirocar.map/src/main/res/drawable/location_bearing.xml @@ -0,0 +1,9 @@ + + + diff --git a/org.envirocar.map/src/main/res/drawable/location_point.xml b/org.envirocar.map/src/main/res/drawable/location_point.xml new file mode 100644 index 000000000..33ec75d5f --- /dev/null +++ b/org.envirocar.map/src/main/res/drawable/location_point.xml @@ -0,0 +1,11 @@ + + + From 41f860723bbac85c2c73eb76b35a7278c0f1c831 Mon Sep 17 00:00:00 2001 From: Hitesh Kumar Saini Date: Tue, 30 Jul 2024 15:11:53 +0530 Subject: [PATCH 05/10] Added MapLibre support to `org.envirocar.map` module (#1008) * build: MapLibre dependency in org.envirocar.map module * chore: mark MapboxMapProvider.DEFAULT_STYLE public * feat: opacity in Polygon * refactor: only display either LocationPointMarker or LocationBearingMarker * feat: MapLibreMapProvider & MapLibreMapController * feat: build option for map providers --- gradle/libs.versions.toml | 4 + org.envirocar.map/build.gradle.kts | 26 +- .../java/org/envirocar/map/MapProvider.kt | 2 - .../map/location/BaseLocationIndicator.kt | 3 +- .../annotation/LocationAccuracyPolygon.kt | 5 +- .../java/org/envirocar/map/model/Polygon.kt | 18 +- .../provider/mapbox/MapboxMapController.kt | 3 + .../map/provider/mapbox/MapboxMapProvider.kt | 2 +- .../maplibre/MapLibreMapController.kt | 374 ++++++++++++++++++ .../provider/maplibre/MapLibreMapProvider.kt | 60 +++ .../main/res/drawable/location_bearing.xml | 11 +- 11 files changed, 493 insertions(+), 15 deletions(-) create mode 100644 org.envirocar.map/src/main/java/org/envirocar/map/provider/maplibre/MapLibreMapController.kt create mode 100644 org.envirocar.map/src/main/java/org/envirocar/map/provider/maplibre/MapLibreMapProvider.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8b64280a4..ea1839bfb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -27,6 +27,8 @@ lifecycleCompiler = "2.7.0" lifecycleExtensions = "2.2.0" lifecycleRuntime = "2.7.0" mapboxMapsAndroid = "11.4.0" +maplibreGlAndroidPluginAnnotationV9 = "3.0.0" +maplibreGlAndroidSdk = "11.0.1" material = "1.12.0" mockitoCore = "5.7.0" opencsv = "4.6" @@ -89,6 +91,8 @@ hellocharts-library = { module = "com.github.lecho:hellocharts-library", version jsr305 = { module = "com.google.code.findbugs:jsr305", version.ref = "jsr305" } junit = { module = "junit:junit", version.ref = "junit" } mapbox-maps-android = { module = "com.mapbox.maps:android", version.ref = "mapboxMapsAndroid" } +maplibre-gl-android-plugin-annotation-v9 = { module = "org.maplibre.gl:android-plugin-annotation-v9", version.ref = "maplibreGlAndroidPluginAnnotationV9" } +maplibre-gl-android-sdk = { module = "org.maplibre.gl:android-sdk", version.ref = "maplibreGlAndroidSdk" } material = { module = "com.google.android.material:material", version.ref = "material" } material-dialogs-core = { module = "com.afollestad.material-dialogs:core", version.ref = "afollestadCore" } mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockitoCore" } diff --git a/org.envirocar.map/build.gradle.kts b/org.envirocar.map/build.gradle.kts index 8b900f356..35b2d93b5 100644 --- a/org.envirocar.map/build.gradle.kts +++ b/org.envirocar.map/build.gradle.kts @@ -1,3 +1,9 @@ +import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +val enableMapbox = gradleLocalProperties(rootDir, providers).getOrDefault("org.envirocar.map.enableMapbox", "true") == "true" +val enableMapLibre = gradleLocalProperties(rootDir, providers).getOrDefault("org.envirocar.map.enableMapLibre", "true") == "true" + plugins { alias(libs.plugins.android.library) alias(libs.plugins.jetbrains.kotlin.android) @@ -38,5 +44,23 @@ dependencies { androidTestImplementation(libs.androidx.espresso.core) // Provider: Mapbox - implementation(libs.mapbox.maps.android) + if (enableMapbox) { + implementation(libs.mapbox.maps.android) + } + + // Provider: MapLibre + if (enableMapLibre) { + implementation(libs.maplibre.gl.android.sdk) + implementation(libs.maplibre.gl.android.plugin.annotation.v9) + } +} + +// https://stackoverflow.com/a/69141612/12825435 +tasks.withType().configureEach { + if (!enableMapbox) { + exclude("**/provider/mapbox/**.kt") + } + if (!enableMapLibre) { + exclude("**/provider/maplibre/**.kt") + } } diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/MapProvider.kt b/org.envirocar.map/src/main/java/org/envirocar/map/MapProvider.kt index 8647c53ec..0c51272a8 100644 --- a/org.envirocar.map/src/main/java/org/envirocar/map/MapProvider.kt +++ b/org.envirocar.map/src/main/java/org/envirocar/map/MapProvider.kt @@ -10,8 +10,6 @@ import android.view.View * Currently available providers are: * * `MapboxMapProvider` * * `MapLibreMapProvider` - * * `OsmDroidMapProvider` - * * `GoogleMapProvider` * * Each provider may require additional setup during compilation. * Please refer to the module's documentation for more information. diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/location/BaseLocationIndicator.kt b/org.envirocar.map/src/main/java/org/envirocar/map/location/BaseLocationIndicator.kt index 00bdb2734..f2c51f4f2 100644 --- a/org.envirocar.map/src/main/java/org/envirocar/map/location/BaseLocationIndicator.kt +++ b/org.envirocar.map/src/main/java/org/envirocar/map/location/BaseLocationIndicator.kt @@ -87,7 +87,6 @@ open class BaseLocationIndicator( clearMarkers() clearPolygons() - markers.add(LocationPointMarker(value.toPoint(), context)) if (value.hasBearing()) { markers.add( LocationBearingMarker( @@ -96,6 +95,8 @@ open class BaseLocationIndicator( context ) ) + } else { + markers.add(LocationPointMarker(value.toPoint(), context)) } if (value.hasAccuracy()) { polygons.add( diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/location/annotation/LocationAccuracyPolygon.kt b/org.envirocar.map/src/main/java/org/envirocar/map/location/annotation/LocationAccuracyPolygon.kt index 2a6d3bfbd..d6772c294 100644 --- a/org.envirocar.map/src/main/java/org/envirocar/map/location/annotation/LocationAccuracyPolygon.kt +++ b/org.envirocar.map/src/main/java/org/envirocar/map/location/annotation/LocationAccuracyPolygon.kt @@ -15,7 +15,7 @@ import kotlin.math.sin * The [Polygon] used to display the current location accuracy in form of a circle. */ internal class LocationAccuracyPolygon(point: Point, radius: Float) : - Polygon(ID, calculatePolygonPoints(point, radius), COLOR) { + Polygon(ID, calculatePolygonPoints(point, radius), COLOR, OPACITY) { companion object { private fun calculatePolygonPoints(point: Point, radius: Float): List { @@ -47,6 +47,7 @@ internal class LocationAccuracyPolygon(point: Point, radius: Float) : private const val ID = -0xAL @ColorInt - private const val COLOR = 0x144384F4 + private const val COLOR = 0xFF4384F4.toInt() + private const val OPACITY = 0.20F } } diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/model/Polygon.kt b/org.envirocar.map/src/main/java/org/envirocar/map/model/Polygon.kt index 8bf7e9194..0d05de6ad 100644 --- a/org.envirocar.map/src/main/java/org/envirocar/map/model/Polygon.kt +++ b/org.envirocar.map/src/main/java/org/envirocar/map/model/Polygon.kt @@ -8,22 +8,28 @@ import androidx.annotation.ColorInt * [Polygon] may be used to draw a polygon on the map. * Utilize [Polygon.Builder] to create a new [Polygon]. * - * @property id The unique identifier. - * @property points The list of geographical points that make up the polygon. - * @property color The fill color for the polygon. + * @property id The unique identifier. + * @property points The list of geographical points that make up the polygon. + * @property color The fill color for the polygon. + * @property opacity The opacity of the polygon. */ open class Polygon internal constructor( val id: Long, val points: List, - @ColorInt val color: Int + @ColorInt val color: Int, + val opacity: Float ) { class Builder(private val points: List) { @ColorInt private var color: Int = DEFAULT_COLOR + private var opacity: Float = DEFAULT_OPACITY /** Sets the fill color for the polygon. */ fun withColor(@ColorInt value: Int) = apply { color = value } + /** Sets the fill opacity for the polygon. */ + fun withOpacity(value: Float) = apply { opacity = value } + /** Builds the polygon. */ fun build(): Polygon { assert(points.isNotEmpty()) { @@ -32,7 +38,8 @@ open class Polygon internal constructor( return Polygon( count++, points, - color + color, + opacity ) } } @@ -46,5 +53,6 @@ open class Polygon internal constructor( private var count = 0L private const val DEFAULT_COLOR = 0xFF000000.toInt() + private const val DEFAULT_OPACITY = 1.0F } } diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/provider/mapbox/MapboxMapController.kt b/org.envirocar.map/src/main/java/org/envirocar/map/provider/mapbox/MapboxMapController.kt index e42111e2d..6b952f7a9 100644 --- a/org.envirocar.map/src/main/java/org/envirocar/map/provider/mapbox/MapboxMapController.kt +++ b/org.envirocar.map/src/main/java/org/envirocar/map/provider/mapbox/MapboxMapController.kt @@ -278,6 +278,9 @@ internal class MapboxMapController(private val viewInstance: MapView) : MapContr polygon.color.let { options = options.withFillColor(it) } + polygon.opacity.let { + options = options.withFillOpacity(it.toDouble()) + } polygons[polygon.id] = polygonAnnotationManager.create(options) } diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/provider/mapbox/MapboxMapProvider.kt b/org.envirocar.map/src/main/java/org/envirocar/map/provider/mapbox/MapboxMapProvider.kt index 929fae9bc..aebe166f1 100644 --- a/org.envirocar.map/src/main/java/org/envirocar/map/provider/mapbox/MapboxMapProvider.kt +++ b/org.envirocar.map/src/main/java/org/envirocar/map/provider/mapbox/MapboxMapProvider.kt @@ -48,6 +48,6 @@ class MapboxMapProvider( } companion object { - private const val DEFAULT_STYLE = "mapbox://styles/mapbox/streets-v12" + const val DEFAULT_STYLE = "mapbox://styles/mapbox/streets-v12" } } diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/provider/maplibre/MapLibreMapController.kt b/org.envirocar.map/src/main/java/org/envirocar/map/provider/maplibre/MapLibreMapController.kt new file mode 100644 index 000000000..a4634df8f --- /dev/null +++ b/org.envirocar.map/src/main/java/org/envirocar/map/provider/maplibre/MapLibreMapController.kt @@ -0,0 +1,374 @@ +package org.envirocar.map.provider.maplibre + +import android.view.View +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.graphics.drawable.toBitmap +import kotlinx.coroutines.flow.update +import org.envirocar.map.MapController +import org.envirocar.map.camera.CameraUpdate +import org.envirocar.map.camera.MutableCameraState +import org.envirocar.map.model.Animation +import org.envirocar.map.model.Marker +import org.envirocar.map.model.Point +import org.envirocar.map.model.Polygon +import org.envirocar.map.model.Polyline +import org.maplibre.android.camera.CameraPosition +import org.maplibre.android.camera.CameraUpdateFactory +import org.maplibre.android.geometry.LatLngBounds +import org.maplibre.android.maps.MapLibreMap +import org.maplibre.android.maps.MapView +import org.maplibre.android.maps.Style +import org.maplibre.android.plugins.annotation.Fill +import org.maplibre.android.plugins.annotation.FillManager +import org.maplibre.android.plugins.annotation.FillOptions +import org.maplibre.android.plugins.annotation.Symbol +import org.maplibre.android.plugins.annotation.SymbolManager +import org.maplibre.android.plugins.annotation.SymbolOptions +import org.maplibre.android.style.expressions.Expression +import org.maplibre.android.style.expressions.Expression.interpolate +import org.maplibre.android.style.expressions.Expression.lineProgress +import org.maplibre.android.style.expressions.Expression.linear +import org.maplibre.android.style.layers.LineLayer +import org.maplibre.android.style.layers.Property +import org.maplibre.android.style.layers.PropertyFactory.lineCap +import org.maplibre.android.style.layers.PropertyFactory.lineColor +import org.maplibre.android.style.layers.PropertyFactory.lineGradient +import org.maplibre.android.style.layers.PropertyFactory.lineJoin +import org.maplibre.android.style.layers.PropertyFactory.lineWidth +import org.maplibre.android.style.sources.GeoJsonSource +import org.maplibre.geojson.Feature +import org.maplibre.geojson.LineString + +/** + * [MapLibreMapController] + * ----------------------- + * [MapLibre](https://www.maplibre.org) based implementation for [MapController]. + */ +internal class MapLibreMapController(private val viewInstance: MapView) : MapController() { + override val camera = MutableCameraState() + private val markers = mutableMapOf() + private val polygons = mutableMapOf() + private val polylines = mutableSetOf() + + // Symbol = Marker + // Fill = Polygon + private lateinit var symbolManager: SymbolManager + private lateinit var fillManager: FillManager + + private lateinit var mapLibreMap: MapLibreMap + private lateinit var style: Style + + init { + viewInstance.getMapAsync { mapLibreMap -> + this.mapLibreMap = mapLibreMap + + // Disable attribution, compass & logo. + mapLibreMap.uiSettings.isAttributionEnabled = false + mapLibreMap.uiSettings.isCompassEnabled = false + mapLibreMap.uiSettings.isLogoEnabled = false + + // Once the map style is loaded, make the view visible & mark this instance as ready. + if (mapLibreMap.style != null) { + style = mapLibreMap.style!! + viewInstance.visibility = View.VISIBLE + symbolManager = SymbolManager(viewInstance, mapLibreMap, style) + fillManager = FillManager(viewInstance, mapLibreMap, style) + readyCompletableDeferred.complete(Unit) + } else { + mapLibreMap.getStyle { + style = it + viewInstance.visibility = View.VISIBLE + symbolManager = SymbolManager(viewInstance, mapLibreMap, style) + fillManager = FillManager(viewInstance, mapLibreMap, style) + readyCompletableDeferred.complete(Unit) + } + } + + // Forward the camera events from Mapbox to CameraState in MapController. + mapLibreMap.addOnCameraMoveListener { + camera.position.update { mapLibreMap.cameraPosition.target!!.toPoint() } + camera.bearing.update { mapLibreMap.cameraPosition.bearing.toFloat() } + camera.tilt.update { mapLibreMap.cameraPosition.tilt.toFloat() } + camera.zoom.update { mapLibreMap.cameraPosition.zoom.toFloat() } + } + } + } + + override fun setMinZoom(minZoom: Float) = runWhenReady { + super.setMinZoom(minZoom) + mapLibreMap.setMinZoomPreference(minZoom.toMapLibreZoom()) + } + + override fun setMaxZoom(maxZoom: Float) = runWhenReady { + super.setMaxZoom(maxZoom) + mapLibreMap.setMaxZoomPreference(maxZoom.toMapLibreZoom()) + } + + override fun notifyCameraUpdate(cameraUpdate: CameraUpdate, animation: Animation?) = + runWhenReady { + super.notifyCameraUpdate(cameraUpdate, animation) + + fun setOrEaseCamera(cameraUpdate: org.maplibre.android.camera.CameraUpdate) { + if (animation == null) { + mapLibreMap.moveCamera(cameraUpdate) + } else { + mapLibreMap.easeCamera(cameraUpdate, animation.duration.toInt()) + } + } + + with(cameraUpdate) { + when (this) { + is CameraUpdate.Companion.CameraUpdateBasedOnBounds -> { + setOrEaseCamera( + CameraUpdateFactory.newLatLngBounds( + LatLngBounds.fromLatLngs(points.map { it.toMapLibreLatLng() }), + padding.toInt() + ) + ) + } + + is CameraUpdate.Companion.CameraUpdateBasedOnPoint -> { + setOrEaseCamera( + CameraUpdateFactory.newLatLng( + point.toMapLibreLatLng() + ) + ) + } + + is CameraUpdate.Companion.CameraUpdateBasedOnPointAndBearing -> { + setOrEaseCamera( + CameraUpdateFactory.newCameraPosition( + CameraPosition.Builder() + .target(point.toMapLibreLatLng()) + .bearing(bearing.toMapLibreBearing()) + .build() + ) + ) + } + + is CameraUpdate.Companion.CameraUpdateBearing -> { + setOrEaseCamera( + CameraUpdateFactory.bearingTo( + bearing.toMapLibreBearing() + ) + ) + } + + is CameraUpdate.Companion.CameraUpdateTilt -> { + setOrEaseCamera( + CameraUpdateFactory.tiltTo( + tilt.toMapLibreTilt() + ) + ) + } + + is CameraUpdate.Companion.CameraUpdateZoom -> { + setOrEaseCamera( + CameraUpdateFactory.zoomTo( + zoom.toMapLibreZoom() + ) + ) + } + } + } + } + + override fun addMarker(marker: Marker) = runWhenReady { + if (markers.contains(marker.id)) { + error("Marker with ID ${marker.id} already exists.") + } + var options = SymbolOptions() + marker.point.let { + options = options.withLatLng(it.toMapLibreLatLng()) + } + marker.title?.let { + options = options.withTextField(it) + } + marker.drawable?.let { + val id = MAPLIBRE_SYMBOL_ID + marker.id + if (style.getImage(id) == null) { + style.addImage( + id, + AppCompatResources.getDrawable(viewInstance.context, it)!!.toBitmap() + ) + } + options = options.withIconImage(id) + } + marker.bitmap?.let { + val id = MAPLIBRE_SYMBOL_ID + marker.id + if (style.getImage(id) == null) { + style.addImage(id, it) + } + options = options.withIconImage(id) + } + marker.scale.let { + // HACK: Use 0.6x the size if drawable is used instead of bitmap. + // Apparently the default marker size in MapLibre is larger compared to the one in Mapbox. + options = options.withIconSize( + it * (marker.drawable?.let { 0.6F } ?: 1.0F) + ) + } + marker.rotation.let { + options = options.withIconRotate(it) + } + markers[marker.id] = symbolManager.create(options) + } + + override fun addPolyline(polyline: Polyline) = runWhenReady { + if (polylines.contains(polyline.id)) { + error("Polyline with ID ${polyline.id} already exists.") + } + polylines.add(polyline.id) + style.addSource( + GeoJsonSource( + MAPLIBRE_POLYLINE_SOURCE_ID + polyline.id, + Feature.fromGeometry( + LineString.fromLngLats( + polyline.points.map { it.toMapLibrePoint() } + ) + ) + ) + ) + var layer = LineLayer( + MAPLIBRE_POLYLINE_LAYER_ID + polyline.id, + MAPLIBRE_POLYLINE_SOURCE_ID + polyline.id + ) + .withProperties( + lineCap(Property.LINE_CAP_ROUND), + lineJoin(Property.LINE_JOIN_ROUND), + lineWidth(polyline.width), + lineColor(polyline.color), + // No support for following attributes in MapLibre: + // polyline.borderWidth + // polyline.borderColor + ) + if (polyline.colors != null) { + layer = layer.withProperties( + lineGradient( + interpolate( + linear(), + lineProgress(), + *polyline.colors.mapIndexed { index, color -> + Expression.stop( + (index + 1).toDouble() / polyline.points.size.toDouble(), + color + ) + }.toTypedArray() + ) + ) + ) + } + style.addLayerBelow(layer, MAPLIBRE_SYMBOL_LAYER_ID) + } + + override fun addPolygon(polygon: Polygon) = runWhenReady { + if (polygons.contains(polygon.id)) { + error("Polygon with ID ${polygon.id} already exists.") + } + var options = FillOptions() + polygon.points.let { + options = options.withLatLngs( + listOf( + it.map { point -> point.toMapLibreLatLng() } + ) + ) + } + polygon.color.let { + options = options.withFillColor(it.toMapLibreColor()) + } + polygon.opacity.let { + options = options.withFillOpacity(it) + } + polygons[polygon.id] = fillManager.create(options) + } + + override fun removeMarker(marker: Marker) = runWhenReady { + if (!markers.contains(marker.id)) { + error("Marker with ID ${marker.id} does not exist.") + } + markers.remove(marker.id)?.also { + if (it.iconImage != null) { + style.removeImage(it.iconImage!!) + } + symbolManager.delete(it) + } + } + + override fun removePolyline(polyline: Polyline) = runWhenReady { + if (!polylines.contains(polyline.id)) { + error("Polyline with ID ${polyline.id} does not exist.") + } + polylines.remove(polyline.id) + style.removeSource(MAPLIBRE_POLYLINE_SOURCE_ID + polyline.id) + style.removeLayer(MAPLIBRE_POLYLINE_LAYER_ID + polyline.id) + } + + override fun removePolygon(polygon: Polygon) = runWhenReady { + if (!polygons.contains(polygon.id)) { + error("Polygon with ID ${polygon.id} does not exist.") + } + polygons.remove(polygon.id)?.also { fillManager.delete(it) } + } + + override fun clearMarkers() = runWhenReady { + markers.values.forEach { + if (it.iconImage != null) { + style.removeImage(it.iconImage!!) + } + symbolManager.delete(it) + } + markers.clear() + } + + override fun clearPolylines() = runWhenReady { + polylines.forEach { + style.removeSource(MAPLIBRE_POLYLINE_SOURCE_ID + it) + style.removeLayer(MAPLIBRE_POLYLINE_LAYER_ID + it) + } + polylines.clear() + } + + override fun clearPolygons() = runWhenReady { + polygons.values.forEach { fillManager.delete(it) } + polygons.clear() + } + + private fun Point.toMapLibrePoint() = org.maplibre.geojson.Point.fromLngLat(longitude, latitude) + private fun Point.toMapLibreLatLng() = org.maplibre.android.geometry.LatLng(latitude, longitude) + + private fun org.maplibre.android.geometry.LatLng.toPoint() = Point(longitude, latitude) + + private fun Float.toMapLibreBearing() = this + .times(MAPLIBRE_CAMERA_BEARING_MAX - MAPLIBRE_CAMERA_BEARING_MIN) + .div(CAMERA_BEARING_MAX - CAMERA_BEARING_MIN) + .toDouble() + + private fun Float.toMapLibreTilt() = this + .times(MAPLIBRE_CAMERA_TILT_MAX - MAPLIBRE_CAMERA_TILT_MIN) + .div(CAMERA_TILT_MAX - CAMERA_TILT_MIN) + .toDouble() + + private fun Float.toMapLibreZoom() = this + .times(MAPLIBRE_CAMERA_ZOOM_MAX - MAPLIBRE_CAMERA_ZOOM_MIN) + .div(CAMERA_ZOOM_MAX - CAMERA_ZOOM_MIN) + .toDouble() + + private fun Int.toMapLibreColor() = String.format("#%06X", 0xFFFFFF and this) + + + companion object { + internal const val MAPLIBRE_CAMERA_BEARING_MIN = 0.0F + internal const val MAPLIBRE_CAMERA_BEARING_MAX = 360.0F + internal const val MAPLIBRE_CAMERA_TILT_MIN = 0.0F + internal const val MAPLIBRE_CAMERA_TILT_MAX = 60.0F + internal const val MAPLIBRE_CAMERA_ZOOM_MIN = 0.0F + internal const val MAPLIBRE_CAMERA_ZOOM_MAX = 22.0F + + internal const val MAPLIBRE_SYMBOL_ID = "symbol-" + + // https://github.com/maplibre/maplibre-plugins-android/blob/main/plugin-annotation/src/main/java/org/maplibre/android/plugins/annotation/SymbolElementProvider.java#L19 + internal const val MAPLIBRE_SYMBOL_LAYER_ID = "mapbox-android-symbol-layer-1" + internal const val MAPLIBRE_POLYLINE_LAYER_ID = "polyline-layer-" + internal const val MAPLIBRE_POLYLINE_SOURCE_ID = "polyline-source-" + } +} diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/provider/maplibre/MapLibreMapProvider.kt b/org.envirocar.map/src/main/java/org/envirocar/map/provider/maplibre/MapLibreMapProvider.kt new file mode 100644 index 000000000..ef094c460 --- /dev/null +++ b/org.envirocar.map/src/main/java/org/envirocar/map/provider/maplibre/MapLibreMapProvider.kt @@ -0,0 +1,60 @@ +package org.envirocar.map.provider.maplibre + +import android.content.Context +import org.envirocar.map.MapController +import org.envirocar.map.MapProvider +import org.maplibre.android.MapLibre +import org.maplibre.android.maps.MapView + +/** + * [MapLibreMapProvider] + * --------------------- + * [MapLibre](https://maplibre.org) based implementation for [MapProvider]. + * + * Following options are available to be configured: + * + * @param style + * The style for the map to be loaded from a specified URI or from a JSON represented as [String]. + * The URI can be one of the following forms: + * * `http://` + * * `https://` + * * `asset://` + * * `file://` + */ +class MapLibreMapProvider( + private val style: String +) : MapProvider { + private lateinit var viewInstance: MapView + private lateinit var controllerInstance: MapLibreMapController + + override fun getView(context: Context): MapView { + if (!initailized) { + initailized = true + // Must be called before consuming any MapLibre API. + MapLibre.getInstance(context) + } + if (!::viewInstance.isInitialized) { + viewInstance = MapView(context).apply { + getMapAsync { + it.setStyle(style) + } + } + } + return viewInstance + } + + override fun getController(): MapController { + if (!::viewInstance.isInitialized) { + error("MapLibreMapProvider is not initialized.") + } + if (!::controllerInstance.isInitialized) { + controllerInstance = MapLibreMapController(viewInstance) + } + return controllerInstance + } + + companion object { + @Volatile + private var initailized = false + } +} diff --git a/org.envirocar.map/src/main/res/drawable/location_bearing.xml b/org.envirocar.map/src/main/res/drawable/location_bearing.xml index 9c5df8c7a..42b18e2ae 100644 --- a/org.envirocar.map/src/main/res/drawable/location_bearing.xml +++ b/org.envirocar.map/src/main/res/drawable/location_bearing.xml @@ -3,7 +3,12 @@ android:height="172dp" android:viewportWidth="172" android:viewportHeight="172"> - + + From 32b407ab4e566a8f3aadb5d0e0a8d3aeae4402a6 Mon Sep 17 00:00:00 2001 From: Hitesh Kumar Saini Date: Tue, 20 Aug 2024 17:25:55 +0530 Subject: [PATCH 06/10] Added option to configure map provider & style in the application settings (#1010) * feat: MapLibreMapProvider default style * feat: MapProviderRepository * refactor: fetch MapProvider from MapProviderRepository * fix: support for gradient polyline in MapLibreMapController * chore: expand_(less|more) drawables * feat: preference_map_view_dialog & preference_map_style_list_item layout * feat: MapViewPreference * chore: update height * chore: update proguard rules * refactor: initialize map provider dynamically using reflection * fix: openstreetmap.org attribution * refactor: expose AttributionSettings & LogoSettings * refactor: enable attribution & logo --- org.envirocar.app/build.gradle | 2 + .../res/drawable/baseline_expand_less_24.xml | 5 + .../res/drawable/baseline_expand_more_24.xml | 5 + .../layout/preference_map_style_list_item.xml | 20 ++ .../res/layout/preference_map_view_dialog.xml | 147 ++++++++ org.envirocar.app/res/values/strings.xml | 4 + .../res/values/strings_activity_settings.xml | 5 + org.envirocar.app/res/xml/settings.xml | 6 + .../app/handler/ApplicationSettings.java | 57 +++- .../app/views/others/OthersFragment.java | 23 +- .../recordingscreen/TrackMapFragment.java | 5 +- .../app/views/settings/SettingsActivity.java | 38 ++- .../settings/custom/MapViewPreference.java | 314 ++++++++++++++++++ .../custom/SamplingRatePreference.java | 3 +- .../trackdetails/MapExpandedActivity.java | 21 +- .../trackdetails/TrackDetailsActivity.java | 21 +- .../tracklist/AbstractTrackListCardAdapter.kt | 18 +- .../app/views/utils/MapProviderRepository.kt | 67 ++++ org.envirocar.map/proguard-rules.pro | 5 +- .../main/assets/maplibre_default_style.json | 21 ++ .../map/model/AttributionSettings.kt | 49 +++ .../org/envirocar/map/model/LogoSettings.kt | 49 +++ .../provider/mapbox/MapboxMapController.kt | 28 +- .../map/provider/mapbox/MapboxMapProvider.kt | 19 +- .../maplibre/MapLibreMapController.kt | 53 ++- .../provider/maplibre/MapLibreMapProvider.kt | 23 +- 26 files changed, 962 insertions(+), 46 deletions(-) create mode 100644 org.envirocar.app/res/drawable/baseline_expand_less_24.xml create mode 100644 org.envirocar.app/res/drawable/baseline_expand_more_24.xml create mode 100644 org.envirocar.app/res/layout/preference_map_style_list_item.xml create mode 100644 org.envirocar.app/res/layout/preference_map_view_dialog.xml create mode 100644 org.envirocar.app/src/org/envirocar/app/views/settings/custom/MapViewPreference.java create mode 100644 org.envirocar.app/src/org/envirocar/app/views/utils/MapProviderRepository.kt create mode 100644 org.envirocar.map/src/main/assets/maplibre_default_style.json create mode 100644 org.envirocar.map/src/main/java/org/envirocar/map/model/AttributionSettings.kt create mode 100644 org.envirocar.map/src/main/java/org/envirocar/map/model/LogoSettings.kt diff --git a/org.envirocar.app/build.gradle b/org.envirocar.app/build.gradle index 8ee9934ad..97c53f82f 100644 --- a/org.envirocar.app/build.gradle +++ b/org.envirocar.app/build.gradle @@ -16,6 +16,8 @@ android { multiDexEnabled true testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + buildConfigField "String", "MAPTILER_API_KEY", "\"${project.properties["MAPTILER_API_KEY"]}\"" } buildFeatures { diff --git a/org.envirocar.app/res/drawable/baseline_expand_less_24.xml b/org.envirocar.app/res/drawable/baseline_expand_less_24.xml new file mode 100644 index 000000000..8a2aaf965 --- /dev/null +++ b/org.envirocar.app/res/drawable/baseline_expand_less_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/org.envirocar.app/res/drawable/baseline_expand_more_24.xml b/org.envirocar.app/res/drawable/baseline_expand_more_24.xml new file mode 100644 index 000000000..26be5ff92 --- /dev/null +++ b/org.envirocar.app/res/drawable/baseline_expand_more_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/org.envirocar.app/res/layout/preference_map_style_list_item.xml b/org.envirocar.app/res/layout/preference_map_style_list_item.xml new file mode 100644 index 000000000..a37f53abe --- /dev/null +++ b/org.envirocar.app/res/layout/preference_map_style_list_item.xml @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/org.envirocar.app/res/layout/preference_map_view_dialog.xml b/org.envirocar.app/res/layout/preference_map_view_dialog.xml new file mode 100644 index 000000000..b4c6f90bb --- /dev/null +++ b/org.envirocar.app/res/layout/preference_map_view_dialog.xml @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/org.envirocar.app/res/values/strings.xml b/org.envirocar.app/res/values/strings.xml index b76156aa6..d296cffcf 100644 --- a/org.envirocar.app/res/values/strings.xml +++ b/org.envirocar.app/res/values/strings.xml @@ -147,6 +147,10 @@ pref_search_interval pref_automatic_recording pref_samplingrate + prefkey_map_view + prefkey_map_view_provider + prefkey_map_view_maplibre_style + prefkey_map_view_mapbox_style pref_gps_connection pref_privacy diff --git a/org.envirocar.app/res/values/strings_activity_settings.xml b/org.envirocar.app/res/values/strings_activity_settings.xml index f048abeaf..37ee80677 100644 --- a/org.envirocar.app/res/values/strings_activity_settings.xml +++ b/org.envirocar.app/res/values/strings_activity_settings.xml @@ -108,4 +108,9 @@ DVFO Campaign + Map View Customization + Customize the look of map views throughout the application by changing the map provider & associated style. + Map Provider: + Style: + diff --git a/org.envirocar.app/res/xml/settings.xml b/org.envirocar.app/res/xml/settings.xml index b4ef9a34a..c540205c7 100644 --- a/org.envirocar.app/res/xml/settings.xml +++ b/org.envirocar.app/res/xml/settings.xml @@ -57,6 +57,12 @@ android:summary="@string/sampling_rate_summary" android:title="@string/sampling_rate_title" app:iconSpaceReserved="false" /> + getAutomaticUploadObservable(Context context){ + public static Observable getAutomaticUploadObservable(Context context) { return getRxSharedPreferences(context) .getBoolean(s(context, R.string.prefkey_automatic_upload), DEFAULT_AUTOMATIC_UPLOAD) .asObservable(); @@ -157,6 +173,39 @@ public static void setSamplingRate(Context context, int samplingRate) { .apply(); } + public static String getMapProvider(Context context) { + return getSharedPreferences(context).getString(s(context, R.string.prefkey_map_view_map_provider), DEFAULT_MAP_PROVIDER); + } + + public static void setMapProvider(Context context, String mapProvider) { + getSharedPreferences(context) + .edit() + .putString(s(context, R.string.prefkey_map_view_map_provider), mapProvider) + .apply(); + } + + public static String getMapLibreStyle(Context context) { + return getSharedPreferences(context).getString(s(context, R.string.prefkey_map_view_maplibre_style), DEFAULT_MAPLIBRE_STYLE); + } + + public static void setMapLibreStyle(Context context, String maplibreStyle) { + getSharedPreferences(context) + .edit() + .putString(s(context, R.string.prefkey_map_view_maplibre_style), maplibreStyle) + .apply(); + } + + public static String getMapboxStyle(Context context) { + return getSharedPreferences(context).getString(s(context, R.string.prefkey_map_view_mapbox_style), DEFAULT_MAPBOX_STYLE); + } + + public static void setMapboxStyle(Context context, String mapboxStyle) { + getSharedPreferences(context) + .edit() + .putString(s(context, R.string.prefkey_map_view_mapbox_style), mapboxStyle) + .apply(); + } + public static Observable getRxSharedSamplingRate(Context context) { return getRxSharedPreferences(context) .getInteger(s(context, R.string.prefkey_samplingrate), DEFAULT_SAMPLING_RATE) @@ -263,7 +312,7 @@ public static SharedPreferences getSharedPreferences(Context context) { return PreferenceManager.getDefaultSharedPreferences(context); } - public static Observable getCampaignProfileObservable(Context context){ + public static Observable getCampaignProfileObservable(Context context) { return getRxSharedPreferences(context) .getString(s(context, R.string.prefkey_campaign_profile), DEFAULT_CAMPAIGN_PROFILE) .asObservable(); @@ -273,7 +322,7 @@ public static String getCampaignProfile(Context context) { return getSharedPreferences(context).getString(s(context, R.string.prefkey_campaign_profile), DEFAULT_CAMPAIGN_PROFILE); } - private static final String s(Context context, int id){ + private static final String s(Context context, int id) { return context.getString(id); } diff --git a/org.envirocar.app/src/org/envirocar/app/views/others/OthersFragment.java b/org.envirocar.app/src/org/envirocar/app/views/others/OthersFragment.java index b1ffd7a4f..02f48d194 100644 --- a/org.envirocar.app/src/org/envirocar/app/views/others/OthersFragment.java +++ b/org.envirocar.app/src/org/envirocar/app/views/others/OthersFragment.java @@ -30,7 +30,10 @@ import android.view.ViewGroup; import android.widget.LinearLayout; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.core.app.ActivityCompat; import androidx.fragment.app.Fragment; @@ -81,8 +84,25 @@ public class OthersFragment extends BaseInjectorFragment { private Scheduler.Worker mMainThreadWorker = AndroidSchedulers.mainThread().createWorker(); private final Scheduler.Worker mBackgroundWorker = Schedulers.newThread().createWorker(); + private ActivityResultLauncher settingsLauncher; + private int REQUEST_PERMISSIONS_REQUEST_CODE = 101; + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + settingsLauncher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + // Re-create the activity if the map view settings have been changed, to reflect the changes. + final Intent data = result.getData(); + if (data != null && data.getBooleanExtra(SettingsActivity.MAP_VIEW_SETTINGS_CHANGED, false)) { + requireActivity().recreate(); + } + } + ); + } + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -130,8 +150,7 @@ protected void onLogBookClicked() { } protected void onSettingsClicked() { - Intent intent = new Intent(getActivity(), SettingsActivity.class); - startActivity(intent); + settingsLauncher.launch(new Intent(getActivity(), SettingsActivity.class)); } protected void onHelpClicked() { diff --git a/org.envirocar.app/src/org/envirocar/app/views/recordingscreen/TrackMapFragment.java b/org.envirocar.app/src/org/envirocar/app/views/recordingscreen/TrackMapFragment.java index 1549b623b..e6c1d0fab 100644 --- a/org.envirocar.app/src/org/envirocar/app/views/recordingscreen/TrackMapFragment.java +++ b/org.envirocar.app/src/org/envirocar/app/views/recordingscreen/TrackMapFragment.java @@ -42,6 +42,7 @@ import org.envirocar.app.injection.BaseInjectorFragment; import org.envirocar.app.injection.components.MainActivityComponent; import org.envirocar.app.injection.modules.MainActivityModule; +import org.envirocar.app.views.utils.MapProviderRepository; import org.envirocar.core.logging.Logger; import org.envirocar.map.MapController; import org.envirocar.map.MapView; @@ -49,7 +50,6 @@ import org.envirocar.map.location.LocationIndicator; import org.envirocar.map.location.LocationIndicatorCameraMode; import org.envirocar.map.model.Polyline; -import org.envirocar.map.provider.mapbox.MapboxMapProvider; import io.reactivex.Scheduler; import io.reactivex.android.schedulers.AndroidSchedulers; @@ -88,8 +88,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa mMapView.setOnTouchListener((v, event) -> onTouchMapView()); mFollowFab.setOnClickListener(v -> onClickFollowFab()); - // TODO(alexmercerind): Retrieve currently selected provider from a common repository. - mMapController = mMapView.getController(new MapboxMapProvider()); + mMapController = mMapView.getController(new MapProviderRepository(requireActivity().getApplication()).getValue()); mMapController.setMinZoom(16.0F); mMapController.notifyCameraUpdate(CameraUpdateFactory.newCameraUpdateZoom(16.0F), null); diff --git a/org.envirocar.app/src/org/envirocar/app/views/settings/SettingsActivity.java b/org.envirocar.app/src/org/envirocar/app/views/settings/SettingsActivity.java index 68f0e7f44..bc44506d8 100644 --- a/org.envirocar.app/src/org/envirocar/app/views/settings/SettingsActivity.java +++ b/org.envirocar.app/src/org/envirocar/app/views/settings/SettingsActivity.java @@ -1,18 +1,18 @@ /** * Copyright (C) 2013 - 2021 the enviroCar community - * + *

* This file is part of the enviroCar app. - * + *

* The enviroCar app is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + *

* The enviroCar app is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General * Public License for more details. - * + *

* You should have received a copy of the GNU General Public License along * with the enviroCar app. If not, see http://www.gnu.org/licenses/. */ @@ -21,6 +21,7 @@ import android.os.Bundle; import android.view.View; +import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; @@ -35,19 +36,43 @@ import org.envirocar.app.views.settings.custom.AutoConnectIntervalPreference; import org.envirocar.app.views.settings.custom.GPSConnectionDurationPreference; import org.envirocar.app.views.settings.custom.GPSTrimDurationPreference; +import org.envirocar.app.views.settings.custom.MapViewPreference; import org.envirocar.app.views.settings.custom.SamplingRatePreference; import org.envirocar.app.views.settings.custom.TimePickerPreferenceDialog; /** * @author dewall */ -public class SettingsActivity extends AppCompatActivity { +public class SettingsActivity extends AppCompatActivity implements MapViewPreference.MapViewPreferenceNotifier { + + public static final String MAP_VIEW_SETTINGS_CHANGED = "MAP_VIEW_SETTINGS_CHANGED"; + + private boolean mapViewSettingsChanged = false; + + @Override + public void notifyMapViewSettingsChanged() { + mapViewSettingsChanged = true; + } @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_settings); + getOnBackPressedDispatcher().addCallback( + this, + new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + if (mapViewSettingsChanged) { + getIntent().putExtra(MAP_VIEW_SETTINGS_CHANGED, true); + setResult(RESULT_OK, getIntent()); + } + finish(); + } + } + ); + // add the settingsfragment getSupportFragmentManager() .beginTransaction() @@ -79,7 +104,6 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat this.gpsTrimDuration = findPreference(getString(R.string.prefkey_track_trim_duration)); this.gpsAutoRecording = findPreference(getString(R.string.prefkey_gps_mode_ar)); - // set initial state this.searchInterval.setVisible(((CheckBoxPreference) automaticRecording).isChecked()); this.gpsTrimDuration.setVisible(((CheckBoxPreference) enableGPSMode).isChecked()); @@ -107,6 +131,8 @@ public void onDisplayPreferenceDialog(Preference preference) { fragment = TimePickerPreferenceDialog.newInstance(preference.getKey()); } else if (preference instanceof SamplingRatePreference) { fragment = SamplingRatePreference.Dialog.newInstance(preference.getKey()); + } else if (preference instanceof MapViewPreference) { + fragment = MapViewPreference.Dialog.newInstance(preference.getKey()); } else if (preference instanceof GPSTrimDurationPreference) { fragment = TimePickerPreferenceDialog.newInstance(preference.getKey()); } else if (preference instanceof GPSConnectionDurationPreference) { diff --git a/org.envirocar.app/src/org/envirocar/app/views/settings/custom/MapViewPreference.java b/org.envirocar.app/src/org/envirocar/app/views/settings/custom/MapViewPreference.java new file mode 100644 index 000000000..125c6dfe2 --- /dev/null +++ b/org.envirocar.app/src/org/envirocar/app/views/settings/custom/MapViewPreference.java @@ -0,0 +1,314 @@ +/** + * Copyright (C) 2013 - 2021 the enviroCar community + *

+ * This file is part of the enviroCar app. + *

+ * The enviroCar app is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + *

+ * The enviroCar app is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + *

+ * You should have received a copy of the GNU General Public License along + * with the enviroCar app. If not, see http://www.gnu.org/licenses/. + */ +package org.envirocar.app.views.settings.custom; + +import android.animation.ValueAnimator; +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.LinearInterpolator; +import android.widget.ArrayAdapter; +import android.widget.LinearLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.preference.DialogPreference; +import androidx.preference.PreferenceDialogFragmentCompat; + +import org.envirocar.app.BuildConfig; +import org.envirocar.app.R; +import org.envirocar.app.databinding.PreferenceMapStyleListItemBinding; +import org.envirocar.app.databinding.PreferenceMapViewDialogBinding; +import org.envirocar.app.handler.ApplicationSettings; +import org.envirocar.app.views.settings.SettingsActivity; +import org.envirocar.app.views.utils.MapProviderRepository; +import org.envirocar.core.logging.Logger; + +import java.util.Map; +import java.util.Objects; + +public class MapViewPreference extends DialogPreference { + + private static final Logger LOG = Logger.getLogger(MapViewPreference.class); + + public interface MapViewPreferenceNotifier { + void notifyMapViewSettingsChanged(); + } + + public MapViewPreference(Context context) { + super(context); + } + + public MapViewPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public int getDialogLayoutResource() { + return R.layout.preference_map_view_dialog; + } + + public static class Dialog extends PreferenceDialogFragmentCompat { + private static boolean mapLibreEnabled = true; + private static boolean mapboxEnabled = true; + + private static Map getMapLibreStyles() { + return Map.of( + "OpenStreetMap", getMapLibreDefaultStyle(), + "MapTiler (Basic)", "https://api.maptiler.com/maps/basic/style.json?key=" + BuildConfig.MAPTILER_API_KEY + ); + } + + private static Map getMapboxStyles() { + return Map.of( + "Mapbox (Streets)", getMapboxDefaultStyle(), + "MapTiler (Basic)", "https://api.maptiler.com/maps/basic/style.json?key=" + BuildConfig.MAPTILER_API_KEY + ); + } + + private static String getMapLibreDefaultStyle() { + try { + return Objects.requireNonNull(Class.forName("org.envirocar.map.provider.maplibre.MapLibreMapProvider").getField("DEFAULT_STYLE").get(null)).toString(); + } catch (IllegalAccessException | NoSuchFieldException | ClassNotFoundException e) { + mapLibreEnabled = false; + return ""; + } + } + + private static String getMapboxDefaultStyle() { + try { + return Objects.requireNonNull(Class.forName("org.envirocar.map.provider.mapbox.MapboxMapProvider").getField("DEFAULT_STYLE").get(null)).toString(); + } catch (IllegalAccessException | NoSuchFieldException | ClassNotFoundException e) { + mapboxEnabled = false; + return ""; + } + } + + PreferenceMapViewDialogBinding binding; + + private String mapProvider; + private String mapLibreStyle; + private String mapboxStyle; + + private boolean isUpdateMapProviderCalled = false; + + public static Dialog newInstance(String key) { + final Dialog fragment = new Dialog(); + final Bundle b = new Bundle(1); + b.putString(ARG_KEY, key); + fragment.setArguments(b); + return fragment; + } + + @Override + protected void onBindDialogView(@NonNull View view) { + binding = PreferenceMapViewDialogBinding.bind(view); + + updateMapProvider(ApplicationSettings.getMapProvider(view.getContext())); + updateMapLibreStyle(ApplicationSettings.getMapLibreStyle(view.getContext())); + updateMapboxStyle(ApplicationSettings.getMapboxStyle(view.getContext())); + + if (!mapLibreEnabled) { + binding.mapLibreTextView.setVisibility(View.GONE); + binding.mapLibreLinearLayout.setVisibility(View.GONE); + } + if (!mapboxEnabled) { + binding.mapboxTextView.setVisibility(View.GONE); + binding.mapboxLinearLayout.setVisibility(View.GONE); + } + + binding.mapLibreTextView.setOnClickListener(v -> { + updateMapProvider(MapProviderRepository.PROVIDER_MAPLIBRE); + }); + binding.mapLibreRadioButton.setOnCheckedChangeListener((v, isChecked) -> { + if (isChecked) { + updateMapProvider(MapProviderRepository.PROVIDER_MAPLIBRE); + } + }); + final ArrayAdapter mapLibreListViewAdapter = new ArrayAdapter( + view.getContext(), + R.layout.preference_map_style_list_item, + R.id.styleTextView, + getMapLibreStyles().keySet().toArray(new String[0]) + ) { + @NonNull + @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + final View view = super.getView(position, convertView, parent); + final PreferenceMapStyleListItemBinding binding = PreferenceMapStyleListItemBinding.bind(view); + binding.styleRadioButton.setChecked(Objects.equals(getMapLibreStyles().get(getItem(position)), mapLibreStyle)); + binding.styleRadioButton.setOnCheckedChangeListener((v, isChecked) -> { + if (isChecked) { + updateMapLibreStyle(getMapLibreStyles().get(getItem(position))); + } + notifyDataSetChanged(); + }); + view.setOnClickListener(v -> { + updateMapLibreStyle(getMapLibreStyles().get(getItem(position))); + notifyDataSetChanged(); + }); + return view; + } + }; + binding.mapLibreListView.setDividerHeight(0); + binding.mapLibreListView.setAdapter(mapLibreListViewAdapter); + + binding.mapboxTextView.setOnClickListener(v -> { + updateMapProvider(MapProviderRepository.PROVIDER_MAPBOX); + }); + binding.mapboxRadioButton.setOnCheckedChangeListener((v, isChecked) -> { + if (isChecked) { + updateMapProvider(MapProviderRepository.PROVIDER_MAPBOX); + } + }); + final ArrayAdapter mapboxListViewAdapter = new ArrayAdapter( + view.getContext(), + R.layout.preference_map_style_list_item, + R.id.styleTextView, + getMapboxStyles().keySet().toArray(new String[0]) + ) { + @NonNull + @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + final View view = super.getView(position, convertView, parent); + final PreferenceMapStyleListItemBinding binding = PreferenceMapStyleListItemBinding.bind(view); + binding.styleRadioButton.setChecked(Objects.equals(getMapboxStyles().get(getItem(position)), mapboxStyle)); + binding.styleRadioButton.setOnCheckedChangeListener((v, isChecked) -> { + if (isChecked) { + updateMapboxStyle(getMapboxStyles().get(getItem(position))); + } + notifyDataSetChanged(); + }); + view.setOnClickListener(v -> { + updateMapboxStyle(getMapboxStyles().get(getItem(position))); + notifyDataSetChanged(); + }); + return view; + } + }; + binding.mapboxListView.setDividerHeight(0); + binding.mapboxListView.setAdapter(mapboxListViewAdapter); + } + + @Override + public void onDialogClosed(boolean positiveResult) { + if (positiveResult) { + ApplicationSettings.setMapProvider(getContext(), mapProvider); + ApplicationSettings.setMapLibreStyle(getContext(), mapLibreStyle); + ApplicationSettings.setMapboxStyle(getContext(), mapboxStyle); + ((SettingsActivity) requireActivity()).notifyMapViewSettingsChanged(); + } + } + + private void updateMapProvider(String value) { + if (mapProvider != null && mapProvider.equals(value)) { + return; + } + mapProvider = value; + + final float mapLibreLinearLayoutMaxHeight = (36.0f + getMapLibreStyles().size() * 48.0f) * getResources().getDisplayMetrics().density; + final float mapboxLinearLayoutMaxHeight = (36.0f + getMapboxStyles().size() * 48.0f) * getResources().getDisplayMetrics().density; + + binding.mapLibreListView.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, (int) mapLibreLinearLayoutMaxHeight)); + binding.mapboxListView.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, (int) mapboxLinearLayoutMaxHeight)); + + final ValueAnimator animator = ValueAnimator.ofFloat(0.0f, 1.0f); + + if (mapProvider.contains(MapProviderRepository.PROVIDER_MAPLIBRE)) { + if (!isUpdateMapProviderCalled) { + final ViewGroup.LayoutParams mapLibreLayoutParams = binding.mapLibreLinearLayout.getLayoutParams(); + mapLibreLayoutParams.height = (int) mapLibreLinearLayoutMaxHeight; + binding.mapLibreLinearLayout.setLayoutParams(mapLibreLayoutParams); + final ViewGroup.LayoutParams mapboxLayoutParams = binding.mapboxLinearLayout.getLayoutParams(); + mapboxLayoutParams.height = 2; + binding.mapboxLinearLayout.setLayoutParams(mapboxLayoutParams); + } + animator.addUpdateListener(animation -> { + final float mapLibreValue = (float) animation.getAnimatedValue() * mapLibreLinearLayoutMaxHeight; + final float mapboxValue = (1.0f - (float) animation.getAnimatedValue()) * mapboxLinearLayoutMaxHeight; + final ViewGroup.LayoutParams mapLibreLayoutParams = binding.mapLibreLinearLayout.getLayoutParams(); + mapLibreLayoutParams.height = Math.max((int) mapLibreValue, 2); + binding.mapLibreLinearLayout.setLayoutParams(mapLibreLayoutParams); + final ViewGroup.LayoutParams mapboxLayoutParams = binding.mapboxLinearLayout.getLayoutParams(); + mapboxLayoutParams.height = Math.max((int) mapboxValue, 2); + binding.mapboxLinearLayout.setLayoutParams(mapboxLayoutParams); + }); + binding.mapLibreRadioButton.setChecked(true); + binding.mapboxRadioButton.setChecked(false); + binding.mapLibreExpandImageView.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.baseline_expand_less_24)); + binding.mapboxExpandImageView.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.baseline_expand_more_24)); + } else if (mapProvider.contains(MapProviderRepository.PROVIDER_MAPBOX)) { + if (!isUpdateMapProviderCalled) { + final ViewGroup.LayoutParams mapLibreLayoutParams = binding.mapLibreLinearLayout.getLayoutParams(); + mapLibreLayoutParams.height = 2; + binding.mapLibreLinearLayout.setLayoutParams(mapLibreLayoutParams); + final ViewGroup.LayoutParams mapboxLayoutParams = binding.mapboxLinearLayout.getLayoutParams(); + mapboxLayoutParams.height = (int) mapboxLinearLayoutMaxHeight; + binding.mapboxLinearLayout.setLayoutParams(mapboxLayoutParams); + } + animator.addUpdateListener(animation -> { + final float mapLibreValue = (1.0f - (float) animation.getAnimatedValue()) * mapLibreLinearLayoutMaxHeight; + final float mapboxValue = (float) animation.getAnimatedValue() * mapboxLinearLayoutMaxHeight; + final ViewGroup.LayoutParams mapLibreLayoutParams = binding.mapLibreLinearLayout.getLayoutParams(); + mapLibreLayoutParams.height = Math.max((int) mapLibreValue, 2); + binding.mapLibreLinearLayout.setLayoutParams(mapLibreLayoutParams); + final ViewGroup.LayoutParams mapboxLayoutParams = binding.mapboxLinearLayout.getLayoutParams(); + mapboxLayoutParams.height = Math.max((int) mapboxValue, 2); + binding.mapboxLinearLayout.setLayoutParams(mapboxLayoutParams); + }); + binding.mapLibreRadioButton.setChecked(false); + binding.mapboxRadioButton.setChecked(true); + binding.mapLibreExpandImageView.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.baseline_expand_more_24)); + binding.mapboxExpandImageView.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.baseline_expand_less_24)); + } else { + LOG.info("Unknown map provider: " + mapProvider); + } + + if (isUpdateMapProviderCalled) { + new Handler().postDelayed( + () -> { + animator.setDuration(200); + animator.setInterpolator(new LinearInterpolator()); + animator.start(); + }, + 400 + ); + } + isUpdateMapProviderCalled = true; + } + + private void updateMapLibreStyle(String value) { + if (mapLibreStyle != null && mapLibreStyle.equals(value)) { + return; + } + mapLibreStyle = value; + } + + private void updateMapboxStyle(String value) { + if (mapboxStyle != null && mapboxStyle.equals(value)) { + return; + } + mapboxStyle = value; + } + } +} diff --git a/org.envirocar.app/src/org/envirocar/app/views/settings/custom/SamplingRatePreference.java b/org.envirocar.app/src/org/envirocar/app/views/settings/custom/SamplingRatePreference.java index 86a19fc1f..29d265c7d 100644 --- a/org.envirocar.app/src/org/envirocar/app/views/settings/custom/SamplingRatePreference.java +++ b/org.envirocar.app/src/org/envirocar/app/views/settings/custom/SamplingRatePreference.java @@ -82,7 +82,8 @@ public static Dialog newInstance(String key) { protected void onBindDialogView(View view) { super.onBindDialogView(view); - + textView = view.findViewById(R.id.preference_sampling_rate_dialog_text); + numberPicker = view.findViewById(R.id.preference_sampling_rate_dialog_picker); // set min/max values this.numberPicker.setMinValue(MIN_VALUE); diff --git a/org.envirocar.app/src/org/envirocar/app/views/trackdetails/MapExpandedActivity.java b/org.envirocar.app/src/org/envirocar/app/views/trackdetails/MapExpandedActivity.java index 0c79c7d6b..6dac4be99 100644 --- a/org.envirocar.app/src/org/envirocar/app/views/trackdetails/MapExpandedActivity.java +++ b/org.envirocar.app/src/org/envirocar/app/views/trackdetails/MapExpandedActivity.java @@ -39,6 +39,7 @@ import org.envirocar.app.databinding.ActivityMapExpandedBinding; import org.envirocar.app.injection.BaseInjectorActivity; import org.envirocar.app.BaseApplicationComponent; +import org.envirocar.app.views.utils.MapProviderRepository; import org.envirocar.core.entity.Measurement; import org.envirocar.core.entity.Track; import org.envirocar.core.logging.Logger; @@ -46,8 +47,9 @@ import org.envirocar.map.MapController; import org.envirocar.map.MapView; import org.envirocar.map.model.Animation; +import org.envirocar.map.model.AttributionSettings; +import org.envirocar.map.model.LogoSettings; import org.envirocar.map.model.Polyline; -import org.envirocar.map.provider.mapbox.MapboxMapProvider; import java.text.DecimalFormat; import java.util.ArrayList; @@ -239,12 +241,23 @@ private void addPolyline(int choice) { } private void initMapView() { - // TODO(alexmercerind): Retrieve currently selected provider from a common repository. if (mMapController != null) { return; } - mMapController = mMapViewExpanded.getController(new MapboxMapProvider()); - + mMapController = mMapViewExpanded.getController( + new MapProviderRepository( + getApplication(), + // Display attribution in top right of the screen. + new AttributionSettings.Builder() + .withGravity(Gravity.TOP | Gravity.END) + .withMargin(new float[]{12.0F, 12.0F, 12.0F, 12.0F}) + .build(), + new LogoSettings.Builder() + .withGravity(Gravity.TOP | Gravity.END) + .withMargin(new float[]{12.0F, 12.0F, 84.0F, 12.0F}) + .build() + ).getValue() + ); final TrackMapFactory factory = new TrackMapFactory(track); mMapController.setMinZoom(factory.getMinZoom()); diff --git a/org.envirocar.app/src/org/envirocar/app/views/trackdetails/TrackDetailsActivity.java b/org.envirocar.app/src/org/envirocar/app/views/trackdetails/TrackDetailsActivity.java index 12141bc33..845498ed4 100644 --- a/org.envirocar.app/src/org/envirocar/app/views/trackdetails/TrackDetailsActivity.java +++ b/org.envirocar.app/src/org/envirocar/app/views/trackdetails/TrackDetailsActivity.java @@ -24,6 +24,7 @@ import android.os.Build; import android.os.Bundle; import android.transition.Slide; +import android.view.Gravity; import android.view.MenuItem; import android.view.View; import android.view.Window; @@ -43,6 +44,7 @@ import org.envirocar.app.databinding.ActivityTrackDetailsLayoutBinding; import org.envirocar.app.handler.ApplicationSettings; import org.envirocar.app.injection.BaseInjectorActivity; +import org.envirocar.app.views.utils.MapProviderRepository; import org.envirocar.core.EnviroCarDB; import org.envirocar.core.entity.Car; import org.envirocar.core.entity.Measurement; @@ -55,7 +57,8 @@ import org.envirocar.core.utils.CarUtils; import org.envirocar.map.MapController; import org.envirocar.map.MapView; -import org.envirocar.map.provider.mapbox.MapboxMapProvider; +import org.envirocar.map.model.AttributionSettings; +import org.envirocar.map.model.LogoSettings; import java.text.DateFormat; import java.text.DecimalFormat; @@ -249,8 +252,20 @@ private void initMapView() { if (mMapController != null) { return; } - mMapController = mMapView.getController(new MapboxMapProvider()); - + mMapController = mMapView.getController( + new MapProviderRepository( + getApplication(), + // Only display logo in the top right of the screen. + // The click event is not sent to the background of [CollapsingToolbarLayout]. + new AttributionSettings.Builder() + .withEnabled(false) + .build(), + new LogoSettings.Builder() + .withGravity(Gravity.TOP | Gravity.END) + .withMargin(new float[]{12.0F, 12.0F, 12.0F, 12.0F}) + .build() + ).getValue() + ); final TrackMapFactory factory = new TrackMapFactory(track); mMapController.setMinZoom(factory.getMinZoom()); diff --git a/org.envirocar.app/src/org/envirocar/app/views/tracklist/AbstractTrackListCardAdapter.kt b/org.envirocar.app/src/org/envirocar/app/views/tracklist/AbstractTrackListCardAdapter.kt index 35297375e..827921e8d 100644 --- a/org.envirocar.app/src/org/envirocar/app/views/tracklist/AbstractTrackListCardAdapter.kt +++ b/org.envirocar.app/src/org/envirocar/app/views/tracklist/AbstractTrackListCardAdapter.kt @@ -1,5 +1,6 @@ package org.envirocar.app.views.tracklist +import android.app.Application import android.view.View import android.widget.ImageButton import android.widget.LinearLayout @@ -10,11 +11,13 @@ import org.envirocar.app.R import org.envirocar.app.databinding.FragmentTracklistCardlayoutLocalBinding import org.envirocar.app.databinding.FragmentTracklistCardlayoutRemoteBinding import org.envirocar.app.views.trackdetails.TrackMapFactory +import org.envirocar.app.views.utils.MapProviderRepository import org.envirocar.core.entity.Track import org.envirocar.core.logging.Logger import org.envirocar.map.MapController import org.envirocar.map.MapView -import org.envirocar.map.provider.mapbox.MapboxMapProvider +import org.envirocar.map.model.AttributionSettings +import org.envirocar.map.model.LogoSettings import java.text.DecimalFormat import java.text.SimpleDateFormat import java.util.Date @@ -180,9 +183,18 @@ abstract class AbstractTrackListCardAdapter Class.forName("org.envirocar.map.provider.maplibre.MapLibreMapProvider") + .getConstructor( + String::class.java, + AttributionSettings::class.java, + LogoSettings::class.java + ) + .newInstance( + ApplicationSettings.getMapLibreStyle(applicationContext), + attribution, + logo + ) as MapProvider + + it.contains(PROVIDER_MAPBOX) -> Class.forName("org.envirocar.map.provider.mapbox.MapboxMapProvider") + .getConstructor( + String::class.java, + AttributionSettings::class.java, + LogoSettings::class.java + ) + .newInstance( + ApplicationSettings.getMapboxStyle(applicationContext), + attribution, + logo + ) as MapProvider + + else -> error("Unknown Class: $it") + } + } + + + companion object { + @Volatile + private var instance: MapProviderRepository? = null + + operator fun invoke(applicationContext: Application) = instance ?: synchronized(this) { + instance ?: MapProviderRepository(applicationContext).also { instance = it } + } + + const val PROVIDER_MAPLIBRE = "MapLibre" + const val PROVIDER_MAPBOX = "Mapbox" + } +} diff --git a/org.envirocar.map/proguard-rules.pro b/org.envirocar.map/proguard-rules.pro index 481bb4348..85b3da9d5 100644 --- a/org.envirocar.map/proguard-rules.pro +++ b/org.envirocar.map/proguard-rules.pro @@ -18,4 +18,7 @@ # If you keep the line number information, uncomment this to # hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file +#-renamesourcefileattribute SourceFile + +# Preserve map provider implementations, if accessed using reflection: +-keep class org.envirocar.map.provider.** { *; } diff --git a/org.envirocar.map/src/main/assets/maplibre_default_style.json b/org.envirocar.map/src/main/assets/maplibre_default_style.json new file mode 100644 index 000000000..a5f81d02b --- /dev/null +++ b/org.envirocar.map/src/main/assets/maplibre_default_style.json @@ -0,0 +1,21 @@ +{ + "version": 8, + "sources": { + "osm": { + "type": "raster", + "tiles": [ + "https://a.tile.openstreetmap.org/{z}/{x}/{y}.png" + ], + "tileSize": 256, + "attribution": "© OpenStreetMap", + "maxzoom": 19 + } + }, + "layers": [ + { + "id": "osm", + "type": "raster", + "source": "osm" + } + ] +} diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/model/AttributionSettings.kt b/org.envirocar.map/src/main/java/org/envirocar/map/model/AttributionSettings.kt new file mode 100644 index 000000000..38c5f3537 --- /dev/null +++ b/org.envirocar.map/src/main/java/org/envirocar/map/model/AttributionSettings.kt @@ -0,0 +1,49 @@ +package org.envirocar.map.model + +import android.view.Gravity + +/** + * [AttributionSettings] + * --------------------- + * [AttributionSettings] encapsulates various options related to attribution present on the map. + * Utilize the [AttributionSettings.Builder] to create a new instance. + * + * @property enabled Whether the attribution is enabled or not. + * @property gravity The gravity for the attribution view. + * @property margin The margin for the attribution view. + */ +class AttributionSettings internal constructor( + val enabled: Boolean, + val gravity: Int, + val margin: FloatArray +) { + class Builder { + private var enabled: Boolean = true + private var gravity: Int = Gravity.BOTTOM or Gravity.START + private var margin: FloatArray = floatArrayOf(320.0F, 12.0F, 12.0F, 12.0F) + + /** Sets whether the attribution is enabled or not. */ + fun withEnabled(value: Boolean) = apply { enabled = value } + + /** Sets the gravity for the attribution view. */ + fun withGravity(value: Int) = apply { gravity = value } + + /** Sets the margin for the attribution view. */ + fun withMargin(value: FloatArray) = apply { margin = value } + + /** Builds the attribution settings. */ + fun build(): AttributionSettings { + return AttributionSettings( + enabled, + gravity, + margin + ) + } + } + + companion object { + + /** Creates a [AttributionSettings] with default style. */ + fun default() = Builder().build() + } +} diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/model/LogoSettings.kt b/org.envirocar.map/src/main/java/org/envirocar/map/model/LogoSettings.kt new file mode 100644 index 000000000..faac6d52c --- /dev/null +++ b/org.envirocar.map/src/main/java/org/envirocar/map/model/LogoSettings.kt @@ -0,0 +1,49 @@ +package org.envirocar.map.model + +import android.view.Gravity + +/** + * [LogoSettings] + * --------------------- + * [LogoSettings] encapsulates various options related to logo present on the map. + * Utilize the [LogoSettings.Builder] to create a new instance. + * + * @property enabled Whether the logo is enabled or not. + * @property gravity The gravity for the logo view. + * @property margin The margin for the logo view. + */ +class LogoSettings internal constructor( + val enabled: Boolean, + val gravity: Int, + val margin: FloatArray +) { + class Builder { + private var enabled: Boolean = true + private var gravity: Int = Gravity.BOTTOM or Gravity.START + private var margin: FloatArray = floatArrayOf(12.0F, 12.0F, 12.0F, 12.0F) + + /** Sets whether the logo is enabled or not. */ + fun withEnabled(value: Boolean) = apply { enabled = value } + + /** Sets the gravity for the logo view. */ + fun withGravity(value: Int) = apply { gravity = value } + + /** Sets the margin for the logo view. */ + fun withMargin(value: FloatArray) = apply { margin = value } + + /** Builds the logo settings. */ + fun build(): LogoSettings { + return LogoSettings( + enabled, + gravity, + margin + ) + } + } + + companion object { + + /** Creates a [LogoSettings] with default style. */ + fun default() = Builder().build() + } +} diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/provider/mapbox/MapboxMapController.kt b/org.envirocar.map/src/main/java/org/envirocar/map/provider/mapbox/MapboxMapController.kt index 6b952f7a9..1039dd24f 100644 --- a/org.envirocar.map/src/main/java/org/envirocar/map/provider/mapbox/MapboxMapController.kt +++ b/org.envirocar.map/src/main/java/org/envirocar/map/provider/mapbox/MapboxMapController.kt @@ -38,6 +38,8 @@ import org.envirocar.map.MapController import org.envirocar.map.camera.CameraUpdate import org.envirocar.map.camera.MutableCameraState import org.envirocar.map.model.Animation +import org.envirocar.map.model.AttributionSettings +import org.envirocar.map.model.LogoSettings import org.envirocar.map.model.Marker import org.envirocar.map.model.Point import org.envirocar.map.model.Polygon @@ -48,7 +50,12 @@ import org.envirocar.map.model.Polyline * --------------------- * [Mapbox](https://www.mapbox.com) based implementation for [MapController]. */ -internal class MapboxMapController(private val viewInstance: MapView) : MapController() { +internal class MapboxMapController( + private val viewInstance: MapView, + private val attribution: AttributionSettings, + private val logo: LogoSettings + +) : MapController() { override val camera = MutableCameraState() private val markers = mutableMapOf() private val polygons = mutableMapOf() @@ -70,10 +77,23 @@ internal class MapboxMapController(private val viewInstance: MapView) : MapContr ) init { - // Disable attribution, compass, logo & scalebar. - viewInstance.attribution.enabled = false + viewInstance.attribution.enabled = attribution.enabled + viewInstance.attribution.position = attribution.gravity + attribution.margin.let { + viewInstance.attribution.marginLeft = it[0] + viewInstance.attribution.marginTop = it[1] + viewInstance.attribution.marginRight = it[2] + viewInstance.attribution.marginBottom = it[3] + } + viewInstance.logo.enabled = logo.enabled + viewInstance.logo.position = logo.gravity + logo.margin.let { + viewInstance.logo.marginLeft = it[0] + viewInstance.logo.marginTop = it[1] + viewInstance.logo.marginRight = it[2] + viewInstance.logo.marginBottom = it[3] + } viewInstance.compass.enabled = false - viewInstance.logo.enabled = false viewInstance.scalebar.enabled = false // Once the map style is loaded, make the view visible & mark this instance as ready. diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/provider/mapbox/MapboxMapProvider.kt b/org.envirocar.map/src/main/java/org/envirocar/map/provider/mapbox/MapboxMapProvider.kt index aebe166f1..815683711 100644 --- a/org.envirocar.map/src/main/java/org/envirocar/map/provider/mapbox/MapboxMapProvider.kt +++ b/org.envirocar.map/src/main/java/org/envirocar/map/provider/mapbox/MapboxMapProvider.kt @@ -4,6 +4,8 @@ import android.content.Context import com.mapbox.maps.MapView import org.envirocar.map.MapController import org.envirocar.map.MapProvider +import org.envirocar.map.model.AttributionSettings +import org.envirocar.map.model.LogoSettings /** * [MapboxMapProvider] @@ -20,10 +22,15 @@ import org.envirocar.map.MapProvider * * `https://` * * `asset://` * * `file://` - * The default style is `mapbox://styles/mapbox/streets-v12`. + * @param attribution + * The attribution settings for the map. + * @param logo + * The logo settings for the map. */ class MapboxMapProvider( - private val style: String = DEFAULT_STYLE + private val style: String = DEFAULT_STYLE, + private val attribution: AttributionSettings = DEFAULT_ATTRIBUTION, + private val logo: LogoSettings = DEFAULT_LOGO ) : MapProvider { private lateinit var viewInstance: MapView private lateinit var controllerInstance: MapboxMapController @@ -42,12 +49,18 @@ class MapboxMapProvider( error("MapboxMapProvider is not initialized.") } if (!::controllerInstance.isInitialized) { - controllerInstance = MapboxMapController(viewInstance) + controllerInstance = MapboxMapController( + viewInstance, + attribution, + logo + ) } return controllerInstance } companion object { const val DEFAULT_STYLE = "mapbox://styles/mapbox/streets-v12" + val DEFAULT_ATTRIBUTION = AttributionSettings.default() + val DEFAULT_LOGO = LogoSettings.default() } } diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/provider/maplibre/MapLibreMapController.kt b/org.envirocar.map/src/main/java/org/envirocar/map/provider/maplibre/MapLibreMapController.kt index a4634df8f..44f18e18f 100644 --- a/org.envirocar.map/src/main/java/org/envirocar/map/provider/maplibre/MapLibreMapController.kt +++ b/org.envirocar.map/src/main/java/org/envirocar/map/provider/maplibre/MapLibreMapController.kt @@ -1,13 +1,18 @@ package org.envirocar.map.provider.maplibre +import android.graphics.Color import android.view.View import androidx.appcompat.content.res.AppCompatResources import androidx.core.graphics.drawable.toBitmap +import com.mapbox.maps.plugin.attribution.attribution +import com.mapbox.maps.plugin.logo.logo import kotlinx.coroutines.flow.update import org.envirocar.map.MapController import org.envirocar.map.camera.CameraUpdate import org.envirocar.map.camera.MutableCameraState import org.envirocar.map.model.Animation +import org.envirocar.map.model.AttributionSettings +import org.envirocar.map.model.LogoSettings import org.envirocar.map.model.Marker import org.envirocar.map.model.Point import org.envirocar.map.model.Polygon @@ -35,8 +40,10 @@ import org.maplibre.android.style.layers.PropertyFactory.lineColor import org.maplibre.android.style.layers.PropertyFactory.lineGradient import org.maplibre.android.style.layers.PropertyFactory.lineJoin import org.maplibre.android.style.layers.PropertyFactory.lineWidth +import org.maplibre.android.style.sources.GeoJsonOptions import org.maplibre.android.style.sources.GeoJsonSource import org.maplibre.geojson.Feature +import org.maplibre.geojson.FeatureCollection import org.maplibre.geojson.LineString /** @@ -44,7 +51,11 @@ import org.maplibre.geojson.LineString * ----------------------- * [MapLibre](https://www.maplibre.org) based implementation for [MapController]. */ -internal class MapLibreMapController(private val viewInstance: MapView) : MapController() { +internal class MapLibreMapController( + private val viewInstance: MapView, + attribution: AttributionSettings, + logo: LogoSettings +) : MapController() { override val camera = MutableCameraState() private val markers = mutableMapOf() private val polygons = mutableMapOf() @@ -62,10 +73,27 @@ internal class MapLibreMapController(private val viewInstance: MapView) : MapCon viewInstance.getMapAsync { mapLibreMap -> this.mapLibreMap = mapLibreMap - // Disable attribution, compass & logo. - mapLibreMap.uiSettings.isAttributionEnabled = false + mapLibreMap.uiSettings.isAttributionEnabled = attribution.enabled + mapLibreMap.uiSettings.attributionGravity = attribution.gravity + attribution.let { + mapLibreMap.uiSettings.setAttributionMargins( + it.margin[0].toInt(), + it.margin[1].toInt(), + it.margin[2].toInt(), + it.margin[3].toInt() + ) + } + mapLibreMap.uiSettings.isLogoEnabled = logo.enabled + mapLibreMap.uiSettings.logoGravity = logo.gravity + logo.let { + mapLibreMap.uiSettings.setLogoMargins( + it.margin[0].toInt(), + it.margin[1].toInt(), + it.margin[2].toInt(), + it.margin[3].toInt() + ) + } mapLibreMap.uiSettings.isCompassEnabled = false - mapLibreMap.uiSettings.isLogoEnabled = false // Once the map style is loaded, make the view visible & mark this instance as ready. if (mapLibreMap.style != null) { @@ -222,11 +250,14 @@ internal class MapLibreMapController(private val viewInstance: MapView) : MapCon style.addSource( GeoJsonSource( MAPLIBRE_POLYLINE_SOURCE_ID + polyline.id, - Feature.fromGeometry( - LineString.fromLngLats( - polyline.points.map { it.toMapLibrePoint() } + FeatureCollection.fromFeatures( + listOf( + Feature.fromGeometry( + LineString.fromLngLats(polyline.points.map { it.toMapLibrePoint() }) + ) ) - ) + ), + GeoJsonOptions().withLineMetrics(true) ) ) var layer = LineLayer( @@ -251,7 +282,11 @@ internal class MapLibreMapController(private val viewInstance: MapView) : MapCon *polyline.colors.mapIndexed { index, color -> Expression.stop( (index + 1).toDouble() / polyline.points.size.toDouble(), - color + Expression.rgb( + Color.red(color).toFloat(), + Color.green(color).toFloat(), + Color.blue(color).toFloat() + ) ) }.toTypedArray() ) diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/provider/maplibre/MapLibreMapProvider.kt b/org.envirocar.map/src/main/java/org/envirocar/map/provider/maplibre/MapLibreMapProvider.kt index ef094c460..0652253c2 100644 --- a/org.envirocar.map/src/main/java/org/envirocar/map/provider/maplibre/MapLibreMapProvider.kt +++ b/org.envirocar.map/src/main/java/org/envirocar/map/provider/maplibre/MapLibreMapProvider.kt @@ -3,8 +3,11 @@ package org.envirocar.map.provider.maplibre import android.content.Context import org.envirocar.map.MapController import org.envirocar.map.MapProvider +import org.envirocar.map.model.AttributionSettings +import org.envirocar.map.model.LogoSettings import org.maplibre.android.MapLibre import org.maplibre.android.maps.MapView +import org.maplibre.android.maps.Style /** * [MapLibreMapProvider] @@ -20,9 +23,15 @@ import org.maplibre.android.maps.MapView * * `https://` * * `asset://` * * `file://` + * @param attribution + * The attribution settings for the map. + * @param logo + * The logo settings for the map. */ class MapLibreMapProvider( - private val style: String + private val style: String = DEFAULT_STYLE, + private val attribution: AttributionSettings = DEFAULT_ATTRIBUTION, + private val logo: LogoSettings = DEFAULT_LOGO ) : MapProvider { private lateinit var viewInstance: MapView private lateinit var controllerInstance: MapLibreMapController @@ -36,7 +45,7 @@ class MapLibreMapProvider( if (!::viewInstance.isInitialized) { viewInstance = MapView(context).apply { getMapAsync { - it.setStyle(style) + it.setStyle(Style.Builder().fromUri(style)) } } } @@ -48,12 +57,20 @@ class MapLibreMapProvider( error("MapLibreMapProvider is not initialized.") } if (!::controllerInstance.isInitialized) { - controllerInstance = MapLibreMapController(viewInstance) + controllerInstance = MapLibreMapController( + viewInstance, + attribution, + logo + ) } return controllerInstance } companion object { + const val DEFAULT_STYLE = "asset://maplibre_default_style.json" + val DEFAULT_ATTRIBUTION = AttributionSettings.default() + val DEFAULT_LOGO = LogoSettings.default() + @Volatile private var initailized = false } From f084c4f0848f18852e5461c6dff6feef27f9c0e2 Mon Sep 17 00:00:00 2001 From: Sebastian Drost Date: Tue, 20 Aug 2024 13:58:41 +0200 Subject: [PATCH 07/10] Add German translation for map style settings --- .../res/values-de/strings_activity_settings.xml | 5 +++++ org.envirocar.app/res/values/strings_activity_settings.xml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/org.envirocar.app/res/values-de/strings_activity_settings.xml b/org.envirocar.app/res/values-de/strings_activity_settings.xml index 96dc7ff49..00f28b7c9 100644 --- a/org.envirocar.app/res/values-de/strings_activity_settings.xml +++ b/org.envirocar.app/res/values-de/strings_activity_settings.xml @@ -103,6 +103,11 @@ Ausgewähltes Messprofil Wähle ein Messprofil, das kontrolliert, welche PIDs während Deiner Fahrt erfasst werden. + + Karten Style + Passe das Aussehen der Karten an, indem ein alternativer Kartenanbieter sowie ein Karten Style ausgewählt wird. + Kartenanbieter: + Style: Standard diff --git a/org.envirocar.app/res/values/strings_activity_settings.xml b/org.envirocar.app/res/values/strings_activity_settings.xml index 37ee80677..3b8929568 100644 --- a/org.envirocar.app/res/values/strings_activity_settings.xml +++ b/org.envirocar.app/res/values/strings_activity_settings.xml @@ -108,7 +108,7 @@ DVFO Campaign - Map View Customization + Map Style Customize the look of map views throughout the application by changing the map provider & associated style. Map Provider: Style: From 77eefe14db443405fb846b1442df0fdc13cb6121 Mon Sep 17 00:00:00 2001 From: Sebastian Drost Date: Tue, 20 Aug 2024 13:58:52 +0200 Subject: [PATCH 08/10] Improve imports --- .../map/provider/maplibre/MapLibreMapController.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/org.envirocar.map/src/main/java/org/envirocar/map/provider/maplibre/MapLibreMapController.kt b/org.envirocar.map/src/main/java/org/envirocar/map/provider/maplibre/MapLibreMapController.kt index 44f18e18f..aa2b0ce19 100644 --- a/org.envirocar.map/src/main/java/org/envirocar/map/provider/maplibre/MapLibreMapController.kt +++ b/org.envirocar.map/src/main/java/org/envirocar/map/provider/maplibre/MapLibreMapController.kt @@ -4,8 +4,6 @@ import android.graphics.Color import android.view.View import androidx.appcompat.content.res.AppCompatResources import androidx.core.graphics.drawable.toBitmap -import com.mapbox.maps.plugin.attribution.attribution -import com.mapbox.maps.plugin.logo.logo import kotlinx.coroutines.flow.update import org.envirocar.map.MapController import org.envirocar.map.camera.CameraUpdate @@ -19,6 +17,7 @@ import org.envirocar.map.model.Polygon import org.envirocar.map.model.Polyline import org.maplibre.android.camera.CameraPosition import org.maplibre.android.camera.CameraUpdateFactory +import org.maplibre.android.geometry.LatLng import org.maplibre.android.geometry.LatLngBounds import org.maplibre.android.maps.MapLibreMap import org.maplibre.android.maps.MapView @@ -369,9 +368,9 @@ internal class MapLibreMapController( } private fun Point.toMapLibrePoint() = org.maplibre.geojson.Point.fromLngLat(longitude, latitude) - private fun Point.toMapLibreLatLng() = org.maplibre.android.geometry.LatLng(latitude, longitude) + private fun Point.toMapLibreLatLng() = LatLng(latitude, longitude) - private fun org.maplibre.android.geometry.LatLng.toPoint() = Point(longitude, latitude) + private fun LatLng.toPoint() = Point(longitude, latitude) private fun Float.toMapLibreBearing() = this .times(MAPLIBRE_CAMERA_BEARING_MAX - MAPLIBRE_CAMERA_BEARING_MIN) From 23b99e16f9e03b8b74c7732eb96d5b47b0ce414e Mon Sep 17 00:00:00 2001 From: Hitesh Kumar Saini Date: Tue, 20 Aug 2024 17:34:40 +0530 Subject: [PATCH 09/10] Added documentation for `org.envirocar.map` module (#1011) * docs: update README * docs: map module README * docs: update README * docs: update map module README --- README.md | 32 +++----- org.envirocar.map/README.md | 147 ++++++++++++++++++++++++++++++++++++ 2 files changed, 158 insertions(+), 21 deletions(-) create mode 100644 org.envirocar.map/README.md diff --git a/README.md b/README.md index 114808870..59e77b886 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,11 @@ This is the app for the enviroCar platform. (www.envirocar.org) enviroCar Mobile is an Android application for smartphones that can be used to collect Extended Floating Car Data (XFCD). The app communicates with an OBD2 Bluetooth adapter while the user drives. This enables read access to data from the vehicle’s engine control. The data is recorded along with the smartphone’s GPS position data.The driver can view statistics about his drives and publish his data as open data. The latter happens by uploading tracks to the enviroCar server, where the data is available under the ODbL license for further analysis and use. The data can also be viewed and analyzed via the enviroCar website. enviroCar Mobile is one of the enviroCar Citizen Science Platform’s components (www.envirocar.org). - **Key Technologies** - Android - Java +- Kotlin **Benefits** @@ -24,10 +24,8 @@ enviroCar Mobile is an Android application for smartphones that can be used to c - Publishing anonymized track data as Open Data - Map based visualization of track data and track statistics - ## Quick Start - ### Installation Use the [Google Play Store](https://play.google.com/store/apps/details?id=org.envirocar.app) to install the app on your device. @@ -40,23 +38,16 @@ This software uses the gradle build system and is optimized to work within Andro The setup of the source code should be straightforward. Just follow the Android Studio guidelines for existing projects. -### Setting up the mapbox SDK -The enviroCar App project uses the ``Mapbox Maven repository``. **Mapbox is a mapping and location cloud platform for developers.** -To build the project you need the mapbox account, you can create an account for free from [here](https://account.mapbox.com/auth/signup/). -Once you have created an account, you will need to configure credentials +### Setting up the map module + +The software includes a [map module](./org.envirocar.map), which is used to visualize the track data & statistics on a map. It may require additional configuration as part of the development process. Please refer to the [respective documentation](./org.envirocar.map/README.md) for more information. -### Configure credentials -1. From your account's [tokens page](https://account.mapbox.com/access-tokens/), click the **Create a token** button. -2. Give your token a name and do check that you have checked ``Downloads:Read`` scope. -3. Make sure you copy your token and save it somehwere as you will not be able to see the token again. +The map module provides support for multiple map providers & libraries. These may be enabled or disabled during compilation. -### Configure your secret token -1. This is a secret token, and we will use it in ``gradle.properties`` file. You should not expose the token in public, that's why add ``gradle.properties`` in ``.gitignore`` . It's also possible to store the sercret token in your local user's _gradle.properties_ file, usually stored at _«USER_HOME»/.gradle/gradle.properties_. -2. Now open the ``gradle.properties`` file and add this line ``MAPBOX_DOWNLOADS_TOKEN = ``. The secret token has to be pasted without any quote marks. -``MAPBOX_DOWNLOADS_TOKEN=sk.dutaksgjdvlsayVDSADUTLASDs@!ahsvdaslud*JVAS@%DLUTSVgdJLA&&>Hdval.sujdvadvasuydgalisy``(this is just a random string, not a real token) -3. That't it. You are good to go! -If you are still facing any problem, checkout the [Mapbox guide](https://docs.mapbox.com/android/maps/guides/install/) or feel free to [create an issue](https://github.com/enviroCar/enviroCar-app/issues/new) +The application also allows to switch between the map providers & styles during runtime as well. + +[MapTiler](https://www.maptiler.com/) API key needs to be specified as `MAPTILER_API_KEY` in the [`gradle.properties` file](./gradle.properties) before compilation. MapTiler (a tile provider) is configured to work with Mapbox map provider & MapLibre map provider. ## License @@ -100,17 +91,16 @@ Check out the [Changelog](https://github.com/enviroCar/enviroCar-app/blob/master ## OBD simulator -The repository also contains a simple OBD simulator (dumb, nothing fancy) that can -be used on another Android device and mock the actual car adapter. +The repository also contains a simple OBD simulator (dumb, nothing fancy) that can be used on another Android device and mock the actual car adapter. ## References This app is in operational use in the [CITRAM - Citizen Science for Traffic Management](https://www.citram.de/) project. Check out the [enviroCar website](https://envirocar.org/) for more information about the enviroCar project. ## How to Contribute -For contributing to the enviroCar Android App, please, have a look at our [Contributor Guidelines](https://github.com/enviroCar/enviroCar-app/blob/master/CONTRIBUTING.md). +For contributing to the enviroCar Android App, please, have a look at our [Contributor Guidelines](https://github.com/enviroCar/enviroCar-app/blob/master/CONTRIBUTING.md). ## Contributors -Here is the list of [contributors to this project](https://github.com/enviroCar/enviroCar-app/blob/master/CONTRIBUTORS.md) +Here is the list of [contributors to this project](https://github.com/enviroCar/enviroCar-app/blob/master/CONTRIBUTORS.md). diff --git a/org.envirocar.map/README.md b/org.envirocar.map/README.md new file mode 100644 index 000000000..623aa9a55 --- /dev/null +++ b/org.envirocar.map/README.md @@ -0,0 +1,147 @@ +# enviroCar Map Module + +The map module is used to visualize the track data & statistics, recorded by the enviroCar Android application on a map. The map module provides support for multiple map providers & libraries, which may be enabled or disabled during compilation. + +The module has two important qualities: + +- **Extensibility**: Multiple map providers will be supported & more can be easily added in the future. +- **Independence**: The map module may be utilized in other projects as well. + +## Supported Providers + +The module currently supports 2 map providers. These may be easily enabled or disabled during compile-time by adding or editing the `local.properties` file in the project as follows: + +```properties +org.envirocar.map.enableMapbox=true +org.envirocar.map.enableMapLibre=true +``` + +By default, all available map providers are enabled. Additional configuration may be required for each map provider, which has been documented below. + +### 1. Mapbox + +`MapboxMapProvider` class provides support for the [Mapbox map provider](https://www.mapbox.com/). + +A public token & private token from Mapbox is required to use this map provider. The steps are provided below: + +1. Create an account [here](https://account.mapbox.com/auth/signup/). +2. Create a new token & provide it a "DOWNLOADS:READ" secret scope. +3. Specify the Mapbox public token as `mapbox_access_token` in the [`developer-config.xml` file](./org.envirocar.map/src/main/res/values/developer-config.xml). +4. Specify the Mapbox secret token as `MAPBOX_DOWNLOADS_TOKEN` in the [`gradle.properties` file](./gradle.properties). + +### 2. MapLibre + +`MapLibreMapProvider` class provides support for the [MapLibre map provider](https://maplibre.org/). + +No changes required. + +## Adding New Provider + +All the providers supported by the module, sit inside the [`provider` directory](org.envirocar.map/src/main/java/org/envirocar/map/provider). A map provider must implement [`MapProvider`](./org.envirocar.map/src/main/java/org/envirocar/map/MapProvider.kt) & [`MapController`](./org.envirocar.map/src/main/java/org/envirocar/map/MapController.kt) interfaces. Existing implementations may be used as a reference. + +## Architecture + +```mermaid +classDiagram + +class `android.view.View` + +class MapView { + + getController(provider: MapProvider) MapController +} + +class MapController { + <> + + camera: CameraState + + setMinZoom(minZoom: Float)* + + setMaxZoom(maxZoom: Float)* + + notifyCameraUpdate(cameraUpdate: CameraUpdate, animation: Animation? = null)* + + addMarker(marker: Marker)* + + addPolyline(polyline: Polyline)* + + addPolygon(polygon: Polygon)* + + removeMarker(marker: Marker)* + + removePolyline(polyline: Polyline)* + + removePolygon(polygon: Polygon)* + + clearMarkers()* + + clearPolylines()* + + clearPolygons()* +} + +class MapLibreController + +class MapboxController + +`android.view.View` <|-- MapView + +MapView <-- MapController + +MapController <|.. MapboxController +MapController <|.. MapLibreController +MapboxController <.. `com.mapbox.maps:android` +MapLibreController <.. `org.maplibre.gl:android-sdk` + +``` + +```mermaid +classDiagram + +BaseLocationIndicator <|-- LocationIndicator +`android.location.LocationListener` <|.. LocationIndicator +LocationIndicator *-- `android.location.LocationManager` + +class BaseLocationIndicator { + +enable() + +disable() + +setCameraMode(value: LocationIndicatorCameraMode) + +notifyLocation(location: Location) + -followCameraIfRequired(location: Location) + -clearMarkers() + -clearPolygons() +} + +class LocationIndicator { + +enable() + +disable() + +onLocationChanged() +} +``` + +## Usage + +The map module is fairly easy to use. The minimal snippet below provides a better idea: + + +```xml + +``` + +```kt +val view = findViewById(R.id.mapView) +val provider = ... // May be [MapboxMapProvider] or [MapLibreMapProvider] etc. +val controller = view.getController(provider) + +controller.addPolyline( + Polyline.Builder(POINTS) + .withWidth(4.0F) + .withColor(0xFF0D53FF.toInt()) + .build() +) +controller.addMarker(Marker.Builder(POINTS.first()).build()) +controller.addMarker(Marker.Builder(POINTS.last()).build()) + +controller.notifyCameraUpdate( + CameraUpdateFactory.newCameraUpdateBasedOnBounds( + POINTS, + 120.0F + ) +) +``` + +In essence, create a map view & initialize it with a map provider to access the map controller for manipulating the map e.g. +1. Creating a marker, polyline or polygon etc. +2. Manipulating the camera e.g. zoom, tilt or bearing etc. +3. Displaying user location. + From 6fd62515b17a2ae77c9d129a3ca99c7a143c0b8f Mon Sep 17 00:00:00 2001 From: Hitesh Kumar Saini Date: Tue, 20 Aug 2024 17:37:05 +0530 Subject: [PATCH 10/10] feat: IntelliJ project icon & name (#1009) --- .gitignore | 4 +++- .idea/.name | 1 + .idea/icon.svg | 10 ++++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 .idea/.name create mode 100644 .idea/icon.svg diff --git a/.gitignore b/.gitignore index d9b7e2c63..838567cbb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ -.idea/ +**/.idea/** +!.idea/.name +!.idea/icon.svg build/ */build/ .gradle/ diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 000000000..1f0f1c9cb --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +enviroCar \ No newline at end of file diff --git a/.idea/icon.svg b/.idea/icon.svg new file mode 100644 index 000000000..f00002c71 --- /dev/null +++ b/.idea/icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + +