Skip to content

Commit c28ded7

Browse files
committed
#42 feat: tap latitude/longitude header to copy coordinates to clipboard
Signed-off-by: sds100 <[email protected]>
1 parent 10dafd5 commit c28ded7

File tree

5 files changed

+126
-25
lines changed

5 files changed

+126
-25
lines changed

app/src/androidTest/java/com/mapcode/map/MapScreenTest.kt

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ class MapScreenTest {
7272
}
7373

7474
@Test
75-
fun show_snack_bar_when_copying_mapcode() {
75+
fun show_snackbar_when_copying_mapcode() {
7676
useCase.knownLocations.add(
7777
FakeLocation(
7878
0.0, 0.0, addresses = emptyList(), mapcodes = listOf(Mapcode("AB.XY", Territory.AAA))
@@ -358,7 +358,7 @@ class MapScreenTest {
358358
}
359359

360360
@Test
361-
fun show_snack_bar_if_fail_to_get_current_location() {
361+
fun show_snackbar_if_fail_to_get_current_location() {
362362
useCase.currentLocation = null
363363
setMapScreenAsContent()
364364

@@ -599,6 +599,30 @@ class MapScreenTest {
599599
}
600600
}
601601

602+
@Test
603+
fun copy_location_to_clipboard_when_tapping_latitude_header() {
604+
setMapScreenAsContent()
605+
viewModel.onCameraMoved(1.0, 2.0, 1f)
606+
607+
composeTestRule.onNode(
608+
hasText("Latitude (Y)").and(hasTestTag("latlngtextfield")),
609+
useUnmergedTree = true
610+
).performClick()
611+
assertThat(useCase.clipboard).isEqualTo("1.0,2.0")
612+
}
613+
614+
@Test
615+
fun copy_location_to_clipboard_when_tapping_longitude_header() {
616+
setMapScreenAsContent()
617+
viewModel.onCameraMoved(1.0, 2.0, 1f)
618+
619+
composeTestRule.onNode(
620+
hasText("Longitude (X)").and(hasTestTag("latlngtextfield")),
621+
useUnmergedTree = true
622+
).performClick()
623+
assertThat(useCase.clipboard).isEqualTo("1.0,2.0")
624+
}
625+
602626
private fun setMapScreenAsContent() {
603627
composeTestRule.setContent {
604628
MapScreen(viewModel = viewModel, renderGoogleMaps = false)

app/src/main/java/com/mapcode/map/InfoScreenPart.kt

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import androidx.annotation.DrawableRes
88
import androidx.compose.foundation.ExperimentalFoundationApi
99
import androidx.compose.foundation.clickable
1010
import androidx.compose.foundation.layout.*
11+
import androidx.compose.foundation.text.ClickableText
1112
import androidx.compose.foundation.text.KeyboardActions
1213
import androidx.compose.foundation.text.KeyboardOptions
1314
import androidx.compose.material.*
@@ -19,6 +20,7 @@ import androidx.compose.ui.focus.FocusRequester
1920
import androidx.compose.ui.focus.focusRequester
2021
import androidx.compose.ui.focus.onFocusChanged
2122
import androidx.compose.ui.platform.LocalFocusManager
23+
import androidx.compose.ui.platform.testTag
2224
import androidx.compose.ui.res.painterResource
2325
import androidx.compose.ui.res.stringResource
2426
import androidx.compose.ui.text.SpanStyle
@@ -32,27 +34,29 @@ import androidx.compose.ui.tooling.preview.Preview
3234
import androidx.compose.ui.unit.dp
3335
import com.mapcode.R
3436
import com.mapcode.theme.MapcodeTheme
35-
import kotlinx.coroutines.launch
3637

3738
@Composable
3839
fun InfoArea(
3940
modifier: Modifier,
4041
viewModel: MapViewModel,
41-
scaffoldState: ScaffoldState,
42+
showSnackbar: (String) -> Unit,
4243
isVerticalLayout: Boolean
4344
) {
4445
val uiState by viewModel.uiState.collectAsState()
45-
val scope = rememberCoroutineScope()
4646
val copiedMessageStr = stringResource(R.string.copied_to_clipboard_snackbar_text)
4747
val onMapcodeClick = remember {
4848
{
4949
val copied = viewModel.copyMapcode()
5050
if (copied && Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
51-
scope.launch {
52-
//dismiss current snack bar so they aren't queued up
53-
scaffoldState.snackbarHostState.currentSnackbarData?.dismiss()
54-
scaffoldState.snackbarHostState.showSnackbar(copiedMessageStr)
55-
}
51+
showSnackbar(copiedMessageStr)
52+
}
53+
}
54+
}
55+
val onLocationClick = remember {
56+
{
57+
viewModel.copyLocation()
58+
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
59+
showSnackbar(copiedMessageStr)
5660
}
5761
}
5862
}
@@ -66,8 +70,10 @@ fun InfoArea(
6670
onTerritoryClick = viewModel::onTerritoryClick,
6771
onChangeLatitude = viewModel::onLatitudeTextChanged,
6872
onSubmitLatitude = viewModel::onSubmitLatitude,
73+
onCopyLatitude = onLocationClick,
6974
onChangeLongitude = viewModel::onLongitudeTextChanged,
7075
onSubmitLongitude = viewModel::onSubmitLongitude,
76+
onCopyLongitude = onLocationClick,
7177
isVerticalLayout = isVerticalLayout
7278
)
7379
}
@@ -80,8 +86,10 @@ private fun InfoArea(
8086
onSubmitAddress: () -> Unit = {},
8187
onChangeLatitude: (String) -> Unit = {},
8288
onSubmitLatitude: () -> Unit = {},
89+
onCopyLatitude: () -> Unit = {},
8390
onChangeLongitude: (String) -> Unit = {},
8491
onSubmitLongitude: () -> Unit = {},
92+
onCopyLongitude: () -> Unit = {},
8593
onTerritoryClick: () -> Unit = {},
8694
onMapcodeClick: () -> Unit = {},
8795
isVerticalLayout: Boolean
@@ -94,8 +102,10 @@ private fun InfoArea(
94102
onSubmitAddress,
95103
onChangeLatitude,
96104
onSubmitLatitude,
105+
onCopyLatitude,
97106
onChangeLongitude,
98107
onSubmitLongitude,
108+
onCopyLongitude,
99109
onTerritoryClick,
100110
onMapcodeClick
101111
)
@@ -107,8 +117,10 @@ private fun InfoArea(
107117
onSubmitAddress,
108118
onChangeLatitude,
109119
onSubmitLatitude,
120+
onCopyLatitude,
110121
onChangeLongitude,
111122
onSubmitLongitude,
123+
onCopyLongitude,
112124
onTerritoryClick,
113125
onMapcodeClick
114126
)
@@ -141,8 +153,10 @@ private fun VerticalInfoArea(
141153
onSubmitAddress: () -> Unit,
142154
onChangeLatitude: (String) -> Unit,
143155
onSubmitLatitude: () -> Unit,
156+
onCopyLatitude: () -> Unit,
144157
onChangeLongitude: (String) -> Unit,
145158
onSubmitLongitude: () -> Unit,
159+
onCopyLongitude: () -> Unit,
146160
onTerritoryClick: () -> Unit,
147161
onMapcodeClick: () -> Unit
148162
) {
@@ -181,6 +195,7 @@ private fun VerticalInfoArea(
181195
showInvalidError = state.locationUi.showLatitudeInvalidError,
182196
onSubmit = onSubmitLatitude,
183197
onChange = onChangeLatitude,
198+
onCopy = onCopyLatitude
184199
)
185200
Spacer(Modifier.height(8.dp))
186201
LongitudeTextBox(
@@ -190,6 +205,7 @@ private fun VerticalInfoArea(
190205
showInvalidError = state.locationUi.showLongitudeInvalidError,
191206
onSubmit = onSubmitLongitude,
192207
onChange = onChangeLongitude,
208+
onCopy = onCopyLongitude
193209
)
194210
}
195211
}
@@ -202,8 +218,10 @@ private fun HorizontalInfoArea(
202218
onSubmitAddress: () -> Unit,
203219
onChangeLatitude: (String) -> Unit,
204220
onSubmitLatitude: () -> Unit,
221+
onCopyLatitude: () -> Unit,
205222
onChangeLongitude: (String) -> Unit,
206223
onSubmitLongitude: () -> Unit,
224+
onCopyLongitude: () -> Unit,
207225
onTerritoryClick: () -> Unit,
208226
onMapcodeClick: () -> Unit
209227
) {
@@ -248,6 +266,7 @@ private fun HorizontalInfoArea(
248266
showInvalidError = state.locationUi.showLatitudeInvalidError,
249267
onSubmit = onSubmitLatitude,
250268
onChange = onChangeLatitude,
269+
onCopy = onCopyLatitude
251270
)
252271
LongitudeTextBox(
253272
modifier = Modifier
@@ -259,6 +278,7 @@ private fun HorizontalInfoArea(
259278
showInvalidError = state.locationUi.showLongitudeInvalidError,
260279
onSubmit = onSubmitLongitude,
261280
onChange = onChangeLongitude,
281+
onCopy = onCopyLongitude
262282
)
263283
}
264284
}
@@ -302,7 +322,8 @@ private fun LatitudeTextBox(
302322
placeHolder: String,
303323
showInvalidError: Boolean,
304324
onSubmit: () -> Unit,
305-
onChange: (String) -> Unit
325+
onChange: (String) -> Unit,
326+
onCopy: () -> Unit
306327
) {
307328
LatLngTextField(
308329
modifier = modifier,
@@ -312,7 +333,8 @@ private fun LatitudeTextBox(
312333
label = stringResource(R.string.latitude_text_field_label),
313334
clearButtonContentDescription = stringResource(R.string.clear_latitude_content_description),
314335
onSubmit = onSubmit,
315-
onChange = onChange
336+
onChange = onChange,
337+
onCopy = onCopy
316338
)
317339
}
318340

@@ -326,7 +348,8 @@ private fun LongitudeTextBox(
326348
placeHolder: String,
327349
showInvalidError: Boolean,
328350
onSubmit: () -> Unit,
329-
onChange: (String) -> Unit
351+
onChange: (String) -> Unit,
352+
onCopy: () -> Unit
330353
) {
331354
LatLngTextField(
332355
modifier = modifier,
@@ -336,7 +359,8 @@ private fun LongitudeTextBox(
336359
label = stringResource(R.string.longitude_text_field_label),
337360
clearButtonContentDescription = stringResource(R.string.clear_longitude_content_description),
338361
onSubmit = onSubmit,
339-
onChange = onChange
362+
onChange = onChange,
363+
onCopy = onCopy
340364
)
341365
}
342366

@@ -350,7 +374,8 @@ private fun LatLngTextField(
350374
label: String,
351375
clearButtonContentDescription: String,
352376
onSubmit: () -> Unit,
353-
onChange: (String) -> Unit
377+
onChange: (String) -> Unit,
378+
onCopy: () -> Unit
354379
) {
355380
Column(modifier) {
356381
val focusManager = LocalFocusManager.current
@@ -379,7 +404,14 @@ private fun LatLngTextField(
379404
.fillMaxWidth(),
380405
value = textValue,
381406
singleLine = true,
382-
label = { Text(label, maxLines = 1) },
407+
label = {
408+
ClickableText(
409+
modifier = Modifier.testTag("latlngtextfield"),
410+
text = buildAnnotatedString { append(label) },
411+
maxLines = 1,
412+
onClick = { onCopy() }
413+
)
414+
},
383415
onValueChange = { value ->
384416
textSelection = value.selection
385417
onChange(value.text)

app/src/main/java/com/mapcode/map/MapScreen.kt

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,15 @@ fun MapScreen(
5656
val scaffoldState = rememberScaffoldState()
5757
val cantFindLocationMessage = stringResource(R.string.cant_find_my_location_snackbar)
5858
val cantFindMapsAppMessage = stringResource(R.string.no_map_app_installed_error)
59+
val scope = rememberCoroutineScope()
60+
61+
val showSnackbar: (String) -> Unit = { message ->
62+
scope.launch {
63+
//dismiss current snack bar so they aren't queued up
64+
scaffoldState.snackbarHostState.currentSnackbarData?.dismiss()
65+
scaffoldState.snackbarHostState.showSnackbar(message)
66+
}
67+
}
5968

6069
if (viewModel.showCantFindLocationSnackBar) {
6170
LaunchedEffect(viewModel.showCantFindLocationSnackBar) {
@@ -87,21 +96,21 @@ fun MapScreen(
8796
.padding(padding)
8897
.fillMaxSize(),
8998
viewModel,
90-
scaffoldState,
99+
showSnackbar,
91100
renderGoogleMaps
92101
)
93102
LayoutType.HorizontalInfoArea -> HorizontalInfoAreaLayout(
94103
Modifier
95104
.padding(padding)
96105
.fillMaxSize(),
97106
viewModel,
98-
scaffoldState,
107+
showSnackbar,
99108
renderGoogleMaps
100109
)
101110
LayoutType.FloatingInfoArea -> FloatingInfoAreaLayout(
102111
Modifier.padding(padding),
103112
viewModel,
104-
scaffoldState,
113+
showSnackbar,
105114
renderGoogleMaps
106115
)
107116
}
@@ -113,7 +122,7 @@ fun MapScreen(
113122
private fun VerticalInfoAreaLayout(
114123
modifier: Modifier = Modifier,
115124
viewModel: MapViewModel,
116-
scaffoldState: ScaffoldState,
125+
showSnackbar: (String) -> Unit,
117126
renderGoogleMaps: Boolean
118127
) {
119128
Row(modifier, horizontalArrangement = Arrangement.End) {
@@ -137,7 +146,7 @@ private fun VerticalInfoAreaLayout(
137146
.imePadding()
138147
.systemBarsPadding(),
139148
viewModel,
140-
scaffoldState,
149+
showSnackbar = showSnackbar,
141150
isVerticalLayout = true
142151
)
143152
}
@@ -147,7 +156,7 @@ private fun VerticalInfoAreaLayout(
147156
private fun HorizontalInfoAreaLayout(
148157
modifier: Modifier = Modifier,
149158
viewModel: MapViewModel,
150-
scaffoldState: ScaffoldState,
159+
showSnackbar: (String) -> Unit,
151160
renderGoogleMaps: Boolean
152161
) {
153162
Column(modifier, verticalArrangement = Arrangement.Bottom) {
@@ -167,7 +176,7 @@ private fun HorizontalInfoAreaLayout(
167176
.wrapContentHeight()
168177
.padding(8.dp),
169178
viewModel,
170-
scaffoldState,
179+
showSnackbar,
171180
isVerticalLayout = false
172181
)
173182
}
@@ -177,7 +186,7 @@ private fun HorizontalInfoAreaLayout(
177186
private fun FloatingInfoAreaLayout(
178187
modifier: Modifier = Modifier,
179188
viewModel: MapViewModel,
180-
scaffoldState: ScaffoldState,
189+
showSnackbar: (String) -> Unit,
181190
renderGoogleMaps: Boolean
182191
) {
183192
Box(modifier) {
@@ -196,7 +205,7 @@ private fun FloatingInfoAreaLayout(
196205
Modifier.width(400.dp),
197206
elevation = 4.dp
198207
) {
199-
InfoArea(Modifier.padding(8.dp), viewModel, scaffoldState, isVerticalLayout = false)
208+
InfoArea(Modifier.padding(8.dp), viewModel, showSnackbar, isVerticalLayout = false)
200209
}
201210
}
202211
}

app/src/main/java/com/mapcode/map/MapViewModel.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import kotlinx.coroutines.flow.*
2424
import kotlinx.coroutines.launch
2525
import kotlinx.coroutines.runBlocking
2626
import java.io.IOException
27+
import java.math.RoundingMode
28+
import java.text.DecimalFormat
2729
import javax.inject.Inject
2830

2931
/**
@@ -296,6 +298,22 @@ class MapViewModel @Inject constructor(
296298
}
297299
}
298300

301+
/**
302+
* Copy the latitude and longitude to the clipboard: <lat>.<long>
303+
*/
304+
fun copyLocation() {
305+
location.value.also { location ->
306+
val decimalFormat = DecimalFormat("0.#######").apply {
307+
roundingMode = RoundingMode.HALF_DOWN
308+
}
309+
310+
val latitudeText = decimalFormat.format(location.latitude).toString()
311+
val longitudeText = decimalFormat.format(location.longitude).toString()
312+
313+
useCase.copyToClipboard("$latitudeText,$longitudeText")
314+
}
315+
}
316+
299317
fun onTerritoryClick() {
300318
if (mapcodeIndex.value == -1) {
301319
return

0 commit comments

Comments
 (0)