SDPTeam15/PolyEvents

View on GitHub
app/src/main/java/com/github/sdpteam15/polyevents/model/map/ZoneAreaMapHelper.kt

Summary

Maintainability
A
1 hr
Test Coverage
B
89%
package com.github.sdpteam15.polyevents.model.map

import android.content.Context
import android.graphics.Color
import com.github.sdpteam15.polyevents.R
import com.github.sdpteam15.polyevents.model.entity.Zone
import com.github.sdpteam15.polyevents.model.map.GoogleMapHelper.dpToPixelsFloat
import com.github.sdpteam15.polyevents.model.map.GoogleMapMode.DEFAULT_ZONE_STROKE_COLOR
import com.github.sdpteam15.polyevents.model.map.GoogleMapMode.colorAreas
import com.github.sdpteam15.polyevents.view.activity.admin.ZoneManagementActivity
import com.github.sdpteam15.polyevents.view.fragments.MapsFragment
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.Marker
import com.google.android.gms.maps.model.Polygon
import com.google.android.gms.maps.model.PolygonOptions
import kotlin.math.pow

/**
 * Helper object that handles the areas
 */
object ZoneAreaMapHelper {
    private const val INDEX_ROTATION_MARKER = 3
    private const val AREA_WIDTH_DP = 4
    var editingZone: String? = null
    var zone: Zone? = null
    var editMode = false
    var deleteMode = false
    val areasPoints: MutableMap<Int, Triple<String, Marker, Polygon>> = mutableMapOf()
    val zonesToArea: MutableMap<String, Pair<Zone?, MutableList<Int>>> = mutableMapOf()
    val waitingZones: MutableList<Zone> = mutableListOf()

    /**
     * Temporary variables when adding and editing an area
     * Markers are to access markers
     * Positions are to remember where the marker was before being moved
     * List of LatLng is to update de area after performing a modification
     * */
    var tempPoly: Polygon? = null
    var tempLatLng: MutableList<LatLng?> = ArrayList()
    var moveRightMarker: Marker? = null
    var moveDownMarker: Marker? = null
    var moveDiagMarker: Marker? = null
    var moveMarker: Marker? = null
    var rotationMarker: Marker? = null
    var moveRightPos: LatLng? = null
    var moveDownPos: LatLng? = null
    var moveDiagPos: LatLng? = null
    var rotationPos: LatLng? = null
    var movePos: LatLng? = null

    var tempTitle: String? = null
    var modifyingArea: Int = -1
    val tempValues: MutableMap<Int, Pair<String, LatLng>> = mutableMapOf()

    /**
     * Import zone from database
     * @param zone zone to import
     */
    fun importNewZone(context: Context?, zone: Zone, drawingMod: Boolean) {
        if (GoogleMapHelper.map == null) {
            waitingZones.add(zone)
            return
        }
        val temp = editingZone
        editingZone = zone.zoneId

        if (zonesToArea.containsKey(zone.zoneId)) {
            removeZoneAreas(zone.zoneId!!)
        }

        zonesToArea[editingZone ?: "zone ${GoogleMapHelper.uidZone++}"] =
            Pair(zone, mutableListOf())

        if (drawingMod)
            for (area in zone.getDrawingPolygons()) {

                addArea(context, GoogleMapHelper.uidArea++, area, zone.zoneName)
            }
        else
            for (area in zone.getZoneCoordinates()) {
                addArea(context, GoogleMapHelper.uidArea++, Pair(area, null), zone.zoneName)
            }
        editingZone = temp
    }

    /**
     * Helper method to add a area to the map and generate an invisible marker in its center to display the area infos
     * @param id id of the area
     * @param coords coordinates of the area (polygons)
     * @param name name of the area
     */
    fun addArea(
        context: Context?,
        id: Int,
        coords: Pair<List<LatLng>, List<List<LatLng>>?>,
        name: String?
    ) {
        if (coords.first.isNotEmpty()) {
            val poly = PolygonOptions()
            poly.addAll(coords.first)
            if(context != null)
                poly.strokeWidth(AREA_WIDTH_DP.dpToPixelsFloat(context))

            if (coords.second != null)
                for (hole in coords.second!!)
                    if (hole.isNotEmpty())
                        poly.addHole(hole)

            poly.clickable(true).strokeColor(GoogleMapMode.DEFAULT_ZONE_STROKE_COLOR)

            val polygon = GoogleMapHelper.map!!.addPolygon(poly)

            var list = coords.first
            if (list.first() == list.last())
                list = coords.first.dropLast(1)

            val anchor = IconAnchor(0f, 0f)
            val bound = IconBound(0, 0, 0, 0)
            val dimension = IconDimension(1, 1)

            val center = LatLngOperator.mean(list)
            val marker = GoogleMapHelper.map!!.addMarker(
                GoogleMapHelperFunctions.newMarker(
                    context,
                    center,
                    anchor,
                    null,
                    name,
                    false,
                    R.drawable.ic_location,
                    bound,
                    dimension
                )
            )

            //.tag of these objects are not mockable
            if (context != null) {
                polygon.tag = id
                marker.tag = editingZone
            }
            areasPoints[id] = Triple(editingZone!!, marker, polygon)
            if (!zonesToArea[editingZone!!]!!.second.contains(id)) {
                zonesToArea[editingZone!!]!!.second.add(id)
            }
        }
    }

