경남드리는 경상남도 관광앱으로 사용자가 경상남도의 지역을 출발지, 목적지에 입력하고 테마를 선택하여 경로 만들 수 있게 하는 사용자 맞춤형 여행 설계 앱이다.
경남드리의 주요 기능에는 여행지 설계 기능, 리뷰기능, 여행지 추천기능이 있다. 이 기능들을 구현하면서 배운 것들에 대해 적어보려고 한다.
1. 여행지 설계기능
1-1. 홈화면
![](https://blog.kakaocdn.net/dn/cHPDZw/btsyMyV8Afo/g6qcRqm9THDPwIFylGwI5K/img.png)
처음 홈화면에는 AutoCompleteTextView로 출발지와 도착지를 타이핑할 수 있게 만들었으며, RadioGroup을 활용하여 원하는 테마를 선택할 수 있게 만들었다.
여기서 AutoCompleteTextView에 자동완성이 뜨게 하려면 Adapter 를 붙여 주어야한다 일단 xml에 AutoCompleteTextView를 만든다음
<AutoCompleteTextView
android:id="@+id/StartPoint"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:background="@drawable/border_radius"
android:ems="10"
android:hint="출발지"
android:inputType="text"
android:padding="5dp"
android:textColor="#000000"
android:textColorHint="@color/black" />
<AutoCompleteTextView
android:id="@+id/EndPoint"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:background="@drawable/border_radius"
android:ems="10"
android:hint="도착지"
android:inputType="text"
android:padding="5dp"
android:textColor="#000000"
android:textColorHint="@color/black" />
<Button
android:id="@+id/MapPoint"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:background="#27005D"
android:text="여행경로만들기"
android:textColor="@color/white" />
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RadioGroup
android:id="@+id/rg"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal">
<RadioButton
android:id="@+id/nature_check"
android:layout_width="130dp"
android:layout_height="match_parent"
android:layout_marginLeft="5dp"
android:layout_marginRight="10dp"
android:buttonTint="#000000"
android:text="자연관광"
android:textAlignment="center"
android:textColor="@color/black"
android:textSize="16sp" />
<RadioButton
android:id="@+id/leisure_check"
android:layout_width="130dp"
android:layout_height="match_parent"
android:layout_marginLeft="5dp"
android:layout_marginRight="10dp"
android:buttonTint="#000000"
android:text="레저관광"
android:textAlignment="center"
android:textColor="@color/black"
android:textSize="16sp" />
<RadioButton
android:id="@+id/culture_check"
android:layout_width="130dp"
android:layout_height="match_parent"
android:layout_marginLeft="5dp"
android:layout_marginRight="10dp"
android:buttonTint="#000000"
android:text="문화관광"
android:textAlignment="center"
android:textColor="@color/black"
android:textSize="16sp" />
</RadioGroup>
</HorizontalScrollView>
Java코드에서 AutoCompleteTextView를 불러와서 자동완성에서 뜨게할 목록을 ArrayList에 add한 다음 setAdapter로 붙여 주면 된다.
searchList = new ArrayList<>();
settingList();
StartPoint = findViewById(R.id.StartPoint);
StartPoint.setAdapter(new ArrayAdapter<String>(this, androidx.appcompat.R.layout.support_simple_spinner_dropdown_item, searchList));
EndPoint = findViewById(R.id.EndPoint);
EndPoint.setAdapter(new ArrayAdapter<String>(this, androidx.appcompat.R.layout.support_simple_spinner_dropdown_item, searchList));
private void settingList() {
searchList.add("창원시");
searchList.add("진주시");
searchList.add("통영시");
searchList.add("사천시");
searchList.add("김해시");
searchList.add("밀양시");
searchList.add("거제시");
searchList.add("양산시");
searchList.add("의령군");
searchList.add("함안군");
searchList.add("창녕군");
searchList.add("고성군");
searchList.add("남해군");
searchList.add("하동군");
searchList.add("산청군");
searchList.add("함양군");
searchList.add("거창군");
searchList.add("합천군");
}
여기서는 settingList()라는 함수를 만들어서 searchList에 값을 추가해서 각 출발 목적지 자동완성뷰에 넣어주었다.
1-2.경로선택화면
![](https://blog.kakaocdn.net/dn/AG4In/btsyTjsBkxr/a0F4YRzpgsVa3OHuaKKLRk/img.jpg)
출발지와 도착지를 설정하고 버튼을 클릭했을 때 테마별 가중치를 적용한 다 익스트라 알고리즘을 통해 계산된 경유지와 도착지에 대한 관광지의 목록이 나오게 된다. 상단에는 관광지의 테마별 카테고리를 선택할 수 있도록 HorizontalScrollView안에 RadioGroup을 두어 선택할 수 있게 하였다.
관광지목록이 뜨는 것은 FrameLayout 안에 ListView를 두어 유동적으로 ListView안의 리스트가 변하는 것에 따라 목록이 바뀔 수 있도록했다.
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#FFFFF0"
android:orientation="vertical">
<FrameLayout
android:id="@+id/routeFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="2">
<ListView
android:id="@+id/search_list"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
</ListView>
</FrameLayout>
</LinearLayout>
1-2-1. 관광지선택
![](https://blog.kakaocdn.net/dn/dE07YX/btsyTzaUyQr/OQDzVlflPO05YPlF2CU8aK/img.jpg)
처음에는 관광지 선택과 정보보기 둘다 버튼으로 구현하려고 했지만 두개 다 버튼으로 구현했을 때 버튼의 Onclick 이벤트가 작동하지 않아서 관광지 선택은 TextView로 구현하고 리스트뷰를 클릭했을 때 인벤트가 작동하도록 만들어서 선택과 정보보기가 둘다 가능하도록 만들었다.
또한 선택했을 때 위의 Texview에 경유지가 추가되고 선택해제 했을 때 다시 없어질 수 있도록 구현 하였으면 이때 해당 관광지의 위경도 값도 같이 ArrayList에 추가하여 다음 지도표시화면에 사용할 수 있도록하였다.
int item_n=mid_data.indexOf(item);
double d1=Double.parseDouble(mid_lat.get(item_n));
result_lat.add(d1);
double d2=Double.parseDouble(mid_long.get(item_n));
result_long.add(d2);
Route_List.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long l) {
TextView text_title=view.findViewById(R.id.title);
TextView route_choice=view.findViewById(R.id.route_choice);
if(is_Checked.get(position)==false){
is_Checked.set(position,true);
route_choice.setBackgroundColor(Color.parseColor("#AED2FF"));
String item=text_title.getText().toString();
result_name.add(item);
if((rot.length-2)>0){
int item_n=mid_data.indexOf(item);
double d1=Double.parseDouble(mid_lat.get(item_n));
result_lat.add(d1);
double d2=Double.parseDouble(mid_long.get(item_n));
result_long.add(d2);
}else {
int item_n=route_data.indexOf(item);
double d1=Double.parseDouble(route_lat.get(item_n));
result_lat.add(d1);
double d2=Double.parseDouble(route_long.get(item_n));
result_long.add(d2);
}
route_text.append(" "+item);
}
else{
is_Checked.set(position,false);
route_choice.setBackgroundColor(Color.parseColor("#FFFFF0"));
String item=text_title.getText().toString();
result_name.remove(item);
String remove_item=route_text.getText().toString();
remove_item=remove_item.replaceAll(item,"");
route_text.setText(remove_item);
if((rot.length-2)>0){
int item_n=mid_data.indexOf(item);
result_lat.remove(Double.parseDouble(mid_lat.get(item_n)));
result_long.remove(Double.parseDouble(mid_long.get(item_n)));
}else {
int item_n=route_data.indexOf(item);
result_lat.remove(Double.parseDouble(route_lat.get(item_n)));
result_long.remove(Double.parseDouble(route_long.get(item_n)));
}
}
}
});
1-2-2. 정보보기
![](https://blog.kakaocdn.net/dn/byY7YF/btsy0jEsUVL/FjcxgnTWk0tucqV1avfTs1/img.jpg)
관광지 정보보기는 FireStore상에서 그 관광지에 대한 사진, 설명을 쿼리를 작성하여 관광지페이지에 전달해서 관광지화면을 띄울 수 있도록 했다.
코드가 길어서 일부분만 첨부한다.
inform_b.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
fileurl.clear();
data_con.clear();
String i=title.getText().toString();
db.collection("nature_data")
.whereEqualTo("data_title",i)
.get()
.addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
@Override
public void onComplete(@NonNull Task<QuerySnapshot> task) {
if (task.isSuccessful()) {
for (QueryDocumentSnapshot document : task.getResult()) {
fileurl.add(document.get("fileurl1",String.class));
data_con.add(document.get("data_content",String.class));
}
if(fileurl.size()>0&&data_con.size()>0){
fileurl1=fileurl.get(0);
data_content=data_con.get(0);
Intent intent=new Intent(context, City_Page_Activity.class);
if(data_content.length()>0 && fileurl1.length()>0 ) {
data_content = data_content.replaceAll("<(/)?([a-zA-Z]*)(\\s[a-zA-Z]*=[^>]*)?(\\s)*(/)?>", "");
data_content = data_content.replaceAll("<[^>]*>", " ");
data_content = data_content.replace("/(<([^>]+)>)/", "");
intent.putExtra("fileurl1", fileurl1);
intent.putExtra("data_content", data_content);
intent.putExtra("name", i);
context.startActivity(intent);
}
3. 여행지 길찾기 및 지도 기능
![](https://blog.kakaocdn.net/dn/cbtEol/btsyTtIPdpX/H2iuQlg661s9rBpk4BVA7K/img.jpg)
경유지와 출발지 도착지의 위경도 값을 전의 Activity에서 받은 뒤 Rest api로 Naver Map api중 하나인 Driving 5를 호출하여 지도 상에 길선을 그리고 마커를 찍는다.
-3-1지도기능
●Driving 5
Rest api에 관한 내용을 설명이 길어서 나중에 글을 작성할 예정이지만 여기서 짧게나마 설명하고 넘어가려고 한다.
Rest api를 사용하는데 중요한 2가지 요소는
해당 서버에 Get요청을 넣을 Interface와 Get 요청을 했을 때 결과값을 받을 Class 이다.
-1 Interface
Interface는 보통 해당 api 공식문서에 보면 어떻게 적어야하는지 잘 나와있다.
https://api.ncloud-docs.com/docs/ai-naver-mapsdirections-driving
driving
api.ncloud-docs.com
![](https://blog.kakaocdn.net/dn/Lq6bN/btsyX0FasaH/pGks6TKxea61MqyoR9hwq1/img.png)
![](https://blog.kakaocdn.net/dn/nNxzM/btsyWgVNdzU/nS2mx0JezwCMXSVKBZIwYk/img.png)
요청해더는 아래와 같이 @Header를 통해 작성하고 요청바디는 @Query로 작성하고 필수여부에서 Y인 부분을 다 적고 나머지 필요한 것은 추가적으로 적으면 된다.
public interface RouteFind {
@GET("v1/driving")
Call<RoutePath> getData(@Header ("X-NCP-APIGW-API-KEY-ID")String ClientID,
@Header("X-NCP-APIGW-API-KEY") String Secret,
@Query("start") String start,@Query("goal") String goal,
@Query("waypoints")String waypoints);
}
-2 Class
![](https://blog.kakaocdn.net/dn/Xvce1/btsyTIeF8on/Kgd13uJPKUzPJmGfMyurWk/img.png)
![](https://blog.kakaocdn.net/dn/bPoelZ/btsyUOyz45P/H1cEBSU2A7LA4AWtekzJP0/img.png)
응답 바디는 필수적인 부분을 Class로 받을 수 있게 공식문서에 나온 타입을 사용하여 작성한다. 또한 get메소드를 작성해서 값을 가져다 쓸 수 있게한다.
package com.example.k_contest;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
import java.nio.DoubleBuffer;
import java.util.List;
public class RoutePath {
@SerializedName("code")
@Expose
private int code;
@SerializedName("messge")
@Expose
private String messge;
@SerializedName("currentDateTime")
@Expose
private String currentDateTime;
@SerializedName("route")
@Expose
private Route route;
public int getCode() {
return code;
}
public Route getRoute() {
return route;
}
public String getCurrentDateTime() {
return currentDateTime;
}
public String getMessge() {
return messge;
}
}
class Route{
@SerializedName("traoptimal")
@Expose
private List<Traoptimal> traoptimal;
public List<Traoptimal> getTraoptimal() {
return traoptimal;
}
}
class Traoptimal{
@SerializedName("summary")
@Expose
private Summary summary;
@SerializedName("path")
@Expose
private List<List<Double>> path;
public Summary getSummary() {
return summary;
}
public List<List<Double>> getPath() {
return path;
}
}
class Summary{
@SerializedName("start")
@Expose
private Start start;
@SerializedName("goal")
@Expose
private Goal goal;
@SerializedName("distance")
@Expose
private int distance;
public Goal getGoal() {
return goal;
}
public Start getStart() {
return start;
}
public int getDistance() {
return distance;
}
}
class Start
{
@SerializedName("location")
@Expose
private List<Double> location;
public List<Double> getLocation() {
return location;
}
}
class Goal
{
@SerializedName("location")
@Expose
private List<Double> location;
public List<Double> getLocation() {
return location;
}
}
Retrofit retrofit0 = new Retrofit.Builder()
.baseUrl("https://naveropenapi.apigw.ntruss.com/map-direction/")
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
RouteFind_nomid routeFind0 = retrofit0.create(RouteFind_nomid.class);
Call<RoutePath> call0 = routeFind0.getData(NavaApIKey,secret,start_lng+","+start_lot,end_lng+","+end_lot); //네이버 길찾기 rest api 시작 출발점 찍으면 됨
call0.enqueue(new Callback<RoutePath>() {
@Override
public void onResponse(Call<RoutePath> call, Response<RoutePath> response) {
if(response.isSuccessful()) {
RoutePath routePath=response.body();
List<List<Double>> path=routePath.getRoute().getTraoptimal().get(0).getPath();
PathOverlay line_path=new PathOverlay(); //길 선 표시할 path 배열
for(int i=0;i<path.size();i++) {
list.add(new LatLng(path.get(i).get(1), path.get(i).get(0)));
}
markers[0]=new Marker();
markers[0].setPosition(new LatLng(start_lot,start_lng));
markers[0].setCaptionText("출발지");
markers[0].setMap(naverMap);
markers[1]=new Marker();
markers[1].setPosition(new LatLng(end_lot,end_lng));
markers[1].setCaptionText("목적지");
markers[1].setMap(naverMap);
line_path.setCoords(list);
line_path.setMap(naverMap);
}
}
Rest api를 호출할때는 Retrofit.Builder를 통하여 빌드하고 Get구문과 결과를 받을 Class를 작성하여 값을 받을 수 있게 한다. 또한 결과를 받는 값을 쓸 때는 response.body()를 통해 사용한다.
도로선을 맵에 사용할때는 path 즉 경로의 수만큼의 ArrrayList를 add하여 사용한다.
-3-2 길찾기
길찾기는 url Scheme로 구현했는데 Naver api의 url Scheme 문서를 참고하여 구현하면 된다.
https://guide.ncloud-docs.com/docs/maps-url-scheme
지도 앱 연동 URL Scheme
guide.ncloud-docs.com
![](https://blog.kakaocdn.net/dn/lrARD/btsyVYAIfLy/KO6YpZqzlhO4JKYaddKatK/img.png)
위의 문서를 참고하여 만약 지도앱이 설치되어있다면 그 앱에 해당하는 url Scheme 기능이 실행되고 아니면 설치화면으로 이동하도록 구현하였다.
list = getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
if (list == null || list.isEmpty()) {
try {
if(item.equals(st)){
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url_f+String.valueOf(region_position[stringToInt(item)][0])+"&dlng="+String.valueOf(region_position[stringToInt(item)][1])+"&dname="+encodeResult+url_b)));
} else if (item.equals(ed)) {
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url_f+String.valueOf(region_position[stringToInt(item)][0])+"&dlng="+String.valueOf(region_position[stringToInt(item)][1])+"&dname="+encodeResult+url_b)));
}else {
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url_f + String.valueOf(result_lat[stringToInt_s(item)]) + "&dlng=" + String.valueOf(result_long[stringToInt_s(item)]) + "&dname=" + encodeResult + url_b)));
}
}catch (Exception e){
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=com.nhn.android.nmap")));
}
} else {
startActivity(intent);
}
'Project > 경남드리' 카테고리의 다른 글
[Android][JAVA]3장. 부가기능 (0) | 2023.11.04 |
---|---|
1장. 학기에서 공모전으로 (4) | 2023.10.17 |
0장. 시작 (0) | 2023.10.17 |