개발/안드로이드

[안드로이드] 공공데이터 (공적마스크_코로나) api 받아서 구글맵에 마커찍기

핸디(Handy) 2020. 3. 15. 19:00

요새 하도 코로나 때문에 흉흉한데. 다행히 많은 능력자 분들께서 다양한 방식으로 사회에 기여를 하시는 모습을 보며 

저도 작게나마 기여를 하기 위해 이렇게 글을 작성합니다.

 

<결과>

 

구현 단계는 크게
1. 안드로이드에 구글맵 넣기
2. 공공데이터 받아오기
3. 받아온 데이터로 마커 찍기 
의 순서로 이루어집니다.
다만 1. 구글맵 넣기 는 다른 예제가 많이 있기때문에 생략하고 진행하겠습니다.

<class 부분>

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.os.Bundle;
import android.os.Handler;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.coordinatorlayout.widget.CoordinatorLayout;

import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.OnMapReadyCallback;
import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;
import com.google.android.material.bottomsheet.BottomSheetBehavior;

import org.techtown.carchap_v11.API_corona.CoronaApi;
import org.techtown.carchap_v11.API_corona.corona_item;
import org.techtown.carchap_v11.API_hydrogenChargingStation.hydrogen_station_item;
import org.techtown.carchap_v11.Dialog.ConnectDialog;
import org.techtown.carchap_v11.databinding.ActivityMapCoronaBinding;

import java.util.ArrayList;

