GPS off
πŸ“·
Kamera
πŸ‘€
Tekan "Buka Kamera" untuk memulai
πŸ—ΊοΈ Foto akan menyertakan watermark: OmahsinauMap Β· 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 Menggunakan OpenStreetMap / Leaflet.js
πŸ–ΌοΈ
Galeri Foto
0 foto
βš™οΈ
Kode Kotlin Android
MainActivity
CameraX
GPS/Location
Watermark
GoogleMaps
build.gradle
// MainActivity.kt
package com.example.omahsinaumap

import android.Manifest
import android.content.pm.PackageManager
import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.example.geomap.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private lateinit var cameraManager: CameraManager
    private lateinit var locationManager: LocationManager
    private lateinit var mapsManager: MapsManager

    private val permissions = arrayOf(
        Manifest.permission.CAMERA,
        Manifest.permission.ACCESS_FINE_LOCATION,
        Manifest.permission.WRITE_EXTERNAL_STORAGE
    )

    private val permLauncher = registerForActivityResult(
        ActivityResultContracts.RequestMultiplePermissions()
    ) { results ->
        val allGranted = results.values.all { it }
        if (allGranted) initFeatures()
        else showPermissionDeniedDialog()
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        checkAndRequestPermissions()
    }

    private fun checkAndRequestPermissions() {
        val denied = permissions.filter {
            ContextCompat.checkSelfPermission(this, it) !=
                PackageManager.PERMISSION_GRANTED
        }
        if (denied.isEmpty()) initFeatures()
        else permLauncher.launch(denied.toTypedArray())
    }

    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)
        locationManager.startTracking()
    }
}
// CameraManager.kt β€” CameraX + Watermark
class CameraManager(
    private val activity: AppCompatActivity,
    private val binding: ActivityMainBinding
) {
    private var imageCapture: ImageCapture? = null
    private var cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

    fun startCamera() {
        val cameraProviderFuture =
            ProcessCameraProvider.getInstance(activity)
        cameraProviderFuture.addListener({
            val cameraProvider = cameraProviderFuture.get()
            val preview = Preview.Builder().build().also {
                it.setSurfaceProvider(binding.previewView.surfaceProvider)
            }
            imageCapture = ImageCapture.Builder()
                .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
                .build()
            try {
                cameraProvider.unbindAll()
                cameraProvider.bindToLifecycle(
                    activity, cameraSelector, preview, imageCapture
                )
            } catch (e: Exception) {
                Log.e("Camera", "Bind failed", e)
            }
        }, ContextCompat.getMainExecutor(activity))
    }

    fun capturePhoto(
        location: Location?,
        address: String,
        identity: String    // NEW: identity name
    ) {
        val outputFile = createOutputFile()
        val outputOptions =
            ImageCapture.OutputFileOptions.Builder(outputFile).build()
        imageCapture?.takePicture(
            outputOptions,
            ContextCompat.getMainExecutor(activity),
            object : ImageCapture.OnImageSavedCallback {
                override fun onImageSaved(
                    output: ImageCapture.OutputFileResults
                ) {
                    WatermarkHelper.addWatermark(
                        outputFile, location, address, identity
                    )
                }
                override fun onError(e: ImageCaptureException) {
                    Log.e("Camera", "Capture error", e)
                }
            }
        )
    }
}
// LocationManager.kt β€” FusedLocationProvider + Geocoder
class LocationManager(
    private val context: Context,
    private val onUpdate: (Location) -> Unit
) {
    private val fusedClient =
        LocationServices.getFusedLocationProviderClient(context)
    private val locationRequest =
        LocationRequest.Builder(
            Priority.PRIORITY_HIGH_ACCURACY, 5000L
        ).setMinUpdateIntervalMillis(2000L).build()

    private val locationCallback = object : LocationCallback() {
        override fun onLocationResult(result: LocationResult) {
            result.lastLocation?.let { onUpdate(it) }
        }
    }

    @SuppressLint("MissingPermission")
    fun startTracking() {
        fusedClient.requestLocationUpdates(
            locationRequest, locationCallback, Looper.getMainLooper()
        )
    }

    fun reverseGeocode(location: Location, onResult: (String) -> Unit) {
        CoroutineScope(Dispatchers.IO).launch {
            try {
                val geocoder = Geocoder(context, Locale.getDefault())
                val address = geocoder.getFromLocation(
                    location.latitude, location.longitude, 1
                )?.firstOrNull()?.getAddressLine(0) ?: "Tidak ditemukan"
                withContext(Dispatchers.Main) { onResult(address) }
            } catch (e: IOException) {
                withContext(Dispatchers.Main) { onResult("Gagal: ${e.message}") }
            }
        }
    }
}
// WatermarkHelper.kt β€” Termasuk peta mini + identitas + OmahsinauMap
object WatermarkHelper {

