diff --git a/app/build.gradle b/app/build.gradle index dffbc140..49e8b7a9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -141,9 +141,12 @@ 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 'mil.nga:mgrs:2.0.0' + implementation 'com.github.MKergall:osmbonuspack:6.9.0' + implementation 'org.osmdroid:osmdroid-wms:6.1.14' + api 'mil.nga.mgrs:mgrs-android:2.2.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/CustomTileSource.kt b/app/src/main/java/com/geeksville/mesh/model/CustomTileSource.kt deleted file mode 100644 index e7b68011..00000000 --- a/app/src/main/java/com/geeksville/mesh/model/CustomTileSource.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.geeksville.mesh.model - -import org.osmdroid.tileprovider.tilesource.ITileSource -import org.osmdroid.tileprovider.tilesource.OnlineTileSourceBase -import org.osmdroid.tileprovider.tilesource.TileSourceFactory -import org.osmdroid.tileprovider.tilesource.TileSourcePolicy -import org.osmdroid.util.MapTileIndex - - -class CustomTileSource { - companion object { - - val ESRI_IMAGERY = object : OnlineTileSourceBase( - "ESRI World Overview", 0, 18, 256, "", arrayOf( - "https://wayback.maptiles.arcgis.com/arcgis/rest/services/World_Imagery/WMTS/1.0.0/default028mm/MapServer/tile/" - ), "Esri, Maxar, Earthstar Geographics, and the GIS User Community" + - "URL\n" + - "View\n", - TileSourcePolicy( - 2, TileSourcePolicy.FLAG_NO_BULK - or TileSourcePolicy.FLAG_NO_PREVENTIVE - or TileSourcePolicy.FLAG_USER_AGENT_MEANINGFUL - or TileSourcePolicy.FLAG_USER_AGENT_NORMALIZED - ) - ) { - override fun getTileURLString(pMapTileIndex: Long): String { - return baseUrl + (MapTileIndex.getZoom(pMapTileIndex) - .toString() + "/" + MapTileIndex.getY(pMapTileIndex) - + "/" + MapTileIndex.getX(pMapTileIndex) - + mImageFilenameEnding) - } - } - val MAPNIK: OnlineTileSourceBase = TileSourceFactory.MAPNIK - val USGS_TOPO: OnlineTileSourceBase = TileSourceFactory.USGS_TOPO - val USGS_SAT: OnlineTileSourceBase = TileSourceFactory.USGS_SAT - val DEFAULT_TILE_SOURCE: OnlineTileSourceBase = TileSourceFactory.DEFAULT_TILE_SOURCE - - /** - * The order in this list must match that in the arrays.xml under map_styles - */ - val mTileSources: List = - listOf(MAPNIK, USGS_TOPO, USGS_SAT, ESRI_IMAGERY) - - - fun getTileSource(aName: String): ITileSource { - for (tileSource: ITileSource in mTileSources) { - if (tileSource.name().equals(aName)) { - return tileSource; - } - } - throw IllegalArgumentException("No such tile source: $aName") - } - } - -} \ No newline at end of file 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 bb8094e5..d79ce445 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 @@ -495,6 +498,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/model/map/CustomOverlayManager.kt b/app/src/main/java/com/geeksville/mesh/model/map/CustomOverlayManager.kt new file mode 100644 index 00000000..74a3bb46 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/model/map/CustomOverlayManager.kt @@ -0,0 +1,44 @@ +package com.geeksville.mesh.model.map + +import android.content.Context +import android.view.MotionEvent +import org.osmdroid.tileprovider.MapTileProviderBase +import org.osmdroid.views.MapView +import org.osmdroid.views.overlay.DefaultOverlayManager +import org.osmdroid.views.overlay.TilesOverlay + + +class CustomOverlayManager +/** + * Default constructor + */ + (tilesOverlay: TilesOverlay?) : DefaultOverlayManager(tilesOverlay) { + /** + * Override event & do nothing + */ + override fun onDoubleTap(e: MotionEvent?, pMapView: MapView?): Boolean { + return true + } + + /** + * Override event & do nothing + */ + override fun onDoubleTapEvent(e: MotionEvent?, pMapView: MapView?): Boolean { + return true + } + + companion object { + /** + * Create MyOverlayManager + */ + fun create(mapView: MapView, context: Context?): CustomOverlayManager { + val mTileProvider: MapTileProviderBase = mapView.tileProvider + val tilesOverlay = TilesOverlay(mTileProvider, context) + mapView.tileProvider + mapView.overlayManager = CustomOverlayManager(tilesOverlay) + //mapView.overlayManager.overlays().add(overlay) + mapView.invalidate() + return CustomOverlayManager(tilesOverlay) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/model/map/CustomTileSource.kt b/app/src/main/java/com/geeksville/mesh/model/map/CustomTileSource.kt new file mode 100644 index 00000000..d9b9e5ee --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/model/map/CustomTileSource.kt @@ -0,0 +1,171 @@ +package com.geeksville.mesh.model.map + +import org.osmdroid.tileprovider.tilesource.ITileSource +import org.osmdroid.tileprovider.tilesource.OnlineTileSourceBase +import org.osmdroid.tileprovider.tilesource.TileSourceFactory +import org.osmdroid.tileprovider.tilesource.TileSourcePolicy +import org.osmdroid.util.MapTileIndex + + +class CustomTileSource { + companion object { + + // Map Server information: https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer + // Arcgis Information: https://www.arcgis.com/home/item.html?id=10df2279f9684e4a9f6a7f08febac2a9 + private val ESRI_IMAGERY = object : OnlineTileSourceBase( + "ESRI World Overview", 0, 18, 256, ".jpg", arrayOf( + "https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/" + ), "Esri, Maxar, Earthstar Geographics, and the GIS User Community", + TileSourcePolicy( + 4, + TileSourcePolicy.FLAG_NO_BULK + or TileSourcePolicy.FLAG_NO_PREVENTIVE + or TileSourcePolicy.FLAG_USER_AGENT_MEANINGFUL + or TileSourcePolicy.FLAG_USER_AGENT_NORMALIZED + ) + ) { + override fun getTileURLString(pMapTileIndex: Long): String { + return baseUrl + (MapTileIndex.getZoom(pMapTileIndex) + .toString() + "/" + MapTileIndex.getY(pMapTileIndex) + + "/" + MapTileIndex.getX(pMapTileIndex) + + mImageFilenameEnding) + } + } + + //Transparent Background + //https://earthlive.maptiles.arcgis.com/arcgis/rest/services/GOES/GOES31D/MapServer/tile/ + private val NOAA_RADAR = object : OnlineTileSourceBase( + "NOAA GOES Radar", + 0, + 18, + 256, + "", + arrayOf( + "https://earthlive.maptiles.arcgis.com/arcgis/rest/services/GOES/GOES31C/MapServer/tile/" + ), + "Dataset Citation: GOES-R Calibration Working Group and GOES-R Series Program, (2017): NOAA GOES-R Series Advanced Baseline Imager (ABI) Level 1b Radiances Band 13. NOAA National Centers for Environmental Information. doi:10.7289/V5BV7DSR", + TileSourcePolicy( + 2, + TileSourcePolicy.FLAG_NO_BULK + or TileSourcePolicy.FLAG_NO_PREVENTIVE + or TileSourcePolicy.FLAG_USER_AGENT_MEANINGFUL + or TileSourcePolicy.FLAG_USER_AGENT_NORMALIZED + ) + ) { + override fun getTileURLString(pMapTileIndex: Long): String { + return baseUrl + (MapTileIndex.getZoom(pMapTileIndex) + .toString() + "/" + MapTileIndex.getY(pMapTileIndex) + + "/" + MapTileIndex.getX(pMapTileIndex) + + mImageFilenameEnding) + } + } + private val USGS_HYDRO_CACHE = object : OnlineTileSourceBase( + "USGS Hydro Cache", + 0, + 18, + 256, + "", + arrayOf( + "https://basemap.nationalmap.gov/arcgis/rest/services/USGSHydroCached/MapServer/tile/" + ), + "USGS", + TileSourcePolicy( + 2, + TileSourcePolicy.FLAG_NO_PREVENTIVE + or TileSourcePolicy.FLAG_USER_AGENT_MEANINGFUL + or TileSourcePolicy.FLAG_USER_AGENT_NORMALIZED + ) + ) { + override fun getTileURLString(pMapTileIndex: Long): String { + return baseUrl + (MapTileIndex.getZoom(pMapTileIndex) + .toString() + "/" + MapTileIndex.getY(pMapTileIndex) + + "/" + MapTileIndex.getX(pMapTileIndex) + + mImageFilenameEnding) + } + } + private val USGS_SHADED_RELIEF = object : OnlineTileSourceBase( + "USGS Shaded Relief Only", + 0, + 18, + 256, + "", + arrayOf( + "https://basemap.nationalmap.gov/arcgis/rest/services/USGSShadedReliefOnly/MapServer/tile/" + ), + "USGS", + TileSourcePolicy( + 2, + TileSourcePolicy.FLAG_NO_PREVENTIVE + or TileSourcePolicy.FLAG_USER_AGENT_MEANINGFUL + or TileSourcePolicy.FLAG_USER_AGENT_NORMALIZED + ) + ) { + override fun getTileURLString(pMapTileIndex: Long): String { + return baseUrl + (MapTileIndex.getZoom(pMapTileIndex) + .toString() + "/" + MapTileIndex.getY(pMapTileIndex) + + "/" + MapTileIndex.getX(pMapTileIndex) + + mImageFilenameEnding) + } + } + + /** + * WMS TILE SERVER + * More research is required to get this to function correctly with overlays + */ + val NOAA_RADAR_WMS = NOAAWmsTileSource( + "Recent Weather Radar", + arrayOf("https://new.nowcoast.noaa.gov/arcgis/services/nowcoast/radar_meteo_imagery_nexrad_time/MapServer/WmsServer?"), + "1", + "1.3.0", + "", + "EPSG%3A3857", + "", + "image/png" + ) + + val NOAA_SATELLITE_RADAR_WMS = NOAAWmsTileSource( + "Weather Satellite Imagery", + arrayOf("https://new.nowcoast.noaa.gov/arcgis/services/nowcoast/sat_meteo_imagery_time/MapServer/WmsServer?"), + "1,5,9,13,17,21,25", + "1.3.0", + "", + "EPSG%3A3857", + "", + "image/png" + ) + + /** + * =============================================================================================== + */ + + private val MAPNIK: OnlineTileSourceBase = TileSourceFactory.MAPNIK + private val USGS_TOPO: OnlineTileSourceBase = TileSourceFactory.USGS_TOPO + private val OPEN_TOPO: OnlineTileSourceBase = TileSourceFactory.OpenTopo + private val USGS_SAT: OnlineTileSourceBase = TileSourceFactory.USGS_SAT + private val SEAMAP: OnlineTileSourceBase = TileSourceFactory.OPEN_SEAMAP + val DEFAULT_TILE_SOURCE: OnlineTileSourceBase = TileSourceFactory.DEFAULT_TILE_SOURCE + + /** + * The order in this list must match that in the arrays.xml under map_styles + */ + val mTileSources: List = + listOf( + MAPNIK, + USGS_TOPO, + OPEN_TOPO, + USGS_SAT, + ESRI_IMAGERY, + ) + + + fun getTileSource(aName: String): ITileSource { + for (tileSource: ITileSource in mTileSources) { + if (tileSource.name().equals(aName)) { + return tileSource; + } + } + throw IllegalArgumentException("No such tile source: $aName") + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/model/map/NOAAWmsTileSource.kt b/app/src/main/java/com/geeksville/mesh/model/map/NOAAWmsTileSource.kt new file mode 100644 index 00000000..434a929e --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/model/map/NOAAWmsTileSource.kt @@ -0,0 +1,146 @@ +package com.geeksville.mesh.model.map + +import android.content.res.Resources +import android.util.Log +import org.osmdroid.api.IMapView +import org.osmdroid.tileprovider.tilesource.OnlineTileSourceBase +import org.osmdroid.util.MapTileIndex +import kotlin.math.atan +import kotlin.math.pow +import kotlin.math.sinh + +open class NOAAWmsTileSource( + aName: String, + aBaseUrl: Array, + layername: String, + version: String, + time: String?, + crs: String, + style: String?, + format: String +) : OnlineTileSourceBase(aName, 0, 22, 256, "png", aBaseUrl) { + + // array indexes for array to hold bounding boxes. + private val MINX = 0 + private val MAXX = 1 + private val MINY = 2 + private val MAXY = 3 + + // Web Mercator n/w corner of the map. + private val TILE_ORIGIN = doubleArrayOf(-20037508.34789244, 20037508.34789244) + + //array indexes for that data + private val ORIG_X = 0 + private val ORIG_Y = 1 // " + + // Size of square world map in meters, using WebMerc projection. + private val MAP_SIZE = 20037508.34789244 * 2 + private var layer = "" + private var version = "1.1.0" + private var crs = "EPSG:3A3857" //used by geo server + private var size = "" + private var format = "" + private var time = "" + private var style: String? = null + private var forceHttps = false + private var forceHttp = false + + init { + Log.i(IMapView.LOGTAG, "WMS support is BETA. Please report any issues") + layer = layername + this.version = version + this.crs = crs + this.style = style + this.format = format + if (time != null) this.time = time + } + +// fun createFrom(endpoint: WMSEndpoint, layer: WMSLayer): WMSTileSource? { +// var srs: String? = "EPSG:900913" +// if (layer.srs.isNotEmpty()) { +// srs = layer.srs[0] +// } +// return if (layer.styles.isEmpty()) { +// WMSTileSource( +// layer.name, arrayOf(endpoint.baseurl), layer.name, +// endpoint.wmsVersion, srs, null, layer.pixelSize +// ) +// } else WMSTileSource( +// layer.name, arrayOf(endpoint.baseurl), layer.name, +// endpoint.wmsVersion, srs, layer.styles[0], layer.pixelSize +// ) +// } + + + private fun tile2lon(x: Int, z: Int): Double { + return x / 2.0.pow(z.toDouble()) * 360.0 - 180 + } + + private fun tile2lat(y: Int, z: Int): Double { + val n = Math.PI - 2.0 * Math.PI * y / 2.0.pow(z.toDouble()) + return Math.toDegrees(atan(sinh(n))) + } + + // Return a web Mercator bounding box given tile x/y indexes and a zoom + // level. + private fun getBoundingBox(x: Int, y: Int, zoom: Int): DoubleArray { + val tileSize = MAP_SIZE / 2.0.pow(zoom.toDouble()) + val minx = TILE_ORIGIN[ORIG_X] + x * tileSize + val maxx = TILE_ORIGIN[ORIG_X] + (x + 1) * tileSize + val miny = TILE_ORIGIN[ORIG_Y] - (y + 1) * tileSize + val maxy = TILE_ORIGIN[ORIG_Y] - y * tileSize + val bbox = DoubleArray(4) + bbox[MINX] = minx + bbox[MINY] = miny + bbox[MAXX] = maxx + bbox[MAXY] = maxy + return bbox + } + + fun isForceHttps(): Boolean { + return forceHttps + } + + fun setForceHttps(forceHttps: Boolean) { + this.forceHttps = forceHttps + } + + fun isForceHttp(): Boolean { + return forceHttp + } + + fun setForceHttp(forceHttp: Boolean) { + this.forceHttp = forceHttp + } + + override fun getTileURLString(pMapTileIndex: Long): String? { + var baseUrl = baseUrl + if (forceHttps) baseUrl = baseUrl.replace("http://", "https://") + if (forceHttp) baseUrl = baseUrl.replace("https://", "http://") + val sb = StringBuilder(baseUrl) + if (!baseUrl.endsWith("&")) + sb.append("service=WMS") + sb.append("&request=GetMap") + sb.append("&version=").append(version) + sb.append("&layers=").append(layer) + if (style != null) sb.append("&styles=").append(style) + sb.append("&format=").append(format) + sb.append("&transparent=true") + sb.append("&height=").append(Resources.getSystem().displayMetrics.heightPixels) + sb.append("&width=").append(Resources.getSystem().displayMetrics.widthPixels) + sb.append("&crs=").append(crs) + sb.append("&bbox=") + val bbox = getBoundingBox( + MapTileIndex.getX(pMapTileIndex), + MapTileIndex.getY(pMapTileIndex), + MapTileIndex.getZoom(pMapTileIndex) + ) + sb.append(bbox[MINX]).append(",") + sb.append(bbox[MINY]).append(",") + sb.append(bbox[MAXX]).append(",") + sb.append(bbox[MAXY]) + + Log.i(IMapView.LOGTAG, sb.toString()) + return sb.toString() + } +} \ No newline at end of file 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 1a583563..4b44917a 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,9 +8,9 @@ import android.graphics.Color import android.graphics.Paint import android.graphics.Rect import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup +import android.util.Log +import android.view.* +import android.widget.* import androidx.core.content.ContextCompat import androidx.fragment.app.activityViewModels import com.geeksville.mesh.BuildConfig @@ -18,45 +19,81 @@ import com.geeksville.mesh.R import com.geeksville.mesh.android.Logging import com.geeksville.mesh.database.entity.Packet import com.geeksville.mesh.databinding.MapViewBinding -import com.geeksville.mesh.model.CustomTileSource import com.geeksville.mesh.model.UIViewModel +import com.geeksville.mesh.model.map.CustomOverlayManager +import com.geeksville.mesh.model.map.CustomTileSource 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.events.MapListener +import org.osmdroid.events.ScrollEvent +import org.osmdroid.events.ZoomEvent +import org.osmdroid.tileprovider.cachemanager.CacheManager +import org.osmdroid.tileprovider.cachemanager.CacheManager.CacheManagerCallback +import org.osmdroid.tileprovider.modules.SqlTileWriter +import org.osmdroid.tileprovider.modules.SqliteArchiveTileWriter import org.osmdroid.tileprovider.tilesource.ITileSource +import org.osmdroid.tileprovider.tilesource.OnlineTileSourceBase +import org.osmdroid.tileprovider.tilesource.TileSourcePolicyException 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 org.osmdroid.views.overlay.gridlines.LatLonGridlineOverlay2 +import java.io.File +import kotlin.math.pow + @AndroidEntryPoint -class MapFragment : ScreenFragment("Map"), Logging { +class MapFragment : ScreenFragment("Map"), Logging, View.OnClickListener { + // UI Elements private lateinit var binding: MapViewBinding private lateinit var map: MapView - private lateinit var mapController: IMapController - private lateinit var mPrefs: SharedPreferences - private val model: UIViewModel by activityViewModels() + private lateinit var downloadBtn: FloatingActionButton + private lateinit var cacheEstimate: TextView + private lateinit var executeJob: Button + private var downloadPrompt: AlertDialog? = null + private var alertDialog: AlertDialog? = null + // constants private val defaultMinZoom = 1.5 + private val defaultMaxZoom = 18.0 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 var wayPoints = listOf() private val nodeLayer = 1 + // Distance of bottom corner to top corner of bounding box + private val zoomLevelLowest = 13.0 // approx 5 miles long + private val zoomLevelMiddle = 12.25 // approx 10 miles long + private val zoomLevelHighest = 11.5 // approx 15 miles long + + private var zoomLevelMin = 0.0 + private var zoomLevelMax = 0.0 + + // Map Elements + private lateinit var mapController: IMapController + private lateinit var mPrefs: SharedPreferences + private lateinit var writer: SqliteArchiveTileWriter + private val model: UIViewModel by activityViewModels() + private lateinit var cacheManager: CacheManager + private lateinit var downloadRegionBoundingBox: BoundingBox + override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { binding = MapViewBinding.inflate(inflater) + downloadBtn = binding.root.findViewById(R.id.downloadButton) + binding.cacheLayout.visibility = View.GONE return binding.root } @@ -69,29 +106,257 @@ class MapFragment : ScreenFragment("Map"), Logging { setupMapProperties() map.setTileSource(loadOnlineTileSourceBase()) + renderDownloadButton() map.let { if (view != null) { mapController = map.controller binding.mapStyleButton.setOnClickListener { chooseMapStyle() } - model.nodeDB.nodes.value?.let { nodes -> + if (binding.cacheLayout.visibility == View.GONE) { + model.nodeDB.nodes.value?.let { nodes -> + onNodesChanged(nodes.values) + drawOverlays() + } + } + } + if (binding.cacheLayout.visibility == View.GONE) { + // Any times nodes change update our map + model.nodeDB.nodes.observe(viewLifecycleOwner) { nodes -> onNodesChanged(nodes.values) drawOverlays() } - } - // Any times nodes change update our map - model.nodeDB.nodes.observe(viewLifecycleOwner) { nodes -> - onNodesChanged(nodes.values) - drawOverlays() - } - model.waypoints.observe(viewLifecycleOwner) { - debug("New waypoints received: ${it.size}") - onWaypointChanged(it.values) - drawOverlays() + model.waypoints.observe(viewLifecycleOwner) { + debug("New waypoints received: ${it.size}") + onWaypointChanged(it.values) + drawOverlays() + } } zoomToNodes(mapController) } + downloadBtn.setOnClickListener(this) + } + + override fun onClick(v: View) { + when (v.id) { + R.id.executeJob -> updateEstimate() + R.id.downloadButton -> showCacheManagerDialog() + R.id.box5miles -> generateBoxOverlay(zoomLevelLowest) + R.id.box10miles -> generateBoxOverlay(zoomLevelMiddle) + R.id.box15miles -> generateBoxOverlay(zoomLevelHighest) + } + } + + + private fun showCacheManagerDialog() { + val alertDialogBuilder = AlertDialog.Builder( + activity + ) + // set title + alertDialogBuilder.setTitle("Offline Manager") + // set dialog message + alertDialogBuilder.setItems( + arrayOf( + "Current Cache size", + "Download Region", + "Clear Downloaded Tiles", + resources.getString(R.string.cancel) + ) + ) { dialog, which -> + when (which) { + 0 -> showCurrentCacheInfo() + 1 -> { + downloadJobAlert() + dialog.dismiss() + } + 2 -> clearCache() + else -> dialog.dismiss() + } + } + // create alert dialog + alertDialog = alertDialogBuilder.create() + + // show it + alertDialog!!.show() + + } + + /** + * Clears active tile source cache + */ + private fun clearCache() { + val b: Boolean = SqlTileWriter().purgeCache() + SqlTileWriter().onDetach() + val title = if (b) "SQL Cache purged" else "SQL Cache purge failed, see logcat for details" + val length = if (b) Toast.LENGTH_SHORT else Toast.LENGTH_LONG + Toast.makeText(activity, title, length).show() + alertDialog!!.dismiss() + } + + + private fun showCurrentCacheInfo() { + Toast.makeText(activity, "Calculating...", Toast.LENGTH_SHORT).show() + cacheManager = CacheManager(map) // Make sure CacheManager has latest from map + 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 . + binding.downloadButton.hide() + binding.mapStyleButton.visibility = View.GONE + binding.cacheLayout.visibility = View.VISIBLE + val builder = AlertDialog.Builder(activity) + binding.box5miles.setOnClickListener(this) + binding.box10miles.setOnClickListener(this) + binding.box15miles.setOnClickListener(this) + cacheEstimate = binding.cacheEstimate + generateBoxOverlay(zoomLevelLowest) + executeJob = binding.executeJob + executeJob.setOnClickListener(this) + binding.cancelDownload.setOnClickListener { + cacheEstimate.text = "" + defaultMapSettings() + + } + builder.setCancelable(true) + } + + /** + * Reset map to default settings & visible buttons + */ + private fun defaultMapSettings() { + binding.downloadButton.show() + binding.mapStyleButton.visibility = View.VISIBLE + binding.cacheLayout.visibility = View.GONE + setupMapProperties() + drawOverlays() + } + + /** + * Creates Box overlay showing what area can be downloaded + */ + private fun generateBoxOverlay(zoomLevel: Double) { + drawOverlays() + map.setMultiTouchControls(false) + zoomLevelMax = zoomLevel // furthest back + zoomLevelMin = + map.tileProvider.tileSource.maximumZoomLevel.toDouble() // furthest in min should be > than max + mapController.setZoom(zoomLevel) + downloadRegionBoundingBox = map.boundingBox + val polygon = Polygon() + polygon.points = Polygon.pointsAsRect(downloadRegionBoundingBox) as MutableList + map.overlayManager.add(polygon) + mapController.setZoom(zoomLevel - 1.0) + cacheManager = CacheManager(map) + val tilecount: Int = + cacheManager.possibleTilesInArea( + downloadRegionBoundingBox, + zoomLevelMax.toInt(), + zoomLevelMin.toInt() + ) + cacheEstimate.text = ("$tilecount tiles") + } + + + /** + * if true, start the job + * if false, just update the dialog box + */ + private fun updateEstimate() { + try { + if (this::downloadRegionBoundingBox.isInitialized) { + val outputName = + Configuration.getInstance().osmdroidBasePath.absolutePath + File.separator + "mainFile.sqlite" // TODO: Accept filename input param from user + writer = SqliteArchiveTileWriter(outputName) + //nesw + if (downloadPrompt != null) { + downloadPrompt!!.dismiss() + downloadPrompt = null + } + try { + cacheManager = + CacheManager(map, writer) // Make sure cacheManager has latest from map + } catch (ex: TileSourcePolicyException) { + Log.d("MapFragment", "Tilesource does not allow archiving: ${ex.message}") + return + } + //this triggers the download + downloadRegion( + downloadRegionBoundingBox, + zoomLevelMax.toInt(), + zoomLevelMin.toInt(), + ) + } + } catch (ex: Exception) { + ex.printStackTrace() + } + } + + private fun downloadRegion(bb: BoundingBox, zoommin: Int, zoommax: Int) { + cacheManager.downloadAreaAsync( + activity, + bb, + zoommin, + zoommax, + object : CacheManagerCallback { + override fun onTaskComplete() { + Toast.makeText(activity, "Download complete!", Toast.LENGTH_LONG) + .show() + writer.onDetach() + defaultMapSettings() + } + + override fun onTaskFailed(errors: Int) { + Toast.makeText( + activity, + "Download complete with $errors errors", + Toast.LENGTH_LONG + ).show() + writer.onDetach() + defaultMapSettings() + } + + 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 + } + }) } private fun chooseMapStyle() { @@ -111,11 +376,20 @@ class MapFragment : ScreenFragment("Map"), Logging { editor.apply() dialog.dismiss() map.setTileSource(loadOnlineTileSourceBase()) + renderDownloadButton() } val dialog = builder.create() dialog.show() } + private fun renderDownloadButton() { + if (!(map.tileProvider.tileSource as OnlineTileSourceBase).tileSourcePolicy.acceptsBulkDownload()) { + downloadBtn.hide() + } else { + downloadBtn.show() + } + } + private fun onWaypointChanged(wayPt: Collection) { /** @@ -166,8 +440,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 @@ -177,9 +450,33 @@ class MapFragment : ScreenFragment("Map"), Logging { nodePositions = getCurrentNodes() } + + /** + * Create LatLong Grid line overlay + * @param enabled: turn on/off gridlines + */ + private fun createLatLongGrid(enabled: Boolean) { + val latLongGridOverlay = LatLonGridlineOverlay2() + latLongGridOverlay.isEnabled = enabled + if (latLongGridOverlay.isEnabled) { + val textPaint = Paint() + textPaint.textSize = 40f + textPaint.color = Color.GRAY + textPaint.isAntiAlias = true + textPaint.isFakeBoldText = true + textPaint.textAlign = Paint.Align.CENTER + latLongGridOverlay.textPaint = textPaint + latLongGridOverlay.setBackgroundColor(Color.TRANSPARENT) + latLongGridOverlay.setLineWidth(3.0f) + latLongGridOverlay.setLineColor(Color.GRAY) + map.overlayManager.add(latLongGridOverlay) + } + } + private fun drawOverlays() { map.overlayManager.overlays().clear() addCopyright() // Copyright is required for certain map sources + createLatLongGrid(false) map.overlayManager.addAll(nodeLayer, nodePositions) map.overlayManager.addAll(nodeLayer, wayPoints) map.invalidate() @@ -189,17 +486,19 @@ 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 copyrightOverlay = CopyrightOverlay(context) - copyrightOverlay.setCopyrightNotice(copyrightNotice) - map.overlays.add(copyrightOverlay) + if (map.tileProvider.tileSource.copyrightNotice != null) { + val copyrightNotice: String = map.tileProvider.tileSource.copyrightNotice + val copyrightOverlay = CopyrightOverlay(context) + copyrightOverlay.setCopyrightNotice(copyrightNotice) + map.overlays.add(copyrightOverlay) + } } private fun setupMapProperties() { if (this::map.isInitialized) { map.setDestroyMode(false) // keeps map instance alive when in the background. map.isVerticalMapRepetitionEnabled = false // disables map repetition + map.overlayManager = CustomOverlayManager.create(map, context) map.setScrollableAreaLimitLatitude( map.overlayManager.tilesOverlay.bounds.actualNorth, map.overlayManager.tilesOverlay.bounds.actualSouth, @@ -209,8 +508,21 @@ class MapFragment : ScreenFragment("Map"), Logging { true // scales the map tiles to the display density of the screen map.minZoomLevel = defaultMinZoom // sets the minimum zoom level (the furthest out you can zoom) + map.maxZoomLevel = defaultMaxZoom map.setMultiTouchControls(true) // Sets gesture controls to true. map.zoomController.setVisibility(CustomZoomButtonsController.Visibility.NEVER) // Disables default +/- button for zooming + map.addMapListener(object : MapListener { + override fun onScroll(event: ScrollEvent): Boolean { + if (binding.cacheLayout.visibility == View.VISIBLE) { + generateBoxOverlay(zoomLevelMax) + } + return true + } + + override fun onZoom(event: ZoomEvent): Boolean { + return false + } + }) } } @@ -224,8 +536,7 @@ class MapFragment : ScreenFragment("Map"), Logging { nodesWithPosition.forEach { points.add( GeoPoint( - it.position!!.latitude, - it.position!!.longitude + it.position!!.latitude, it.position!!.longitude ) ) } @@ -249,6 +560,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() } diff --git a/app/src/main/res/layout/map_view.xml b/app/src/main/res/layout/map_view.xml index 479f75d3..85597ab6 100644 --- a/app/src/main/res/layout/map_view.xml +++ b/app/src/main/res/layout/map_view.xml @@ -24,16 +24,99 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> + + + + + + +