public class Map_corona extends AppCompatActivity
        implements
        OnMapReadyCallback,
        GoogleMap.OnCameraIdleListener,
        GoogleMap.OnCameraMoveStartedListener,
        GoogleMap.OnMarkerClickListener {

    private int apiRequestCount;
    public static GoogleMap mMap;
    public static boolean startFlagForCoronaApi;
    private ArrayList<Marker> markerList = new ArrayList();
    public static ArrayList<corona_item> corona_list = new ArrayList();

    private ActivityMapCoronaBinding binding;
    private BottomSheetBehavior mBottomSheetBehavior;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding =ActivityMapCoronaBinding.inflate(getLayoutInflater());
        View view = binding.getRoot();
        setContentView(view);

        SupportMapFragment mapFragment =
                (SupportMapFragment) getSupportFragmentManager().findFragmentById(R.id.map_corona);
        mapFragment.getMapAsync(this);

        binding.rootBottomSheet.setVisibility(View.INVISIBLE);
        mBottomSheetBehavior = BottomSheetBehavior.from(binding.bottomSheet);

        binding.mapZoomPlus.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                mMap.animateCamera(CameraUpdateFactory.zoomIn());
            }
        });

        binding.mapZoomMinus.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                mMap.animateCamera(CameraUpdateFactory.zoomOut());
            }
        });



    }

    @Override
    public void onMapReady(GoogleMap googleMap) {
        mMap = googleMap;

        //리스너 달아주기
        mMap.setOnCameraIdleListener(this);
        mMap.setOnMarkerClickListener(this);
        mMap.setOnCameraMoveStartedListener(this);


        //zoom level 별 지도 크기 예시
        //  1   : 세계
        //  5   : 대륙
        //  10  : 시
        //  15  : 거리
        //  20  : 건물
        mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(37.510759, 126.977943), 15));


    }

    @Override
    public void onCameraIdle() {
        removeMarkerAll();

        String lat = String.valueOf(mMap.getCameraPosition().target.latitude);
        String lon = String.valueOf(mMap.getCameraPosition().target.longitude);
        startFlagForCoronaApi = true;
        CoronaApi coronaApi = new CoronaApi();
        coronaApi.execute(lat,lon,"");

        apiRequestCount = 0;
        final Handler temp_handler = new Handler();
        temp_handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                if (apiRequestCount < 100) {
                    if (startFlagForCoronaApi) {
                        apiRequestCount++;
                        temp_handler.postDelayed(this, 100);
                    } else {
                        //api 호출이 완료 되었을 떄
                        drawMarker();
                    }
                } else {
                    //api 호출이 10초 이상 경괴했을 때
                    Toast.makeText(getApplicationContext(), "호출에 실패하였습니다. 다시 시도해주세요.", Toast.LENGTH_LONG).show();
                }

            }


        }, 100);
    }

    private void removeMarkerAll() {
        for (Marker marker : markerList) {
            marker.remove();
        }

    }

    private void drawMarker() {
        for (int i =0 ; i< corona_list.size(); i++){
            corona_item item = corona_list.get(i);
            String remain_stat =item.getRemain_stat();
            switch (remain_stat) {
                case "plenty" : {
                    remain_stat = "100개이상";
                    break;
                }
                case "some" : {
                    remain_stat = "30개 이상 100개 미만";
                    break;
                }
                case "few" : {
                    remain_stat = "2개 이상 30개 미만";
                    break;
                }
                case "empty" : {
                    remain_stat = "1개 이하";
                    break;
                }
            }
            Marker marker = mMap.addMarker(new MarkerOptions()
                    .position(new LatLng(Double.parseDouble(item.getLat()), Double.parseDouble(item.getLng()) ))
                    .title(item.getName())
                    .snippet(item.getAddr()+"@"+item.getCreated_at()+"@"+item.getRemain_stat()+"@"+item.getStock_at()+"@"+item.getType())
                    .icon(BitmapDescriptorFactory.fromResource(R.drawable.marker_xingxing)));
            markerList.add(marker);

        }
        return;
    }


    @Override
    public boolean onMarkerClick(Marker marker) {
        Log.d("onMarkerClick", "click");

        binding.rootBottomSheet.setVisibility(View.GONE);
        mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
        Animation animation_up = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.enter_from_bottom);
        binding.rootBottomSheet.setAnimation(animation_up);
        binding.rootBottomSheet.setVisibility(View.VISIBLE);

        String addr= marker.getSnippet().split("@")[0];
        String created_at= marker.getSnippet().split("@")[1];
        String remain_stat= marker.getSnippet().split("@")[2];
        String stock_at= marker.getSnippet().split("@")[3];
        String type= marker.getSnippet().split("@")[4];

        switch (type) {
            case "01" :{
                type = "약국";
                break;
            }
            case "02" :{
                type = "우체국";
                break;
            }
            case "03" :{
                type = "농협";
                break;
            }
        }

        switch (remain_stat) {
            case "plenty" : {
                remain_stat = "100개이상";
                break;
            }
            case "some" : {
                remain_stat = "30개 이상 100개 미만";
                break;
            }
            case "few" : {
                remain_stat = "2개 이상 30개 미만";
                break;
            }
            case "empty" : {
                remain_stat = "1개 이하";
                break;
            }
        }
        binding.bottomInfoAddress.setText(addr);
        binding.bottomInfoPlaceName.setText(marker.getTitle());
        binding.bottomInfoType.setText(type);
        binding.bottomInfoRemain.setText(remain_stat);
        binding.bottomInfoUpdate.setText(stock_at);


        return true;
    }

    @Override
    public void onCameraMoveStarted(int i) {
        mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
    }
}

 


세부 코드에 대해 설명하겠습니다.

public class Map_corona extends AppCompatActivity
        implements
        OnMapReadyCallback,
        GoogleMap.OnCameraIdleListener,
        GoogleMap.OnCameraMoveStartedListener,
        GoogleMap.OnMarkerClickListener

OnMapReadyCallback은 구글맵을 사용하기 위해 기본적으로 필요합니다. 구글맵이 로딩되었음을 알려줍니다.
GoogleMap.OnCamereaIdleListener 는 구글맵 화면이 바뀔 때마다 호출됩니다.
GoogleMap.OnCameraMoveStartedLisnener 는 맵을 드래그 시작했을때 호출됩니다
GoogleMap.OnMarkerClickListener 는 마커아이템을 클릭했을때 호출됩니다.

 

 private ActivityMapCoronaBinding binding;

이 코드는 바인딩을 쓰기 위해서 선언이 필요합니다. 
binding에 대해 알고 싶다면

