Skip to content

Commit

Permalink
feat(android): display container information on marker click (#216)
Browse files Browse the repository at this point in the history
Refs: closes #17, closes #20

## Summary

Add an info window that displays the container information of the
clicked marker.
Also add a button to give the user directions to that marker.
  • Loading branch information
Goncalo-Marques authored May 25, 2024
1 parent 2db8c7e commit 1aa3f04
Show file tree
Hide file tree
Showing 11 changed files with 340 additions and 10 deletions.
137 changes: 133 additions & 4 deletions android/app/src/main/java/com/ecomap/ecomap/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,30 @@ import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Button
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.Group
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.ecomap.ecomap.clients.ecomap.http.ApiClient
import com.ecomap.ecomap.clients.ecomap.http.ApiRequestQueue
import com.ecomap.ecomap.data.UserStore
import com.ecomap.ecomap.domain.ContainerCategory
import com.ecomap.ecomap.domain.ContainersPaginated
import com.ecomap.ecomap.map.ContainerCategoriesRecyclerViewAdapter
import com.ecomap.ecomap.map.ContainerCategoryRecyclerViewData
import com.ecomap.ecomap.map.ContainerClusterRenderer
import com.ecomap.ecomap.map.ContainerMarker
import com.ecomap.ecomap.signin.SignInActivity
Expand Down Expand Up @@ -57,6 +68,36 @@ class MainActivity : AppCompatActivity(), OnMapReadyCallback {
*/
private lateinit var containerClusterManager: ClusterManager<ContainerMarker>

/**
* Defines the group of buttons view.
*/
private lateinit var groupButtonsView: Group

/**
* Defines the container info window view.
*/
private lateinit var containerInfoWindowView: ConstraintLayout

/**
* Defines the container info window title view.
*/
private lateinit var containerInfoWindowTitleText: TextView

/**
* Defines the container info window snippet view.
*/
private lateinit var containerInfoWindowSnippetText: TextView

/**
* Defines the container info window categories recycler view.
*/
private lateinit var containerInfoWindowRecyclerCategories: RecyclerView

/**
* Defines the container info window directions button.
*/
private lateinit var containerInfoWindowDirectionsButton: Button

/**
* Defines the authentication token.
*/
Expand Down Expand Up @@ -116,10 +157,21 @@ class MainActivity : AppCompatActivity(), OnMapReadyCallback {
// Get activity views.
val chipGroupContainerFilter: ChipGroup = findViewById(R.id.chip_group_container_filter)
val buttonMyLocation: FloatingActionButton = findViewById(R.id.button_my_location)
groupButtonsView = findViewById(R.id.group_buttons)
containerInfoWindowView = findViewById(R.id.info_window)
containerInfoWindowTitleText = findViewById(R.id.info_window_text_title)
containerInfoWindowSnippetText = findViewById(R.id.info_window_text_snippet)
containerInfoWindowRecyclerCategories = findViewById(R.id.info_window_recycler_categories)
containerInfoWindowRecyclerCategories.layoutManager =
GridLayoutManager(this, CONTAINER_INFO_WINDOW_RECYCLER_CATEGORIES_SPAN_COUNT)
containerInfoWindowDirectionsButton = findViewById(R.id.info_window_button_directions)

// Set button functions.
populateChipGroupContainerFilter(chipGroupContainerFilter)
buttonMyLocation.setOnClickListener { focusMyLocation() }

// Start with the container info window closed.
closeContainerInfoWindow()
}

/**
Expand Down Expand Up @@ -166,6 +218,28 @@ class MainActivity : AppCompatActivity(), OnMapReadyCallback {
map.setOnCameraIdleListener(containerClusterManager)
map.setOnMarkerClickListener(containerClusterManager)

// Set container info window functions.
containerClusterManager.setOnClusterItemClickListener { container ->
// Display the container information window and move to the container location.
showContainerInfoWindow(container)
map.animateCamera(
CameraUpdateFactory.newLatLngZoom(
container.position,
map.cameraPosition.zoom
)
)

// Returns true so that the default info window is not displayed.
true
}
containerClusterManager.setOnClusterClickListener {
closeContainerInfoWindow()

// Returns false, so the default behavior is still used.
false
}
map.setOnMapClickListener { closeContainerInfoWindow() }

// Prompt the user for permission.
getLocationPermission()

Expand Down Expand Up @@ -283,15 +357,16 @@ class MainActivity : AppCompatActivity(), OnMapReadyCallback {
val existingContainer = filteredContainers[containerPosition]
if (existingContainer == null) {
val containerMarker = ContainerMarker(
containerPosition,
container.geoJSON.properties.getLocationName(this),
arrayListOf(container.category.getStringResource(this))
this,
container.id,
container.geoJSON,
arrayListOf(container.category)
)

containerClusterManager.addItem(containerMarker)
filteredContainers[containerPosition] = containerMarker
} else {
existingContainer.categories.add(container.category.getStringResource(this))
existingContainer.categories.add(container.category)
}
}

Expand Down Expand Up @@ -360,6 +435,58 @@ class MainActivity : AppCompatActivity(), OnMapReadyCallback {
}
}

/**
* Opens the info window for the given container marker.
* It hides the main buttons and shows the info window with the given container marker data.
*/
private fun showContainerInfoWindow(container: ContainerMarker) {
groupButtonsView.visibility = View.GONE
containerInfoWindowView.visibility = View.VISIBLE
containerInfoWindowTitleText.text = container.geoJSON.properties.municipalityName
containerInfoWindowSnippetText.text = container.geoJSON.properties.getWayName(this)

val containerCategoriesDataSet =
ArrayList<ContainerCategoryRecyclerViewData>(container.categories.size)
for (containerCategory in container.categories) {
val data = ContainerCategoryRecyclerViewData(
containerCategory.getIconResource(),
containerCategory.getStringResource(this)
)
if (containerCategoriesDataSet.contains(data)) {
// The category already exists in the current data set.
continue
}

containerCategoriesDataSet.add(data)
}
containerInfoWindowRecyclerCategories.adapter =
ContainerCategoriesRecyclerViewAdapter(containerCategoriesDataSet.toTypedArray())

containerInfoWindowDirectionsButton.setOnClickListener {
val intentMapDirections = Intent(Intent.ACTION_VIEW)
intentMapDirections.data =
Uri.parse("geo:0,0?q=${container.position.latitude},${container.position.longitude}(${container.snippet})")

if (intentMapDirections.resolveActivity(packageManager) != null) {
// Start activity only if there is an app that can resolve it.
startActivity(intentMapDirections)
}
}
}

/**
* Closes the container info window.
* It makes the main buttons visible and hides the info window.
*/
private fun closeContainerInfoWindow() {
if (!containerInfoWindowView.isVisible) {
return
}

groupButtonsView.visibility = View.VISIBLE
containerInfoWindowView.visibility = View.GONE
}

companion object {
private val LOG_TAG = MainActivity::class.java.simpleName

Expand All @@ -378,5 +505,7 @@ class MainActivity : AppCompatActivity(), OnMapReadyCallback {
private const val MAP_CAMERA_ZOOM_DEFAULT = 15.0F

private const val REQUEST_LIST_CONTAINER_LIMIT = 100

private const val CONTAINER_INFO_WINDOW_RECYCLER_CATEGORIES_SPAN_COUNT = 2
}
}
14 changes: 14 additions & 0 deletions android/app/src/main/java/com/ecomap/ecomap/domain/GeoJSON.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,22 @@ data class GeoJSONProperties(
var wayName: String = "",
var municipalityName: String = "",
) {
/**
* Returns the way name or unknown if not defined.
* @param context Activity context.
* @return Way name or unknown way.
*/
fun getWayName(context: Context): String {
if (wayName.isBlank()) {
return context.getString(R.string.way_unknown)
}

return this.wayName
}

/**
* Returns the location name based on the way and municipality name.
* @param context Activity context.
* @return Location name.
*/
fun getLocationName(context: Context): String {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.ecomap.ecomap.map

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.ecomap.ecomap.R

/**
* Represents the structure of an item in the container category recycler view.
* @param iconResourceID Category icon resource ID.
* @param category Category description.
*/
data class ContainerCategoryRecyclerViewData(
val iconResourceID: Int,
val category: String,
)

/**
* Recycler view adapter for the container categories.
*/
class ContainerCategoriesRecyclerViewAdapter(private val dataSet: Array<ContainerCategoryRecyclerViewData>) :
RecyclerView.Adapter<ContainerCategoriesRecyclerViewAdapter.ViewHolder>() {

/**
* Defines the views in the adapter.
*/
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val imageView: ImageView = view.findViewById(R.id.image_view_icon)
val textView: TextView = view.findViewById(R.id.text_view_category)
}

override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
// Create a new view, which defines the UI of the list item.
val view = LayoutInflater.from(viewGroup.context)
.inflate(R.layout.frame_layout_container_categories, viewGroup, false)

return ViewHolder(view)
}

override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
val data = dataSet[position]

viewHolder.imageView.setImageResource(data.iconResourceID)
viewHolder.textView.text = data.category
}

override fun getItemCount(): Int {
return dataSet.size
}
}
23 changes: 17 additions & 6 deletions android/app/src/main/java/com/ecomap/ecomap/map/ContainerMarker.kt
Original file line number Diff line number Diff line change
@@ -1,26 +1,37 @@
package com.ecomap.ecomap.map

import android.content.Context
import com.ecomap.ecomap.domain.ContainerCategory
import com.ecomap.ecomap.domain.GeoJSONFeaturePoint
import com.google.android.gms.maps.model.LatLng
import com.google.maps.android.clustering.ClusterItem

/**
* A marker that represents one or more containers at the same position on the map.
*/
class ContainerMarker(
private val position: LatLng,
private val locationName: String,
val categories: ArrayList<String>
internal val context: Context,
internal val id: String,
internal val geoJSON: GeoJSONFeaturePoint,
val categories: ArrayList<ContainerCategory>
) : ClusterItem {
override fun getPosition(): LatLng {
return position
val coordinates = geoJSON.geometry.coordinates
return LatLng(coordinates[1], coordinates[0])
}

override fun getTitle(): String {
return locationName
return geoJSON.properties.getLocationName(context)
}

override fun getSnippet(): String {
return categories.toSet().joinToString(", ")
val categoriesString = ArrayList<String>(categories.size)
for (category in categories) {
categoriesString.add(category.getStringResource(context))
}

// Convert to set to avoid duplicate categories.
return categoriesString.toSet().joinToString(", ")
}

override fun getZIndex(): Float {
Expand Down
Loading

0 comments on commit 1aa3f04

Please sign in to comment.