    /**
     * Clears the temporary variables to have a clean start for editing the area
     */
    fun createNewArea(context: Context?) {
        //If in delete mode, deactivate delete mode
        if (deleteMode)
            deleteMode(context)
        //If in edit mode, deactivate edit mode
        if (editMode)
            editMode(context)
        clearTemp()
        setupEditZone(context, GoogleMapHelper.map!!.cameraPosition!!.target)
    }

    /**
     * Adds an area in the map
     */
    fun saveNewArea(context: Context?) {
        if (tempPoly != null) {
            val name: String
            val id: Int
            if (tempTitle != null) {
                name = tempTitle!!
                id = modifyingArea
            } else {
                id = GoogleMapHelper.uidArea++
                name = "Area $id"

            }
            addArea(context, id, Pair(tempPoly!!.points, null), name)
            colorAreas(
                editingZone!!,
                GoogleMapMode.EDITED_ZONE_STROKE_COLOR
            )
        }
        clearTemp()
    }

    /**
     * Activate/Deactivate the delete mode
     */
    fun deleteMode(context: Context?) {
        //If in edit mode, deactivate edit mode
        if (editMode)
            editMode(context)

        //If a polygon was being created, then removes it, else changes the deletion mode
        if (tempPoly != null) {
            tempPoly!!.remove()
            clearTemp()
        } else {
            deleteMode = !deleteMode
            MapsFragment.instance?.switchIconDeleteArea()

            //If already in delete mode, restores the map else prepare for deletion
            if (deleteMode) {
                for (a in areasPoints) {
                    tempValues[a.key] = Pair(a.value.second.title, a.value.second.position)
                    a.value.second.remove()
                }
                colorAreas(editingZone!!, Color.RED)
            } else {
                colorAreas(editingZone!!, DEFAULT_ZONE_STROKE_COLOR)
                restoreMarkers(context)
            }
        }
    }

    /**
     * Saves an area
     */
    fun saveArea() {
        editMode = false
        clearTemp()
        //TODO : Save the areas in the map
        //val location = GoogleMapHelper.areasToFormattedStringLocations(from = startId)
        val location =
            GoogleMapHelperFunctions.zoneAreasToFormattedStringLocation(editingZone!!)
        zone!!.location = location
        ZoneManagementActivity.zoneObservable.postValue(
            Zone(
                zoneName = zone!!.zoneName,
                zoneId = zone!!.zoneId,
                location = location,
                description = zone!!.description
            )
        )
    }

    /**
     * Clears the edition markers and temporary data for area edition
     */
    fun clearTemp() {
        tempPoly?.remove()
        tempLatLng.clear()
        moveRightMarker?.remove()
        moveDownMarker?.remove()
        moveDiagMarker?.remove()
        moveMarker?.remove()
        rotationMarker?.remove()
        tempPoly = null
        moveRightMarker = null
        moveDownMarker = null
        moveDiagMarker = null
        moveMarker = null
        rotationMarker = null

        tempPoly = null
        moveRightPos = null
        moveDownPos = null
        moveDiagPos = null
        movePos = null
        rotationPos = null

        tempValues.clear()
        tempTitle = null
        editMode = false
        deleteMode = false
    }

