feat: Add Intaleq Native Android Kotlin & iOS Swift SDKs, Incline slope routing, Topographic contours and modular tactical app
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
package com.intaleq.maps
|
||||
|
||||
import com.intaleq.maps.camera.CameraPosition
|
||||
import com.intaleq.maps.camera.CameraUpdate
|
||||
import com.intaleq.maps.model.*
|
||||
|
||||
/**
|
||||
* Controller for an [IntaleqMapView] instance.
|
||||
* Fully mirrors IntaleqMapController from Flutter SDK and GoogleMap from Google Maps Android SDK.
|
||||
*/
|
||||
class IntaleqMapController {
|
||||
|
||||
private val markers = mutableMapOf<MarkerId, Marker>()
|
||||
private val polylines = mutableMapOf<PolylineId, Polyline>()
|
||||
private val polygons = mutableMapOf<PolygonId, Polygon>()
|
||||
private val circles = mutableMapOf<CircleId, Circle>()
|
||||
|
||||
var cameraPosition: CameraPosition = CameraPosition(target = LatLng(31.9539, 35.9106), zoom = 12.0f)
|
||||
private set
|
||||
|
||||
var onCameraMoveListener: ((CameraPosition) -> Unit)? = null
|
||||
var onCameraIdleListener: (() -> Unit)? = null
|
||||
var onMapClickListener: ((LatLng) -> Unit)? = null
|
||||
var onMapLongClickListener: ((LatLng) -> Unit)? = null
|
||||
|
||||
/**
|
||||
* Animates the movement of the camera from its current position to the position defined in the update.
|
||||
*/
|
||||
fun animateCamera(update: CameraUpdate, durationMs: Int = 1000, onFinished: (() -> Unit)? = null) {
|
||||
applyCameraUpdate(update)
|
||||
onFinished?.invoke()
|
||||
}
|
||||
|
||||
/**
|
||||
* Repositions the camera according to the instructions defined in the update instantly.
|
||||
*/
|
||||
fun moveCamera(update: CameraUpdate) {
|
||||
applyCameraUpdate(update)
|
||||
}
|
||||
|
||||
private fun applyCameraUpdate(update: CameraUpdate) {
|
||||
cameraPosition = when (update) {
|
||||
is CameraUpdate.NewLatLng -> cameraPosition.copy(target = update.latLng)
|
||||
is CameraUpdate.NewLatLngZoom -> cameraPosition.copy(target = update.latLng, zoom = update.zoom)
|
||||
is CameraUpdate.NewCameraPosition -> update.cameraPosition
|
||||
is CameraUpdate.ZoomBy -> cameraPosition.copy(zoom = cameraPosition.zoom + update.amount)
|
||||
}
|
||||
onCameraMoveListener?.invoke(cameraPosition)
|
||||
onCameraIdleListener?.invoke()
|
||||
}
|
||||
|
||||
// ── Markers ─────────────────────────────────────────────────────────────
|
||||
|
||||
fun addMarker(marker: Marker): Marker {
|
||||
markers[marker.markerId] = marker
|
||||
return marker
|
||||
}
|
||||
|
||||
fun removeMarker(markerId: MarkerId) {
|
||||
markers.remove(markerId)
|
||||
}
|
||||
|
||||
fun clearMarkers() {
|
||||
markers.clear()
|
||||
}
|
||||
|
||||
fun getMarkers(): Set<Marker> = markers.values.toSet()
|
||||
|
||||
// ── Polylines ───────────────────────────────────────────────────────────
|
||||
|
||||
fun addPolyline(polyline: Polyline): Polyline {
|
||||
polylines[polyline.polylineId] = polyline
|
||||
return polyline
|
||||
}
|
||||
|
||||
fun removePolyline(polylineId: PolylineId) {
|
||||
polylines.remove(polylineId)
|
||||
}
|
||||
|
||||
fun clearPolylines() {
|
||||
polylines.clear()
|
||||
}
|
||||
|
||||
fun getPolylines(): Set<Polyline> = polylines.values.toSet()
|
||||
|
||||
// ── Polygons ────────────────────────────────────────────────────────────
|
||||
|
||||
fun addPolygon(polygon: Polygon): Polygon {
|
||||
polygons[polygon.polygonId] = polygon
|
||||
return polygon
|
||||
}
|
||||
|
||||
fun removePolygon(polygonId: PolygonId) {
|
||||
polygons.remove(polygonId)
|
||||
}
|
||||
|
||||
fun clearPolygons() {
|
||||
polygons.clear()
|
||||
}
|
||||
|
||||
fun getPolygons(): Set<Polygon> = polygons.values.toSet()
|
||||
|
||||
// ── Circles ─────────────────────────────────────────────────────────────
|
||||
|
||||
fun addCircle(circle: Circle): Circle {
|
||||
circles[circle.circleId] = circle
|
||||
return circle
|
||||
}
|
||||
|
||||
fun removeCircle(circleId: CircleId) {
|
||||
circles.remove(circleId)
|
||||
}
|
||||
|
||||
fun clearCircles() {
|
||||
circles.clear()
|
||||
}
|
||||
|
||||
fun getCircles(): Set<Circle> = circles.values.toSet()
|
||||
|
||||
/**
|
||||
* Clears all overlays (markers, polylines, polygons, circles) from the map.
|
||||
*/
|
||||
fun clear() {
|
||||
clearMarkers()
|
||||
clearPolylines()
|
||||
clearPolygons()
|
||||
clearCircles()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.intaleq.maps
|
||||
|
||||
import android.content.Context
|
||||
|
||||
/**
|
||||
* Main entry point for Intaleq Sovereign Maps Commercial SDK for Android (Kotlin).
|
||||
* Compatible with Google Maps SDK structure.
|
||||
*/
|
||||
object IntaleqMaps {
|
||||
|
||||
var apiKey: String = ""
|
||||
private set
|
||||
|
||||
var baseUrl: String = "https://map-saas.intaleqapp.com"
|
||||
private set
|
||||
|
||||
var isInitialized: Boolean = false
|
||||
private set
|
||||
|
||||
/**
|
||||
* Initializes the Intaleq Maps SDK with your SaaS API Key.
|
||||
*/
|
||||
fun initialize(context: Context, apiKey: String, baseUrl: String = "https://map-saas.intaleqapp.com") {
|
||||
this.apiKey = apiKey
|
||||
this.baseUrl = baseUrl.trimEnd('/')
|
||||
this.isInitialized = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.intaleq.maps.camera
|
||||
|
||||
import com.intaleq.maps.model.LatLng
|
||||
import java.io.Serializable
|
||||
|
||||
/**
|
||||
* An immutable class representing the camera position in terms of target, zoom, tilt, and bearing.
|
||||
* Fully compatible with Google Maps SDK CameraPosition API.
|
||||
*/
|
||||
data class CameraPosition(
|
||||
val target: LatLng,
|
||||
val zoom: Float = 12.0f,
|
||||
val tilt: Float = 0.0f,
|
||||
val bearing: Float = 0.0f
|
||||
) : Serializable {
|
||||
|
||||
class Builder {
|
||||
private var target: LatLng = LatLng(0.0, 0.0)
|
||||
private var zoom: Float = 12.0f
|
||||
private var tilt: Float = 0.0f
|
||||
private var bearing: Float = 0.0f
|
||||
|
||||
fun target(target: LatLng): Builder = apply { this.target = target }
|
||||
fun zoom(zoom: Float): Builder = apply { this.zoom = zoom }
|
||||
fun tilt(tilt: Float): Builder = apply { this.tilt = tilt }
|
||||
fun bearing(bearing: Float): Builder = apply { this.bearing = bearing }
|
||||
|
||||
fun build(): CameraPosition = CameraPosition(target, zoom, tilt, bearing)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun builder(): Builder = Builder()
|
||||
fun fromLatLngZoom(target: LatLng, zoom: Float): CameraPosition =
|
||||
CameraPosition(target = target, zoom = zoom)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CameraUpdate encapsulates instructions for changing the map camera viewport.
|
||||
*/
|
||||
sealed class CameraUpdate {
|
||||
data class NewLatLng(val latLng: LatLng) : CameraUpdate()
|
||||
data class NewLatLngZoom(val latLng: LatLng, val zoom: Float) : CameraUpdate()
|
||||
data class NewCameraPosition(val cameraPosition: CameraPosition) : CameraUpdate()
|
||||
data class ZoomBy(val amount: Float) : CameraUpdate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory class for creating CameraUpdate instances.
|
||||
* Fully compatible with Google Maps SDK CameraUpdateFactory API.
|
||||
*/
|
||||
object CameraUpdateFactory {
|
||||
fun newLatLng(latLng: LatLng): CameraUpdate = CameraUpdate.NewLatLng(latLng)
|
||||
fun newLatLngZoom(latLng: LatLng, zoom: Float): CameraUpdate = CameraUpdate.NewLatLngZoom(latLng, zoom)
|
||||
fun newCameraPosition(cameraPosition: CameraPosition): CameraUpdate = CameraUpdate.NewCameraPosition(cameraPosition)
|
||||
fun zoomIn(): CameraUpdate = CameraUpdate.ZoomBy(1.0f)
|
||||
fun zoomOut(): CameraUpdate = CameraUpdate.ZoomBy(-1.0f)
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.intaleq.maps.model
|
||||
|
||||
import android.graphics.Color
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// ID Types (Matching Flutter SDK & Google Maps Android SDK)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
data class MarkerId(val value: String)
|
||||
data class PolylineId(val value: String)
|
||||
data class PolygonId(val value: String)
|
||||
data class CircleId(val value: String)
|
||||
|
||||
/**
|
||||
* An immutable class representing a latitude/longitude bounding box.
|
||||
*/
|
||||
data class LatLngBounds(
|
||||
val southwest: LatLng,
|
||||
val northeast: LatLng
|
||||
) {
|
||||
init {
|
||||
require(southwest.latitude <= northeast.latitude) {
|
||||
"Southern latitude (${southwest.latitude}) must be <= northern latitude (${northeast.latitude})"
|
||||
}
|
||||
}
|
||||
|
||||
fun contains(point: LatLng): Boolean {
|
||||
val inLat = point.latitude in southwest.latitude..northeast.latitude
|
||||
val inLng = if (southwest.longitude <= northeast.longitude) {
|
||||
point.longitude in southwest.longitude..northeast.longitude
|
||||
} else {
|
||||
point.longitude >= southwest.longitude || point.longitude <= northeast.longitude
|
||||
}
|
||||
return inLat && inLng
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Map Overlay Elements (Matching Flutter SDK)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
data class Marker(
|
||||
val markerId: MarkerId,
|
||||
val position: LatLng,
|
||||
val alpha: Float = 1.0f,
|
||||
val anchorX: Float = 0.5f,
|
||||
val anchorY: Float = 1.0f,
|
||||
val isDraggable: Boolean = false,
|
||||
val isFlat: Boolean = false,
|
||||
val icon: InlqBitmap = InlqBitmap.defaultMarker,
|
||||
val infoWindow: InfoWindow = InfoWindow.NO_TEXT,
|
||||
val rotation: Float = 0.0f,
|
||||
val isVisible: Boolean = true,
|
||||
val zIndex: Float = 0.0f,
|
||||
val onTap: (() -> Unit)? = null
|
||||
)
|
||||
|
||||
data class Polyline(
|
||||
val polylineId: PolylineId,
|
||||
val points: List<LatLng>,
|
||||
val color: Int = Color.parseColor("#0071E3"),
|
||||
val width: Float = 10.0f,
|
||||
val isGeodesic: Boolean = true,
|
||||
val isVisible: Boolean = true,
|
||||
val zIndex: Float = 0.0f,
|
||||
val onTap: (() -> Unit)? = null
|
||||
)
|
||||
|
||||
data class Polygon(
|
||||
val polygonId: PolygonId,
|
||||
val points: List<LatLng>,
|
||||
val holes: List<List<LatLng>> = emptyList(),
|
||||
val strokeColor: Int = Color.parseColor("#0071E3"),
|
||||
val strokeWidth: Float = 5.0f,
|
||||
val fillColor: Int = Color.parseColor("#330071E3"),
|
||||
val isVisible: Boolean = true,
|
||||
val zIndex: Float = 0.0f,
|
||||
val onTap: (() -> Unit)? = null
|
||||
)
|
||||
|
||||
data class Circle(
|
||||
val circleId: CircleId,
|
||||
val center: LatLng,
|
||||
val radiusMeters: Double,
|
||||
val strokeColor: Int = Color.parseColor("#22C55E"),
|
||||
val strokeWidth: Float = 4.0f,
|
||||
val fillColor: Int = Color.parseColor("#3322C55E"),
|
||||
val isVisible: Boolean = true,
|
||||
val zIndex: Float = 0.0f,
|
||||
val onTap: (() -> Unit)? = null
|
||||
)
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.intaleq.maps.model
|
||||
|
||||
import android.graphics.Bitmap
|
||||
|
||||
/**
|
||||
* Defines a bitmap image used for rendering custom markers on the map.
|
||||
* Mirrors InlqBitmap / BitmapDescriptor from Flutter SDK and Google Maps Android SDK.
|
||||
*/
|
||||
class InlqBitmap private constructor(
|
||||
val type: Type,
|
||||
val bitmap: Bitmap? = null,
|
||||
val assetPath: String? = null,
|
||||
val hue: Float = 0.0f
|
||||
) {
|
||||
enum class Type {
|
||||
DEFAULT,
|
||||
DEFAULT_WITH_HUE,
|
||||
ASSET,
|
||||
BITMAP
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val HUE_RED = 0.0f
|
||||
const val HUE_ORANGE = 30.0f
|
||||
const val HUE_YELLOW = 60.0f
|
||||
const val HUE_GREEN = 120.0f
|
||||
const val HUE_CYAN = 180.0f
|
||||
const val HUE_AZURE = 210.0f
|
||||
const val HUE_BLUE = 240.0f
|
||||
const val HUE_VIOLET = 270.0f
|
||||
const val HUE_MAGENTA = 300.0f
|
||||
const val HUE_ROSE = 330.0f
|
||||
|
||||
val defaultMarker: InlqBitmap = InlqBitmap(Type.DEFAULT)
|
||||
|
||||
fun defaultMarkerWithHue(hue: Float): InlqBitmap =
|
||||
InlqBitmap(Type.DEFAULT_WITH_HUE, hue = hue)
|
||||
|
||||
fun fromAsset(assetPath: String): InlqBitmap =
|
||||
InlqBitmap(Type.ASSET, assetPath = assetPath)
|
||||
|
||||
fun fromBitmap(bitmap: Bitmap): InlqBitmap =
|
||||
InlqBitmap(Type.BITMAP, bitmap = bitmap)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Text information window displayed above a marker when tapped.
|
||||
* Mirrors InfoWindow from Flutter SDK.
|
||||
*/
|
||||
data class InfoWindow(
|
||||
val title: String? = null,
|
||||
val snippet: String? = null,
|
||||
val anchorX: Float = 0.5f,
|
||||
val anchorY: Float = 0.0f,
|
||||
val onTap: (() -> Unit)? = null
|
||||
) {
|
||||
companion object {
|
||||
val NO_TEXT = InfoWindow()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.intaleq.maps.model
|
||||
|
||||
import java.io.Serializable
|
||||
import kotlin.math.*
|
||||
|
||||
/**
|
||||
* An immutable class representing a pair of latitude and longitude coordinates in degrees.
|
||||
* Fully compatible with Google Maps SDK LatLng signature.
|
||||
*/
|
||||
data class LatLng(
|
||||
val latitude: Double,
|
||||
val longitude: Double
|
||||
) : Serializable {
|
||||
|
||||
init {
|
||||
require(latitude in -90.0..90.0) { "Latitude must be between -90.0 and 90.0 degrees" }
|
||||
require(longitude in -180.0..180.0) { "Longitude must be between -180.0 and 180.0 degrees" }
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates spherical distance in meters to another LatLng
|
||||
*/
|
||||
fun distanceTo(other: LatLng): Double {
|
||||
val earthRadiusM = 6371000.0
|
||||
val dLat = Math.toRadians(other.latitude - this.latitude)
|
||||
val dLon = Math.toRadians(other.longitude - this.longitude)
|
||||
val a = sin(dLat / 2).pow(2) +
|
||||
cos(Math.toRadians(this.latitude)) * cos(Math.toRadians(other.latitude)) *
|
||||
sin(dLon / 2).pow(2)
|
||||
val c = 2 * atan2(sqrt(a), sqrt(1 - a))
|
||||
return earthRadiusM * c
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates bearing in degrees to another LatLng
|
||||
*/
|
||||
fun bearingTo(other: LatLng): Double {
|
||||
val lat1 = Math.toRadians(this.latitude)
|
||||
val lon1 = Math.toRadians(this.longitude)
|
||||
val lat2 = Math.toRadians(other.latitude)
|
||||
val lon2 = Math.toRadians(other.longitude)
|
||||
|
||||
val dLon = lon2 - lon1
|
||||
val y = sin(dLon) * cos(lat2)
|
||||
val x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dLon)
|
||||
|
||||
val rad = atan2(y, x)
|
||||
return (Math.toDegrees(rad) + 360.0) % 360.0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package com.intaleq.maps.model
|
||||
|
||||
import android.graphics.Color
|
||||
|
||||
/**
|
||||
* Defines MarkerOptions for creating and rendering a tactical / commercial marker.
|
||||
* Fully compatible with Google Maps SDK MarkerOptions API.
|
||||
*/
|
||||
class MarkerOptions {
|
||||
var position: LatLng = LatLng(0.0, 0.0)
|
||||
private set
|
||||
var title: String? = null
|
||||
private set
|
||||
var snippet: String? = null
|
||||
private set
|
||||
var iconHue: Float = 210.0f // Azure Blue default
|
||||
private set
|
||||
var isDraggable: Boolean = false
|
||||
private set
|
||||
var isVisible: Boolean = true
|
||||
private set
|
||||
var zIndex: Float = 0.0f
|
||||
private set
|
||||
|
||||
fun position(latLng: LatLng): MarkerOptions = apply { this.position = latLng }
|
||||
fun title(title: String?): MarkerOptions = apply { this.title = title }
|
||||
fun snippet(snippet: String?): MarkerOptions = apply { this.snippet = snippet }
|
||||
fun iconHue(hue: Float): MarkerOptions = apply { this.iconHue = hue }
|
||||
fun draggable(draggable: Boolean): MarkerOptions = apply { this.isDraggable = draggable }
|
||||
fun visible(visible: Boolean): MarkerOptions = apply { this.isVisible = visible }
|
||||
fun zIndex(zIndex: Float): MarkerOptions = apply { this.zIndex = zIndex }
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines PolylineOptions for creating and rendering paths and routes.
|
||||
* Fully compatible with Google Maps SDK PolylineOptions API.
|
||||
*/
|
||||
class PolylineOptions {
|
||||
val points: MutableList<LatLng> = mutableListOf()
|
||||
var color: Int = Color.parseColor("#0071E3")
|
||||
private set
|
||||
var width: Float = 10.0f
|
||||
private set
|
||||
var isGeodesic: Boolean = true
|
||||
private set
|
||||
var isVisible: Boolean = true
|
||||
private set
|
||||
var zIndex: Float = 0.0f
|
||||
private set
|
||||
|
||||
fun add(point: LatLng): PolylineOptions = apply { this.points.add(point) }
|
||||
fun addAll(points: Iterable<LatLng>): PolylineOptions = apply { this.points.addAll(points) }
|
||||
fun color(color: Int): PolylineOptions = apply { this.color = color }
|
||||
fun width(width: Float): PolylineOptions = apply { this.width = width }
|
||||
fun geodesic(geodesic: Boolean): PolylineOptions = apply { this.isGeodesic = geodesic }
|
||||
fun visible(visible: Boolean): PolylineOptions = apply { this.isVisible = visible }
|
||||
fun zIndex(zIndex: Float): PolylineOptions = apply { this.zIndex = zIndex }
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines PolygonOptions for creating tactical zones and reachability polygons.
|
||||
* Fully compatible with Google Maps SDK PolygonOptions API.
|
||||
*/
|
||||
class PolygonOptions {
|
||||
val points: MutableList<LatLng> = mutableListOf()
|
||||
val holes: MutableList<List<LatLng>> = mutableListOf()
|
||||
var strokeColor: Int = Color.parseColor("#0071E3")
|
||||
private set
|
||||
var strokeWidth: Float = 5.0f
|
||||
private set
|
||||
var fillColor: Int = Color.parseColor("#330071E3")
|
||||
private set
|
||||
var isVisible: Boolean = true
|
||||
private set
|
||||
var zIndex: Float = 0.0f
|
||||
private set
|
||||
|
||||
fun add(point: LatLng): PolygonOptions = apply { this.points.add(point) }
|
||||
fun addAll(points: Iterable<LatLng>): PolygonOptions = apply { this.points.addAll(points) }
|
||||
fun addHole(hole: List<LatLng>): PolygonOptions = apply { this.holes.add(hole) }
|
||||
fun strokeColor(color: Int): PolygonOptions = apply { this.strokeColor = color }
|
||||
fun strokeWidth(width: Float): PolygonOptions = apply { this.strokeWidth = width }
|
||||
fun fillColor(color: Int): PolygonOptions = apply { this.fillColor = color }
|
||||
fun visible(visible: Boolean): PolygonOptions = apply { this.isVisible = visible }
|
||||
fun zIndex(zIndex: Float): PolygonOptions = apply { this.zIndex = zIndex }
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines CircleOptions for circular accuracy and buffer zones.
|
||||
* Fully compatible with Google Maps SDK CircleOptions API.
|
||||
*/
|
||||
class CircleOptions {
|
||||
var center: LatLng = LatLng(0.0, 0.0)
|
||||
private set
|
||||
var radiusMeters: Double = 0.0
|
||||
private set
|
||||
var strokeColor: Int = Color.parseColor("#22C55E")
|
||||
private set
|
||||
var strokeWidth: Float = 4.0f
|
||||
private set
|
||||
var fillColor: Int = Color.parseColor("#3322C55E")
|
||||
private set
|
||||
var isVisible: Boolean = true
|
||||
private set
|
||||
var zIndex: Float = 0.0f
|
||||
private set
|
||||
|
||||
fun center(center: LatLng): CircleOptions = apply { this.center = center }
|
||||
fun radius(radiusMeters: Double): CircleOptions = apply { this.radiusMeters = radiusMeters }
|
||||
fun strokeColor(color: Int): CircleOptions = apply { this.strokeColor = color }
|
||||
fun strokeWidth(width: Float): CircleOptions = apply { this.strokeWidth = width }
|
||||
fun fillColor(color: Int): CircleOptions = apply { this.fillColor = color }
|
||||
fun visible(visible: Boolean): CircleOptions = apply { this.isVisible = visible }
|
||||
fun zIndex(zIndex: Float): CircleOptions = apply { this.zIndex = zIndex }
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package com.intaleq.maps.offline
|
||||
|
||||
import com.intaleq.maps.model.LatLngBounds
|
||||
|
||||
data class OfflineRegionDefinition(
|
||||
val bounds: LatLngBounds,
|
||||
val minZoom: Double,
|
||||
val maxZoom: Double,
|
||||
val styleUrl: String
|
||||
)
|
||||
|
||||
data class OfflineRegion(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val definition: OfflineRegionDefinition,
|
||||
val completedTiles: Long = 0,
|
||||
val totalTiles: Long = 0
|
||||
)
|
||||
|
||||
/**
|
||||
* Service for managing offline vector map tile packages in Intaleq Native Android SDK.
|
||||
* Mirrors OfflineService from Flutter SDK.
|
||||
*/
|
||||
class IntaleqOfflineService {
|
||||
|
||||
private val regions = mutableMapOf<Long, OfflineRegion>()
|
||||
|
||||
fun downloadOfflineRegion(
|
||||
definition: OfflineRegionDefinition,
|
||||
name: String,
|
||||
progressCallback: ((Double) -> Unit)? = null
|
||||
): OfflineRegion {
|
||||
val id = System.currentTimeMillis()
|
||||
val region = OfflineRegion(id, name, definition)
|
||||
regions[id] = region
|
||||
progressCallback?.invoke(1.0)
|
||||
return region
|
||||
}
|
||||
|
||||
fun getListOfRegions(): List<OfflineRegion> = regions.values.toList()
|
||||
|
||||
fun deleteOfflineRegion(id: Long): Boolean {
|
||||
return regions.remove(id) != null
|
||||
}
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
package com.intaleq.maps.service
|
||||
|
||||
import com.intaleq.maps.IntaleqMaps
|
||||
import com.intaleq.maps.model.LatLng
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.net.URLEncoder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
data class GeocodingPlace(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val nameAr: String?,
|
||||
val nameEn: String?,
|
||||
val category: String?,
|
||||
val city: String?,
|
||||
val neighbourhood: String?,
|
||||
val address: String?,
|
||||
val position: LatLng
|
||||
)
|
||||
|
||||
/**
|
||||
* Commercial Geocoding & Reverse Geocoding Service calling Intaleq SaaS API.
|
||||
*/
|
||||
class IntaleqGeocodingService {
|
||||
|
||||
/**
|
||||
* Search for places, addresses, and landmarks by query string.
|
||||
*/
|
||||
suspend fun search(
|
||||
query: String,
|
||||
proximity: LatLng? = null,
|
||||
limit: Int = 10
|
||||
): List<GeocodingPlace> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val encodedQuery = URLEncoder.encode(query, "UTF-8")
|
||||
var urlString = "${IntaleqMaps.baseUrl}/api/geocoding/search?q=$encodedQuery&limit=$limit"
|
||||
if (proximity != null) {
|
||||
urlString += "&lat=${proximity.latitude}&lng=${proximity.longitude}"
|
||||
}
|
||||
|
||||
val url = URL(urlString)
|
||||
val conn = (url.openConnection() as HttpURLConnection).apply {
|
||||
requestMethod = "GET"
|
||||
connectTimeout = 8000
|
||||
readTimeout = 8000
|
||||
setRequestProperty("x-api-key", IntaleqMaps.apiKey)
|
||||
setRequestProperty("Accept", "application/json")
|
||||
}
|
||||
|
||||
if (conn.responseCode == 200) {
|
||||
val responseText = BufferedReader(InputStreamReader(conn.inputStream)).use { it.readText() }
|
||||
return@withContext parsePlaces(responseText)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return@withContext emptyList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse geocode GPS coordinate to get Arabic street name, neighborhood, and city.
|
||||
*/
|
||||
suspend fun reverseGeocode(position: LatLng): GeocodingPlace? = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val urlString = "${IntaleqMaps.baseUrl}/api/geocoding/reverse?lat=${position.latitude}&lng=${position.longitude}"
|
||||
val url = URL(urlString)
|
||||
val conn = (url.openConnection() as HttpURLConnection).apply {
|
||||
requestMethod = "GET"
|
||||
connectTimeout = 8000
|
||||
readTimeout = 8000
|
||||
setRequestProperty("x-api-key", IntaleqMaps.apiKey)
|
||||
setRequestProperty("Accept", "application/json")
|
||||
}
|
||||
|
||||
if (conn.responseCode == 200) {
|
||||
val responseText = BufferedReader(InputStreamReader(conn.inputStream)).use { it.readText() }
|
||||
val list = parsePlaces(responseText)
|
||||
return@withContext list.firstOrNull()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
private fun parsePlaces(jsonString: String): List<GeocodingPlace> {
|
||||
val places = mutableListOf<GeocodingPlace>()
|
||||
val root = JSONObject(jsonString)
|
||||
val features = if (root.has("features")) root.getJSONArray("features") else JSONArray()
|
||||
|
||||
for (i in 0 until features.length()) {
|
||||
val f = features.getJSONObject(i)
|
||||
val props = f.optJSONObject("properties") ?: JSONObject()
|
||||
val geom = f.optJSONObject("geometry") ?: JSONObject()
|
||||
val coords = geom.optJSONArray("coordinates")
|
||||
|
||||
if (coords != null && coords.length() >= 2) {
|
||||
val lat = coords.getDouble(1)
|
||||
val lng = coords.getDouble(0)
|
||||
places.add(
|
||||
GeocodingPlace(
|
||||
id = f.optString("id", i.toString()),
|
||||
name = props.optString("name", "موقع محدد"),
|
||||
nameAr = props.optString("name_ar", null),
|
||||
nameEn = props.optString("name_en", null),
|
||||
category = props.optString("category", null),
|
||||
city = props.optString("city", null),
|
||||
neighbourhood = props.optString("neighbourhood", null),
|
||||
address = props.optString("address", null),
|
||||
position = LatLng(lat, lng)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
return places
|
||||
}
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
package com.intaleq.maps.service
|
||||
|
||||
import com.intaleq.maps.IntaleqMaps
|
||||
import com.intaleq.maps.model.LatLng
|
||||
import org.json.JSONObject
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
data class RouteStep(
|
||||
val instructionAr: String,
|
||||
val distanceMeters: Double,
|
||||
val durationSeconds: Double,
|
||||
val maneuverSign: Int,
|
||||
val inclinePercentage: Double = 0.0
|
||||
)
|
||||
|
||||
data class CommercialRoute(
|
||||
val coordinates: List<LatLng>,
|
||||
val steps: List<RouteStep>,
|
||||
val totalDistanceMeters: Double,
|
||||
val totalDurationSeconds: Double,
|
||||
val elevationGainMeters: Double = 0.0,
|
||||
val elevationLossMeters: Double = 0.0,
|
||||
val averageInclinePercentage: Double = 0.0,
|
||||
val maxInclinePercentage: Double = 0.0
|
||||
)
|
||||
|
||||
/**
|
||||
* Commercial Routing Service calling Intaleq SaaS Routing Engine.
|
||||
*/
|
||||
class IntaleqRoutingService {
|
||||
|
||||
suspend fun getRoute(
|
||||
origin: LatLng,
|
||||
destination: LatLng,
|
||||
profile: String = "car",
|
||||
locale: String = "ar"
|
||||
): CommercialRoute? = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val urlString = "${IntaleqMaps.baseUrl}/api/maps/route?" +
|
||||
"fromLat=${origin.latitude}&fromLng=${origin.longitude}&" +
|
||||
"toLat=${destination.latitude}&toLng=${destination.longitude}&" +
|
||||
"profile=$profile&steps=true&locale=$locale"
|
||||
|
||||
val url = URL(urlString)
|
||||
val conn = (url.openConnection() as HttpURLConnection).apply {
|
||||
requestMethod = "GET"
|
||||
connectTimeout = 10000
|
||||
readTimeout = 10000
|
||||
setRequestProperty("x-api-key", IntaleqMaps.apiKey)
|
||||
setRequestProperty("Accept", "application/json")
|
||||
}
|
||||
|
||||
if (conn.responseCode == 200) {
|
||||
val reader = BufferedReader(InputStreamReader(conn.inputStream))
|
||||
val responseText = reader.use { it.readText() }
|
||||
return@withContext parseRouteResponse(responseText)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
private fun parseRouteResponse(jsonString: String): CommercialRoute {
|
||||
val root = JSONObject(jsonString)
|
||||
val paths = root.getJSONArray("paths")
|
||||
if (paths.length() == 0) throw IllegalStateException("No path returned")
|
||||
|
||||
val path = paths.getJSONObject(0)
|
||||
val distance = path.optDouble("distance", 0.0)
|
||||
val timeMs = path.optDouble("time", 0.0)
|
||||
|
||||
// Coordinates
|
||||
val coords = mutableListOf<LatLng>()
|
||||
val pointsObj = path.optJSONObject("points")
|
||||
if (pointsObj != null && pointsObj.has("coordinates")) {
|
||||
val arr = pointsObj.getJSONArray("coordinates")
|
||||
for (i in 0 until arr.length()) {
|
||||
val c = arr.getJSONArray(i)
|
||||
coords.add(LatLng(c.getDouble(1), c.getDouble(0)))
|
||||
}
|
||||
}
|
||||
|
||||
// Steps
|
||||
val steps = mutableListOf<RouteStep>()
|
||||
val instrs = path.optJSONArray("instructions")
|
||||
if (instrs != null) {
|
||||
for (i in 0 until instrs.length()) {
|
||||
val inst = instrs.getJSONObject(i)
|
||||
steps.add(
|
||||
RouteStep(
|
||||
instructionAr = inst.optString("text", ""),
|
||||
distanceMeters = inst.optDouble("distance", 0.0),
|
||||
durationSeconds = inst.optDouble("time", 0.0) / 1000.0,
|
||||
maneuverSign = inst.optInt("sign", 0)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return CommercialRoute(
|
||||
coordinates = coords,
|
||||
steps = steps,
|
||||
totalDistanceMeters = distance,
|
||||
totalDurationSeconds = timeMs / 1000.0
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.intaleq.maps.styles
|
||||
|
||||
import com.intaleq.maps.IntaleqMaps
|
||||
|
||||
/**
|
||||
* Predefined map visual styles for the Intaleq Sovereign Map SDK.
|
||||
* Mirrors IntaleqStyles from Flutter SDK.
|
||||
*/
|
||||
object IntaleqStyles {
|
||||
/**
|
||||
* Dark Obsidian style — Intaleq standard default.
|
||||
*/
|
||||
val DARK: String
|
||||
get() = "${IntaleqMaps.baseUrl}/api/v1/styles/dark.json?key=${IntaleqMaps.apiKey}"
|
||||
|
||||
/**
|
||||
* High-contrast Light style.
|
||||
*/
|
||||
val LIGHT: String
|
||||
get() = "${IntaleqMaps.baseUrl}/api/v1/styles/light.json?key=${IntaleqMaps.apiKey}"
|
||||
|
||||
/**
|
||||
* Classic Standard street map style.
|
||||
*/
|
||||
val NORMAL: String
|
||||
get() = "${IntaleqMaps.baseUrl}/api/v1/styles/normal.json?key=${IntaleqMaps.apiKey}"
|
||||
|
||||
/**
|
||||
* High-resolution Satellite imagery with road and boundary overlays.
|
||||
*/
|
||||
val SATELLITE: String
|
||||
get() = "${IntaleqMaps.baseUrl}/api/v1/styles/satellite.json?key=${IntaleqMaps.apiKey}"
|
||||
|
||||
/**
|
||||
* Topographic Outdoors style with elevation contour lines.
|
||||
*/
|
||||
val OUTDOORS: String
|
||||
get() = "${IntaleqMaps.baseUrl}/api/v1/styles/outdoors.json?key=${IntaleqMaps.apiKey}"
|
||||
}
|
||||
|
||||
enum class IntaleqMapType {
|
||||
NONE,
|
||||
NORMAL,
|
||||
LIGHT,
|
||||
SATELLITE,
|
||||
DARK
|
||||
}
|
||||
Reference in New Issue
Block a user