package org.terst.nav import android.Manifest import android.content.Intent import android.content.pm.PackageManager import android.graphics.Bitmap import android.graphics.Canvas import android.media.MediaPlayer import android.os.Build import android.os.Bundle import android.util.Log import android.view.View import android.widget.FrameLayout import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.lifecycle.lifecycleScope import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.floatingactionbutton.FloatingActionButton import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.maplibre.android.MapLibre import org.maplibre.android.maps.MapView import org.maplibre.android.maps.Style import org.maplibre.android.style.layers.RasterLayer import org.maplibre.android.style.sources.RasterSource import org.maplibre.android.style.sources.TileSet import org.terst.nav.ui.* import org.terst.nav.ui.doc.DocFragment import org.terst.nav.ui.safety.SafetyFragment import org.terst.nav.ui.voicelog.VoiceLogFragment import java.util.* class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { private var mapView: MapView? = null private var mobHandler: MobHandler? = null private var instrumentHandler: InstrumentHandler? = null private var mapHandler: MapHandler? = null private var anchorWatchHandler: AnchorWatchHandler? = null private val loadedStyleFlow = MutableStateFlow(null) private lateinit var bottomSheetBehavior: BottomSheetBehavior private lateinit var fragmentContainer: FrameLayout private lateinit var fabRecordTrack: FloatingActionButton private val safetyFragment = SafetyFragment().apply { setSafetyListener(this@MainActivity) } private val viewModel: MainViewModel by viewModels() private var pendingServiceStart = false override fun onResume() { super.onResume() mapView?.onResume() if (pendingServiceStart) { pendingServiceStart = false startServices() } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) MapLibre.getInstance(this) setContentView(R.layout.activity_main) checkForegroundPermissions() initializeUI() } private fun initializeUI() { fragmentContainer = findViewById(R.id.fragment_container) setupMap() setupBottomSheet() setupBottomNavigation() setupHandlers() findViewById(R.id.fab_mob).setOnClickListener { onActivateMob() } fabRecordTrack = findViewById(R.id.fab_record_track) fabRecordTrack.setOnClickListener { if (viewModel.isRecording.value) viewModel.stopTrack() else viewModel.startTrack() } // Observe immediately — pure UI state, not gated on GPS permission lifecycleScope.launch { viewModel.isRecording.collect { recording -> val icon = if (recording) R.drawable.ic_close else R.drawable.ic_track_record fabRecordTrack.setImageResource(icon) fabRecordTrack.contentDescription = if (recording) "Stop Recording" else "Record Track" } } } private fun setupBottomSheet() { val sheet = findViewById(R.id.instrument_bottom_sheet) bottomSheetBehavior = BottomSheetBehavior.from(sheet) bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED } private fun setupBottomNavigation() { val nav = findViewById(R.id.bottom_navigation) nav.setOnItemSelectedListener { item -> when (item.itemId) { R.id.nav_map -> { hideOverlays() bottomSheetBehavior.isHideable = false bottomSheetBehavior.peekHeight = 120.dpToPx() bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED true } R.id.nav_instruments -> { hideOverlays() bottomSheetBehavior.isHideable = false bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED true } R.id.nav_log -> { showOverlay(VoiceLogFragment()) bottomSheetBehavior.isHideable = true bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN true } R.id.nav_safety -> { showOverlay(safetyFragment) bottomSheetBehavior.isHideable = true bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN true } else -> false } } } private fun showOverlay(fragment: androidx.fragment.app.Fragment) { fragmentContainer.visibility = View.VISIBLE supportFragmentManager.beginTransaction() .replace(R.id.fragment_container, fragment) .commit() } private fun hideOverlays() { fragmentContainer.visibility = View.GONE } override fun onActivateMob() { lifecycleScope.launch { LocationService.locationFlow.firstOrNull()?.let { gpsData -> val mediaPlayer = MediaPlayer.create(this@MainActivity, R.raw.mob_alarm) mobHandler?.activateMob(gpsData.latitude, gpsData.longitude, mediaPlayer) } } } override fun onConfigureAnchor() { anchorWatchHandler?.toggleVisibility() } private fun setupHandlers() { instrumentHandler = InstrumentHandler( valueAws = findViewById(R.id.value_aws), valueTws = findViewById(R.id.value_tws), valueHdg = findViewById(R.id.value_hdg), valueCog = findViewById(R.id.value_cog), valueBsp = findViewById(R.id.value_bsp), valueSog = findViewById(R.id.value_sog), valueVmg = findViewById(R.id.value_vmg), valueDepth = findViewById(R.id.value_depth), valuePolarPct = findViewById(R.id.value_polar_pct), valueBaro = findViewById(R.id.value_baro), labelTrend = null, // simplified barometerTrendView = null, // simplified polarDiagramView = findViewById(R.id.polar_diagram_view) ) // anchorWatchHandler is initialized when the anchor config UI is available val mockPolarTable = createMockPolarTable() findViewById(R.id.polar_diagram_view).setPolarTable(mockPolarTable) startInstrumentSimulation(mockPolarTable) } // Helper to convert dp to px private fun Int.dpToPx(): Int = (this * resources.displayMetrics.density).toInt() // ... (Keep existing permission and service logic) private fun checkForegroundPermissions() { val fineLocationPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) val coarseLocationPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) if (fineLocationPermission == PackageManager.PERMISSION_GRANTED || coarseLocationPermission == PackageManager.PERMISSION_GRANTED) { startServices() } else { requestPermissionLauncher.launch(arrayOf( Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION )) } } private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> if (permissions[Manifest.permission.ACCESS_FINE_LOCATION] == true) { pendingServiceStart = true } } private fun startServices() { val intent = Intent(this, LocationService::class.java).apply { action = LocationService.ACTION_START_FOREGROUND_SERVICE } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { startForegroundService(intent) } else { startService(intent) } observeDataSources() } private fun setupMap() { mapView = findViewById(R.id.mapView) mapView?.onCreate(null) mapView?.getMapAsync { maplibreMap -> mapHandler = MapHandler(maplibreMap) val style = Style.Builder() .fromUri("https://tiles.openfreemap.org/styles/liberty") .withSource(RasterSource("openseamap-source", TileSet("2.2.0", "https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png").also { it.setMaxZoom(18f) }, 256)) .withLayer(RasterLayer("openseamap-layer", "openseamap-source")) maplibreMap.setStyle(style) { style -> loadedStyleFlow.value = style val anchorBitmap = rasterizeDrawable(R.drawable.ic_anchor) val arrowBitmap = rasterizeDrawable(R.drawable.ic_tidal_arrow) mapHandler?.setupLayers(style, anchorBitmap, arrowBitmap) } } } private fun observeDataSources() { lifecycleScope.launch { LocationService.locationFlow.collect { gpsData -> mapHandler?.centerOnLocation(gpsData.latitude, gpsData.longitude) val sogKnots = gpsData.speedOverGround * 1.94384 viewModel.addGpsPoint(gpsData.latitude, gpsData.longitude, sogKnots, gpsData.courseOverGround.toDouble()) } } lifecycleScope.launch { LocationService.anchorWatchState.collect { state -> safetyFragment.updateAnchorStatus(if (state.isActive) "Active: ${state.watchCircleRadiusMeters}m" else "Inactive") } } lifecycleScope.launch { loadedStyleFlow.filterNotNull() .combine(viewModel.trackPoints) { style, points -> style to points } .collect { (style, points) -> mapHandler?.updateTrackLayer(style, points) } } } private fun startInstrumentSimulation(polarTable: PolarTable) { lifecycleScope.launch { var simulatedTws = 8.0 var simulatedTwa = 40.0 while (true) { val bsp = polarTable.interpolateBsp(simulatedTws, simulatedTwa) instrumentHandler?.updateDisplay( aws = "%.1f".format(Locale.getDefault(), simulatedTws * 1.1), tws = "%.1f".format(Locale.getDefault(), simulatedTws), bsp = "%.1f".format(Locale.getDefault(), bsp), sog = "%.1f".format(Locale.getDefault(), bsp * 0.95), vmg = "%.1f".format(Locale.getDefault(), polarTable.curves.firstOrNull { it.twS == simulatedTws }?.calculateVmg(simulatedTwa, bsp) ?: 0.0), polarPct = "%.0f%%".format(Locale.getDefault(), polarTable.calculatePolarPercentage(simulatedTws, simulatedTwa, bsp)), baro = "1013.2" ) instrumentHandler?.updatePolarDiagram(simulatedTws, simulatedTwa, bsp) simulatedTwa = (simulatedTwa + 0.5).let { if (it > 170) 40.0 else it } delay(1000) } } } private fun rasterizeDrawable(drawableId: Int): Bitmap { val drawable = ContextCompat.getDrawable(this, drawableId)!! val bitmap = Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, Bitmap.Config.ARGB_8888) val canvas = Canvas(bitmap) drawable.setBounds(0, 0, canvas.width, canvas.height) drawable.draw(canvas) return bitmap } private fun createMockPolarTable(): PolarTable { val curves = listOf(6.0, 8.0, 10.0).map { tws -> PolarCurve(tws, listOf(30.0, 45.0, 60.0, 90.0, 120.0, 150.0, 180.0).map { twa -> PolarPoint(twa, tws * (0.4 + twa / 200.0)) }) } return PolarTable(curves) } override fun onStart() { super.onStart(); mapView?.onStart() } override fun onPause() { super.onPause(); mapView?.onPause() } override fun onStop() { super.onStop(); mapView?.onStop() } override fun onDestroy() { super.onDestroy(); mapView?.onDestroy() } override fun onLowMemory() { super.onLowMemory(); mapView?.onLowMemory() } }