GPS off
πŸ“·
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
0 foto
βš™οΈ
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"
}