    /**
     * Add a new area at the coordinates and add the markers to edit the area
     * @param pos position of the center of the rectangle
     * */
    fun setupEditZone(context: Context?, pos: LatLng) {
        // Generate the corners of the area
        val zoom = GoogleMapHelper.map!!.cameraPosition!!.zoom
        val divisor = 2.0.pow(zoom.toDouble())
        val longDiff = 188.0 / divisor / 2
        val latDiff = longDiff / 2
        val pos1 = LatLng(pos.latitude + latDiff, pos.longitude - longDiff)
        val pos2 = LatLng(pos.latitude - latDiff, pos.longitude - longDiff)
        val pos3 = LatLng(pos.latitude - latDiff, pos.longitude + longDiff)
        val pos4 = LatLng(pos.latitude + latDiff, pos.longitude + longDiff)
        tempLatLng.add(pos1)
        tempLatLng.add(pos2)
        tempLatLng.add(pos3)
        tempLatLng.add(pos4)
        tempPoly = GoogleMapHelper.map!!.addPolygon(
            PolygonOptions().add(pos1).add(pos2).add(pos3).add(pos4).strokeColor(Color.RED)
        )
        if(context != null)
            tempPoly!!.strokeWidth = AREA_WIDTH_DP.dpToPixelsFloat(context)

        setupModifyMarkers(context)
    }

    /**
     * Creates all the markers used to edit the areas
     */
    fun setupModifyMarkers(context: Context?) {
        val pos2 = tempLatLng[1]!!
        val pos3 = tempLatLng[2]!!
        val pos4 = tempLatLng[3]!!

        val posMidRight = LatLngOperator.mean(listOf(pos3, pos4))
        val posMidDown = LatLngOperator.mean(listOf(pos3, pos2))
        val posCenter = LatLngOperator.mean(listOf(pos4, pos2))

        var dim_dp = 35
        if(context != null){
            dim_dp = dim_dp.dpToPixelsFloat(context).toInt()
        }

        val anchor = IconAnchor(0.5f, 0.5f)
        if(context != null){}
        val bound = IconBound(0, 0, dim_dp, dim_dp)
        val dimension = IconDimension(dim_dp, dim_dp)

        moveDiagMarker = GoogleMapHelper.map!!.addMarker(
            GoogleMapHelperFunctions.newMarker(
                context,
                pos3,
                anchor,
                PolygonAction.DIAG.toString(),
                null,
                true,
                R.drawable.ic_downleftarrow,
                bound,
                dimension
            )
        )
        moveDiagPos = moveDiagMarker!!.position

        moveRightMarker = GoogleMapHelper.map!!.addMarker(
            GoogleMapHelperFunctions.newMarker(
                context,
                posMidRight,
                anchor,
                PolygonAction.RIGHT.toString(),
                null,
                true,
                R.drawable.ic_rightarrow,
                bound,
                dimension
            )
        )
        moveRightPos = moveRightMarker!!.position

        moveDownMarker = GoogleMapHelper.map!!.addMarker(
            GoogleMapHelperFunctions.newMarker(
                context,
                posMidDown,
                anchor,
                PolygonAction.DOWN.toString(),
                null,
                true,
                R.drawable.ic_downarrow,
                bound,
                dimension
            )
        )
        moveDownPos = moveDownMarker!!.position

        moveMarker = GoogleMapHelper.map!!.addMarker(
            GoogleMapHelperFunctions.newMarker(
                context,
                posCenter,
                anchor,
                PolygonAction.MOVE.toString(),
                null,
                true,
                R.drawable.ic_move,
                bound,
                dimension
            )
        )
        movePos = moveMarker!!.position

        rotationMarker = GoogleMapHelper.map!!.addMarker(
            GoogleMapHelperFunctions.newMarker(
                context,
                pos4,
                anchor,
                PolygonAction.ROTATE.toString(),
                null,
                true,
                R.drawable.ic_rotation,
                bound,
                dimension
            )
        )
        rotationPos = rotationMarker!!.position
    }

    /**
     * Translate all the corners and the edition markers from the rectangle by the same distance the "moveMarker" is moved
     * @param pos marker that has been dragged : target position for the area
     */
    fun translatePolygon(pos: Marker) {
        val diff = LatLngOperator.minus(pos.position, movePos!!)

        tempLatLng = tempLatLng.map { latLng ->
            LatLngOperator.plus(latLng!!, diff)
        }.toMutableList()

        // Moves the edition markers
        moveMarker!!.position = LatLngOperator.plus(movePos!!, diff)
        movePos = moveMarker!!.position

        moveDiagMarker!!.position = LatLngOperator.plus(moveDiagPos!!, diff)
        moveDiagPos = moveDiagMarker!!.position

        moveRightMarker!!.position = LatLngOperator.plus(moveRightPos!!, diff)
        moveRightPos = moveRightMarker!!.position

        moveDownMarker!!.position = LatLngOperator.plus(moveDownPos!!, diff)
        moveDownPos = moveDownMarker!!.position

        rotationMarker!!.position = LatLngOperator.plus(rotationPos!!, diff)
        rotationPos = rotationMarker!!.position
    }

