diff --git a/app/build.gradle b/app/build.gradle index 4314d903..a9e2201b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -140,9 +140,11 @@ dependencies { kapt "androidx.room:room-compiler:$room_version" kapt "com.google.dagger:hilt-compiler:$hilt_version" - //OSMAND + //OSMDROID, mgrs, implementation 'org.osmdroid:osmdroid-android:6.1.14' + implementation 'com.github.MKergall:osmbonuspack:6.9.0' implementation 'mil.nga:mgrs:2.0.0' + implementation 'org.osmdroid:osmdroid-geopackage:6.1.14' // optional - Kotlin Extensions and Coroutines support for Room implementation "androidx.room:room-ktx:$room_version" diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index 9837b697..60730594 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -37,6 +37,9 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.osmdroid.bonuspack.kml.KmlDocument +import org.osmdroid.views.MapView +import org.osmdroid.views.overlay.FolderOverlay import java.io.BufferedWriter import java.io.FileNotFoundException import java.io.FileWriter @@ -480,6 +483,31 @@ class UIViewModel @Inject constructor( } } + + fun parseUrl(url: String, map: MapView) { + viewModelScope.launch(Dispatchers.IO) { + parseIt(url, map) + } + } + + // For Future Use + // model.parseUrl( + // "https://www.google.com/maps/d/kml?forcekml=1&mid=1FmqWhZG3PG3dY92x9yf2RlREcK7kMZs&lid=-ivSjBCePsM", + // map + // ) + private fun parseIt(url: String, map: MapView) { + val kmlDoc = KmlDocument() + try { + kmlDoc.parseKMLUrl(url) + val kmlOverlay = kmlDoc.mKmlRoot.buildOverlay(map, null, null, kmlDoc) as FolderOverlay + kmlDoc.mKmlRoot.mItems + kmlDoc.mKmlRoot.mName + map.overlayManager.overlays().add(kmlOverlay) + } catch (ex: Exception) { + debug("Failed to download .kml $ex") + } + } + fun addQuickChatAction(name: String, value: String, mode: QuickChatAction.Mode) { viewModelScope.launch(Dispatchers.Main) { val action = QuickChatAction(0, name, value, mode, _quickChatActions.value.size) diff --git a/app/src/main/java/com/geeksville/mesh/ui/MapFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/MapFragment.kt index eeb84946..9674761d 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/MapFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/MapFragment.kt @@ -1,5 +1,6 @@ package com.geeksville.mesh.ui +import android.app.AlertDialog import android.content.Context import android.content.SharedPreferences import android.graphics.Canvas @@ -7,33 +8,41 @@ import android.graphics.Color import android.graphics.Paint import android.graphics.Rect import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.* +import android.widget.SeekBar.OnSeekBarChangeListener import androidx.core.content.ContextCompat import androidx.fragment.app.activityViewModels -import com.geeksville.mesh.android.Logging import com.geeksville.mesh.BuildConfig import com.geeksville.mesh.NodeInfo import com.geeksville.mesh.R +import com.geeksville.mesh.android.Logging import com.geeksville.mesh.databinding.MapViewBinding import com.geeksville.mesh.model.CustomTileSource import com.geeksville.mesh.model.UIViewModel import com.geeksville.mesh.util.formatAgo import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.floatingactionbutton.FloatingActionButton import dagger.hilt.android.AndroidEntryPoint import org.osmdroid.api.IMapController import org.osmdroid.config.Configuration +import org.osmdroid.tileprovider.cachemanager.CacheManager import org.osmdroid.tileprovider.tilesource.ITileSource import org.osmdroid.util.BoundingBox import org.osmdroid.util.GeoPoint import org.osmdroid.views.CustomZoomButtonsController import org.osmdroid.views.MapView -import org.osmdroid.views.overlay.CopyrightOverlay -import org.osmdroid.views.overlay.Marker +import org.osmdroid.views.overlay.* +import kotlin.math.pow + @AndroidEntryPoint -class MapFragment : ScreenFragment("Map"), Logging { +class MapFragment : ScreenFragment("Map"), Logging, View.OnClickListener, OnSeekBarChangeListener, + TextWatcher { private lateinit var binding: MapViewBinding private lateinit var map: MapView @@ -41,20 +50,38 @@ class MapFragment : ScreenFragment("Map"), Logging { private lateinit var mPrefs: SharedPreferences private val model: UIViewModel by activityViewModels() + private lateinit var cacheManager: CacheManager + private lateinit var btnCache: FloatingActionButton + + private lateinit var cacheNorth: EditText + private lateinit var cacheSouth: EditText + private lateinit var cacheEast: EditText + private lateinit var cacheWest: EditText + + private lateinit var cacheEstimate: TextView + + private lateinit var zoomMin: SeekBar + private lateinit var zoomMax: SeekBar + + private var downloadPrompt: AlertDialog? = null + private var alertDialog: AlertDialog? = null + private lateinit var executeJob: Button + + private val defaultMinZoom = 1.5 private val nodeZoomLevel = 8.5 private val defaultZoomSpeed = 3000L - private val prefsName = "org.andnav.osm.prefs" + private val prefsName = "org.geeksville.osm.prefs" private val mapStyleId = "map_style_id" private var nodePositions = listOf() private val nodeLayer = 1 override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { binding = MapViewBinding.inflate(inflater) + btnCache = binding.root.findViewById(R.id.downloadButton) return binding.root } @@ -89,6 +116,191 @@ class MapFragment : ScreenFragment("Map"), Logging { } zoomToNodes(mapController) } + btnCache.setOnClickListener(this) + } + + override fun onClick(v: View) { + when (v.id) { + R.id.executeJob -> updateEstimate(true) + R.id.downloadButton -> showCacheManagerDialog() + } + } + + + private fun showCacheManagerDialog() { + val alertDialogBuilder = AlertDialog.Builder( + activity + ) + // set title + alertDialogBuilder.setTitle("Cache Manager") + //.setMessage(R.string.cache_manager_description); + + // set dialog message + alertDialogBuilder.setItems( + arrayOf( + "Cache current size", + "Cache Download", + resources.getString(R.string.cancel) + ) + ) { dialog, which -> + when (which) { + 0 -> showCurrentCacheInfo() + 1 -> { + downloadJobAlert() + dialog.dismiss() + } + else -> dialog.dismiss() + } + } + + + // create alert dialog + alertDialog = alertDialogBuilder.create() + + // show it + alertDialog!!.show() + + } + + + private fun showCurrentCacheInfo() { + Toast.makeText(activity, "Calculating...", Toast.LENGTH_SHORT).show() + Thread { + val alertDialogBuilder = AlertDialog.Builder( + activity + ) + + + // set title + alertDialogBuilder.setTitle("Cache Manager") + .setMessage( + """ + Cache Capacity (mb): ${cacheManager.cacheCapacity() * 2.0.pow(-20.0)} + Cache Usage (mb): ${cacheManager.currentCacheUsage() * 2.0.pow(-20.0)} + """.trimIndent() + ) + + // set dialog message + alertDialogBuilder.setItems( + arrayOf( + resources.getString(R.string.cancel) + ) + ) { dialog, _ -> dialog.dismiss() } + activity!!.runOnUiThread { // show it + // create alert dialog + val alertDialog = alertDialogBuilder.create() + alertDialog.show() + } + }.start() + } + + + private fun downloadJobAlert() { + //prompt for input params + val builder = AlertDialog.Builder(activity) + val view = View.inflate(activity, R.layout.cache_mgr_input, null) + val boundingBox: BoundingBox = map.boundingBox + zoomMax = view.findViewById(R.id.slider_zoom_max) + zoomMax.max = map.maxZoomLevel.toInt() + zoomMax.setOnSeekBarChangeListener(this) + zoomMin = view.findViewById(R.id.slider_zoom_min) + zoomMin.max = map.maxZoomLevel.toInt() + zoomMin.progress = map.minZoomLevel.toInt() + zoomMin.setOnSeekBarChangeListener(this) + cacheEast = view.findViewById(R.id.cache_east) + cacheEast.setText(boundingBox.lonEast.toString() + "") + cacheNorth = view.findViewById(R.id.cache_north) + cacheNorth.setText(boundingBox.latNorth.toString() + "") + cacheSouth = view.findViewById(R.id.cache_south) + cacheSouth.setText(boundingBox.latSouth.toString() + "") + cacheWest = view.findViewById(R.id.cache_west) + cacheWest.setText(boundingBox.lonWest.toString() + "") + cacheEstimate = view.findViewById(R.id.cache_estimate) + + //change listeners for both validation and to trigger the download estimation + cacheEast.addTextChangedListener(this) + cacheNorth.addTextChangedListener(this) + cacheSouth.addTextChangedListener(this) + cacheWest.addTextChangedListener(this) + executeJob = view.findViewById(R.id.executeJob) + executeJob.setOnClickListener { + builder.setOnCancelListener { + cacheEast.text = null + cacheSouth.text = null + cacheEstimate.text = "" + cacheNorth.text = null + cacheWest.text = null + zoomMin.progress = 0 + zoomMax.progress = 0 + } + } + builder.setView(view) + builder.setCancelable(true) + downloadPrompt = builder.create() + downloadPrompt!!.show() + } + + /** + * if true, start the job + * if false, just update the dialog box + */ + private fun updateEstimate(startJob: Boolean) { + try { + if (cacheWest.text != null && cacheNorth.text != null && cacheSouth.text != null && zoomMax.progress != 0 && zoomMin.progress != 0) { + val n: Double = cacheNorth.text.toString().toDouble() + val s: Double = cacheSouth.text.toString().toDouble() + val e: Double = cacheEast.text.toString().toDouble() + val w: Double = cacheWest.text.toString().toDouble() + val zoommin: Int = zoomMin.progress + val zoommax: Int = zoomMax.progress + //nesw + val bb = BoundingBox(n, e, s, w) + val tilecount: Int = cacheManager.possibleTilesInArea(bb, zoommin, zoommax) + cacheEstimate.text = ("$tilecount tiles") + if (startJob) { + if (downloadPrompt != null) { + downloadPrompt!!.dismiss() + downloadPrompt = null + } + + //this triggers the download + cacheManager.downloadAreaAsync(activity, + bb, + zoommin, + zoommax, + object : CacheManager.CacheManagerCallback { + override fun onTaskComplete() { + Toast.makeText(activity, "Download complete!", Toast.LENGTH_LONG) + .show() + } + + override fun onTaskFailed(errors: Int) { + Toast.makeText( + activity, + "Download complete with $errors errors", + Toast.LENGTH_LONG + ).show() + } + + override fun updateProgress( + progress: Int, currentZoomLevel: Int, zoomMin: Int, zoomMax: Int + ) { + //NOOP since we are using the build in UI + } + + override fun downloadStarted() { + //NOOP since we are using the build in UI + } + + override fun setPossibleTilesInArea(total: Int) { + //NOOP since we are using the build in UI + } + }) + } + } + } catch (ex: Exception) { + ex.printStackTrace() + } } private fun chooseMapStyle() { @@ -138,8 +350,7 @@ class MapFragment : ScreenFragment("Map"), Logging { marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM) marker.position = GeoPoint(p.latitude, p.longitude) marker.icon = ContextCompat.getDrawable( - requireActivity(), - R.drawable.ic_baseline_location_on_24 + requireActivity(), R.drawable.ic_baseline_location_on_24 ) } marker @@ -159,8 +370,7 @@ class MapFragment : ScreenFragment("Map"), Logging { * Adds copyright to map depending on what source is showing */ private fun addCopyright() { - val copyrightNotice: String = - map.tileProvider.tileSource.copyrightNotice + val copyrightNotice: String = map.tileProvider.tileSource.copyrightNotice val copyrightOverlay = CopyrightOverlay(context) copyrightOverlay.setCopyrightNotice(copyrightNotice) map.overlays.add(copyrightOverlay) @@ -168,6 +378,7 @@ class MapFragment : ScreenFragment("Map"), Logging { private fun setupMapProperties() { if (this::map.isInitialized) { + cacheManager = CacheManager(map) map.setDestroyMode(false) // keeps map instance alive when in the background. map.isVerticalMapRepetitionEnabled = false // disables map repetition map.setScrollableAreaLimitLatitude( @@ -194,8 +405,7 @@ class MapFragment : ScreenFragment("Map"), Logging { nodesWithPosition.forEach { points.add( GeoPoint( - it.position!!.latitude, - it.position!!.longitude + it.position!!.latitude, it.position!!.longitude ) ) } @@ -219,6 +429,12 @@ class MapFragment : ScreenFragment("Map"), Logging { override fun onPause() { map.onPause() + if (alertDialog != null && alertDialog!!.isShowing) { + alertDialog!!.dismiss() + } + if (downloadPrompt != null && downloadPrompt!!.isShowing) { + downloadPrompt!!.dismiss() + } super.onPause() } @@ -270,6 +486,26 @@ class MapFragment : ScreenFragment("Map"), Logging { c.drawText(mLabel, (p.x - 0f), (p.y - 110f), textPaint) } } + + override fun onProgressChanged(p0: SeekBar?, p1: Int, p2: Boolean) { + updateEstimate(false) + } + + override fun onStartTrackingTouch(p0: SeekBar?) { + } + + override fun onStopTrackingTouch(p0: SeekBar?) { + } + + override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) { + } + + override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) { + updateEstimate(false) + } + + override fun afterTextChanged(p0: Editable?) { + } } diff --git a/app/src/main/res/layout/cache_mgr_input.xml b/app/src/main/res/layout/cache_mgr_input.xml new file mode 100644 index 00000000..ba4932fb --- /dev/null +++ b/app/src/main/res/layout/cache_mgr_input.xml @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +