summaryrefslogtreecommitdiff
path: root/platform/android/wearapp/src/main/java/com/mapbox/mapboxsdk/wearapp/ui/MapboxMapView.kt
blob: 04fe3828cc9092811e22ad2f8d976534ce11ea45 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
package com.example.mapbox.ui

import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Resources
import android.location.Location
import android.view.Gravity
import android.view.MotionEvent
import android.view.View
import com.example.mapbox.R
import com.example.mapbox.breadcrumb.ExerciseEngineState
import com.example.mapbox.extensions.getBitmapFromDrawable
import com.example.mapbox.extensions.latLong
import com.mapbox.mapboxsdk.camera.CameraPosition
import com.mapbox.mapboxsdk.camera.CameraUpdateFactory
import com.mapbox.mapboxsdk.geometry.LatLng
import com.mapbox.mapboxsdk.location.LocationComponentOptions
import com.mapbox.mapboxsdk.location.OnCameraTrackingChangedListener
import com.mapbox.mapboxsdk.location.OnLocationCameraTransitionListener
import com.mapbox.mapboxsdk.location.OnLocationStaleListener
import com.mapbox.mapboxsdk.location.modes.CameraMode
import com.mapbox.mapboxsdk.location.modes.RenderMode
import com.mapbox.mapboxsdk.maps.*
import com.soy.android.maps.breadcrumb.Breadcrumb
import com.soy.android.maps.compass.WassCompassEngine
import com.soy.android.maps.extensions.getActivatedLocationComponent
import com.soy.android.maps.extensions.isCameraInTrackingMode
import timber.log.Timber
import kotlin.math.roundToInt

internal const val INITIAL_CAMERA_ZOOM_LEVEL_INDEX_WITH_LOCATION = 14
/**
 * Map is centered at Helsinki when we don't have user's current or any last location
 */
private const val DEFAULT_INITIAL_CAMERA_POSITION_LATITUDE = 60.1699
private const val DEFAULT_INITIAL_CAMERA_POSITION_LONGITUDE = 24.9384

private const val CAMERA_ANIMATION_DURATION = 200L

internal fun buildCameraPosition(
    target: LatLng = LatLng(DEFAULT_INITIAL_CAMERA_POSITION_LATITUDE, DEFAULT_INITIAL_CAMERA_POSITION_LONGITUDE),
    zoomIndex: Int = INITIAL_CAMERA_ZOOM_LEVEL_INDEX_WITH_LOCATION
) = CameraPosition.Builder().target(target).zoom(zoomIndex.toDouble()).build()

/**
 * [MapboxMapOptions] can either be applied via xml or programmatically when [MapView] is
 * constructed. This function provides a predefined set of options.
 */
private fun createMapboxMapOptions(resources: Resources, location: Location?): MapboxMapOptions {
    val cameraPosition = if (location == null) {
        // Use default location and zoom level
        buildCameraPosition()
    } else {
        buildCameraPosition(LatLng(location.latitude, location.longitude), INITIAL_CAMERA_ZOOM_LEVEL_INDEX_WITH_LOCATION)
    }
    return MapboxMapOptions().apply {
        val compassImageTopMargin = resources.getDimension(R.dimen.size_spacing_xxsmall).roundToInt()
        compassMargins(intArrayOf(0, compassImageTopMargin, 0, 0))
        compassEnabled(true)
        compassImage(resources.getDrawable(R.drawable.ic_shape_north_indicator, null))
        compassGravity(Gravity.CENTER)
        textureMode(true)
        attributionEnabled(false)
        logoEnabled(false)
        zoomGesturesEnabled(true)
        compassFadesWhenFacingNorth(false)
        rotateGesturesEnabled(false)
        camera(cameraPosition)
    }
}

internal const val START_SYMBOL_ICON_ID = "id-start-icon"
internal const val PAUSE_LINE_PATTERN = "id-stale-line-pattern"

/**
 * This class extends [MapView] and is suggested to use when creating map programmatically with a
 * given cameraPosition which is used to center the camera on map. This view doesn't allow touch
 * interactions for the child views of [MapView] such as compass, logo, attribute.
 */
