*네트워크-데이터통신/Open API 사용, 마커설정(*레트로핏(Retrofit) 및 converter-gson, Glide 라이브러리)
-Open API : 데이터 또는 서비스를 공개해 일반 개발자들이 사용할 수 있도록 제공하는 인터페이스
-Open API는 주로 인터넷 주소 형태로 제공됨
-마커설정 : 마커에 tag를 달아 클릭리스너 이벤트 적용 및 클러스터링(묶어서 하나의 그룹으로 표시) 가능
*도서관 현황정보 Open API 사용하여 앱 구현하기
-Open API 데이터 제공(서울 열린데이터 광장) : https://data.seoul.go.kr/
-서울특별시 공공도서관 현황정보 API 데이터 사용 : https://data.seoul.go.kr/dataList/OA-15480/S/1/datasetView.do
-인증키 신청 및 발급
-서울특별시 공공도서관 현황정보 Open API 구조
--문서 형식 : JSON과 XML지원, API에 따라 둘 중 하나만 지원할수도 있음
--서비스ID : 서비스를 구분하는 ID
--요청개수 : 한 번에 요청하는 개수, 트레픽 문제로인해 1,000개까지 요청 가능함, 1,000개가 넘어가면 페이지 값을 증가시키면서 요청
-https://data.seoul.go.kr/dataList/OA-15480/S/1/datasetView.do 하단 출력값 중 일부 데이터 사용
-지도 정보가 필요하므로 Google Maps Activity로 프로젝트 생성
-AndroidManifest.xml
-- 구글 API키 추가 참고(https://heeyjinny.tistory.com/109, https://heeyjinny.tistory.com/108)
--위치권한 및 인터넷 권한 명세
<!-- 위치권한 2가지 명세 -->
<!-- 도시 블록 내에서의 정확한 위치(네트워크 위치) -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<!-- 정확한 위치 확보(네트워크 + GPS위치) -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<!-- 인터넷 권한 명세 -->
<uses-permission android:name="android.permission.INTERNET"/>
<!-- 도서관 정보 API가 HTTPS(보안프로토콜)이 아닌
HTTP를 사용하기 때문에 usesCleartextTraffic="true" 추가 -->
<application
android:usesCleartextTraffic="true"
-build.gradle
-- android{} 에 뷰바인딩 추가
--보안저장한 APIKey 적용
--dependencies{} 에 레트로핏, JSON컨버터 라이브러리 추가
//뷰바인딩
buildFeatures{
viewBinding true
}
//local.properties 불러오기
Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())
defaultConfig {
//타입 - 키(변수명) - 정의된 키
buildConfigField "String", "LBRRY_DOMAIN", properties ['LBRRY_DOMAIN']
buildConfigField "String", "LBRRY_API_KEY", properties ['LBRRY_API_KEY']
buildConfigField "String", "MAP_API_KEY", properties ['MAP_API_KEY']
//레트로핏, JSON컨버터 라이브러리 추가
dependencies {
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
-MapsActivity.kt
/** 서울 공공도서관 앱 개발하기 **/
//1
//구글 지도 API키 추가
//레트로핏, JSON컨버터 라이브러리 추가
//build.gradle(:app)
//2
//위치권한 및 인터넷권한 추가
//AndroidManifest.xml
//3
//사용할 JSON 샘플데이터를 사용하여
//코틀린 데이터 클래스 생성
//클래스의 개수가 여러 개일 경우 관리를 위해
//기본 패키지 밑에 data패키지 생성
//패키지 우클릭 - New - Package - data패키지 생성
//data패키지 폴더 우클릭 - New - Kotlin data class File from Json 클릭
//빈 여백에 샘플 데이터 붙여넣기, Format으로 정렬 후
//Class Name은 Library 입력
//4
//Open API 사용을 위해
//기본정보를 담아두는 클래스와
//클래스 안에 레트로핏에서 사용할 인터페이스 생성
class MapsActivity : AppCompatActivity(), OnMapReadyCallback {
private lateinit var mMap: GoogleMap
private lateinit var binding: ActivityMapsBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMapsBinding.inflate(layoutInflater)
setContentView(binding.root)
// Obtain the SupportMapFragment and get notified when the map is ready to be used.
val mapFragment = supportFragmentManager
.findFragmentById(R.id.map) as SupportMapFragment
mapFragment.getMapAsync(this)
}//onCreate
/**
* Manipulates the map once available.
* This callback is triggered when the map is ready to be used.
* This is where we can add markers or lines, add listeners or move the camera. In this case,
* we just add a marker near Sydney, Australia.
* If Google Play services is not installed on the device, the user will be prompted to install
* it inside the SupportMapFragment. This method will only be triggered once the user has
* installed Google Play services and returned to the app.
*/
override fun onMapReady(googleMap: GoogleMap) {
mMap = googleMap
//8
//loadLibraries()호출
loadLibraries()
//9-2
//마커에 tag단 것을 이용해
//마커를 클릭했을 때 홈페이지 주소를 웹브라우저로 생성
mMap.setOnMarkerClickListener {
//9-3
//마커의 tag가 null값이 아니라면
if (it.tag != null){
//9-4
//마커의 tag를 String으로 형변환하고
//tag가 http로 시작하지 않으면
//http://문자열을 앞에 추가
var url = it.tag as String
if (!url.startsWith("http")){
url = "http://${url}"
}
//9-5
//완성된 url을 intent로 생성한 후
//액티비티 호출
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
startActivity(intent)
}
true
}
}//onMapReady
//5
//정의한 인터페이스를 정의하고 데이터 불러오는 코드 작성
//loadLibraries() 메서드 생성
fun loadLibraries(){
//5-1
//도메인 주소와 JSON컨버터를 설정하여
//레트로핏 생성
val retrofit = Retrofit.Builder()
.baseUrl(SeoulOpenApi.DOMAIN)
.addConverterFactory(GsonConverterFactory.create())
.build()
//5-2
//레트로핏을 사용해 정의해두었던 인터페이스를
//실행가능한 서비스 객체로 변환
val seoulOpenService = retrofit.create(SeoulOpenService::class.java)
//5-3
//인터페이스에 정의된 getLibrary()메서드에 파라미터로
//SeoulOpenApi의 API_KEY를 입력하고
//enqueue()메서드를 호출하여 서버에 요청
seoulOpenService.getLibrary(SeoulOpenApi.API_KEY).enqueue(object: Callback<Library>{
//5-4
//인터페이스의 코드 2개 자동 생성
override fun onResponse(call: Call<Library>, response: Response<Library>) {
//5-5
//서버 요청이 정상적으로 되었다면
//지도에 마커를 표시하는 메서드 호출(마커 표시 메서드 아래에 생성필요...)
showLibraries(response.body() as Library)
}
override fun onFailure(call: Call<Library>, t: Throwable) {
//5-6
//서버 요청이 실패했을 경우 토스트메시지 생성
Toast.makeText(this@MapsActivity, "서버요청 실패", Toast.LENGTH_SHORT).show()
}
})
}//loadLibraries
//6
//지도에 도서관 마커 표시하는 메서드생성
fun showLibraries(libraries: Library){
//7
//마커가 지도에 표기되지만 지도를 보여주는 카메라는 시드니를 가리킴
//카메라 위치조정 필요
//마커 전체의 영역으로 먼저 구해
//마커의 영역만큼 보여주는 코드 작성
//마커의 영역으로 저장하는 LatLngBounds.Builder생성
val latLngBounds = LatLngBounds.Builder()
//6-1
//파라미터로 전달된 libraries의
//SeoulPublicLibraryInfo.row에 도서관 목록이 있음
//반복문으로 하나씩 꺼내어 미커 생성하여 추가
for (lib in libraries.SeoulPublicLibraryInfo.row){
//6-2
//마커좌표 생성
val position = LatLng(lib.XCNTS.toDouble(), lib.YDNTS.toDouble())
//6-3
//좌표와 도서관 이름으로 마커 생성
val marker = MarkerOptions().position(position).title(lib.LBRRY_NAME)
//6-4
//마커를 지도에 추가
//mMap.addMarker(marker)
//9
//도서관 이름 클릭 시 홈페이지로 이동하기
//도서관 홈페이지 URL검사 후
//홈페이지를 웹 프라우저에 띄우는 코드 작성
//9-1
//6-4코드 수정
//마커에 tag정보 추가
//지도에 마커를 추가하고 그 마커 tag값에 홈페이지 주소 저장
var obj = mMap.addMarker(marker)
obj?.tag = lib.HMPG_URL
//7-1
//지도에 마커 추가 후 latLngBounds에도 마커 추가
latLngBounds.include(marker.position)
}
//7-2
//앞에서 저장해둔 마커의 영역 구하기
//padding변수는 마커의 영역에 여백을 얼마나 줄 것인지 정함
val bounds = latLngBounds.build()
val padding = 0
//7-3
//카메라 업데이트
//bounds와 padding설정하여 카메라 업데이트
val updated = CameraUpdateFactory.newLatLngBounds(bounds, padding)
//7-4
//업데이트된 카메라 지도에 반영
mMap.moveCamera(updated)
}//showLibraries
}//MapsActivity
-SeoulOpenApi.kt
//1
//Open API 사용을 위해
//기본정보를 담아두는 클래스와
//클래스 안에 레트로핏에서 사용할 인터페이스 생성
class SeoulOpenApi {
//2
//companion object{}를 만들어
//도메인 주소와 API키 저장해놓은 변수 2개 생성
//companion object: 블록안에 변수 생성 시 '클래스명.변수명' 형식으로
//바로 사용할 수 있음
companion object{
val DOMAIN = BuildConfig.LBRRY_DOMAIN
val API_KEY = BuildConfig.LBRRY_API_KEY
}
}//SeoulOpenApi
//3
//레트로핏에서 사용할 인터페이스 SeoulOpenService생성
interface SeoulOpenService{
//3-1
//도서관 데이터를 가져오는 getLibrary()메서드 정의
//@GET 애노테이션 사용해 호출할 주소 지정
//레트로핏에서 사용할 때 @GET에 입력된 주소와
//정의해둔 DOMAIN주소를 조합해 사용할 것임
//getLibrary()메서드의 파라미터로 사용된 key는
//SeoulOpenApi클래스에 정의한 API_KEY를
//레트로핏을 실행하는 코드에서 넘겨받은 후 주소와 결합하고
//반환 값은 Call<JSON변환된 클래스>
//도서관의 개수가 총 120개 이므로 한 페이지에 모두 불러오기 위해
//주소 끝 부분에 페이지1/가져올개수200 입력
// @GET("/json/SeoulPublicLibraryInfo/1/200/")
// fun getLibrary(key: String): Call<Library>
//위 코드 수정하기...
//@Path애노테이션 사용
//메서드의 파라미터로 넘어온 값을
//@GET에 정의된 주소에 동적으로 삽일할 수 있음...
//메서드의 파라미터 변수 앞에 @Path 애노테이션으로
//@GET 주소에 매핑할 이름 작성하고
//@GET문자열에 {매핑할 이름}형식으로 삽입하면
//메서드가 호출되는 순간 매핑할 이름이
//정의된 파라미터의 값으로 대체된 후 사용됨
@GET("{api_key}/json/SeoulPublicLibraryInfo/1/200/")
fun getLibrary(@Path("api_key") key: String): Call<Library>
}
//4
//정의한 인터페이스를 정의하고 데이터 불러오는 코드 작성
//MapsActivity.kr
*결과
*맵 클러스터링 사용하여 마커 설정
-구글 맵 클러스터링(Google Map Clustering)
--지도에 나타나는 여러 개의 마커를 묶어 하나의 그룹으로 표시할 수 있음
--클러스터 매니저(ClusterManager)를 통해 사용
--https://developers.google.com/maps/documentation/android-sdk/utility/marker-clustering
--위 프로젝트에 이어서 진행
-build.gradle
--dependencies{} 에 클러스터 아이템 라이브러리 추가
--https://developers.google.com/maps/documentation/android-sdk/utility/setup
//++ 클러스터 아이템 라이브러리 추가
dependencies {
implementation 'com.google.maps.android:android-maps-utils:2.3.0'
-Row.kt
import com.google.android.gms.maps.model.LatLng
import com.google.maps.android.clustering.ClusterItem
//++1
//마커에 해당하는 클래스가 Row클래스 이기 때문에
//클러스터아이템 상속 받은 후
//좌표를 반환하는 함수와 부가 정보를 반환하는 함수들 구현
data class Row(
val ADRES: String,
val CODE_VALUE: String,
val FDRM_CLOSE_DATE: String,
val GU_CODE: String,
val HMPG_URL: String,
val LBRRY_NAME: String,
val LBRRY_SEQ_NO: String,
val LBRRY_SE_NAME: String,
val OP_TIME: String,
val TEL_NO: String,
val XCNTS: String,
val YDNTS: String
): ClusterItem {
//++2
//데이터 클래스에 클러스터 아이템을 추가하고 필수 메서드 오버라이드
//++3
//개별 마커가 표시될 좌표
override fun getPosition(): LatLng {
return LatLng(XCNTS.toDouble(), YDNTS.toDouble())
}
//++4
//마커를 클릭했을 때 나타나는 타이틀
override fun getTitle(): String? {
return LBRRY_NAME
}
//++5
//마커를 클릭했을 때 나타나는 서브 타이틀
override fun getSnippet(): String? {
return ADRES
}
//++6
//id에 해당하는 유일한 값을 Int로 반환
//값 중에 null이 있을 경우 hashCoda생성 시 오류 발생
override fun hashCode(): Int {
return LBRRY_SEQ_NO.toInt()
}
}
//++7
//앱이 실행되고 지도에 마커표시가 될 때
//안드로이드는 Row클래스의 getPosition()메서드 호출
//해당 마커의 좌표 계산한 뒤
//특정 범위 안에 있는 마커들을 묶어 하나의 마커로 만들고
//몇 개의 마커가 포함되어 있는지 숫자로 표시함
//클러스터 매니저를 통해 클러스터 사용...
//MapsActivity.kt 에 선언
-MapsActivity.kt
--++1 ~ ++5 참고
/** 서울 공공도서관 앱 개발하기 **/
/** ++구글 맵 클러스터링 사용(마커) **/
//++1 구글 맵 클러스터링
//지도에 나타나는 여러 개의 마커를 묶어 하나의 그룹으로 표시
//마커에 해당하는 클래스가 Row클래스 이기 때문에
//data - Row클래스에서 클러스터아이템 상속 받은 후
//좌표를 반환하는 함수와 부가 정보를 반환하는 함수들 구현
//Row.kt 수정
//1
//구글 지도 API키 추가
//레트로핏, JSON컨버터 라이브러리 추가
//build.gradle(:app)
//2
//위치권한 및 인터넷권한 추가
//AndroidManifest.xml
//3
//사용할 JSON 샘플데이터를 사용하여
//코틀린 데이터 클래스 생성
//클래스의 개수가 여러 개일 경우 관리를 위해
//기본 패키지 밑에 data패키지 생성
//패키지 우클릭 - New - Package - data패키지 생성
//data패키지 폴더 우클릭 - New - Kotlin data class File from Json 클릭
//빈 여백에 샘플 데이터 붙여넣기, Format으로 정렬 후
//Class Name은 Library 입력
//4
//Open API 사용을 위해
//기본정보를 담아두는 클래스와
//클래스 안에 레트로핏에서 사용할 인터페이스 생성
class MapsActivity : AppCompatActivity(), OnMapReadyCallback {
//++2
//클러스터 매니저를 통해 클러스터 사용...
//클러스터 매니저 프로퍼티 선언
private lateinit var clusterManager: ClusterManager<Row>
private lateinit var mMap: GoogleMap
private lateinit var binding: ActivityMapsBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMapsBinding.inflate(layoutInflater)
setContentView(binding.root)
// Obtain the SupportMapFragment and get notified when the map is ready to be used.
val mapFragment = supportFragmentManager
.findFragmentById(R.id.map) as SupportMapFragment
mapFragment.getMapAsync(this)
}//onCreate
/**
* Manipulates the map once available.
* This callback is triggered when the map is ready to be used.
* This is where we can add markers or lines, add listeners or move the camera. In this case,
* we just add a marker near Sydney, Australia.
* If Google Play services is not installed on the device, the user will be prompted to install
* it inside the SupportMapFragment. This method will only be triggered once the user has
* installed Google Play services and returned to the app.
*/
override fun onMapReady(googleMap: GoogleMap) {
mMap = googleMap
//++3
//클러스터 매니저 초기화 및 설정
//마커를 표시하기 전 설정
//Map이 생성 된 직후 설정함
clusterManager = ClusterManager(this, mMap)
//++3-1
//화면 이동 후 멈췄을 때 설정
mMap.setOnCameraIdleListener(clusterManager)
//++3-2
//마커 클릭 설정
mMap.setOnMarkerClickListener(clusterManager)
//8
//loadLibraries()호출
loadLibraries()
//++4
//기존 마커 세팅 코드 삭제
// //9-2
// //마커에 tag단 것을 이용해
// //마커를 클릭했을 때 홈페이지 주소를 웹브라우저로 생성
// mMap.setOnMarkerClickListener {
//
// //9-3
// //마커의 tag가 null값이 아니라면
// if (it.tag != null){
//
// //9-4
// //마커의 tag를 String으로 형변환하고
// //tag가 http로 시작하지 않으면
// //http://문자열을 앞에 추가
// var url = it.tag as String
// if (!url.startsWith("http")){
// url = "http://${url}"
// }
// //9-5
// //완성된 url을 intent로 생성한 후
// //액티비티 호출
// val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
// startActivity(intent)
// }
// true
// }
}//onMapReady
//5
//정의한 인터페이스를 정의하고 데이터 불러오는 코드 작성
//loadLibraries() 메서드 생성
fun loadLibraries(){
//5-1
//도메인 주소와 JSON컨버터를 설정하여
//레트로핏 생성
val retrofit = Retrofit.Builder()
.baseUrl(SeoulOpenApi.DOMAIN)
.addConverterFactory(GsonConverterFactory.create())
.build()
//5-2
//레트로핏을 사용해 정의해두었던 인터페이스를
//실행가능한 서비스 객체로 변환
val seoulOpenService = retrofit.create(SeoulOpenService::class.java)
//5-3
//인터페이스에 정의된 getLibrary()메서드에 파라미터로
//SeoulOpenApi의 API_KEY를 입력하고
//enqueue()메서드를 호출하여 서버에 요청
seoulOpenService.getLibrary(SeoulOpenApi.API_KEY).enqueue(object: Callback<Library>{
//5-4
//인터페이스의 코드 2개 자동 생성
override fun onResponse(call: Call<Library>, response: Response<Library>) {
//5-5
//서버 요청이 정상적으로 되었다면
//지도에 마커를 표시하는 메서드 호출(마커 표시 메서드 아래에 생성필요...)
showLibraries(response.body() as Library)
}
override fun onFailure(call: Call<Library>, t: Throwable) {
//5-6
//서버 요청이 실패했을 경우 토스트메시지 생성
Toast.makeText(this@MapsActivity, "서버요청 실패", Toast.LENGTH_SHORT).show()
}
})
}//loadLibraries
//6
//지도에 도서관 마커 표시하는 메서드생성
fun showLibraries(libraries: Library){
//7
//마커가 지도에 표기되지만 지도를 보여주는 카메라는 시드니를 가리킴
//카메라 위치조정 필요
//마커 전체의 영역으로 먼저 구해
//마커의 영역만큼 보여주는 코드 작성
//마커의 영역으로 저장하는 LatLngBounds.Builder생성
//latLngBounds 는 전체 마커를 화면에 보여주기 위한 용도
//현재 내 위치 기준으로 마커표시한다면 필요하지 않음...
val latLngBounds = LatLngBounds.Builder()
//6-1
//파라미터로 전달된 libraries의
//SeoulPublicLibraryInfo.row에 도서관 목록이 있음
//반복문으로 하나씩 꺼내어 미커 생성하여 추가
//++5
//반복문에 마커 생성코드 삭제
//클러스터 메니저에 직접 데이터 추가
for (lib in libraries.SeoulPublicLibraryInfo.row){
//++5-1
//클러스터 매니저에 데이터 추가
clusterManager.addItem(lib)
//++5-2
//첫 화면에 보여줄 범위를 정하기 위해 코드 남겨두기...
//6-2
//마커좌표 생성
val position = LatLng(lib.XCNTS.toDouble(), lib.YDNTS.toDouble())
//
// //6-3
// //좌표와 도서관 이름으로 마커 생성
// val marker = MarkerOptions().position(position).title(lib.LBRRY_NAME)
//
// //6-4
// //마커를 지도에 추가
// mMap.addMarker(marker)
//
// //9
// //도서관 이름 클릭 시 홈페이지로 이동하기
// //도서관 홈페이지 URL검사 후
// //홈페이지를 웹 프라우저에 띄우는 코드 작성
// //9-1
// //6-4코드 수정
// //마커에 tag정보 추가
// //지도에 마커를 추가하고 그 마커 tag값에 홈페이지 주소 저장
// var obj = mMap.addMarker(marker)
// obj?.tag = lib.HMPG_URL
//
//++5-2
//첫 화면에 보여줄 범위를 정하기 위해 코드 남겨두기...
//7-1
//지도에 마커 추가 후 latLngBounds에도 마커 추가
latLngBounds.include(position)
}
//7-2
//앞에서 저장해둔 마커의 영역 구하기
//padding변수는 마커의 영역에 여백을 얼마나 줄 것인지 정함
val bounds = latLngBounds.build()
val padding = 0
//7-3
//카메라 업데이트
//bounds와 padding설정하여 카메라 업데이트
val updated = CameraUpdateFactory.newLatLngBounds(bounds, padding)
//7-4
//업데이트된 카메라 지도에 반영
mMap.moveCamera(updated)
}//showLibraries
}//MapsActivity
*결과
이 포스팅에 작성한 내용은 고돈호, ⌜이것이 안드로이드다⌟, 한빛미디어(주), 2022 에서 발췌하였습니다.
'Android App > Kotlin' 카테고리의 다른 글
파이어베이스-리얼타임 데이터베이스(Realtime Database) 생성 (0) | 2022.12.12 |
---|---|
파이어베이스(Firebase)-프로젝트 생성 (0) | 2022.12.12 |
네트워크-데이터통신/Github API 사용(*레트로핏(Retrofit) 및 converter-gson, Glide 라이브러리) (0) | 2022.12.07 |
네트워크-데이터통신(레트로핏(Retrofit)라이브러리, JSON) (1) | 2022.12.07 |
네트워크-데이터 통신(HttpURLConnection) (0) | 2022.12.06 |