2020/03/11 - [개발] - [안드로이드] findViewById를 없애는 갓기능 : viewbinding

    @Override
    public void onCameraIdle() {
        removeMarkerAll();

        String lat = String.valueOf(mMap.getCameraPosition().target.latitude);
        String lon = String.valueOf(mMap.getCameraPosition().target.longitude);
        startFlagForCoronaApi = true;
        CoronaApi coronaApi = new CoronaApi();
        coronaApi.execute(lat,lon,"");

        apiRequestCount = 0;
        final Handler temp_handler = new Handler();
        temp_handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                if (apiRequestCount < 100) {
                    if (startFlagForCoronaApi) {
                        apiRequestCount++;
                        temp_handler.postDelayed(this, 100);
                    } else {
                        //api 호출이 완료 되었을 떄
                        drawMarker();
                    }
                } else {
                    //api 호출이 10초 이상 경괴했을 때
                    Toast.makeText(getApplicationContext(), "호출에 실패하였습니다. 다시 시도해주세요.", Toast.LENGTH_LONG).show();
                }

            }


        }, 100);
    }

보시다시피 맵 이동끝나고 onCameraIdle(), 요 친구가 호출이 됩니다.

그렇다면  removeMarkerAll()로 기존에 그려진 마커를 전부 제거하고

    private void removeMarkerAll() {
        for (Marker marker : markerList) {
            marker.remove();
        }

    }

현재 맵의 중심좌표을 구하고

코로나 api 을 호출합니다. 이때 방금 구한 맵의 중심좌표를 함께 넘겨줍니다.

저기 handler로 한 것은 10초안에 호출이 안됬을때 사용자에게 호출에 실패했음을 알려주기 위해 제가 편법으로 구현해논은 것입니다. 100번 카운트 * 100ms = 10초가 됩니다. 무튼 10초안에 호출이 완료된다면 다음 drawMarker()로 넘어갑니다.

public class CoronaApi extends AsyncTask<String, String, String> {

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
    }

    @Override
    protected void onPostExecute(String s) {
        super.onPostExecute(s);
    }

    @Override
    protected String doInBackground(String... strings) {
        Log.d("Task3", "POST");
        String temp = "Not Gained";
        try {
            temp = GET(strings[0],strings[1]);
            Log.d("REST", temp);
            return temp;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return temp;
    }

    private String GET(String x,String y) throws IOException {
        String corona_API = "https://8oi9s0nnth.apigw.ntruss.com/corona19-masks/v1/storesByGeo/json?lat="+x+"&lng="+y+"&m=1000";

        String data = "";
        String myUrl3 = String.format(corona_API, x);



        try {
            URL url = new URL(myUrl3);
            Log.d("CoronaApi", "The response is :" + url);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setReadTimeout(10000);
            conn.setConnectTimeout(15000);
            conn.setRequestMethod("GET");
            conn.setDoInput(true);
            conn.connect();

            String line;
            String result = "";

            BufferedReader bf;
            bf = new BufferedReader(new InputStreamReader(url.openStream()));
            while ((line = bf.readLine()) != null) {
                result = result.concat(line);

            }
            Log.d("CoronaApi", "The response is :" + result);
            JSONObject root = new JSONObject(result);

            JSONArray coronaArray = root.getJSONArray("stores");
            for(int i = 0; i< coronaArray.length() ; i++){
                JSONObject item = coronaArray.getJSONObject(i);
                Log.d("corona",item.getString("name"));
                corona_item corona_item = new corona_item(
                        item.getString("lat"),
                        item.getString("lng"),
                        item.getString("addr"),
                        item.getString("code"),
                        item.getString("created_at"),
                        item.getString("name"),
                        item.getString("remain_stat"),
                        item.getString("stock_at"),
                        item.getString("type")
                );
                Map_corona.corona_list.add(corona_item);
            }
            startFlagForCoronaApi=false;



        } catch (NullPointerException | JsonSyntaxException | JSONException e) {
            e.printStackTrace();
        }


        return data;
    }
public class corona_item {

    private String addr;
    private String code;
    private String created_at;
    private String lat;
    private String lng;
    private String name;
    private String remain_stat;
    private String stock_at;
    private String type;


    public corona_item( String lat, String lng,String addr, String code, String created_at, String name, String remain_stat, String stock_at, String type) {
        this.addr = addr;
        this.code = code;
        this.created_at = created_at;
        this.lat = lat;
        this.lng = lng;
        this.name = name;
        this.remain_stat = remain_stat;
        this.stock_at = stock_at;
        this.type = type;
    }
}

getter와 setter는 공간상의 문제로 생략합니다. corona_item 구현할때 그냥 넣어주시는게 편합니다.

 

String corona_API = "https://8oi9s0nnth.apigw.ntruss.com/corona19-masks/v1/storesByGeo/json?lat="+x+"&lng="+y+"&m=1000";

 이 부분이 바로 호출 api url 입니다. lat과 lon 그리고 반경(0~5000m) 을 통해 공적마스크 판매점 데이터를 json형태로 가져오게 됩니다.

공적 마스크 api 응답

그리고 받아온 데이터를 보고 json 파싱 해주시면 됩니다.

호출이 완료되면 drawMarker()가 호출됩니다.

    private void drawMarker() {
        for (int i =0 ; i< corona_list.size(); i++){
            corona_item item = corona_list.get(i);
            String remain_stat =item.getRemain_stat();
            switch (remain_stat) {
                case "plenty" : {
                    remain_stat = "100개이상";
                    break;
                }
                case "some" : {
                    remain_stat = "30개 이상 100개 미만";
                    break;
                }
                case "few" : {
                    remain_stat = "2개 이상 30개 미만";
                    break;
                }
                case "empty" : {
                    remain_stat = "1개 이하";
                    break;
                }
            }
            Marker marker = mMap.addMarker(new MarkerOptions()
                    .position(new LatLng(Double.parseDouble(item.getLat()), Double.parseDouble(item.getLng()) ))
                    .title(item.getName())
                    .snippet(item.getAddr()+"@"+item.getCreated_at()+"@"+item.getRemain_stat()+"@"+item.getStock_at()+"@"+item.getType())
                    .icon(BitmapDescriptorFactory.fromResource(R.drawable.marker_xingxing)));
            markerList.add(marker);

        }
        return;
    }

보시면 corona_api 에서 가져온 item을 corona_list에 저장했었습니다. 그것을 가져와 이제 마커를 찍어주는 단계입니다.

remina_stat이 각 지점별로 마스크가 몇개가 남았나 데이터가 들어있습니다. 이것을 토대로 마커 이미지를 커스텀해주시면 되겠습니다. 

가 다음에는 마커 클릭 이벤트입니다. 마커를 클릭하면 이벤트가 있어줘야 합니다.

 @Override
    public boolean onMarkerClick(Marker marker) {
        Log.d("onMarkerClick", "click");

        binding.rootBottomSheet.setVisibility(View.GONE);
        mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
        Animation animation_up = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.enter_from_bottom);
        binding.rootBottomSheet.setAnimation(animation_up);
        binding.rootBottomSheet.setVisibility(View.VISIBLE);

        String addr= marker.getSnippet().split("@")[0];
        String created_at= marker.getSnippet().split("@")[1];
        String remain_stat= marker.getSnippet().split("@")[2];
        String stock_at= marker.getSnippet().split("@")[3];
        String type= marker.getSnippet().split("@")[4];

        switch (type) {
            case "01" :{
                type = "약국";
                break;
            }
            case "02" :{
                type = "우체국";
                break;
            }
            case "03" :{
                type = "농협";
                break;
            }
        }

        switch (remain_stat) {
            case "plenty" : {
                remain_stat = "100개이상";
                break;
            }
            case "some" : {
                remain_stat = "30개 이상 100개 미만";
                break;
            }
            case "few" : {
                remain_stat = "2개 이상 30개 미만";
                break;
            }
            case "empty" : {
                remain_stat = "1개 이하";
                break;
            }
        }
        binding.bottomInfoAddress.setText(addr);
        binding.bottomInfoPlaceName.setText(marker.getTitle());
        binding.bottomInfoType.setText(type);
        binding.bottomInfoRemain.setText(remain_stat);
        binding.bottomInfoUpdate.setText(stock_at);


        return true;
    }

 

 

    @Override
    public void onCameraMoveStarted(int i) {
        mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
    }

이 부분은 맵이동이 시작되었을때 화면을 깔끔하게 보여주기 위해 만들었습니다.


<xml 부분>

map_corona를 아래에 깔고 맵 줌인 줌아웃 버튼을 만들었습니다.

 

 

 

 

그리고 하단에 bottom_sheet을 만들어서 사용자에 의해 조절이 가능하도록 만들었습니다.

 

 

<최종 결과물>