Skip to content

Commit dbc6883

Browse files
committed
Mapbox integration, location tracking added
1 parent 8c48f05 commit dbc6883

File tree

11 files changed

+275
-14
lines changed

11 files changed

+275
-14
lines changed

app/build.gradle.kts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,12 @@ dependencies {
4444
testImplementation("junit:junit:4.13.2")
4545
androidTestImplementation("androidx.test.ext:junit:1.1.5")
4646
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
47+
48+
// Mapbox
49+
implementation("com.mapbox.maps:android:10.16.1")
50+
implementation("com.mapbox.navigation:android:2.16.0")
51+
52+
// Google play services dependency to use integrated google's location provider
53+
implementation("com.google.android.gms:play-services-location:21.1.0")
54+
4755
}

app/src/main/AndroidManifest.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
xmlns:tools="http://schemas.android.com/tools">
44

55
<uses-permission android:name="android.permission.INTERNET" />
6+
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
7+
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
68

79
<application
810
android:allowBackup="true"

app/src/main/java/com/honz/itsvisualizer/MapFragment.kt

Lines changed: 206 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,98 @@
11
package com.honz.itsvisualizer
22

3+
import android.Manifest
4+
import android.app.AlertDialog
35
import android.content.BroadcastReceiver
46
import android.content.Context
57
import android.content.Intent
68
import android.content.IntentFilter
9+
import android.content.pm.PackageManager
10+
import android.location.Location
711
import android.os.Bundle
812
import android.util.Log
913
import androidx.fragment.app.Fragment
1014
import android.view.LayoutInflater
1115
import android.view.View
1216
import android.view.ViewGroup
17+
import androidx.appcompat.content.res.AppCompatResources
18+
import androidx.core.app.ActivityCompat
1319
import androidx.localbroadcastmanager.content.LocalBroadcastManager
1420
import com.google.android.material.floatingactionbutton.FloatingActionButton
21+
import com.mapbox.android.gestures.MoveGestureDetector
22+
import com.mapbox.geojson.Point
23+
import com.mapbox.maps.CameraOptions
24+
import com.mapbox.maps.EdgeInsets
25+
import com.mapbox.maps.MapView
26+
import com.mapbox.maps.Style
27+
import com.mapbox.maps.plugin.LocationPuck2D
28+
import com.mapbox.maps.plugin.animation.MapAnimationOptions
29+
import com.mapbox.maps.plugin.animation.camera
30+
import com.mapbox.maps.plugin.compass.compass
31+
import com.mapbox.maps.plugin.gestures.OnMoveListener
32+
import com.mapbox.maps.plugin.gestures.addOnMoveListener
33+
import com.mapbox.maps.plugin.locationcomponent.location
34+
import com.mapbox.navigation.base.options.NavigationOptions
35+
import com.mapbox.navigation.core.MapboxNavigation
36+
import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp
37+
import com.mapbox.navigation.core.lifecycle.MapboxNavigationObserver
38+
import com.mapbox.navigation.core.lifecycle.requireMapboxNavigation
39+
import com.mapbox.navigation.core.trip.session.LocationMatcherResult
40+
import com.mapbox.navigation.core.trip.session.LocationObserver
41+
import com.mapbox.navigation.ui.maps.location.NavigationLocationProvider
1542