@SuppressLint("ViewConstructor")
class MapboxMapView(
    context: Context,
    location: Location? = null,
    private val isBreadcrumbEnabled: Boolean = true
) : MapView(context, createMapboxMapOptions(context.resources, location)), OnLocationStaleListener,
    OnCameraTrackingChangedListener, MapboxMap.OnMapClickListener, OnMapReadyCallback, MapboxMap.OnMapLongClickListener {

    init {
        id = View.generateViewId()
    }

    private var map: MapboxMap? = null

    private var breadcrumb: Breadcrumb? = null

    private var exerciseEngineState = ExerciseEngineState.Recording

    /**
     * A boolean which keeps track of location stale status. Initially location is assigned to be stale since there is
     * no gps fix at start up. The value changes in two places:
     * 1- When [onStaleStateChange] is invoked, the value is updated based on the parameter
     * 2- Whenever there is new gps fix it is set to be false
     */
    private var isLocationStale = true

    internal var compassEngine: WassCompassEngine? = null
    @CameraMode.Mode
    private var lastCameraTrackingMode = CameraMode.TRACKING_COMPASS
    private var cameraZoomLevelIndex = INITIAL_CAMERA_ZOOM_LEVEL_INDEX_WITH_LOCATION
    private var isDefaultCameraPositionInUse = false

    override fun onMapReady(mapboxMap: MapboxMap) {
        map = mapboxMap.apply {
            setStyle(Style.Builder()
                .withImage(START_SYMBOL_ICON_ID, resources.getBitmapFromDrawable(R.drawable.ic_map_start_pin))
                .withImage(PAUSE_LINE_PATTERN, resources.getBitmapFromDrawable(R.drawable.ic_stale_track_dot))
                .fromUrl(context.getString(R.string.asoy_mapbox_style))
            ) { style ->
                startLocationComponent(mapboxMap, style)
                if (isBreadcrumbEnabled) {
                    breadcrumb = Breadcrumb(this@MapboxMapView, mapboxMap, style)
                }
            }
            addOnMapLongClickListener(this@MapboxMapView)
            addOnMapClickListener(this@MapboxMapView)
        }
    }

    // Location
    @SuppressLint("MissingPermission")
    private fun startLocationComponent(mapboxMap: MapboxMap, style: Style) {
        mapboxMap.locationComponent.apply {
            // Activate with options. If location engine is null, push location updates to
            // the component without any internal engine management. No engine is going to
            // be initialized and you can push location updates with
            // [LocationComponent#forceLocationUpdate(Location)].
            activateLocationComponent(
                context,
                style,
                null,
                LocationComponentOptions.createFromAttributes(context, R.style.mapbox_location)
            )
            compassEngine = this@MapboxMapView.compassEngine
            addOnCameraTrackingChangedListener(this@MapboxMapView)
            isLocationComponentEnabled = true
            Timber.d("Mapbox Location Component is enabled")
            addOnLocationStaleListener(this@MapboxMapView)
            renderMode = RenderMode.GPS
            cameraMode = lastCameraTrackingMode
        }
    }

    internal fun onNewLocation(location: Location) {
        isLocationStale = false
        map?.run {
            getActivatedLocationComponent()?.run {
                if (isDefaultCameraPositionInUse) {
                    updateMapCameraZoom(INITIAL_CAMERA_ZOOM_LEVEL_INDEX_WITH_LOCATION)
                    isDefaultCameraPositionInUse = false
                }
                forceLocationUpdate(location)
                breadcrumb?.onNewLocation(location.latLong(), exerciseEngineState)
            }
        }
    }

    override fun onStaleStateChange(isStale: Boolean) {
        Timber.d("Location stale state change. Is Stale: $isStale")
        isLocationStale = isStale
        map?.getActivatedLocationComponent()?.run {
            renderMode = if (isStale) RenderMode.NORMAL else RenderMode.GPS
            val locationComponentStyle = if (isStale) R.style.mapbox_stale_location else R.style.mapbox_location
            applyStyle(LocationComponentOptions.createFromAttributes(context, locationComponentStyle))
        }
    }

    // Camera
    private fun updateMapCameraZoom(zoomLevelIndex: Int) = map?.getActivatedLocationComponent()?.run {
        cameraZoomLevelIndex = zoomLevelIndex
        if (cameraMode != CameraMode.NONE) {
            zoomWhileTracking(getCameraZoomLevel(), CAMERA_ANIMATION_DURATION)
        } else {
            map?.easeCamera(CameraUpdateFactory.zoomTo(getCameraZoomLevel()))
        }
    }

    private fun getCameraZoomLevel(): Double = map?.cameraPosition?.zoom ?: 0.0

    override fun onCameraTrackingChanged(currentMode: Int) {
        Timber.d("Camera tracking mode changed. Current mode: $currentMode")
        if (map?.isCameraInTrackingMode() == true) {
            lastCameraTrackingMode = currentMode
        }
    }

    /**
     * Invoked whenever camera tracking is broken.
     */
    override fun onCameraTrackingDismissed() {
        Timber.d("Exiting from camera tracking mode")
        updateCameraMode(CameraMode.NONE_COMPASS)
    }

    private fun updateCameraMode(@CameraMode.Mode mode: Int) {
        // We need to
        map?.getActivatedLocationComponent()?.apply {
            if (cameraMode != CameraMode.NONE) {
                setCameraMode(mode, object : OnLocationCameraTransitionListener {
                    override fun onLocationCameraTransitionFinished(cameraMode: Int) {
                        zoomWhileTracking(getCameraZoomLevel(), CAMERA_ANIMATION_DURATION)
                    }

                    override fun onLocationCameraTransitionCanceled(cameraMode: Int) {
                        // No impl.
                    }
                })
            } else {
                cameraMode = mode
            }
        }
    }

    // Gestures
    /**
     * When [MapboxMap.OnMapClickListener.onMapClick] is called camera mode is updated:
     * There are two camera tracking modes enabled:
     * The [CameraMode.TRACKING_GPS_NORTH] sets the camera always to the north and the [CameraMode.TRACKING_COMPASS]
     * sets the location indicator to north and map rotates according to heading
     * If camera mode is [CameraMode.TRACKING_GPS_NORTH], onClick switches the mode to [CameraMode.TRACKING_COMPASS]
     * If camera mode is [CameraMode.TRACKING_COMPASS]  onClick switches the mode to [CameraMode.TRACKING_GPS_NORTH]
     * If user starts panning while in tracking mode, camera switches to [CameraMode.NONE_COMPASS] mode and user is not
     * tracked by camera. On click switches the camera back to last used tracking mode
     */
    override fun onMapClick(point: LatLng): Boolean {
        map?.getActivatedLocationComponent()?.apply {
            when (cameraMode) {
                CameraMode.TRACKING_GPS_NORTH -> updateCameraMode(CameraMode.TRACKING_COMPASS)
                CameraMode.TRACKING_COMPASS -> updateCameraMode(CameraMode.TRACKING_GPS_NORTH)
                CameraMode.NONE_COMPASS,
                CameraMode.NONE,
                CameraMode.NONE_GPS -> updateCameraMode(lastCameraTrackingMode)
                else -> Timber.w("Using unsupported camera mode")
            }
        }
        return true
    }

    /**
     * Used for mocking exercise pause / resmume controls
     */
    override fun onMapLongClick(point: LatLng): Boolean {
        if (exerciseEngineState == ExerciseEngineState.Paused) {
            exerciseEngineState = ExerciseEngineState.Recording
        } else if (exerciseEngineState == ExerciseEngineState.Recording) {
            exerciseEngineState = ExerciseEngineState.Paused
        }
        return true
    }

    /**
     * By overriding this method, we prevent compass view component of the [MapView] from consuming the clicks
     */
    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        super.onInterceptTouchEvent(ev)
        return true
    }

    override fun onResume() {
        super.onResume()
        breadcrumb?.onResume()
    }

    override fun onStop() {
        breadcrumb?.onStop()
        super.onStop()
    }

    override fun onDestroy() {
        breadcrumb?.onDestroy()
        super.onDestroy()
    }
}