    /**
     * Transforms the size of the rectangle, either by moving the the right wall(right), the down wall(down) or both(diag)
     * @param pos marker that has been dragged : target position for the area
     */
    fun transformPolygon(pos: Marker) {
        val latlng1 = tempLatLng[1]!!
        val latlng2 = tempLatLng[2]!!
        val latlng3 = tempLatLng[3]!!

        // Vector of the marker
        val vec = LatLngOperator.minus(pos.position, moveDiagPos!!)

        //Projection of the vector on the two axis (in cartesian space)
        var diffCoord =
            GoogleMapVectorHelper.projectionVectorThroughCartesian(vec, latlng2, latlng3)
        var diffCoord1 =
            GoogleMapVectorHelper.projectionVectorThroughCartesian(vec, latlng1, latlng2)


        // Move the corresponding corners of the rectangle
        when (pos.snippet) {
            PolygonAction.RIGHT.toString() -> {
                diffCoord = LatLng(0.0, 0.0)
                tempLatLng[2] = LatLngOperator.plus(latlng2, diffCoord1)
                tempLatLng[3] = LatLngOperator.plus(latlng3, diffCoord1)
            }
            PolygonAction.DOWN.toString() -> {
                diffCoord1 = LatLng(0.0, 0.0)
                tempLatLng[1] = LatLngOperator.plus(latlng1, diffCoord)
                tempLatLng[2] = LatLngOperator.plus(latlng2, diffCoord)
            }
            else -> { //Should only be DIAG
                tempLatLng[1] = LatLngOperator.plus(latlng1, diffCoord)
                tempLatLng[2] =
                    LatLngOperator.plus(LatLngOperator.plus(latlng2, diffCoord), diffCoord1)
                tempLatLng[3] = LatLngOperator.plus(latlng3, diffCoord1)
            }
        }

        // Moves the edition markers
        val v = LatLngOperator.plus(diffCoord, diffCoord1)

        moveDiagMarker!!.position = LatLngOperator.plus(moveDiagPos!!, v)
        moveDiagPos = moveDiagMarker!!.position

        moveMarker!!.position = LatLngOperator.plus(movePos!!, LatLngOperator.divide(v, 2.0))
        movePos = moveMarker!!.position

        moveRightMarker!!.position = LatLngOperator.plus(
            LatLngOperator.plus(moveRightPos!!, diffCoord1),
            LatLngOperator.divide(diffCoord, 2.0)
        )
        moveRightPos = moveRightMarker!!.position

        moveDownMarker!!.position = LatLngOperator.plus(
            LatLngOperator.plus(moveDownPos!!, diffCoord),
            LatLngOperator.divide(diffCoord1, 2.0)
        )
        moveDownPos = moveDownMarker!!.position

        rotationMarker!!.position = LatLngOperator.plus(rotationPos!!, diffCoord1)
        rotationPos = rotationMarker!!.position
    }

    /**
     * Rotate the current polygon according to the given Marker. It also aligns this
     * marker so that it remains on the circle around the center of rotation.
     * @param pos: the Marker indicating the rotation.
     */
    fun rotatePolygon(pos: Marker) {
        // Get the center of the projection
        val center = GoogleMapVectorHelper.getCenter(tempLatLng)

        val posProj = GoogleMapVectorHelper.equirectangularProjection(pos.position, center)
        val oldPosProj = GoogleMapVectorHelper.equirectangularProjection(rotationPos, center)

        val rotationAngle =
            GoogleMapVectorHelper.getDirection(posProj) - GoogleMapVectorHelper.getDirection(
                oldPosProj
            )
        val rotationAngleDegree = GoogleMapVectorHelper.radianToDegree(rotationAngle).toFloat()

        // Rotate all the points of the polygon
        val cornersRotatedLatLng = tempLatLng.map {
            GoogleMapVectorHelper.applyRotation(
                it,
                rotationAngle,
                center
            )
        }
        for (i in cornersRotatedLatLng.indices) {
            tempLatLng[i] = cornersRotatedLatLng[i]
        }

        // Move all the markers on the corresponding corner of the polygon
        rotationMarker!!.position = tempLatLng[INDEX_ROTATION_MARKER]
        rotationPos = rotationMarker!!.position

        moveDiagMarker!!.position =
            GoogleMapVectorHelper.applyRotation(moveDiagMarker!!.position, rotationAngle, center)
        moveDiagPos = moveDiagMarker!!.position
        moveDiagMarker!!.rotation -= rotationAngleDegree

        moveMarker!!.position =
            GoogleMapVectorHelper.applyRotation(moveMarker!!.position, rotationAngle, center)
        movePos = moveMarker!!.position

        moveRightMarker!!.position =
            GoogleMapVectorHelper.applyRotation(moveRightMarker!!.position, rotationAngle, center)
        moveRightPos = moveRightMarker!!.position
        moveRightMarker!!.rotation -= rotationAngleDegree

        moveDownMarker!!.position =
            GoogleMapVectorHelper.applyRotation(moveDownMarker!!.position, rotationAngle, center)
        moveDownPos = moveDownMarker!!.position
        moveDownMarker!!.rotation -= rotationAngleDegree
    }