1643
class MapFragment : Fragment() {
1744

45+
private lateinit var mapView: MapView
1846
private lateinit var connectionToggleFab: FloatingActionButton
47+
private lateinit var cameraCenteringToggleFab: FloatingActionButton
48+
49+
private var isTripSessionStarted = false
50+
private var centerCamera = true
51+
private var lastLocation: Location? = null
52+
53+
private val navigationLocationProvider = NavigationLocationProvider()
54+
55+
/**
56+
* locationObserver passes new location data to update camera position
57+
*/
58+
private val locationObserver = object: LocationObserver {
59+
override fun onNewLocationMatcherResult(locationMatcherResult: LocationMatcherResult) {
60+
val enhancedLocation = locationMatcherResult.enhancedLocation
61+
navigationLocationProvider.changePosition(
62+
enhancedLocation,
63+
locationMatcherResult.keyPoints
64+
)
65+
66+
lastLocation = enhancedLocation
67+
updateCameraPosition(enhancedLocation)
68+
}
69+
70+
// Not implemented
71+
override fun onNewRawLocation(rawLocation: Location) {}
72+
}
73+
74+
/**
75+
* OnMoveListener that disables camera centering after moves the map
76+
*/
77+
private val onMoveListener = object : OnMoveListener {
78+
override fun onMoveBegin(detector: MoveGestureDetector) {
79+
if(centerCamera) setCameraCentering(false)
80+
}
81+
82+
override fun onMove(detector: MoveGestureDetector): Boolean {
83+
return false
84+
}
85+
86+
override fun onMoveEnd(detector: MoveGestureDetector) {}
87+
}
1988

2089
override fun onCreate(savedInstanceState: Bundle?) {
2190
super.onCreate(savedInstanceState)
22-
Log.i("[MAP FRAGMENT]", "onCreate()")
2391

2492
// Signals from SocketService
2593
val stateFilter = IntentFilter("itsVisualizer.SERVICE_STATE")
2694
LocalBroadcastManager.getInstance(requireContext()).registerReceiver(stateReceiver, stateFilter)
2795

28-
// TODO: Init map
2996
}
3097

3198
override fun onCreateView(
@@ -34,16 +101,84 @@ class MapFragment : Fragment() {
34101
): View? {
35102
val view = inflater.inflate(R.layout.fragment_map, container, false)
36103

104+
105+
// Connection toggle FAB
37106
connectionToggleFab = view.findViewById(R.id.connectionToggleFab)
38107
connectionToggleFab.setOnClickListener { toggleConnection() }
39108

40-
// Update FAB icon based on current state
109+
// Update icon based on current state
41110
val intent = Intent("itsVisualizer.SOCKET_SERVICE_STATE_REQUEST")
42111
LocalBroadcastManager.getInstance(requireContext()).sendBroadcast(intent)
43112

113+
// Camera centering FAB
114+
cameraCenteringToggleFab = view.findViewById(R.id.cameraCenteringToggleFab)
115+
cameraCenteringToggleFab.setOnClickListener {
116+
setCameraCentering(null)
117+
}
118+
119+
// Init mapbox
120+
mapView = view.findViewById(R.id.mapView)
121+
mapView.getMapboxMap().loadStyleUri(Style.TRAFFIC_DAY)
122+
mapView.getMapboxMap().addOnMoveListener(onMoveListener)
123+
//mapView.scalebar.enabled = false
124+
mapView.compass.updateSettings {
125+
marginTop = 100.0f
126+
}
127+
128+
// Location init
129+
initNavigation()
130+
44131
return view
45132
}
46133

134+
/**
135+
* Mapbox navigation element that matches current position to nearest road
136+
*/
137+
private val mapboxNavigation by requireMapboxNavigation(
138+
onResumedObserver = object : MapboxNavigationObserver {
139+
override fun onAttached(mapboxNavigation: MapboxNavigation) {
140+
mapboxNavigation.registerLocationObserver(locationObserver)
141+
142+
// Check if user granted location permissions
143+
if (ActivityCompat.checkSelfPermission(
144+
requireContext(),
145+
Manifest.permission.ACCESS_FINE_LOCATION
146+
) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(
147+
requireContext(),
148+
Manifest.permission.ACCESS_COARSE_LOCATION
149+
) != PackageManager.PERMISSION_GRANTED
150+
) {
151+
// Permissions denied, show a message to the user
152+
AlertDialog.Builder(activity)
153+
.setTitle(R.string.perm_disabled_title)
154+
.setMessage(R.string.perm_disabled_text)
155+
.setNeutralButton(R.string.cancel) { dialog, _ ->
156+
dialog.dismiss()
157+
}
158+
.setCancelable(false)
159+
.show()
160+
161+
setCameraCentering(false)
162+
cameraCenteringToggleFab.isEnabled = false
163+
return
164+
}
165+
166+
if (!isTripSessionStarted) {
167+
mapboxNavigation.startTripSession()
168+
isTripSessionStarted = true
169+
}
170+
else {
171+
lastLocation?.let { updateCameraPosition(it) }
172+
}
173+
174+
}
175+
176+
override fun onDetached(mapboxNavigation: MapboxNavigation) {
177+
mapboxNavigation.unregisterLocationObserver(locationObserver)
178+
}
179+
},
180+
)
181+
47182
/**
48183
* Gets boolean representing current state of socket to update FAB image
49184
*/
@@ -58,6 +193,74 @@ class MapFragment : Fragment() {
58193
}
59194
}
60195

196+
/**
197+
* Sets up the location provider and location puck image
198+
*/
199+
private fun initNavigation() {
200+
MapboxNavigationApp.setup(
201+
NavigationOptions.Builder(requireActivity().applicationContext)
202+
.accessToken(getString(R.string.mapbox_access_token))
203+
.build()
204+
)
205+
206+
mapView.location.apply {
207+
setLocationProvider(navigationLocationProvider)
208+
locationPuck = LocationPuck2D(
209+
bearingImage = AppCompatResources.getDrawable(
210+
requireActivity().applicationContext,
211+
com.mapbox.navigation.ui.maps.R.drawable.mapbox_navigation_puck_icon2
212+
),
213+
shadowImage = AppCompatResources.getDrawable(
214+
requireActivity().applicationContext,
215+
com.mapbox.navigation.ui.maps.R.drawable.mapbox_navigation_puck_icon2_shadow
216+
),
217+
)
218+
219+
enabled = true
220+
}
221+
}
222+
223+
/**
224+
* Eases the camera to position provided in 'location' parameter
225+
*/
226+
private fun updateCameraPosition(location: Location) {
227+
if(!centerCamera) return
228+
229+
val mapAnimationOptions =
230+
MapAnimationOptions.Builder()
231+
.duration(1500L)
232+
.build()
233+
234+
mapView.camera.easeTo(
235+
CameraOptions.Builder()
236+
.center(Point.fromLngLat(location.longitude, location.latitude))
237+
.bearing(location.bearing.toDouble())
238+
.zoom(18.0)
239+
.pitch(45.0)
240+
.padding(EdgeInsets(500.0, 0.0, 0.0, 0.0))
241+
.build(),
242+
mapAnimationOptions
243+
)
244+
}
245+
246+
/**
247+
* Controls if camera follows current location.
248+
* Passing 'null' as a parameter toggles current state.
249+
*/
250+
private fun setCameraCentering(enabled: Boolean?) {
251+
centerCamera = enabled ?: !centerCamera
252+
253+
if(centerCamera) {
254+
cameraCenteringToggleFab.setImageResource(R.drawable.location)
255+
}
256+
else {
257+
cameraCenteringToggleFab.setImageResource(R.drawable.location_off)
258+
}
259+
}
260+
261+
/**
262+
* Toggles the current state of socket connection
263+
*/
61264
private fun toggleConnection() {
62265
val intent = Intent("itsVisualizer.TOGGLE_SOCKET_SERVICE")
63266
LocalBroadcastManager.getInstance(requireContext()).sendBroadcast(intent)

app/src/main/java/utils/socket/SocketService.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,6 @@ class SocketService : Service() {
106106
if (!attemptConnection) continue
107107
if (ipAddress.isNullOrEmpty() || port == -1) {
108108
// IP or Port is not set
109-
// TODO: Handle if port or IP is not set
110109
continue
111110
}
112111

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<vector android:height="24dp" android:tint="#000000"
2+
android:viewportHeight="24" android:viewportWidth="24"
3+
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
4+
<path android:fillColor="@android:color/white" android:pathData="M12,8c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM20.94,11c-0.46,-4.17 -3.77,-7.48 -7.94,-7.94L13,1h-2v2.06C6.83,3.52 3.52,6.83 3.06,11L1,11v2h2.06c0.46,4.17 3.77,7.48 7.94,7.94L11,23h2v-2.06c4.17,-0.46 7.48,-3.77 7.94,-7.94L23,13v-2h-2.06zM12,19c-3.87,0 -7,-3.13 -7,-7s3.13,-7 7,-7 7,3.13 7,7 -3.13,7 -7,7z"/>
5+
</vector>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<vector android:height="24dp" android:tint="#000000"
2+
android:viewportHeight="24" android:viewportWidth="24"
3+
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
4+
<path android:fillColor="@android:color/white" android:pathData="M20.94,11c-0.46,-4.17 -3.77,-7.48 -7.94,-7.94L13,1h-2v2.06c-1.13,0.12 -2.19,0.46 -3.16,0.97l1.5,1.5C10.16,5.19 11.06,5 12,5c3.87,0 7,3.13 7,7 0,0.94 -0.19,1.84 -0.52,2.65l1.5,1.5c0.5,-0.96 0.84,-2.02 0.97,-3.15L23,13v-2h-2.06zM3,4.27l2.04,2.04C3.97,7.62 3.25,9.23 3.06,11L1,11v2h2.06c0.46,4.17 3.77,7.48 7.94,7.94L11,23h2v-2.06c1.77,-0.2 3.38,-0.91 4.69,-1.98L19.73,21 21,19.73 4.27,3 3,4.27zM16.27,17.54C15.09,18.45 13.61,19 12,19c-3.87,0 -7,-3.13 -7,-7 0,-1.61 0.55,-3.09 1.46,-4.27l9.81,9.81z"/>
5+
</vector>

app/src/main/res/layout/fragment_map.xml

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,20 @@
77
android:layout_height="match_parent"
88
tools:context=".MapFragment">
99

10-
<!-- TODO: Update blank fragment layout -->
11-
12-
<TextView
13-
android:id="@+id/textView"
14-
android:layout_width="wrap_content"
15-
android:layout_height="wrap_content"
16-
android:text="map"
10+
<com.mapbox.maps.MapView
11+
android:id="@+id/mapView"
12+
android:layout_width="0dp"
13+
android:layout_height="0dp"
1714
app:layout_constraintBottom_toBottomOf="parent"
1815
app:layout_constraintEnd_toEndOf="parent"
1916
app:layout_constraintStart_toStartOf="parent"
20-
app:layout_constraintTop_toTopOf="parent" />
17+
18+
app:layout_constraintTop_toTopOf="parent"
19+
app:mapbox_cameraTargetLat="49.83173097700292"
20+
app:mapbox_cameraTargetLng="18.160851157066798"
21+
app:mapbox_cameraZoom="9.0"
22+
app:mapbox_logoGravity="right|top"
23+
/>
2124

2225
<com.google.android.material.floatingactionbutton.FloatingActionButton
2326
android:id="@+id/connectionToggleFab"
@@ -26,8 +29,23 @@
2629
android:layout_marginEnd="16dp"
2730
android:layout_marginBottom="16dp"
2831
android:clickable="true"
32+
android:focusable="true"
2933
android:src="@drawable/wifi"
3034
app:layout_constraintBottom_toBottomOf="parent"
31-
app:layout_constraintEnd_toEndOf="parent" />
35+
app:layout_constraintEnd_toEndOf="parent"
36+
android:importantForAccessibility="no" />
37+
38+
<com.google.android.material.floatingactionbutton.FloatingActionButton
39+
android:id="@+id/cameraCenteringToggleFab"
40+
android:layout_width="wrap_content"
41+
android:layout_height="wrap_content"
42+
android:layout_marginStart="16dp"
43+
android:layout_marginBottom="16dp"
44+
android:clickable="true"
45+
android:focusable="true"
46+
android:src="@drawable/location"
47+
app:layout_constraintBottom_toBottomOf="parent"
48+
app:layout_constraintStart_toStartOf="parent"
49+
android:importantForAccessibility="no" />
3250

3351
</androidx.constraintlayout.widget.ConstraintLayout>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<resources xmlns:tools="http://schemas.android.com/tools">
3+
<string name="mapbox_access_token" translatable="false" tools:ignore="UnusedResources">PUBLIC_MAPBOX_TOKEN</string>
4+
</resources>

app/src/main/res/values/strings.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,7 @@
1010
<string name="save">Save settings</string>
1111
<string name="saved_successfully">Saved successfully!</string>
1212
<string name="disconnected">Disconnected</string>
13+
<string name="perm_disabled_title">Permission Required</string>
14+
<string name="perm_disabled_text">Location permissions are required to use this app, please enable them in settings.</string>
15+
<string name="cancel">Cancel</string>
1316
</resources>

gradle.properties

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,6 @@ kotlin.code.style=official
2020
# Enables namespacing of each library's R class so that its R class includes only the
2121
# resources declared in the library itself and none from the library's dependencies,
2222
# thereby reducing the size of the R class for that library
23-
android.nonTransitiveRClass=true
23+
android.nonTransitiveRClass=true
24+
# Mapbox API keys
25+
MAPBOX_DOWNLOAD_TOKEN=PRIVATE_MAPBOX_TOKEN

0 commit comments

Comments
 (0)