Skip to content
Snippets Groups Projects
Unverified Commit d69cd3a9 authored by AC5636's avatar AC5636 :ghost:
Browse files

Add custom info window

parent 585ac4be
No related branches found
No related tags found
No related merge requests found
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
......
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
</profile>
</component>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
......
package com.example.e10favouritecities
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
// https://ptm.fi/materials/golfcourses/golf_courses.json - Golf courses response url
val golfCoursesApiBaseURL = "https://ptm.fi/materials/golfcourses/"
private val apiRetro = Retrofit.Builder()
.baseUrl(golfCoursesApiBaseURL)
.addConverterFactory(GsonConverterFactory.create())
.build()
val GolfCoursesApiService = apiRetro.create(ApiService::class.java)
interface ApiService {
@GET("golf_courses.json")
suspend fun getGolfCourses(): GolfCoursesResponse
}
package com.example.e10favouritecities
import android.util.Log
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.android.gms.maps.model.LatLng
import kotlinx.coroutines.launch
class GolfCoursesViewModel: ViewModel() {
data class GolfCoursesState(
val isLoading: Boolean = false,
val allGolfCourses: List<Course> = emptyList(),
val error: String? = null
)
private val _golfCoursesState = mutableStateOf(GolfCoursesState())
val state: State<GolfCoursesState> = _golfCoursesState
init {
if (!_golfCoursesState.value.isLoading) fetchGolfCourses()
}
private fun updateAllGolfCoursesState(allGolfCourses: List<Course> = emptyList(), error: String? = null){
if (error == null) {
_golfCoursesState.value = _golfCoursesState.value.copy(
isLoading = false,
allGolfCourses = allGolfCourses,
error = null
)
} else{
_golfCoursesState.value = _golfCoursesState.value.copy(
isLoading = false,
allGolfCourses = emptyList(),
error = error
)
}
}
private fun fetchGolfCourses(){
viewModelScope.launch {
try{
_golfCoursesState.value = _golfCoursesState.value.copy(isLoading = true)
val response = GolfCoursesApiService.getGolfCourses()
updateAllGolfCoursesState(allGolfCourses = response.courses)
} catch(e: Exception){
updateAllGolfCoursesState(error="Error occurred while fetching data: ${e.message}")
} finally {
_golfCoursesState.value = _golfCoursesState.value.copy(isLoading = false)
}
}
}
}
data class GolfCoursesResponse(
val info: String,
val courses: List<Course>
)
data class Course(
val lat: Double,
val lng: Double,
val type: String,
val course: String,
val address: String,
val phone: String,
val email: String,
val web: String,
val image: String,
val text: String
){
fun getPosition(): LatLng {
return LatLng(lat, lng)
}
fun getImageURL(): String {
println("${golfCoursesApiBaseURL}${image}")
return "${golfCoursesApiBaseURL}${image}"
}
}
// Sample api response
//{
// "info": "SGKY:N JÄSENKENTÄT 2016",
// "courses": [
// {
// "type": "Kulta",
// "lat": 62.2653926,
// "lng": 22.6415612,
// "course": "Alastaro Golf",
// "address": "Golfkentäntie 195, 32560 Virttaa",
// "phone": "(02) 724 7824",
// "email": "minna.nenonen@alastarogolf.fi",
// "web": "http://alastarogolf.fi/",
// "image": "kuvat/kulta.jpg",
// "text": "Alastaro Golfin Virttaankankaan golfkenttä kunnioittaa alustansa puolesta perinteitä. Virttaan kenttä on alkuperäisten merenranta-linksien tavoin kokonaan hiekkapohjainen. Pelaaminen on säästä riippumatta miellyttävää, kun pallo rullaa väylillä ja kentän pohja imee sateet nopeasti sisäänsä."
// },
// {
// "type": "Kulta/Etu",
// "lat": 60.3113719,
// "lng": 22.2653926,
// "course": "Archipelagia Golf Oy",
// "address": "Finbyntie 87, 21600 Parainen",
// "phone": "(02) 458 2001",
// "email": "nina.katajainen@argc.fi",
// "web": "http://www.archipelagiagolf.fi/fi/www/",
// "image": "kuvat/kulta.jpg",
// "text": "Paraisten kenttä täyttää kaikki vaatimukset mitä odotetaan hyvältä 18-väyläiseltä golfkentältä. Sopivasti vettä ja hiekkaa kierroksen aikana.... tekevät pelistä mielenkiintoisen ja haastavan!"
// },
// ]
//}
package com.example.e10favouritecities
import android.content.Context
import android.graphics.Bitmap
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.annotation.DrawableRes
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.rememberNavController
import coil.compose.AsyncImage
import coil.compose.SubcomposeAsyncImage
import coil.compose.rememberAsyncImagePainter
import coil.request.ImageRequest
import com.example.e10favouritecities.ui.theme.E10FavouriteCitiesTheme
import com.google.android.gms.maps.model.BitmapDescriptor
import com.google.android.gms.maps.model.BitmapDescriptorFactory
import com.google.android.gms.maps.model.CameraPosition
import com.google.android.gms.maps.model.LatLng
import com.google.maps.android.compose.GoogleMap
import com.google.maps.android.compose.Marker
import com.google.maps.android.compose.MarkerInfoWindow
import com.google.maps.android.compose.MarkerState
import com.google.maps.android.compose.rememberCameraPositionState
import compose.icons.TablerIcons
import compose.icons.tablericons.ExternalLink
import compose.icons.tablericons.MapPin
import compose.icons.tablericons.PhoneOutgoing
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val appViewModel: GolfCoursesViewModel = viewModel()
val appState by appViewModel.state
E10FavouriteCitiesTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
App()
App(appState.allGolfCourses)
}
}
}
......@@ -36,37 +84,118 @@ class MainActivity : ComponentActivity() {
}
@Composable
fun App() {
val markerPositions = listOf(
MarkerData(LatLng(62.241046, 25.750684), "Jyvaskyla", "Marker in Jyvaskyla"),
MarkerData(LatLng(60.167464, 24.938049), "Helsinki", "Marker in Helsinki"),
MarkerData(LatLng(60.426297, 22.277340), "Turku", "Marker in Turku"),
MarkerData(LatLng(61.486884, 23.779909), "Tampere", "Marker in Tampere"),
MarkerData(LatLng(63.076507, 21.752947), "Vaasa", "Marker in Vaasa"),
MarkerData(LatLng(64.985984, 25.527193), "Oulu", "Marker in Oulu"),
MarkerData(LatLng(62.890296, 27.655160), "Kuopio", "Marker in Kuopio"),
)
fun App(golfCourses: List<Course>) {
var configuration = LocalConfiguration.current
var camPos = LatLng(62.241046, 25.750684)
if (golfCourses.isNotEmpty()) camPos = golfCourses[0].getPosition()
val cameraPositionState = rememberCameraPositionState {
position = CameraPosition.fromLatLngZoom(markerPositions[0].position, 5.5f)
position = CameraPosition.fromLatLngZoom(camPos, 5.5f)
}
GoogleMap(
modifier = Modifier.fillMaxSize(),
cameraPositionState = cameraPositionState
){
for (marker in markerPositions){
Marker(
state = MarkerState(position = marker.position),
title = marker.title,
snippet = marker.snippet
)
) {
for (course in golfCourses) {
val painter = rememberAsyncImagePainter(model = course.getImageURL())
MarkerInfoWindow(
state = MarkerState(position = course.getPosition()),
title = course.course,
snippet = course.text,
) {
OutlinedCard(modifier = Modifier
.fillMaxWidth(((configuration.screenWidthDp * 0.85) / configuration.screenWidthDp).toFloat())
.heightIn(max = (configuration.screenHeightDp * 0.75).dp)
) {
Column(
modifier = Modifier
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
// Image(painter = rememberAsyncImagePainter(model = course.getImageURL()), contentDescription = null)
Text(
text = "${course.course} - ${course.type}",
style = MaterialTheme.typography.titleLarge,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 6.dp)
)
Column {
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
Icon(
imageVector = TablerIcons.PhoneOutgoing,
contentDescription = null
)
Text(text = course.phone)
}
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
Icon(imageVector = TablerIcons.MapPin, contentDescription = null)
Text(text = course.address)
}
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
Icon(
imageVector = TablerIcons.ExternalLink,
contentDescription = null
)
Text(text = course.web)
}
}
Text(
text = course.text,
modifier = Modifier
.fillMaxWidth()
.verticalScroll(
rememberScrollState()
)
)
}
}
}
}
}
}
data class MarkerData(
val position: LatLng,
val title: String,
val snippet: String
)
\ No newline at end of file
@Composable
fun MapMarker(
context: Context,
position: LatLng,
title: String,
snippet: String,
@DrawableRes iconResourceId: Int
) {
val icon = bitmapDescriptorFromVector(
context, iconResourceId
)
Marker(
state = MarkerState(position = position),
title = title,
snippet = snippet,
icon = icon,
)
}
fun bitmapDescriptorFromVector(
context: Context,
vectorResId: Int
): BitmapDescriptor? {
// retrieve the actual drawable
val drawable = ContextCompat.getDrawable(context, vectorResId) ?: return null
drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
val bm = Bitmap.createBitmap(
drawable.intrinsicWidth,
drawable.intrinsicHeight,
Bitmap.Config.ARGB_8888
)
// draw it onto the bitmap
val canvas = android.graphics.Canvas(bm)
drawable.draw(canvas)
return BitmapDescriptorFactory.fromBitmap(bm)
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment