본문 바로가기
개발/안드로이드

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

by 핸디(Handy) 2020. 3. 15.

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

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

 

<결과>

 

구현 단계는 크게
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을 만들어서 사용자에 의해 조절이 가능하도록 만들었습니다.

 

 

<최종 결과물>

댓글