    fun addWatermark(
        file: File,
        location: Location?,
        address: String,
        identity: String   // identitas pengguna
    ) {
        val original = BitmapFactory.decodeFile(file.path)
        val mutable = original.copy(Bitmap.Config.ARGB_8888, true)
        val canvas = Canvas(mutable)
        val W = mutable.width.toFloat()
        val H = mutable.height.toFloat()
        val stripH = H * 0.22f

        // ── Semi-transparent background ──
        Paint().apply { color = Color.argb(200,8,8,18) }.also {
            canvas.drawRect(0f, H - stripH, W, H, it)
        }

        // ── GeoMap logo area (left column) ──
        val logoSize = stripH * 0.38f
        val textPaint = Paint().apply {
            isAntiAlias = true; typeface = Typeface.MONOSPACE
            color = Color.WHITE; setShadowLayer(3f,1f,1f,Color.BLACK)
        }

// ═══════════════════════════════════════════════════════════
//  WATERMARK  β€” OmahsinauMap logo Β· name Β· info Β· mini-map (Kotlin)
        textPaint.textSize = logoSize; textPaint.color = Color.parseColor("#00e5ff")
        canvas.drawText("OmahsinauMap", W*0.02f, H - stripH + logoSize + W*0.005f, textPaint)

        // Divider line
        Paint().apply { color = Color.parseColor("#00e5ff"); strokeWidth = 2f; alpha = 120 }.also {
            canvas.drawLine(W*0.02f, H - stripH + logoSize*1.3f, W*0.28f, H - stripH + logoSize*1.3f, it)
        }

        // Identity, timestamp, address, coords
        textPaint.textSize = logoSize * 0.55f; textPaint.color = Color.WHITE
        val lineH = textPaint.textSize * 1.5f
        val x = W * 0.02f
        var y = H - stripH + logoSize * 1.3f + textPaint.textSize + 8f
        if (identity.isNotBlank()) {
            textPaint.color = Color.parseColor("#f59e0b")
            canvas.drawText("πŸ‘€ $identity", x, y, textPaint); y += lineH
            textPaint.color = Color.WHITE
        }
        val ts = SimpleDateFormat("dd-MM-yyyy HH:mm", Locale.getDefault()).format(Date())
        canvas.drawText("πŸ“… $ts", x, y, textPaint); y += lineH
        canvas.drawText("πŸ“ ${address.take(38)}", x, y, textPaint); y += lineH
        val coord = location?.let { "πŸ›° %.6f, %.6f".format(it.latitude, it.longitude) } ?: "πŸ›° Tidak tersedia"
        canvas.drawText(coord, x, y, textPaint)

        // ── Mini-map (right column) ──
        MapThumbnailHelper.draw(canvas, location, W, H, stripH)

        // Save
        FileOutputStream(file).use { mutable.compress(Bitmap.CompressFormat.JPEG, 95, it) }
        original.recycle(); mutable.recycle()
    }
}

// MapThumbnailHelper.kt β€” draws a stylized mini-map on Canvas
object MapThumbnailHelper {
    fun draw(canvas: Canvas, loc: Location?, W: Float, H: Float, stripH: Float) {
        val mapW = W * 0.28f
        val mapH = stripH * 0.88f
        val mapX = W - mapW - W * 0.02f
        val mapY = H - stripH + (stripH - mapH) / 2f
        // Background + border drawn, grid + pin drawn inside
        // (full implementation in repo)
    }
}
// MapsManager.kt β€” Google Maps Marker
class MapsManager(
    private val mapFragment: SupportMapFragment
) : OnMapReadyCallback {
    private var googleMap: GoogleMap? = null
    private var currentMarker: Marker? = null

    init { mapFragment.getMapAsync(this) }

    override fun onMapReady(map: GoogleMap) {
        googleMap = map
        googleMap?.apply {
            uiSettings.isZoomControlsEnabled = true
            mapType = GoogleMap.MAP_TYPE_NORMAL
        }
    }

    fun updateMarker(location: Location) {
        val latLng = LatLng(location.latitude, location.longitude)
        googleMap?.apply {
            currentMarker?.remove()
            currentMarker = addMarker(
                MarkerOptions().position(latLng).title("Lokasi Anda")
                    .icon(BitmapDescriptorFactory.defaultMarker(
                        BitmapDescriptorFactory.HUE_CYAN))
            )
            animateCamera(CameraUpdateFactory.newLatLngZoom(latLng, 16f))
        }
    }
}
// build.gradle (app level)
android {
    compileSdk 34
    defaultConfig { minSdk 24; targetSdk 34 }
    buildFeatures { viewBinding = true }
}
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"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0"
    implementation "androidx.appcompat:appcompat:1.6.1"
    implementation "com.google.android.material:material:1.11.0"
}