A smart Android compass application that guides you to the nearest convenience store or supermarket using real-time location tracking and sensor fusion.
Features β’ Screenshots β’ Architecture β’ Installation β’ Usage β’ Tech Stack
|
|
graph TB
subgraph "Presentation Layer"
A[MainActivity] --> B[Navigation Graph]
B --> C[WelcomeScreen]
B --> D[CompassScreen]
D --> E[CompassView Component]
end
subgraph "ViewModel Layer"
F[LocationViewModel]
G[CompassViewModel]
H[StoreViewModel]
end
subgraph "Data Layer"
I[LocationManager]
J[CompassManager]
K[OverpassRepository]
L[ApiClient]
end
subgraph "Model Layer"
M[LocationData]
N[Store]
O[OverpassModels]
end
subgraph "Utils"
P[LocationsUtils]
end
D --> F
D --> G
D --> H
F --> I
G --> J
H --> K
K --> L
I --> M
K --> N
K --> O
F --> P
G --> P
H --> P
style A fill:#ff6b6b
style D fill:#4ecdc4
style F fill:#95e1d3
style G fill:#95e1d3
style H fill:#95e1d3
style I fill:#f38181
style J fill:#f38181
style K fill:#f38181
sequenceDiagram
participant User
participant UI as CompassScreen
participant LVM as LocationViewModel
participant CVM as CompassViewModel
participant SVM as StoreViewModel
participant LM as LocationManager
participant CM as CompassManager
participant API as Overpass API
User->>UI: Open App
UI->>LVM: startLocationUpdates()
UI->>CVM: startCompass()
LVM->>LM: getLocationUpdates()
LM-->>LVM: Location Flow
LVM-->>UI: currentLocation StateFlow
UI->>SVM: searchNearbyStores(location)
SVM->>API: getNearbyStores(lat, lon)
API-->>SVM: Store List
SVM-->>UI: nearestStore StateFlow
CVM->>CM: getCompassFlow()
CM-->>CVM: Azimuth Flow
UI->>CVM: updateTargetBearing(bearing)
CVM-->>UI: compassRotation StateFlow
UI->>User: Display Navigation
com.stargazer.nearestcompass/
β
βββ π± MainActivity.kt # Application entry point
β
βββ π§ navigation/
β βββ Screen.kt # Navigation routes
β βββ NavGraph0.kt # Navigation graph setup
β
βββ π¨ ui/
β βββ components/
β β βββ CompassView.kt # Custom compass UI component
β βββ screens/
β β βββ WelcomeScreen.kt # Onboarding & permissions
β β βββ CompassScreen.kt # Main navigation screen
β βββ theme/
β βββ NearestCompassTheme.kt # Material 3 theme
β
βββ π§ viewmodel/
β βββ LocationViewModel.kt # Location state management
β βββ CompassViewModel.kt # Compass sensor management
β βββ StoreViewModel.kt # Store search logic
β
βββ πΎ data/
β βββ model/
β β βββ LocationData.kt # Location data class
β β βββ Store.kt # Store entity + enum
β β βββ OverpassModels.kt # API response models
β βββ location/
β β βββ LocationManager.kt # GPS & location services
β β βββ CompassManager.kt # Sensor fusion logic
β βββ repository/
β βββ OverpassRepository.kt # API data source
β βββ ApiClient.kt # Ktor HTTP client
β
βββ π οΈ utils/
βββ LocationsUtils.kt # Haversine, bearing calculations
|
|
| Category | Library | Version | Purpose |
|---|---|---|---|
| UI | Jetpack Compose | 1.5.0 | Declarative UI framework |
| Navigation | Compose Navigation | 2.7.0 | Screen navigation |
| Architecture | Lifecycle ViewModel | 2.6.1 | State management |
| Async | Kotlin Coroutines | 1.7.3 | Asynchronous programming |
| Location | Google Play Services Location | 21.0.1 | GPS & location tracking |
| Sensors | Android Sensor Framework | Native | Accelerometer + Magnetometer |
| Network | Ktor Client Android | 2.3.2 | HTTP client |
| Serialization | Gson | 2.10.1 | JSON parsing |
| Permissions | Accompanist Permissions | 0.32.0 | Runtime permission handling |
- Android Studio: Hedgehog (2023.1.1) or later
- JDK: Version 17 or higher
- *Target SDK: 36
- Gradle: 8.0+
-
Clone the repository
git clone https://github.com/berat-karabuga/NearestCompass.git cd NearestCompass -
Open in Android Studio
- Launch Android Studio
- Select
File > Open - Navigate to the cloned directory
- Wait for Gradle sync to complete
-
Configure API Keys (Optional)
Note: This app uses the public Overpass API, no API key required!
-
Build the project
./gradlew build
-
Run on device/emulator
- Connect an Android device with USB debugging enabled, or
- Start an Android Virtual Device (AVD)
- Click
Run > Run 'app'or pressShift + F10
-
Launch the App
- Open Nearest Compass on your device
- You'll see the welcome screen with "Find nearest!" tagline
-
Grant Location Permissions
- Tap "Let's Find" button
- Allow precise location access when prompted
- Choose "While using the app" for best experience
-
Wait for Location Lock
- App will acquire your GPS coordinates
- Accuracy information displayed at the top
- Typical accuracy: 5-20 meters
-
Store Search Begins Automatically
- App searches for stores within 2km radius
- Queries both convenience stores and supermarkets
- Results sorted by distance
-
Follow the Compass
- Red arrow points to nearest store
- Distance shown in meters or kilometers
- Turn-by-turn instructions update as you move
- Compass rotates based on device orientation
- Sensor Fusion: Combines accelerometer and magnetometer data
- Smoothing Algorithm: 10-sample moving average for stability
- Bearing Calculation: Accurate angular measurement using trigonometry
- Real-time Updates: 2-5 second refresh intervals
- High Accuracy Mode: GPS + Network + Sensors
- Continuous Updates: Every 5 seconds (minimum 2s interval)
- Battery Optimized: Smart location request intervals
- Accuracy Threshold: Waits for <20m accuracy before searching
- Search Radius: 2000 meters (configurable)
- Store Types:
shop=convenience(Local groceries, small markets)shop=supermarket(Large chain supermarkets)
- Data Source: OpenStreetMap via Overpass API
- Sorting: Automatic distance-based ranking
The app uses a custom Overpass QL query to find nearby stores:
[out:json][timeout:25];
(
node["shop"="convenience"](around:2000,41.025756,28.694833);
node["shop"="supermarket"](around:2000,41.025756,28.694833);
);
out body;
>;
out skel qt;Query Breakdown:
[out:json]: Response format[timeout:25]: Maximum query time (25 seconds)node["shop"="convenience"]: Filter for convenience stores(around:2000,lat,lon): Search within 2km radiusout body: Return full node data including tags
data class OverpassModels(
val version: String?,
val elements: List<OverpassElement>
)
data class OverpassElement(
val type: String?,
val id: Long?,
val lat: Double?,
val lon: Double?,
val tags: OverpassTags?
)Calculates the great-circle distance between two GPS coordinates:
fun calculateDistance(from: LocationData, to: LocationData): Double {
val earthRadius = 6371000.0 // meters
val lat1 = Math.toRadians(from.latitude)
val lat2 = Math.toRadians(to.latitude)
val deltaLat = Math.toRadians(to.latitude - from.latitude)
val deltaLon = Math.toRadians(to.longitude - from.longitude)
val a = sin(deltaLat / 2).pow(2) +
cos(lat1) * cos(lat2) *
sin(deltaLon / 2).pow(2)
val c = 2 * atan2(sqrt(a), sqrt(1 - a))
return earthRadius * c
}Accuracy: Β±0.5% for distances up to 100km
Computes the initial bearing (forward azimuth) to target:
fun calculateBearing(from: LocationData, to: LocationData): Float {
val lat1 = Math.toRadians(from.latitude)
val lat2 = Math.toRadians(to.latitude)
val deltaLon = Math.toRadians(to.longitude - from.longitude)
val y = sin(deltaLon) * cos(lat2)
val x = cos(lat1) * sin(lat2) -
sin(lat1) * cos(lat2) * cos(deltaLon)
val bearing = Math.toDegrees(atan2(y, x))
return ((bearing + 360) % 360).toFloat()
}Output: 0-360Β° where 0Β° = North, 90Β° = East
Combines accelerometer and magnetometer readings:
private fun updateOrientation(): Float? {
val rotationMatrix = FloatArray(9)
val orientationAngles = FloatArray(3)
val success = SensorManager.getRotationMatrix(
rotationMatrix,
null,
accelerometerReading,
magnetometerReading
)
if (success) {
SensorManager.getOrientation(rotationMatrix, orientationAngles)
val azimuthInRadians = orientationAngles[0]
val azimuthInDegrees = Math.toDegrees(azimuthInRadians.toDouble()).toFloat()
return (azimuthInDegrees + 360) % 360
}
return null
}Smoothing: 10-sample moving average filter reduces jitter
Custom Canvas-based compass implementation:
@Composable
fun CompassView(rotation: Float, modifier: Modifier = Modifier) {
Canvas(modifier = Modifier.size(300.dp)) {
val center = Offset(size.width / 2f, size.height / 2f)
val radius = size.minDimension / 2f
// Outer ring
drawCircle(color = Color.LightGray, radius = radius, center = center)
// Inner ring
drawCircle(color = Color.Gray, radius = radius * 0.9f, center = center)
// Rotatable red arrow
rotate(degrees = rotation, pivot = center) {
val arrowPath = Path().apply {
moveTo(center.x, center.y)
lineTo(center.x - 20f, center.y + 40f)
lineTo(center.x, center.y - radius * 0.7f)
lineTo(center.x + 20f, center.y + 40f)
close()
}
drawPath(path = arrowPath, color = Color.Red)
}
}
}Design Philosophy: Minimalist, high-contrast for outdoor visibility
<!-- Manifest.xml -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.INTERNET" />@OptIn(ExperimentalPermissionsApi::class)
val locationPermissionsState = rememberMultiplePermissionsState(
permissions = listOf(
android.Manifest.permission.ACCESS_FINE_LOCATION,
android.Manifest.permission.ACCESS_COARSE_LOCATION
)
)User Experience:
- Requests only when needed
- Explains why permissions are necessary
- Graceful degradation if denied
The project uses GitHub Actions for continuous integration:
# .github/workflows/android.yml
name: Android CI
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle
run: ./gradlew build- Location permission granted/denied scenarios
- GPS accuracy in urban/rural environments
- Compass rotation accuracy (use known landmarks)
- API timeout handling (airplane mode test)
- Battery consumption over 30 minutes
- Sensor calibration prompts
β οΈ Indoor Accuracy: GPS struggles inside buildings (expected behavior)β οΈ Magnetic Interference: Compass affected by metal objectsβ οΈ API Rate Limit: Overpass API has usage limits (rarely hit)
MIT License
Copyright (c) 2024 Berat Karabuga
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Berat Karabuga
- OpenStreetMap Contributors - For the comprehensive location database
- Overpass API - For the powerful query interface
- Google - For Android Location Services and Material Design
- JetBrains - For the amazing Kotlin language
- Android Community - For countless helpful resources
Made with β€οΈ and β by Berat Karabuga