    /**
     * Restores all markers to the area they belong
     */
    fun restoreMarkers(context: Context?) {
        val anchor = IconAnchor(0f, 0f)
        val bound = IconBound(0, 0, 0, 0)
        val dimension = IconDimension(1, 1)

        for (value in tempValues) {
            areasPoints[value.key] = Triple(
                areasPoints.get(value.key)!!.first,
                GoogleMapHelper.map!!.addMarker(
                    GoogleMapHelperFunctions.newMarker(
                        context,
                        value.value.second,
                        anchor,
                        null,
                        value.value.first,
                        false,
                        R.drawable.ic_location,
                        bound,
                        dimension
                    )
                ), areasPoints.get(value.key)!!.third
            )
        }
    }

    /**
     * Can edit the selected area if the area is in the list of areas of the current editingZone
     * @param tag: tag of the area to begin editing
     * @return true if can edit, false if not
     */
    fun canEdit(tag: String): Boolean {
        val t = tag.toInt()
        return zonesToArea[editingZone]?.second?.contains(t) ?: false
    }

    /**
     * Set up the area with the tag in parameter
     * @param tag of the area to edit
     */
    fun editArea(context: Context?, tag: String) {
        val t = tag.toInt()
        val area = areasPoints[t] ?: return
        editMode = false
        tempTitle = tempValues[t]!!.first
        modifyingArea = t
        tempValues.remove(t)
        restoreMarkers(context)

        tempPoly = area.third
        tempPoly!!.strokeColor = Color.RED
        tempLatLng = area.third.points.dropLast(1).toMutableList()

        setupModifyMarkers(context)
    }

    /**
     * Removes the area with given ID
     * @param id id of the area to remove
     */
    fun removeArea(id: Int) {
        areasPoints[id] ?: return
        zonesToArea[areasPoints[id]!!.first]!!.second.remove(id)
        tempValues.remove(id)
        areasPoints[id]!!.second.remove()
        areasPoints[id]!!.third.remove()
        areasPoints.remove(id)
    }

    /**
     * Deletes the areas of a zone
     * @param id id of the zone to remove areas
     */
    fun removeZoneAreas(id: String) {
        zonesToArea[id] ?: return
        val areas = zonesToArea[id]!!.second.toList()
        for (uid in areas) {
            removeArea(uid)
        }
        zonesToArea[id] = Pair(null, mutableListOf())
    }

    /**
     * Remove a zone from the map
     * @param id id of the zone to remove
     */
    fun removeZone(id: String) {
        removeZoneAreas(id)
        zonesToArea.remove(id)
    }

    /**
     * Switches the edit mode, and remove/recreates the markers for edition purpose
     */
    fun editMode(context: Context?) {
        //If in delete mode, deactivate delete mode
        if (deleteMode)
            deleteMode(context)
        editMode = !editMode

        //If already in edit mode, restores the map to go outside of edit mode, else prepare for edition
        if (editMode) {
            for (a in areasPoints) {
                tempValues[a.key] =
                    Pair(a.value.second.title, a.value.second.position)
                a.value.second.remove()
            }
            colorAreas(
                editingZone!!,
                GoogleMapMode.EDITED_ZONE_STROKE_COLOR
            )
        } else {
            colorAreas(
                editingZone!!,
                DEFAULT_ZONE_STROKE_COLOR
            )
            restoreMarkers(context)
        }
    }
}