summaryrefslogtreecommitdiff
path: root/benchmark/app/src/main/java/io/trentetroim
diff options
context:
space:
mode:
authorMartial Simon <msimon_fr@hotmail.com>2025-09-15 01:07:58 +0200
committerMartial Simon <msimon_fr@hotmail.com>2025-09-15 01:07:58 +0200
commit967be9e750221ab2ab783f95df79bb26d290a45e (patch)
tree6802900a5e975f9f68b169f0f503f040056d6952 /benchmark/app/src/main/java/io/trentetroim
add: added projectsHEADmain
Diffstat (limited to 'benchmark/app/src/main/java/io/trentetroim')
-rw-r--r--benchmark/app/src/main/java/io/trentetroim/benchmark/AddBenchActivity.kt274
-rw-r--r--benchmark/app/src/main/java/io/trentetroim/benchmark/CustomAdapter.kt90
-rw-r--r--benchmark/app/src/main/java/io/trentetroim/benchmark/MainActivity.kt100
-rw-r--r--benchmark/app/src/main/java/io/trentetroim/benchmark/MapActivity.kt811
-rw-r--r--benchmark/app/src/main/java/io/trentetroim/benchmark/ReviewAdapter.kt37
-rw-r--r--benchmark/app/src/main/java/io/trentetroim/benchmark/api/models/ApiResponse.kt16
-rw-r--r--benchmark/app/src/main/java/io/trentetroim/benchmark/api/models/Point.kt24
-rw-r--r--benchmark/app/src/main/java/io/trentetroim/benchmark/api/models/Review.kt20
-rw-r--r--benchmark/app/src/main/java/io/trentetroim/benchmark/api/repository/PointRepository.kt164
-rw-r--r--benchmark/app/src/main/java/io/trentetroim/benchmark/api/repository/ReviewRepository.kt69
-rw-r--r--benchmark/app/src/main/java/io/trentetroim/benchmark/api/service/ApiService.kt38
-rw-r--r--benchmark/app/src/main/java/io/trentetroim/benchmark/api/service/RetrofitClient.kt43
-rw-r--r--benchmark/app/src/main/java/io/trentetroim/benchmark/ui/theme/Color.kt17
-rw-r--r--benchmark/app/src/main/java/io/trentetroim/benchmark/ui/theme/Theme.kt55
-rw-r--r--benchmark/app/src/main/java/io/trentetroim/benchmark/ui/theme/Type.kt36
15 files changed, 1794 insertions, 0 deletions
diff --git a/benchmark/app/src/main/java/io/trentetroim/benchmark/AddBenchActivity.kt b/benchmark/app/src/main/java/io/trentetroim/benchmark/AddBenchActivity.kt
new file mode 100644
index 0000000..62432e3
--- /dev/null
+++ b/benchmark/app/src/main/java/io/trentetroim/benchmark/AddBenchActivity.kt
@@ -0,0 +1,274 @@
+package io.trentetroim.benchmark
+
+import android.app.Activity
+import android.graphics.drawable.shapes.Shape
+import android.net.Uri
+import android.os.Bundle
+import android.util.Log
+import android.widget.Toast
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.compose.setContent
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Button
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
+import coil.compose.rememberAsyncImagePainter
+import com.gowtham.ratingbar.RatingBar
+import com.gowtham.ratingbar.RatingBarStyle
+import io.trentetroim.benchmark.ui.theme.BenchmarkTheme
+import io.trentetroim.benchmark.ui.theme.Grass
+import io.trentetroim.benchmark.ui.theme.Typography
+import io.trentetroim.benchmark.api.models.ApiResponse
+import io.trentetroim.benchmark.api.models.Point
+import io.trentetroim.benchmark.api.models.Review
+import io.trentetroim.benchmark.api.repository.PointRepository
+import io.trentetroim.benchmark.api.repository.ReviewRepository
+import kotlinx.coroutines.launch
+import androidx.lifecycle.lifecycleScope
+import io.trentetroim.benchmark.ui.theme.Lagoon
+import io.trentetroim.benchmark.ui.theme.Tildeeth
+import io.trentetroim.benchmark.ui.theme.TildeethAlpha
+import org.osmdroid.views.overlay.simplefastpoint.SimpleFastPointOverlayOptions
+
+class AddBenchActivity : ComponentActivity() {
+ private val defaultImageUrl = "https://guillotinemelody.dev/resources/icon.png"
+ private val pointRepository = PointRepository()
+ private val reviewRepository = ReviewRepository()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ installSplashScreen()
+ val latitude = intent.getDoubleExtra("currentLatitude", 0.0)
+ val longitude = intent.getDoubleExtra("currentLongitude", 0.0)
+
+ setContent {
+ BenchmarkTheme {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ modifier = Modifier.fillMaxSize().background(
+ color = Grass
+ ).padding(16.dp)
+ ) {
+ Text(text = stringResource(R.string.new_bench), textAlign = TextAlign.Center)
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ var description by remember { mutableStateOf("") }
+ Input(value = description, onValueChange = { description = it })
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ var rating by remember { mutableFloatStateOf(3.0f) }
+ Rating(value = rating, onValueChange = { rating = it })
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ var imageUrl by remember { mutableStateOf(defaultImageUrl) }
+ var selectedImageUri by remember { mutableStateOf<Uri?>(null) }
+ var isUploading by remember { mutableStateOf(false) }
+ val context = LocalContext.current
+
+ val pickImageLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.GetContent()
+ ) { uri: Uri? ->
+ uri?.let {
+ selectedImageUri = it
+ isUploading = true
+
+ lifecycleScope.launch {
+ pointRepository.uploadImage(context, it).collect { response ->
+ when (response) {
+ is ApiResponse.Success -> {
+ imageUrl = response.data
+ isUploading = false
+ Toast.makeText(context, "Image uploaded successfully", Toast.LENGTH_SHORT).show()
+ }
+ is ApiResponse.Error -> {
+ isUploading = false
+ Toast.makeText(context, "Error: ${response.errorMessage}", Toast.LENGTH_SHORT).show()
+ }
+ is ApiResponse.Loading -> {
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Button(
+ onClick = { pickImageLauncher.launch("image/*") },
+ enabled = !isUploading
+ ) {
+ Text("Ajouter une photo")
+ }
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ if (isUploading) {
+ CircularProgressIndicator(
+ modifier = Modifier.size(24.dp),
+ color = Color.White
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ }
+
+ selectedImageUri?.let {
+ Image(
+ painter = rememberAsyncImagePainter(it),
+ contentDescription = "Selected image",
+ modifier = Modifier
+ .size(150.dp)
+ .padding(8.dp)
+ )
+ }
+
+ Text(
+ text = "URL de l'image: $imageUrl",
+ modifier = Modifier.padding(8.dp)
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Row(
+ horizontalArrangement = Arrangement.SpaceEvenly,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Button(onClick = { finish() }) {
+ Text(stringResource(R.string.cancel))
+ }
+
+ Button(onClick = {
+ val prefixedImageUrl = if (!imageUrl.startsWith("http://la-banquise.fr:5431")) {
+ "http://la-banquise.fr:5431" + if (imageUrl.startsWith("/")) imageUrl else "/$imageUrl"
+ } else {
+ imageUrl
+ }
+
+ val point = Point(
+ x = latitude,
+ y = longitude,
+ type = "bench",
+ name = description,
+ picture = prefixedImageUrl,
+ id = ""
+ )
+
+ lifecycleScope.launch {
+ pointRepository.createPoint(point).collect { response ->
+ when (response) {
+ is ApiResponse.Success -> {
+ Log.d("AddBenchActivity", "Point created: $point")
+
+ val createdPoint = response.data
+ val initialReview = Review(
+ id = "",
+ pointId = createdPoint.id,
+ grade = rating.toInt(),
+ comment = "Initial rating",
+ pictures = null
+ )
+
+ reviewRepository.addReview(createdPoint.id, initialReview).collect { reviewResponse ->
+ when (reviewResponse) {
+ is ApiResponse.Success -> {
+ Log.d("AddBenchActivity", "Initial review added: ${reviewResponse.data}")
+ Toast.makeText(
+ this@AddBenchActivity,
+ "Banc et note initiale ajoutés avec succès!",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ is ApiResponse.Error -> {
+ Log.e("AddBenchActivity", "Error adding initial review: ${reviewResponse.errorMessage}")
+ Toast.makeText(
+ this@AddBenchActivity,
+ "Banc ajouté mais erreur lors de l'ajout de la note initiale",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ is ApiResponse.Loading -> {
+ }
+ }
+ finish()
+ }
+ }
+ is ApiResponse.Error -> {
+ Log.e("AddBenchActivity", "Error creating point: ${response.errorMessage}")
+ Toast.makeText(
+ this@AddBenchActivity,
+ "Erreur lors de la création du banc: ${response.errorMessage}",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ is ApiResponse.Loading -> {
+ }
+ }
+ }
+ }
+ }) {
+ Text(stringResource(R.string.save))
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun Input(value: String, onValueChange: (String) -> Unit) {
+ OutlinedTextField(
+ value = value,
+ onValueChange = onValueChange,
+ label = { Text("Description", color = Tildeeth) },
+ placeholder = { Text("Smash", color = TildeethAlpha) },
+ maxLines = 3,
+ textStyle = TextStyle(color = Tildeeth, fontFamily = Typography.bodyLarge.fontFamily),
+ modifier = Modifier.padding(8.dp).background(Lagoon).border(3.dp, Tildeeth)
+ )
+}
+
+@Composable
+fun Rating(value: Float, onValueChange: (Float) -> Unit) {
+ RatingBar(
+ value = value,
+ style = RatingBarStyle.Fill(),
+ onValueChange = onValueChange,
+ onRatingChanged = {
+ Log.d("TAG", "onRatingChanged: $it")
+ }
+ )
+}
diff --git a/benchmark/app/src/main/java/io/trentetroim/benchmark/CustomAdapter.kt b/benchmark/app/src/main/java/io/trentetroim/benchmark/CustomAdapter.kt
new file mode 100644
index 0000000..5b77dcc
--- /dev/null
+++ b/benchmark/app/src/main/java/io/trentetroim/benchmark/CustomAdapter.kt
@@ -0,0 +1,90 @@
+package io.trentetroim.benchmark
+
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.RatingBar
+import android.widget.TextView
+import androidx.lifecycle.LifecycleCoroutineScope
+import androidx.recyclerview.widget.RecyclerView
+import com.bumptech.glide.Glide
+import io.trentetroim.benchmark.api.models.ApiResponse
+import io.trentetroim.benchmark.api.models.Point
+import io.trentetroim.benchmark.api.repository.ReviewRepository
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+
+interface OnBenchClickListener {
+ fun onBenchClick(point: Point)
+}
+
+class CustomAdapter(
+ private val dataSet: List<Point>,
+ private val lifecycleScope: LifecycleCoroutineScope,
+ private val onBenchClickListener: OnBenchClickListener? = null
+) : RecyclerView.Adapter<CustomAdapter.ViewHolder>() {
+
+ private val reviewRepository = ReviewRepository()
+
+ class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+ val cardPfp: ImageView = view.findViewById(R.id.benchPfp)
+ val cardName: TextView
+ val cardPosition: TextView
+ val cardRating: RatingBar
+ val itemView: View = view
+
+ init {
+ cardName = view.findViewById(R.id.benchName)
+ cardPosition = view.findViewById(R.id.benchPos)
+ cardRating = view.findViewById(R.id.benchRating)
+ }
+ }
+
+ override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
+ val view = LayoutInflater.from(viewGroup.context)
+ .inflate(R.layout.card_layout, viewGroup, false)
+
+ return ViewHolder(view)
+ }
+
+ override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
+ val point = dataSet[position]
+ viewHolder.cardName.text = point.name
+
+ val decimalFormat = java.text.DecimalFormat("0.000")
+ viewHolder.cardPosition.text = "Position: ${decimalFormat.format(point.x)}, ${decimalFormat.format(point.y)}"
+
+ // Fetch and display the average rating
+ lifecycleScope.launch {
+ reviewRepository.getAverageRating(point.id).collectLatest { response ->
+ when (response) {
+ is ApiResponse.Success -> {
+ viewHolder.cardRating.rating = response.data
+ }
+ else -> {
+ viewHolder.cardRating.rating = 0f
+ }
+ }
+ }
+ }
+
+ // Set click listener on the card view
+ viewHolder.itemView.setOnClickListener {
+ onBenchClickListener?.onBenchClick(point)
+ }
+
+ if (!point.picture.isNullOrEmpty()) {
+ Glide.with(viewHolder.itemView.context)
+ .load(point.picture)
+ .placeholder(R.drawable.ic_bench_icon)
+ .error(R.drawable.ic_bench_icon)
+ .into(viewHolder.cardPfp)
+ } else {
+ viewHolder.cardPfp.setImageResource(R.drawable.ic_bench_icon)
+ }
+ }
+
+ override fun getItemCount() = dataSet.size
+}
diff --git a/benchmark/app/src/main/java/io/trentetroim/benchmark/MainActivity.kt b/benchmark/app/src/main/java/io/trentetroim/benchmark/MainActivity.kt
new file mode 100644
index 0000000..d99db2e
--- /dev/null
+++ b/benchmark/app/src/main/java/io/trentetroim/benchmark/MainActivity.kt
@@ -0,0 +1,100 @@
+package io.trentetroim.benchmark
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import io.trentetroim.benchmark.ui.theme.BenchmarkTheme
+import android.content.Context
+import android.content.Intent
+import android.os.UserManager
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
+import io.trentetroim.benchmark.ui.theme.Lagoon
+import io.trentetroim.benchmark.ui.theme.Salmon
+import io.trentetroim.benchmark.ui.theme.White
+
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ installSplashScreen()
+ enableEdgeToEdge()
+ setContent {
+ BenchmarkTheme {
+ MainContent(Modifier.background(Lagoon))
+ }
+ }
+ }
+
+ @Composable
+ @Preview
+ fun MainContent(modifier : Modifier = Modifier) {
+ Column(
+ modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.SpaceEvenly
+ ) {
+ val user: UserManager = applicationContext.getSystemService(USER_SERVICE) as UserManager
+ Text("User is a goat: " + user.isUserAGoat().toString())
+ Infos()
+ IntentToSwitch(LocalContext.current)
+ Footer()
+ }
+ }
+
+ @Composable
+ @Preview
+ fun Infos() {
+ Image(
+ painter = painterResource(R.drawable.ic_bench_icon),
+ modifier = Modifier.size(200.dp),
+ contentDescription = "App icon",
+ contentScale = ContentScale.Fit
+ )
+ Text(text = stringResource(R.string.app_name), fontSize = 50.sp)
+ }
+
+ // composable with a button
+ @Composable
+ fun IntentToSwitch(context: Context) {
+ Button(
+ onClick = {
+ context.startActivity(Intent(context, MapActivity::class.java))
+ },
+ colors = ButtonDefaults.buttonColors(containerColor = Salmon),
+ ) {
+ Text("Démarrer", color = White)
+ }
+ }
+
+ @Composable
+ @Preview
+ fun Footer() {
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ Text(
+ text = stringResource(R.string.devs),
+ )
+ Text(stringResource(R.string.quote), fontStyle = FontStyle.Italic, fontFamily = FontFamily.Cursive)
+ }
+ }
+} \ No newline at end of file
diff --git a/benchmark/app/src/main/java/io/trentetroim/benchmark/MapActivity.kt b/benchmark/app/src/main/java/io/trentetroim/benchmark/MapActivity.kt
new file mode 100644
index 0000000..09376b2
--- /dev/null
+++ b/benchmark/app/src/main/java/io/trentetroim/benchmark/MapActivity.kt
@@ -0,0 +1,811 @@
+package io.trentetroim.benchmark
+
+import android.Manifest
+import android.animation.ValueAnimator
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.graphics.Bitmap
+import android.graphics.drawable.Drawable
+import android.os.Bundle
+import android.preference.PreferenceManager.getDefaultSharedPreferences
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Button
+import android.widget.EditText
+import android.widget.ImageButton
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.RatingBar
+import android.widget.TextView
+import android.widget.Toast
+import androidx.activity.ComponentActivity
+import androidx.coordinatorlayout.widget.CoordinatorLayout
+import androidx.core.app.ActivityCompat
+import androidx.core.animation.doOnEnd
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.updateLayoutParams
+import androidx.lifecycle.lifecycleScope
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.bumptech.glide.Glide
+import com.bumptech.glide.request.target.CustomTarget
+import com.bumptech.glide.request.transition.Transition
+import com.google.android.gms.location.FusedLocationProviderClient
+import com.google.android.gms.location.LocationServices
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import io.trentetroim.benchmark.api.models.ApiResponse
+import io.trentetroim.benchmark.api.models.Point
+import io.trentetroim.benchmark.api.models.Review
+import io.trentetroim.benchmark.api.repository.PointRepository
+import io.trentetroim.benchmark.api.repository.ReviewRepository
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+import org.osmdroid.config.Configuration.getInstance
+import org.osmdroid.tileprovider.tilesource.TileSourceFactory
+import org.osmdroid.util.GeoPoint
+import org.osmdroid.views.MapView
+import org.osmdroid.views.overlay.ItemizedIconOverlay
+import org.osmdroid.views.overlay.ItemizedOverlayWithFocus
+import org.osmdroid.views.overlay.OverlayItem
+import org.osmdroid.views.overlay.gestures.RotationGestureOverlay
+import org.osmdroid.views.overlay.MapEventsOverlay
+import org.osmdroid.events.MapEventsReceiver
+import androidx.core.graphics.drawable.toDrawable
+import org.osmdroid.bonuspack.routing.OSRMRoadManager
+import org.osmdroid.bonuspack.routing.Road
+import org.osmdroid.bonuspack.routing.RoadManager
+import org.osmdroid.views.overlay.Polyline
+
+class MapActivity : ComponentActivity(), MapEventsReceiver, OnBenchClickListener {
+ private lateinit var map: MapView
+
+ private lateinit var locationClient: FusedLocationProviderClient
+ private val locationPermissionRequest = 1001
+ private var latitude = 46.0837
+ private var longitude = 6.0452
+
+ private val pointRepository = PointRepository()
+ private val reviewRepository = ReviewRepository()
+ private var points: List<Point> = emptyList()
+ private val TAG = "MapActivity"
+ private lateinit var bottomText: TextView;
+ private lateinit var listContainer: CoordinatorLayout;
+ private lateinit var sheetBehavior: BottomSheetBehavior<LinearLayout>
+
+
+ private var isDialogShowing = false
+
+ override fun onBenchClick(point: Point) {
+ val startPoint = map.mapCenter as GeoPoint
+ val endPoint = GeoPoint(point.x, point.y)
+ val startZoom = map.zoomLevelDouble
+ var endZoom = map.zoomLevelDouble
+ if (endZoom < 17.5)
+ endZoom = 17.5
+
+ val animator = ValueAnimator.ofFloat(0f, 1f)
+ animator.duration = 1000
+ animator.addUpdateListener { animation ->
+ val fraction = animation.animatedFraction
+ val lat = startPoint.latitude + (endPoint.latitude - startPoint.latitude) * fraction
+ val lon = startPoint.longitude + (endPoint.longitude - startPoint.longitude) * fraction
+ val zoom = startZoom + (endZoom - startZoom) * fraction
+ map.controller.setCenter(GeoPoint(lat, lon))
+ map.controller.setZoom(zoom)
+ }
+ animator.start()
+ }
+
+ override fun singleTapConfirmedHelper(p0: GeoPoint): Boolean {
+ return false
+ }
+
+ override fun longPressHelper(p0: GeoPoint): Boolean {
+ val startPoint = map.mapCenter as GeoPoint
+ val endPoint = p0
+ val startZoom = map.zoomLevelDouble
+ var endZoom = map.zoomLevelDouble
+ if (endZoom < 17.5)
+ endZoom = 17.5
+ val animator = ValueAnimator.ofFloat(0f, 1f)
+ animator.duration = 1000
+ animator.addUpdateListener { animation ->
+ val fraction = animation.animatedFraction
+ val lat = startPoint.latitude + (endPoint.latitude - startPoint.latitude) * fraction
+ val lon = startPoint.longitude + (endPoint.longitude - startPoint.longitude) * fraction
+ val zoom = startZoom + (endZoom - startZoom) * fraction
+ map.controller.setCenter(GeoPoint(lat, lon))
+ map.controller.setZoom(zoom)
+ }
+ animator.doOnEnd {
+ Thread.sleep(400)
+ showAddBenchDialog(p0.latitude, p0.longitude)
+ }
+ animator.start()
+ return true
+ }
+
+ private fun showAddBenchDialog(lat: Double, lon: Double) {
+ val intent = Intent(baseContext, AddBenchActivity::class.java)
+ intent.putExtra("currentLatitude", lat)
+ intent.putExtra("currentLongitude", lon)
+ startActivity(intent)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ // enableEdgeToEdge()
+
+ getInstance().load(this, getDefaultSharedPreferences(this))
+
+ setContentView(R.layout.main_layout)
+ sheetBehavior = BottomSheetBehavior.from<LinearLayout>(findViewById<LinearLayout>(R.id.slideUpContainer))
+
+ sheetBehavior.setBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
+ override fun onStateChanged(bottomSheet: View, newState: Int) {
+ when (newState) {
+ BottomSheetBehavior.STATE_HIDDEN -> {
+ bottomText.text = "Steak haché"
+ }
+
+ BottomSheetBehavior.STATE_EXPANDED ->
+ bottomText.text = "Bas!"
+
+ BottomSheetBehavior.STATE_COLLAPSED ->
+ bottomText.text = "Haut!"
+
+ BottomSheetBehavior.STATE_DRAGGING -> {
+ bottomText.text = "Oh oui tire plus fort"
+ }
+
+ BottomSheetBehavior.STATE_SETTLING -> {
+ bottomText.text = "je câble"
+ }
+
+ BottomSheetBehavior.STATE_HALF_EXPANDED -> {
+ TODO()
+ }
+ }
+ }
+
+ override fun onSlide(bottomSheet: View, slideOffset: Float) {
+ bottomText.text = "Oh oui maître tirez encore " + ((1 - slideOffset) * 100) + "% de plus"
+ }
+ })
+
+
+ map = findViewById(R.id.map)
+ map.setTileSource(TileSourceFactory.MAPNIK)
+
+ val mapController = map.controller
+ mapController.setZoom(15.0)
+ val startPoint = GeoPoint(latitude, longitude)
+ mapController.setCenter(startPoint)
+
+ val rotationGestureOverlay = RotationGestureOverlay(map)
+ rotationGestureOverlay.isEnabled
+ map.setMultiTouchControls(true)
+ map.overlays.add(rotationGestureOverlay)
+
+ val mapEventsOverlay = MapEventsOverlay(this)
+ map.overlays.add(mapEventsOverlay)
+
+ ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.greenwashed)) { v, windowInsets ->
+ val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+ v.updateLayoutParams<ViewGroup.MarginLayoutParams> {
+ leftMargin = insets.left
+ bottomMargin = insets.bottom
+ rightMargin = insets.right
+ }
+
+ WindowInsetsCompat.CONSUMED
+ }
+
+ val newBenchBtn: ImageButton = findViewById(R.id.plusBtn)
+ locationClient = LocationServices.getFusedLocationProviderClient(this)
+ newBenchBtn.setOnClickListener {
+ val center = map.mapCenter as GeoPoint
+ val intent = Intent(baseContext, AddBenchActivity::class.java)
+ intent.putExtra("currentLatitude", center.latitude)
+ intent.putExtra("currentLongitude", center.longitude)
+ startActivity(intent)
+ }
+ val centerPosBtn: ImageButton = findViewById(R.id.posBtn)
+ centerPosBtn.setOnClickListener {
+ sendCurrentLocation(false)
+ }
+
+ val recyclerView: RecyclerView = findViewById(R.id.greenwashed)
+ recyclerView.layoutManager = LinearLayoutManager(this)
+
+ bottomText = findViewById(R.id.divider)
+ listContainer = findViewById(R.id.listWrapper)
+
+ sendCurrentLocation(false)
+ }
+
+ private fun sendCurrentLocation(launchDialog: Boolean = true) {
+ if (ActivityCompat.checkSelfPermission(
+ this,
+ Manifest.permission.ACCESS_FINE_LOCATION
+ ) != PackageManager.PERMISSION_GRANTED
+ ) {
+ ActivityCompat.requestPermissions(
+ this,
+ arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
+ locationPermissionRequest
+ )
+ return
+ }
+ locationClient.lastLocation.addOnSuccessListener { location ->
+ if (location != null) {
+ latitude = location.latitude
+ longitude = location.longitude
+
+ if (!launchDialog) {
+ val startPoint = map.mapCenter as GeoPoint
+ val endPoint = GeoPoint(location.latitude, location.longitude)
+ val startZoom = map.zoomLevelDouble
+ var endZoom = map.zoomLevelDouble
+ if (endZoom < 18.0)
+ endZoom = 18.0
+ val animator = ValueAnimator.ofFloat(0f, 1f)
+ animator.duration = 1000
+ animator.addUpdateListener { animation ->
+ val fraction = animation.animatedFraction
+ val lat = startPoint.latitude + (endPoint.latitude - startPoint.latitude) * fraction
+ val lon = startPoint.longitude + (endPoint.longitude - startPoint.longitude) * fraction
+ val zoom = startZoom + (endZoom - startZoom) * fraction
+ map.controller.setCenter(GeoPoint(lat, lon))
+ map.controller.setZoom(zoom)
+ }
+ animator.start()
+
+ val recyclerView: RecyclerView = findViewById(R.id.greenwashed)
+ fetchPoints(recyclerView)
+ } else {
+ val intent = Intent(baseContext, AddBenchActivity::class.java)
+ intent.putExtra("currentLatitude", location.latitude)
+ intent.putExtra("currentLongitude", location.longitude)
+ startActivity(intent)
+ }
+ } else {
+ Toast.makeText(
+ this@MapActivity,
+ "Merci d'activer la localisation",
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ }
+ }
+
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array<String>,
+ grantResults: IntArray
+ ) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+
+ if (requestCode == locationPermissionRequest &&
+ grantResults.isNotEmpty() &&
+ grantResults[0] == PackageManager.PERMISSION_GRANTED
+ ) {
+ sendCurrentLocation()
+ } else {
+ Toast.makeText(
+ this@MapActivity,
+ "La permission d'accès à la position est requise pour ajouter un banc",
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ }
+
+ private fun calculateDistance(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Double {
+ val r = 6371
+ val dLat = Math.toRadians(lat2 - lat1)
+ val dLon = Math.toRadians(lon2 - lon1)
+ val a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
+ Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) *
+ Math.sin(dLon / 2) * Math.sin(dLon / 2)
+ val c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
+ return r * c
+ }
+
+ private fun fetchPoints(recyclerView: RecyclerView) {
+ lifecycleScope.launch {
+ pointRepository.getPoints().collectLatest { response ->
+ when (response) {
+ is ApiResponse.Loading -> {
+ bottomText.text = "Chargement..."
+ }
+
+ is ApiResponse.Success -> {
+ points = response.data.sortedBy { point ->
+ calculateDistance(latitude, longitude, point.x, point.y)
+ }
+
+ val customAdapter = CustomAdapter(points, lifecycleScope, this@MapActivity)
+ recyclerView.adapter = customAdapter
+
+ addMarkersToMap()
+ bottomText.text = "Up!"
+ }
+
+ is ApiResponse.Error -> {
+ Toast.makeText(
+ this@MapActivity,
+ "Error loading points: ${response.errorMessage}",
+ Toast.LENGTH_LONG
+ ).show()
+
+ /* val fallbackPoints = listOf(
+ Point(id = "1", x = 48.8566, y = 2.3522, type = "bench", name = "Bench 1"),
+ Point(id = "2", x = 48.8566, y = 2.3523, type = "bench", name = "Bench 2"),
+ Point(id = "3", x = 48.8566, y = 2.3524, type = "bench", name = "Bench 3"),
+ Point(id = "4", x = 48.8566, y = 2.3525, type = "bench", name = "Bench 4"),
+ Point(id = "5", x = 48.8566, y = 2.3526, type = "bench", name = "Bench 5")
+ )
+
+ points = fallbackPoints
+ val customAdapter = CustomAdapter(fallbackPoints, lifecycleScope, this@MapActivity)
+ recyclerView.adapter = customAdapter
+
+ addMarkersToMap()*/
+ bottomText.text = "Erreur bozo"
+ }
+ }
+ }
+ }
+ }
+
+ private fun addMarkersToMap() {
+ while (map.overlays.size > 2) {
+ map.overlays.removeAt(map.overlays.size - 1)
+ }
+
+ val items = ArrayList<OverlayItem>()
+ val decimalFormat = java.text.DecimalFormat("0.00")
+ val decimalFormatPopup = java.text.DecimalFormat("0.000")
+
+ for (point in points) {
+ val item = OverlayItem(
+ point.name,
+ "Type: ${point.type} | Position: ${decimalFormat.format(point.x)}, ${decimalFormat.format(point.y)}",
+ GeoPoint(point.x, point.y)
+ )
+ items.add(item)
+ }
+
+ if (items.isNotEmpty()) {
+ val overlay = ItemizedOverlayWithFocus(
+ items,
+ object : ItemizedIconOverlay.OnItemGestureListener<OverlayItem> {
+ override fun onItemSingleTapUp(index: Int, item: OverlayItem): Boolean {
+ val point = points.getOrNull(index)
+ if (point != null) {
+ lifecycleScope.launch {
+ reviewRepository.getAverageRating(point.id).collectLatest { response ->
+ when (response) {
+ is ApiResponse.Success -> {
+ val rating = response.data
+ if (!point.picture.isNullOrEmpty()) {
+ showBenchPictureDialog(
+ point, item, rating,
+ decimalFormatPopup.format(point.x),
+ decimalFormatPopup.format(point.y)
+ )
+ } else {
+ Toast.makeText(
+ this@MapActivity,
+ "${item.title}\nType: ${point.type} | Position: ${
+ decimalFormatPopup.format(
+ point.x
+ )
+ }, ${decimalFormatPopup.format(point.y)}\nRating: $rating/5",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+
+ else -> {
+ if (!point.picture.isNullOrEmpty()) {
+ showBenchPictureDialog(
+ point, item, 0f,
+ decimalFormatPopup.format(point.x),
+ decimalFormatPopup.format(point.y)
+ )
+ } else {
+ Toast.makeText(
+ this@MapActivity,
+ "${item.title}\nType: ${point.type} | Position: ${
+ decimalFormatPopup.format(
+ point.x
+ )
+ }, ${decimalFormatPopup.format(point.y)}",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+ }
+ }
+ }
+ } else {
+ Toast.makeText(
+ this@MapActivity,
+ "${item.title}\n${item.snippet}",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ return true
+ }
+
+ override fun onItemLongPress(index: Int, item: OverlayItem): Boolean {
+ return false
+ }
+ },
+ this
+ )
+
+ overlay.setFocusItemsOnTap(false)
+ map.overlays.add(overlay)
+ map.invalidate()
+ }
+ }
+
+ private fun loadMarkerImage(point: Point, item: OverlayItem) {
+ try {
+ Log.d(TAG, "Attempting to fetch image for marker: ${point.picture}")
+
+ Glide.with(this)
+ .asBitmap()
+ .load(point.picture)
+ .into(object : CustomTarget<Bitmap>(100, 100) {
+ override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
+ val drawable = resource.toDrawable(resources)
+ item.setMarker(drawable)
+ map.invalidate()
+ }
+
+ override fun onLoadCleared(placeholder: Drawable?) {
+ }
+
+ override fun onLoadFailed(errorDrawable: Drawable?) {
+ Log.e(TAG, "Echec de chargement de l'image du point ${point.id}")
+ }
+ })
+ } catch (e: Exception) {
+ Log.e(TAG, "Echec de chargement du marqueur: ${e.message}")
+ }
+ }
+
+ private fun showBenchPictureDialog(point: Point, item: OverlayItem, rating: Float, xTrunc: String, yTrunc: String) {
+ if (isDialogShowing) {
+ Log.d(TAG, "Dialogue déja ouvert!")
+ return
+ }
+
+ isDialogShowing = true
+
+ val dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_bench_details, null)
+ val dialog = android.app.AlertDialog.Builder(this)
+ .setView(dialogView)
+ .setOnDismissListener {
+ isDialogShowing = false
+ }
+ .create()
+
+ val titleTextView = dialogView.findViewById<TextView>(R.id.dialogTitle)
+ val messageTextView = dialogView.findViewById<TextView>(R.id.dialogMessage)
+ val benchImageView = dialogView.findViewById<ImageView>(R.id.benchImageView)
+
+ titleTextView.text = item.title
+
+ messageTextView.text = "Chargement des informations..."
+
+ Glide.with(this)
+ .load(point.picture)
+ .placeholder(R.drawable.ic_bench_icon)
+ .error(R.drawable.ic_bench_icon)
+ .into(benchImageView)
+
+ dialogView.findViewById<Button>(R.id.btnAddReview).setOnClickListener {
+ dialog.dismiss()
+ isDialogShowing = false
+ openReviewDialog(point)
+ }
+
+ dialogView.findViewById<Button>(R.id.btnViewReviews).setOnClickListener {
+ dialog.dismiss()
+ isDialogShowing = false
+ showReviewsDialog(point)
+ }
+
+ dialogView.findViewById<Button>(R.id.btnShowPath).setOnClickListener {
+ dialog.dismiss()
+ isDialogShowing = false
+ showPathToBench(point)
+ }
+
+ dialogView.findViewById<Button>(R.id.btnClose).setOnClickListener {
+ dialog.dismiss()
+ isDialogShowing = false
+ }
+
+ dialog.show()
+
+ lifecycleScope.launch {
+ reviewRepository.getAverageRating(point.id).collectLatest { response ->
+ when (response) {
+ is ApiResponse.Success -> {
+ val avgRating = response.data
+ messageTextView.text = "Type: ${point.type} | Position: $xTrunc, $yTrunc\nNote moyenne: ${String.format("%.1f", avgRating)}/5"
+ }
+ is ApiResponse.Error -> {
+ messageTextView.text = "Type: ${point.type} | Position: $xTrunc, $yTrunc\nRating: $rating/5"
+ Log.e(TAG, "Error fetching average rating: ${response.errorMessage}")
+ }
+ is ApiResponse.Loading -> {
+ }
+ }
+ }
+ }
+ }
+
+ private fun openReviewDialog(point: Point) {
+ val dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_review, null)
+ val ratingBar = dialogView.findViewById<RatingBar>(R.id.ratingBar)
+ val commentEditText = dialogView.findViewById<EditText>(R.id.commentEditText)
+
+ val dialog = android.app.AlertDialog.Builder(this)
+ .setTitle("Ajouter un avis pour ${point.name}")
+ .setView(dialogView)
+ .setPositiveButton("Soumettre", null)
+ .setNegativeButton("Annuler") { dialog, _ ->
+ dialog.dismiss()
+ }
+ .create()
+
+ dialog.setOnShowListener {
+ val positiveButton = dialog.getButton(android.app.AlertDialog.BUTTON_POSITIVE)
+ positiveButton.setOnClickListener {
+ val rating = ratingBar.rating.toInt()
+ val comment = commentEditText.text.toString()
+
+ if (rating == 0) {
+ Toast.makeText(this, "Veuillez donner une note", Toast.LENGTH_SHORT).show()
+ return@setOnClickListener
+ }
+
+ val review = Review(
+ id = "",
+ pointId = point.id,
+ grade = rating,
+ comment = comment.ifEmpty { null },
+ pictures = null
+ )
+
+ submitReview(point.id, review)
+ dialog.dismiss()
+ }
+ }
+
+ dialog.show()
+ }
+
+ private fun submitReview(pointId: String, review: Review) {
+ val reviewRepository = ReviewRepository()
+
+ lifecycleScope.launch {
+ reviewRepository.addReview(pointId, review).collectLatest { response ->
+ when (response) {
+ is ApiResponse.Loading -> {
+ }
+ is ApiResponse.Success -> {
+ Toast.makeText(
+ this@MapActivity,
+ "Avis soumis avec succès!",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ is ApiResponse.Error -> {
+ Toast.makeText(
+ this@MapActivity,
+ "Erreur: ${response.errorMessage}",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+ }
+ }
+ }
+
+ private fun showPathToBench(point: Point) {
+ if (ActivityCompat.checkSelfPermission(
+ this,
+ Manifest.permission.ACCESS_FINE_LOCATION
+ ) != PackageManager.PERMISSION_GRANTED
+ ) {
+ ActivityCompat.requestPermissions(
+ this,
+ arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
+ locationPermissionRequest
+ )
+ Toast.makeText(
+ this,
+ "Permission de localisation nécessaire pour afficher l'itinéraire",
+ Toast.LENGTH_LONG
+ ).show()
+ return
+ }
+
+ Toast.makeText(
+ this,
+ "Calcul de l'itinéraire en cours...",
+ Toast.LENGTH_SHORT
+ ).show()
+
+ locationClient.lastLocation.addOnSuccessListener { location ->
+ if (location != null) {
+ val roadManager = OSRMRoadManager(this, "OsmDroid/6.1.0")
+ roadManager.setMean(OSRMRoadManager.MEAN_BY_FOOT)
+ lifecycleScope.launch {
+ try {
+ for (overlay in map.overlays) {
+ if (overlay is Polyline) {
+ map.overlays.remove(overlay)
+ }
+ }
+
+ val waypoints = ArrayList<GeoPoint>()
+ waypoints.add(GeoPoint(location.latitude, location.longitude))
+ waypoints.add(GeoPoint(point.x, point.y))
+
+ val road = roadManager.getRoad(waypoints)
+
+ val roadOverlay = RoadManager.buildRoadOverlay(road)
+ roadOverlay.outlinePaint.color = android.graphics.Color.BLUE
+ roadOverlay.outlinePaint.strokeWidth = 10f
+
+ map.overlays.add(roadOverlay)
+
+ val bounds = road.mBoundingBox
+ map.zoomToBoundingBox(bounds, true, 100)
+
+ map.invalidate()
+
+ val distance = road.mLength
+ val duration = road.mDuration / 60
+
+ Toast.makeText(
+ this@MapActivity,
+ "Distance: ${String.format("%.2f", distance)} km\nDurée: ${String.format("%.0f", duration)} min",
+ Toast.LENGTH_LONG
+ ).show()
+ } catch (e: Exception) {
+ Log.e(TAG, "Error calculating route: ${e.message}")
+ Toast.makeText(
+ this@MapActivity,
+ "Erreur lors du calcul de l'itinéraire: ${e.message}",
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ }
+ } else {
+ Toast.makeText(
+ this@MapActivity,
+ "Impossible d'obtenir votre position actuelle",
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ }
+ }
+
+ private fun showReviewsDialog(point: Point) {
+ val dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_reviews_list, null)
+ val recyclerView = dialogView.findViewById<RecyclerView>(R.id.reviewsRecyclerView)
+ val averageRatingText = dialogView.findViewById<TextView>(R.id.averageRatingText)
+ val noReviewsText = dialogView.findViewById<TextView>(R.id.noReviewsText)
+ val reviewsTitle = dialogView.findViewById<TextView>(R.id.reviewsTitle)
+
+ reviewsTitle.text = "Avis pour ${point.name}"
+
+ recyclerView.layoutManager = LinearLayoutManager(this)
+ val adapter = ReviewAdapter(emptyList())
+ recyclerView.adapter = adapter
+
+ val dialog = android.app.AlertDialog.Builder(this)
+ .setView(dialogView)
+ .setPositiveButton("Fermer", null)
+ .create()
+
+ dialog.show()
+
+ lifecycleScope.launch {
+ reviewRepository.getReviews(point.id).collectLatest { response ->
+ when (response) {
+ is ApiResponse.Loading -> {
+ }
+ is ApiResponse.Success -> {
+ val reviews = response.data
+ if (reviews.isEmpty()) {
+ recyclerView.visibility = View.GONE
+ noReviewsText.visibility = View.VISIBLE
+ } else {
+ recyclerView.visibility = View.VISIBLE
+ noReviewsText.visibility = View.GONE
+ adapter.updateReviews(reviews)
+ }
+
+ reviewRepository.getAverageRating(point.id).collectLatest { ratingResponse ->
+ when (ratingResponse) {
+ is ApiResponse.Success -> {
+ val avgRating = ratingResponse.data
+ averageRatingText.text = "Note moyenne: ${String.format("%.1f", avgRating)}/5"
+ }
+ else -> {
+ }
+ }
+ }
+ }
+ is ApiResponse.Error -> {
+ Toast.makeText(
+ this@MapActivity,
+ "Erreur: ${response.errorMessage}",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+ }
+ }
+ }
+
+
+ override fun onResume() {
+ super.onResume()
+ map.onResume()
+
+ val recyclerView: RecyclerView = findViewById(R.id.greenwashed)
+ fetchPoints(recyclerView)
+ }
+
+ override fun onPause() {
+ super.onPause()
+ map.onPause()
+ }
+
+ /*override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ val permissionsToRequest = ArrayList<String>()
+ var i = 0
+ while (i < grantResults.size) {
+ permissionsToRequest.add(permissions[i])
+ i++
+ }
+ if (permissionsToRequest.size > 0) {
+ ActivityCompat.requestPermissions(
+ this,
+ permissionsToRequest.toTypedArray(),
+ REQUEST_PERMISSIONS_REQUEST_CODE)
+ }
+ }*/
+
+
+ /*private fun requestPermissionsIfNecessary(String[] permissions) {
+ val permissionsToRequest = ArrayList<String>();
+ permissions.forEach { permission ->
+ if (ContextCompat.checkSelfPermission(this, permission)
+ != PackageManager.PERMISSION_GRANTED) {
+ permissionsToRequest.add(permission);
+ }
+ }
+ if (permissionsToRequest.size() > 0) {
+ ActivityCompat.requestPermissions(
+ this,
+ permissionsToRequest.toArray(new String[0]),
+ REQUEST_PERMISSIONS_REQUEST_CODE);
+ }
+ }*/
+}
diff --git a/benchmark/app/src/main/java/io/trentetroim/benchmark/ReviewAdapter.kt b/benchmark/app/src/main/java/io/trentetroim/benchmark/ReviewAdapter.kt
new file mode 100644
index 0000000..2dffb3a
--- /dev/null
+++ b/benchmark/app/src/main/java/io/trentetroim/benchmark/ReviewAdapter.kt
@@ -0,0 +1,37 @@
+package io.trentetroim.benchmark
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.RatingBar
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import io.trentetroim.benchmark.api.models.Review
+
+class ReviewAdapter(private var reviews: List<Review>) :
+ RecyclerView.Adapter<ReviewAdapter.ViewHolder>() {
+
+ class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+ val ratingBar: RatingBar = view.findViewById(R.id.itemRatingBar)
+ val commentText: TextView = view.findViewById(R.id.itemCommentText)
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.item_review, parent, false)
+ return ViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val review = reviews[position]
+ holder.ratingBar.rating = review.grade.toFloat()
+ holder.commentText.text = review.comment ?: "Pas de commentaire"
+ }
+
+ override fun getItemCount() = reviews.size
+
+ fun updateReviews(newReviews: List<Review>) {
+ reviews = newReviews
+ notifyDataSetChanged()
+ }
+} \ No newline at end of file
diff --git a/benchmark/app/src/main/java/io/trentetroim/benchmark/api/models/ApiResponse.kt b/benchmark/app/src/main/java/io/trentetroim/benchmark/api/models/ApiResponse.kt
new file mode 100644
index 0000000..a3dcd56
--- /dev/null
+++ b/benchmark/app/src/main/java/io/trentetroim/benchmark/api/models/ApiResponse.kt
@@ -0,0 +1,16 @@
+package io.trentetroim.benchmark.api.models
+
+sealed class ApiResponse<out T> {
+ data class Success<out T>(val data: T) : ApiResponse<T>()
+ data class Error(val errorMessage: String, val code: Int? = null) : ApiResponse<Nothing>()
+ object Loading : ApiResponse<Nothing>()
+}
+
+
+data class PointsResponse(
+ val points: List<Point>
+)
+
+data class ReviewsResponse(
+ val reviews: List<Review>
+) \ No newline at end of file
diff --git a/benchmark/app/src/main/java/io/trentetroim/benchmark/api/models/Point.kt b/benchmark/app/src/main/java/io/trentetroim/benchmark/api/models/Point.kt
new file mode 100644
index 0000000..123300f
--- /dev/null
+++ b/benchmark/app/src/main/java/io/trentetroim/benchmark/api/models/Point.kt
@@ -0,0 +1,24 @@
+package io.trentetroim.benchmark.api.models
+
+import com.google.gson.annotations.SerializedName
+
+
+data class Point(
+ @SerializedName("id")
+ val id: String,
+
+ @SerializedName("x")
+ val x: Double,
+
+ @SerializedName("y")
+ val y: Double,
+
+ @SerializedName("type")
+ val type: String,
+
+ @SerializedName("name")
+ val name: String,
+
+ @SerializedName("picture")
+ val picture: String? = null,
+) \ No newline at end of file
diff --git a/benchmark/app/src/main/java/io/trentetroim/benchmark/api/models/Review.kt b/benchmark/app/src/main/java/io/trentetroim/benchmark/api/models/Review.kt
new file mode 100644
index 0000000..6539608
--- /dev/null
+++ b/benchmark/app/src/main/java/io/trentetroim/benchmark/api/models/Review.kt
@@ -0,0 +1,20 @@
+package io.trentetroim.benchmark.api.models
+
+import com.google.gson.annotations.SerializedName
+
+data class Review(
+ @SerializedName("id")
+ val id: String,
+
+ @SerializedName("pointId")
+ val pointId: String,
+
+ @SerializedName("grade")
+ val grade: Int,
+
+ @SerializedName("comment")
+ val comment: String? = null,
+
+ @SerializedName("pictures")
+ val pictures: List<String>? = null
+) \ No newline at end of file
diff --git a/benchmark/app/src/main/java/io/trentetroim/benchmark/api/repository/PointRepository.kt b/benchmark/app/src/main/java/io/trentetroim/benchmark/api/repository/PointRepository.kt
new file mode 100644
index 0000000..ad404a0
--- /dev/null
+++ b/benchmark/app/src/main/java/io/trentetroim/benchmark/api/repository/PointRepository.kt
@@ -0,0 +1,164 @@
+package io.trentetroim.benchmark.api.repository
+
+import android.content.Context
+import android.net.Uri
+import android.util.Log
+import io.trentetroim.benchmark.api.models.ApiResponse
+import io.trentetroim.benchmark.api.models.Point
+import io.trentetroim.benchmark.api.models.Review
+import io.trentetroim.benchmark.api.service.RetrofitClient
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOn
+import okhttp3.MediaType.Companion.toMediaTypeOrNull
+import okhttp3.MultipartBody
+import okhttp3.RequestBody.Companion.asRequestBody
+import java.io.File
+import java.io.FileOutputStream
+
+class PointRepository {
+ private val apiService = RetrofitClient.apiService
+
+ fun getPoints(type: String? = null): Flow<ApiResponse<List<Point>>> = flow {
+ emit(ApiResponse.Loading)
+ try {
+ val response = apiService.getPoints(type)
+ if (response.isSuccessful) {
+ response.body()?.let {
+ emit(ApiResponse.Success(it))
+ } ?: emit(ApiResponse.Error("Empty response body"))
+ } else {
+ emit(ApiResponse.Error("Error: ${response.code()} ${response.message()}"))
+ }
+ } catch (e: Exception) {
+ emit(ApiResponse.Error(e.message ?: "Unknown error"))
+ }
+ }.flowOn(Dispatchers.IO)
+
+ fun getPointById(id: String): Flow<ApiResponse<Point>> = flow {
+ emit(ApiResponse.Loading)
+ try {
+ val response = apiService.getPointById(id)
+ if (response.isSuccessful) {
+ response.body()?.let {
+ emit(ApiResponse.Success(it))
+ } ?: emit(ApiResponse.Error("Empty response body"))
+ } else {
+ emit(ApiResponse.Error("Error: ${response.code()} ${response.message()}"))
+ }
+ } catch (e: Exception) {
+ emit(ApiResponse.Error(e.message ?: "Unknown error"))
+ }
+ }.flowOn(Dispatchers.IO)
+
+
+ fun getReviews(pointId: String): Flow<ApiResponse<List<Review>>> = flow {
+ emit(ApiResponse.Loading)
+ try {
+ val response = apiService.getReviews(pointId)
+ if (response.isSuccessful) {
+ response.body()?.let {
+ emit(ApiResponse.Success(it))
+ } ?: emit(ApiResponse.Error("Empty response body"))
+ } else {
+ emit(ApiResponse.Error("Error: ${response.code()} ${response.message()}"))
+ }
+ } catch (e: Exception) {
+ emit(ApiResponse.Error(e.message ?: "Unknown error"))
+ }
+ }.flowOn(Dispatchers.IO)
+
+ fun addReview(pointId: String, review: Review): Flow<ApiResponse<Review>> = flow {
+ emit(ApiResponse.Loading)
+ try {
+ val response = apiService.addReview(pointId, review)
+ if (response.isSuccessful) {
+ response.body()?.let {
+ emit(ApiResponse.Success(it))
+ } ?: emit(ApiResponse.Error("Empty response body"))
+ } else {
+ emit(ApiResponse.Error("Error: ${response.code()} ${response.message()}"))
+ }
+ } catch (e: Exception) {
+ emit(ApiResponse.Error(e.message ?: "Unknown error"))
+ }
+ }.flowOn(Dispatchers.IO)
+
+ fun createPoint(point: Point): Flow<ApiResponse<Point>> = flow {
+ emit(ApiResponse.Loading)
+
+ Log.d("PointRepository", "Creating point: $point")
+
+ try {
+ val response = apiService.createPoint(point)
+
+ Log.d("PointRepository", "Response: ${response.code()} ${response.message()}")
+
+ if (response.isSuccessful) {
+ response.body()?.let {
+ emit(ApiResponse.Success(it))
+ } ?: emit(ApiResponse.Error("Empty response body"))
+ } else {
+ emit(ApiResponse.Error("Error: ${response.code()} ${response.message()}"))
+ }
+ } catch (e: Exception) {
+ emit(ApiResponse.Error(e.message ?: "Unknown error"))
+ }
+ }.flowOn(Dispatchers.IO)
+
+ fun uploadImage(context: Context, imageUri: Uri): Flow<ApiResponse<String>> = flow {
+ emit(ApiResponse.Loading)
+
+ try {
+ val file = createTempFileFromUri(context, imageUri)
+
+ if (file == null) {
+ emit(ApiResponse.Error("Failed to create file from URI"))
+ return@flow
+ }
+
+ if (file.length() > 8 * 1024 * 1024) {
+ emit(ApiResponse.Error("File size exceeds 8MB limit"))
+ return@flow
+ }
+ val requestBody = file.asRequestBody("image/*".toMediaTypeOrNull())
+ val part = MultipartBody.Part.createFormData("picture", file.name, requestBody)
+
+ val response = apiService.uploadImage(part)
+
+ if (response.isSuccessful) {
+ response.body()?.let { responseMap ->
+ val url = responseMap["url"]
+ if (url != null) {
+ emit(ApiResponse.Success(url))
+ } else {
+ emit(ApiResponse.Error("URL not found in response"))
+ }
+ } ?: emit(ApiResponse.Error("Empty response body"))
+ } else {
+ emit(ApiResponse.Error("Error: ${response.code()} ${response.message()}"))
+ }
+ } catch (e: Exception) {
+ emit(ApiResponse.Error(e.message ?: "Unknown error"))
+ }
+ }.flowOn(Dispatchers.IO)
+
+ private fun createTempFileFromUri(context: Context, uri: Uri): File? {
+ return try {
+ val inputStream = context.contentResolver.openInputStream(uri)
+ val tempFile = File.createTempFile("upload", ".jpg", context.cacheDir)
+
+ inputStream?.use { input ->
+ FileOutputStream(tempFile).use { output ->
+ input.copyTo(output)
+ }
+ }
+
+ tempFile
+ } catch (e: Exception) {
+ Log.e("PointRepository", "Error creating temp file: ${e.message}")
+ null
+ }
+ }
+}
diff --git a/benchmark/app/src/main/java/io/trentetroim/benchmark/api/repository/ReviewRepository.kt b/benchmark/app/src/main/java/io/trentetroim/benchmark/api/repository/ReviewRepository.kt
new file mode 100644
index 0000000..4636239
--- /dev/null
+++ b/benchmark/app/src/main/java/io/trentetroim/benchmark/api/repository/ReviewRepository.kt
@@ -0,0 +1,69 @@
+package io.trentetroim.benchmark.api.repository
+
+import android.util.Log
+import io.trentetroim.benchmark.api.models.ApiResponse
+import io.trentetroim.benchmark.api.models.Review
+import io.trentetroim.benchmark.api.service.RetrofitClient
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOn
+
+class ReviewRepository {
+ private val apiService = RetrofitClient.apiService
+ private val TAG = "ReviewRepository"
+
+ fun getReviews(pointId: String): Flow<ApiResponse<List<Review>>> = flow {
+ emit(ApiResponse.Loading)
+ try {
+ val response = apiService.getReviews(pointId)
+ if (response.isSuccessful) {
+ response.body()?.let {
+ emit(ApiResponse.Success(it))
+ } ?: emit(ApiResponse.Error("Empty response body"))
+ } else {
+ emit(ApiResponse.Error("Error: ${response.code()} ${response.message()}"))
+ }
+ } catch (e: Exception) {
+ emit(ApiResponse.Error(e.message ?: "Unknown error"))
+ }
+ }.flowOn(Dispatchers.IO)
+
+ fun addReview(pointId: String, review: Review): Flow<ApiResponse<Review>> = flow {
+ emit(ApiResponse.Loading)
+ try {
+ val response = apiService.addReview(pointId, review)
+ if (response.isSuccessful) {
+ response.body()?.let {
+ emit(ApiResponse.Success(it))
+ } ?: emit(ApiResponse.Error("Empty response body"))
+ } else {
+ emit(ApiResponse.Error("Error: ${response.code()} ${response.message()}"))
+ }
+ } catch (e: Exception) {
+ emit(ApiResponse.Error(e.message ?: "Unknown error"))
+ }
+ }.flowOn(Dispatchers.IO)
+
+ fun getAverageRating(pointId: String): Flow<ApiResponse<Float>> = flow {
+ emit(ApiResponse.Loading)
+ try {
+ val response = apiService.getReviews(pointId)
+ if (response.isSuccessful) {
+ response.body()?.let { reviews ->
+ if (reviews.isEmpty()) {
+ emit(ApiResponse.Success(0f))
+ } else {
+ val averageRating = reviews.map { it.grade }.average().toFloat()
+ Log.d(TAG, "Average rating for point $pointId: $averageRating")
+ emit(ApiResponse.Success(averageRating))
+ }
+ } ?: emit(ApiResponse.Error("Empty response body"))
+ } else {
+ emit(ApiResponse.Error("Error: ${response.code()} ${response.message()}"))
+ }
+ } catch (e: Exception) {
+ emit(ApiResponse.Error(e.message ?: "Unknown error"))
+ }
+ }.flowOn(Dispatchers.IO)
+} \ No newline at end of file
diff --git a/benchmark/app/src/main/java/io/trentetroim/benchmark/api/service/ApiService.kt b/benchmark/app/src/main/java/io/trentetroim/benchmark/api/service/ApiService.kt
new file mode 100644
index 0000000..91e8c24
--- /dev/null
+++ b/benchmark/app/src/main/java/io/trentetroim/benchmark/api/service/ApiService.kt
@@ -0,0 +1,38 @@
+package io.trentetroim.benchmark.api.service
+
+import io.trentetroim.benchmark.api.models.Point
+import io.trentetroim.benchmark.api.models.Review
+import okhttp3.MultipartBody
+import retrofit2.Response
+import retrofit2.http.*
+
+interface ApiService {
+
+ @GET("/api/points")
+ suspend fun getPoints(@Query("type") type: String? = null): Response<List<Point>>
+
+
+ @GET("/api/points/{id}")
+ suspend fun getPointById(@Path("id") id: String): Response<Point>
+
+ @POST("/api/points")
+ suspend fun createPoint(@Body point: Point): Response<Point>
+
+
+ @GET("/api/points/{id}/reviews")
+ suspend fun getReviews(@Path("id") pointId: String): Response<List<Review>>
+
+
+ @POST("/api/points/{id}/reviews")
+ suspend fun addReview(
+ @Path("id") pointId: String,
+ @Body review: Review
+ ): Response<Review>
+
+ @Multipart
+ @POST("/api/upload")
+ suspend fun uploadImage(
+ @Part image: MultipartBody.Part
+ ): Response<Map<String, String>>
+
+}
diff --git a/benchmark/app/src/main/java/io/trentetroim/benchmark/api/service/RetrofitClient.kt b/benchmark/app/src/main/java/io/trentetroim/benchmark/api/service/RetrofitClient.kt
new file mode 100644
index 0000000..7185d3c
--- /dev/null
+++ b/benchmark/app/src/main/java/io/trentetroim/benchmark/api/service/RetrofitClient.kt
@@ -0,0 +1,43 @@
+package io.trentetroim.benchmark.api.service
+
+import com.google.gson.GsonBuilder
+import okhttp3.OkHttpClient
+import okhttp3.logging.HttpLoggingInterceptor
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+import java.util.concurrent.TimeUnit
+
+
+object RetrofitClient {
+ private const val BASE_URL = "http://89.168.39.144:5431"
+ private const val TIMEOUT = 30L
+
+ private fun createOkHttpClient(): OkHttpClient {
+ val loggingInterceptor = HttpLoggingInterceptor().apply {
+ level = HttpLoggingInterceptor.Level.BODY
+ }
+
+ return OkHttpClient.Builder()
+ .addInterceptor(loggingInterceptor)
+ .connectTimeout(TIMEOUT, TimeUnit.SECONDS)
+ .readTimeout(TIMEOUT, TimeUnit.SECONDS)
+ .writeTimeout(TIMEOUT, TimeUnit.SECONDS)
+ .build()
+ }
+
+ private val retrofit: Retrofit by lazy {
+ val gson = GsonBuilder()
+ .setLenient()
+ .create()
+
+ Retrofit.Builder()
+ .baseUrl(BASE_URL)
+ .client(createOkHttpClient())
+ .addConverterFactory(GsonConverterFactory.create(gson))
+ .build()
+ }
+
+ val apiService: ApiService by lazy {
+ retrofit.create(ApiService::class.java)
+ }
+}
diff --git a/benchmark/app/src/main/java/io/trentetroim/benchmark/ui/theme/Color.kt b/benchmark/app/src/main/java/io/trentetroim/benchmark/ui/theme/Color.kt
new file mode 100644
index 0000000..d85d9b7
--- /dev/null
+++ b/benchmark/app/src/main/java/io/trentetroim/benchmark/ui/theme/Color.kt
@@ -0,0 +1,17 @@
+package io.trentetroim.benchmark.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val Purple80 = Color(0xFFD0BCFF)
+val PurpleGrey80 = Color(0xFFCCC2DC)
+val Pink80 = Color(0xFFEFB8C8)
+
+val Purple40 = Color(0xFF6650a4)
+val PurpleGrey40 = Color(0xFF625b71)
+val White = Color(0xFFFFFFFF)
+val Pink40 = Color(0xFF7D5260)
+val Salmon = Color(0xFF8C69FF)
+val Grass = Color(0xff81c9b5)
+val Lagoon = Color(0xff90d8c4)
+val Tildeeth = Color(0xff99004d)
+val TildeethAlpha = Color(0x7799004d) \ No newline at end of file
diff --git a/benchmark/app/src/main/java/io/trentetroim/benchmark/ui/theme/Theme.kt b/benchmark/app/src/main/java/io/trentetroim/benchmark/ui/theme/Theme.kt
new file mode 100644
index 0000000..cf2034d
--- /dev/null
+++ b/benchmark/app/src/main/java/io/trentetroim/benchmark/ui/theme/Theme.kt
@@ -0,0 +1,55 @@
+package io.trentetroim.benchmark.ui.theme
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.*
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+
+private val DarkColorScheme = darkColorScheme(
+ primary = Purple80,
+ secondary = PurpleGrey80,
+ tertiary = Pink80,
+ background = Grass
+)
+
+private val LightColorScheme = lightColorScheme(
+ primary = Purple40,
+ secondary = PurpleGrey40,
+ tertiary = Pink40,
+ background = Lagoon,
+
+// Other default colors to override
+ surface = Color(0xFFFFFBFE),
+ onPrimary = Color.White,
+ onSecondary = Color.White,
+ onTertiary = Color.White,
+ onBackground = Color(0xFF1C1B1F),
+ onSurface = Color(0xFF1C1B1F),
+)
+
+@Composable
+fun BenchmarkTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ // Dynamic color is available on Android 12+
+ dynamicColor: Boolean = true,
+ content: @Composable () -> Unit
+) {
+ val colorScheme = when {
+ dynamicColor -> {
+ val context = LocalContext.current
+ if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(
+ context
+ )
+ }
+
+ darkTheme -> DarkColorScheme
+ else -> LightColorScheme
+ }
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = Typography,
+ content = content
+ )
+} \ No newline at end of file
diff --git a/benchmark/app/src/main/java/io/trentetroim/benchmark/ui/theme/Type.kt b/benchmark/app/src/main/java/io/trentetroim/benchmark/ui/theme/Type.kt
new file mode 100644
index 0000000..b731516
--- /dev/null
+++ b/benchmark/app/src/main/java/io/trentetroim/benchmark/ui/theme/Type.kt
@@ -0,0 +1,36 @@
+package io.trentetroim.benchmark.ui.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+import io.trentetroim.benchmark.R
+
+// Set of Material typography styles to start with
+val Typography = Typography(
+ bodyLarge = TextStyle(
+ fontFamily = FontFamily(Font(resId = R.font.nokiafc22, weight = FontWeight.Normal)),
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp
+ ),
+ /* Other default text styles to override
+ titleLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 22.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.sp
+ ),
+ */
+ labelLarge = TextStyle(
+ fontFamily = FontFamily(Font(resId = R.font.nokiafc22, weight = FontWeight.Medium)),
+ fontWeight = FontWeight.Medium,
+ fontSize = 11.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp
+ )
+) \ No newline at end of file