경남드리는 경상남도 관광앱으로 사용자가 경상남도의 지역을 출발지, 목적지에 입력하고 테마를 선택하여 경로 만들 수 있게 하는 사용자 맞춤형 여행 설계 앱이다. 
경남드리의 주요 기능에는 여행지 설계 기능, 리뷰기능, 여행지 추천기능이 있다. 이 기능들을 구현하면서 배운 것들에 대해 적어보려고 한다.

1. 여행지 설계기능 

1-1. 홈화면

 처음 홈화면에는 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.경로선택화면

 출발지와 도착지를 설정하고 버튼을 클릭했을 때 테마별 가중치를 적용한 다 익스트라 알고리즘을 통해 계산된 경유지와 도착지에 대한 관광지의 목록이 나오게 된다. 상단에는 관광지의 테마별 카테고리를 선택할  수 있도록  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. 관광지선택
 

처음에는 관광지 선택과 정보보기 둘다 버튼으로 구현하려고 했지만 두개 다 버튼으로 구현했을 때 버튼의 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. 정보보기 

정보 보기를 클릭했을 때의 관광지 화

관광지 정보보기는 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. 여행지 길찾기 및 지도 기능

경유지와 출발지 도착지의 위경도 값을 전의 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

요청 바디
요청헤더

요청해더는 아래와 같이 @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
 

응답 바디는 필수적인 부분을 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

위의 문서를 참고하여 만약 지도앱이 설치되어있다면 그 앱에 해당하는 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

+ Recent posts