Kamera
Tekan "Buka Kamera" untuk memulai
πΊοΈ
Foto akan menyertakan watermark: GeoMap Β· nama Β· koordinat Β· peta mini
Lokasi GPS
Latitude
β menunggu β
Longitude
β menunggu β
Alamat (Reverse Geocoding)
Belum ada data lokasi
Peta Lokasi
πΊοΈ
Peta akan muncul setelah lokasi didapat
Cari Nama Tempat
Ketik nama tempat, gedung, jalan, atau kota β peta akan langsung tampil.
π
π
Cari tempat untuk melihat peta
Galeri Foto
π
Belum ada foto tersimpan
Ambil foto dari tab Kamera
Kode Kotlin Android
MainActivity
PlaceSearch
CameraX
GPS/Location
Watermark
build.gradle
// MainActivity.kt package com.example.geomap class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private lateinit var cameraManager: CameraManager private lateinit var locationManager: LocationManager private lateinit var mapsManager: MapsManager private lateinit var placeSearchManager: PlaceSearchManager override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) checkAndRequestPermissions() } private fun initFeatures() { cameraManager = CameraManager(this, binding) locationManager = LocationManager(this) { location -> binding.tvLat.text = location.latitude.toString() binding.tvLng.text = location.longitude.toString() mapsManager.updateMarker(location) locationManager.reverseGeocode(location) { address -> binding.tvAddress.text = address } } mapsManager = MapsManager(binding.mapView) // NEW: Place Search initialization placeSearchManager = PlaceSearchManager( context = this, searchInput = binding.etPlaceSearch, suggestionsView = binding.rvSuggestions, mapFragment = binding.placeMapFragment ) { selectedPlace -> binding.tvPlaceName.text = selectedPlace.name binding.tvPlaceAddress.text = selectedPlace.address binding.tvPlaceCoords.text = "%.6f, %.6f".format(selectedPlace.lat, selectedPlace.lng) } locationManager.startTracking() } }
// PlaceSearchManager.kt β Nominatim (OpenStreetMap) class PlaceSearchManager( private val context: Context, private val searchInput: EditText, private val suggestionsView: RecyclerView, private val mapFragment: SupportMapFragment, private val onPlaceSelected: (PlaceResult) -> Unit ) { data class PlaceResult( val name: String, val address: String, val lat: Double, val lng: Double, val type: String = "" ) private var googleMap: GoogleMap? = null private var searchJob: Job? = null private val adapter = PlaceSuggestionsAdapter { place -> selectPlace(place) } init { suggestionsView.adapter = adapter mapFragment.getMapAsync { map -> googleMap = map map.uiSettings.isZoomControlsEnabled = true map.mapType = GoogleMap.MAP_TYPE_NORMAL } searchInput.addTextChangedListener { text -> searchJob?.cancel() if (text.isNullOrBlank() || text.length < 3) { adapter.submitList(emptyList()); return@addTextChangedListener } searchJob = CoroutineScope(Dispatchers.Main).launch { delay(400) // debounce searchPlaces(text.toString()) } } } private suspend fun searchPlaces(query: String) { try { val url = Uri.parse("https://nominatim.openstreetmap.org/search") .buildUpon() .appendQueryParameter("q", query) .appendQueryParameter("format", "json") .appendQueryParameter("addressdetails", "1") .appendQueryParameter("limit", "6") .appendQueryParameter("accept-language", "id") .build().toString() val results = withContext(Dispatchers.IO) { URL(url).readText() // parse JSON β List} adapter.submitList(parseNominatimResponse(results)) } catch (e: Exception) { Log.e("PlaceSearch", "Search error", e) } } private fun selectPlace(place: PlaceResult) { searchInput.setText(place.name) adapter.submitList(emptyList()) val latLng = LatLng(place.lat, place.lng) googleMap?.apply { clear() addMarker(MarkerOptions() .position(latLng).title(place.name) .icon(BitmapDescriptorFactory .defaultMarker(BitmapDescriptorFactory.HUE_CYAN))) animateCamera(CameraUpdateFactory.newLatLngZoom(latLng, 15f)) } onPlaceSelected(place) } } // PlaceSuggestionsAdapter.kt (RecyclerView) class PlaceSuggestionsAdapter( private val onClick: (PlaceSearchManager.PlaceResult) -> Unit ) : ListAdapter<PlaceSearchManager.PlaceResult, PlaceSuggestionsAdapter.VH>(DiffCallback()) { inner class VH(val binding: ItemSuggestionBinding) : RecyclerView.ViewHolder(binding.root) { fun bind(item: PlaceSearchManager.PlaceResult) { binding.tvName.text = item.name binding.tvAddress.text = item.address binding.root.setOnClickListener { onClick(item) } } } override fun onCreateViewHolder(...) = VH(...) override fun onBindViewHolder(holder: VH, pos: Int) = holder.bind(getItem(pos)) }
// CameraManager.kt β CameraX + Watermark class CameraManager( private val activity: AppCompatActivity, private val binding: ActivityMainBinding ) { fun capturePhoto(location: Location?, address: String, identity: String) { val outputFile = createOutputFile() imageCapture?.takePicture( ImageCapture.OutputFileOptions.Builder(outputFile).build(), ContextCompat.getMainExecutor(activity), object : ImageCapture.OnImageSavedCallback { override fun onImageSaved(output: ImageCapture.OutputFileResults) { WatermarkHelper.addWatermark(outputFile, location, address, identity) } override fun onError(e: ImageCaptureException) { } } ) } }
// LocationManager.kt β FusedLocationProvider class LocationManager( private val context: Context, private val onUpdate: (Location) -> Unit ) { private val fusedClient = LocationServices.getFusedLocationProviderClient(context) @SuppressLint("MissingPermission") fun startTracking() { fusedClient.requestLocationUpdates( LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 5000L).build(), locationCallback, Looper.getMainLooper() ) } fun reverseGeocode(location: Location, onResult: (String) -> Unit) { CoroutineScope(Dispatchers.IO).launch { val address = Geocoder(context).getFromLocation( location.latitude, location.longitude, 1 )?.firstOrNull()?.getAddressLine(0) ?: "-" withContext(Dispatchers.Main) { onResult(address) } } } }
// WatermarkHelper.kt β logo + identitas + mini-map object WatermarkHelper { fun addWatermark(file: File, location: Location?, address: String, identity: String) { val canvas = Canvas(mutable) val stripH = H * 0.22f // Background panel, logo, identity, timestamp, address, coords, mini-map MapThumbnailHelper.draw(canvas, location, W, H, stripH) FileOutputStream(file).use { mutable.compress(Bitmap.CompressFormat.JPEG, 95, it) } } }
// build.gradle (app) dependencies { implementation "androidx.camera:camera-core:1.3.1" implementation "androidx.camera:camera-camera2:1.3.1" implementation "androidx.camera:camera-lifecycle:1.3.1" implementation "androidx.camera:camera-view:1.3.1" implementation "com.google.android.gms:play-services-location:21.2.0" implementation "com.google.android.gms:play-services-maps:18.2.0" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3" // Place Search β menggunakan Nominatim (gratis, tidak perlu API key) implementation "com.squareup.okhttp3:okhttp:4.12.0" implementation "org.json:json:20231013" }