package mx.trackermap.TrackerMap.android.map import android.graphics.Bitmap import android.graphics.Color import android.graphics.Typeface import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat import androidx.core.graphics.drawable.toBitmap import com.mousebird.maply.* import com.soywiz.krypto.md5 import mil.nga.sf.Polygon import mil.nga.sf.util.SFException import mil.nga.sf.wkt.GeometryReader import java.io.File import mx.trackermap.TrackerMap.android.R import mx.trackermap.TrackerMap.android.shared.MarkerTransformations import mx.trackermap.TrackerMap.client.models.Geofence import mx.trackermap.TrackerMap.client.models.MapLayer import mx.trackermap.TrackerMap.utils.MapCalculus import mx.trackermap.TrackerMap.utils.MarkerType typealias SetupCallback = () -> Unit typealias MarkerCallback = (Int?) -> Unit open class MapFragment : GlobeMapFragment() { private var loader: QuadImageLoader? = null data class Marker( val id: Int, val name: String, val latitude: Double, val longitude: Double, val type: MarkerType = MarkerType.DEFAULT ) var hasStarted: Boolean = false val setupCallbacks = mutableListOf() var markerCallback: MarkerCallback? = null private val objects = mutableListOf() private val geofenceObjects = mutableListOf() private var tileInfo: TileInfoNew? = null override fun chooseDisplayType(): MapDisplayType { return MapDisplayType.Map } override fun preControlCreated() { super.preControlCreated() mapSettings.clearColor = Color.WHITE } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { super.onCreateView(inflater, container, savedInstanceState) return baseControl.contentView!! } override fun controlHasStarted() { Log.d("MapFragment", "controlHasStarted") if (tileInfo == null) { val layer = MapLayer.defaultLayer // Load default map layer val info = RemoteTileInfoNew(layer.url, layer.minZoom, layer.maxZoom) tileInfo = tileInfoSetCacheDir(layer.url, info) tileInfo?.let { setZoomLimits(layer.minZoom, layer.maxZoom) } } val params = SamplingParams() params.coordSystem = SphericalMercatorCoordSystem() params.coverPoles = true params.edgeMatching = true params.minZoom = tileInfo!!.minZoom params.maxZoom = tileInfo!!.maxZoom params.singleLevel = true params.maxTiles = 25 loader = QuadImageLoader(params, tileInfo, baseControl) loader?.setImageFormat(RenderController.ImageFormat.MaplyImageUShort565) val latitude = 23.191 * Math.PI / 180 val longitude = -100.36 * Math.PI / 180 mapControl.setPositionGeo(longitude, latitude, 0.4) hasStarted = true setupCallbacks.forEach { it() } } override fun onStop() { super.onStop() hasStarted = false } override fun mapDidStopMoving( mapControl: MapController?, corners: Array?, userMotion: Boolean ) { super.mapDidStopMoving(mapControl, corners, userMotion) Log.d("MapFragment", "Height: %7.7f".format(mapControl?.height)) } override fun userDidSelect( mapControl: MapController?, selObjs: Array?, loc: Point2d?, screenLoc: Point2d? ) { super.userDidSelect(mapControl, selObjs, loc, screenLoc) selObjs?.forEach { selectedObject -> if (selectedObject.selObj is ScreenMarker) { val screenMarker = selectedObject.selObj as ScreenMarker val markerId = screenMarker.userObject as Int Log.d("MapFragment", "Selected marker with id: $markerId") markerCallback?.let { it(markerId) } } } } override fun userDidTap(mapControl: MapController?, loc: Point2d?, screenLoc: Point2d?) { super.userDidTap(mapControl, loc, screenLoc) markerCallback?.let { it(null) } } private fun clear(geofences: Boolean = false) { if (mapControl == null) { return } if (geofences) { mapControl.removeObjects( geofenceObjects, ThreadMode.ThreadAny ) geofenceObjects.clear() } else { mapControl.removeObjects( objects, ThreadMode.ThreadAny ) objects.clear() } } fun display(markers: Array, isReport: Boolean, center: Boolean = true) { Log.d("MapFragment", "Displaying markers") clear() val points = markers.map { marker -> Point2d.FromDegrees(marker.longitude, marker.latitude) }.toTypedArray() val fontSize = context?.resources?.getDimensionPixelSize(R.dimen.marker_label_text_size) val colorReport = ContextCompat.getColor(context!!, R.color.colorReport) val colorLabel = ContextCompat.getColor(context!!, R.color.colorMarkerLabel) val colorLabelOutline = ContextCompat.getColor(context!!, R.color.colorMarkerLabelOutline) val vectorWidth = context?.resources?.getDimensionPixelSize(R.dimen.report_label_width)?.toFloat() val vectorInfo = VectorInfo() vectorInfo.setColor(colorReport) vectorInfo.setLineWidth(vectorWidth ?: 20f) val labelInfo = LabelInfo() labelInfo.typeface = Typeface.DEFAULT_BOLD labelInfo.textColor = colorLabel labelInfo.outlineColor = colorLabelOutline labelInfo.outlineSize = 3.0f fontSize?.let { labelInfo.fontSize = it.toFloat() } /* Draw markers for positions */ val screenMarkers = markers.mapIndexed { i, marker -> val screenMarker = ScreenMarker() screenMarker.loc = Point2d.FromDegrees(marker.longitude, marker.latitude) screenMarker.image = if (isReport) { // For reports, position, start and end, icons must be different when (i) { 0 -> getIcon(MarkerType.REPORT_START) markers.size - 1 -> getIcon(MarkerType.REPORT_END) else -> getIcon(MarkerType.REPORT_POSITION) } } else getIcon(marker.type) screenMarker.size = if (isReport) { // For reports, position, start and end, size must be different when (i) { 0 -> Point2d(144.0, 144.0) markers.size - 1 -> Point2d(144.0, 144.0) else -> Point2d(82.0, 82.0) } } else Point2d(144.0, 144.0) screenMarker.userObject = marker.id screenMarker.selectable = true screenMarker } objects.add(mapControl.addScreenMarkers( screenMarkers, MarkerInfo(), ThreadMode.ThreadAny )) /* Add labels for markers */ if (!isReport && markers.isNotEmpty()) { val screenLabels = markers.map { marker -> val label = ScreenLabel() label.layoutImportance = Float.MAX_VALUE label.text = if (marker.name.length >= 20) { marker.name.substring(0 until 20) + "..." } else marker.name label.loc = Point2d.FromDegrees(marker.longitude, marker.latitude) label.offset = Point2d(0.0, 92.0) label } objects.add(mapControl.addScreenLabels( screenLabels, labelInfo, ThreadMode.ThreadAny )) } /* Draw polyline for report */ if (isReport && markers.isNotEmpty()) { val vector = VectorObject() vector.addAreal(points) objects.add(mapControl.addVector( vector, vectorInfo, ThreadMode.ThreadAny )) } // Center map to bounds if (center && markers.isNotEmpty()) { val mbr = Mbr() points.forEach { mbr.addPoint(it) } mbr.expandByFraction(0.1) mapControl?.addPostSurfaceRunnable { mapControl?.let { val zoom = it.findHeightToViewBounds(mbr, mbr.middle()) .coerceAtLeast(mapControl.zoomLimitMin) it.setPositionGeo(mbr.middle(), zoom) } } } } fun displayGeofences(geofences: Array) { Log.d("MapFragment", "Displaying geofences") clear(true) val fontSize = context?.resources?.getDimensionPixelSize(R.dimen.marker_label_text_size) val colorFill = ContextCompat.getColor(context!!, R.color.colorGeofence) val colorLabel = ContextCompat.getColor(context!!, R.color.colorGeofenceLabel) val colorLabelOutline = ContextCompat.getColor(context!!, R.color.colorMarkerLabelOutline) val vectorWidth = context?.resources?.getDimensionPixelSize(R.dimen.geofence_label_width)?.toFloat() val vectorInfo = VectorInfo() vectorInfo.setColor(colorFill) vectorInfo.setLineWidth(vectorWidth ?: 4f) val labelInfo = LabelInfo() labelInfo.typeface = Typeface.DEFAULT_BOLD labelInfo.textColor = colorLabel labelInfo.outlineColor = colorLabelOutline labelInfo.outlineSize = 3.0f fontSize?.let { labelInfo.fontSize = it.toFloat() } val shapes = mutableListOf() val vectors = mutableListOf() val labels = mutableListOf() geofences.forEach { geofence -> geofence.area?.let { area -> try { val geometry = GeometryReader.readGeometry(area) if (!geometry.isEmpty) { when (geometry) { is Polygon -> { val vector = VectorObject() vector.addAreal(geometry.exteriorRing.points.map { Point2d.FromDegrees(it.y, it.x) }.toTypedArray()) vectors.add(vector) val label = ScreenLabel() label.text = geofence.name label.loc = Point2d.FromDegrees(geometry.centroid.y, geometry.centroid.x) label.layoutImportance = Float.MAX_VALUE labels.add(label) } } } } catch (e: SFException) {} } } geofenceObjects.add(mapControl.addShapes( shapes, ShapeInfo(), ThreadMode.ThreadAny )) geofenceObjects.add(mapControl.addVectors( vectors, vectorInfo, ThreadMode.ThreadAny )) geofenceObjects.add(mapControl.addScreenLabels( labels, labelInfo, ThreadMode.ThreadAny )) } fun focusOn( latitude: Double, longitude: Double, height: Double = 0.00001, animated: Boolean = true ) { val lat = latitude * Math.PI / 180 val lon = longitude * Math.PI / 180 // Ensure height is equal or higher than bottom limit val z = height.coerceAtLeast(mapControl.zoomLimitMin) if (animated) { mapControl.animatePositionGeo(lon, lat, z, 0.2) } else { mapControl.setPositionGeo(lon, lat, z) } } fun zoomIn() { val pos = mapControl.positionGeo.toPoint2d().toDegrees() val zoom = mapControl.currentMapScale() / 2 focusOn(pos.y, pos.x, mapControl.heightForMapScale(zoom)) } fun zoomOut() { val pos = mapControl.positionGeo.toPoint2d().toDegrees() val zoom = mapControl.currentMapScale() * 2 focusOn(pos.y, pos.x, mapControl.heightForMapScale(zoom)) } private fun tileInfoSetCacheDir(url: String, tileInfo: TileInfoNew): TileInfoNew? { return context?.let { val cacheDirName = url.toByteArray(Charsets.UTF_8).md5().hex val cacheDirMap = File(it.cacheDir, cacheDirName) cacheDirMap.mkdir() Log.d("MapFragment", "Cache dir for $url = ${cacheDirMap.absolutePath}") (tileInfo as? RemoteTileInfoNew)?.cacheDir = cacheDirMap tileInfo } } fun updateLayer(layer: MapLayer.Type) { MapLayer.layers[layer]?.let { val tileInfo = RemoteTileInfoNew(it.url, it.minZoom, it.maxZoom) this.tileInfo = tileInfoSetCacheDir(it.url, tileInfo) this.tileInfo?.let { t -> loader?.changeTileInfo(t) setZoomLimits(tileInfo.minZoom, tileInfo.maxZoom) } } } private fun setZoomLimits(minZoom: Int, maxZoom: Int) { mapControl?.let { it.setZoomLimits( it.heightForMapScale( MapCalculus.zoomLevelToScale(maxZoom) ?: MapCalculus.zoomLevelToScale(21)!! ), it.heightForMapScale( MapCalculus.zoomLevelToScale(minZoom) ?: MapCalculus.zoomLevelToScale(1)!! ) ) } } private fun getIcon(markerType: MarkerType): Bitmap { return ResourcesCompat.getDrawable( activity!!.resources, MarkerTransformations.markerTypeToResourceId(markerType), activity!!.theme)!!.toBitmap(144, 144) } }