Chargement de la carte¶
Vue d'ensemble¶
La fonctionnalité Chargement de carte est un composant central de MINE qui permet de charger, parser et rendre des données de carte intérieure depuis différentes sources. Qu'il s'agisse de stockage local, de serveurs distants ou du cloud, MINE offre un système flexible et performant prenant en charge plusieurs formats et scénarios de déploiement.
Capacités clés
- 📁 Chargement local : depuis le stockage ou les assets de l'app
- 🌐 Chargement distant : via API REST ou stockage cloud
- 🔄 Chargement progressif : streaming incrémental pour les grandes cartes
- 💾 Cache intelligent : mise en cache automatique pour l'offline
- 🗺️ Multi-format : JSON, GeoJSON et formats personnalisés
- 🏢 Gestion multi-étages : bâtiments complexes à plusieurs niveaux
Format de fichier de carte¶
MINE utilise un JSON structuré pour décrire les cartes intérieures avec métadonnées et géométrie riches :
Structure de base¶
{
"version": "1.0",
"venue": {
"id": "venue_001",
"name": "Shopping Mall XYZ",
"location": {
"latitude": 40.7128,
"longitude": -74.0060
}
},
"floors": [
{
"id": "floor_1",
"level": 1,
"name": "Ground Floor",
"height": 3.5,
"model": {
"url": "models/floor_1.glb",
"format": "glb"
},
"floorPlan": {
"url": "images/floor_1.png",
"bounds": {
"minX": 0,
"minY": 0,
"maxX": 200,
"maxY": 150
}
},
"pois": [
{
"id": "poi_001",
"name": "Main Entrance",
"category": "entrance",
"position": {
"x": 10.5,
"y": 20.3,
"z": 0.0
},
"metadata": {
"description": "Main entrance from parking lot",
"icon": "entrance"
}
}
],
"paths": [
{
"id": "path_001",
"nodes": [
{"x": 10.5, "y": 20.3},
{"x": 15.0, "y": 25.0},
{"x": 20.5, "y": 30.2}
],
"width": 2.0,
"accessible": true
}
],
"obstacles": [
{
"type": "wall",
"geometry": {
"points": [
{"x": 0, "y": 0},
{"x": 100, "y": 0},
{"x": 100, "y": 2},
{"x": 0, "y": 2}
]
}
}
]
}
],
"connections": [
{
"type": "elevator",
"fromFloor": "floor_1",
"toFloor": "floor_2",
"position": {"x": 50, "y": 50}
}
]
}
Spécification¶
{
"venue": {
"id": "unique_venue_id",
"name": "Venue Name",
"location": {
"latitude": 40.7128,
"longitude": -74.0060
},
"timezone": "America/New_York",
"metadata": {
"address": "123 Main St",
"city": "New York",
"country": "USA"
}
}
}
{
"floor": {
"id": "floor_1",
"level": 1,
"name": "Ground Floor",
"height": 3.5,
"model": {
"url": "path/to/model.glb",
"format": "glb",
"scale": 1.0
},
"floorPlan": {
"url": "path/to/floorplan.png",
"bounds": {
"minX": 0, "minY": 0,
"maxX": 200, "maxY": 150
}
}
}
}
{
"poi": {
"id": "poi_001",
"name": "Store Name",
"category": "retail",
"subcategory": "clothing",
"position": {
"x": 10.5,
"y": 20.3,
"z": 0.0
},
"metadata": {
"description": "Description",
"icon": "store",
"phone": "+1234567890",
"website": "https://example.com"
},
"searchable": true
}
}
{
"path": {
"id": "path_001",
"nodes": [
{"x": 10, "y": 20},
{"x": 15, "y": 25}
],
"width": 2.0,
"accessible": true,
"bidirectional": true,
"properties": {
"surface": "smooth",
"indoor": true
}
}
}
Version du format
Le format de carte est versionné pour garantir la compatibilité. Spécifiez toujours le champ version. Version actuelle : 1.0
Chargement local¶
Charger des cartes depuis le stockage local, les assets ou les fichiers de l'app :
Depuis les assets¶
import com.machinestalk.indoornavigationengine.resources.MapLoader
import com.machinestalk.indoornavigationengine.models.IndoorMap
class MapActivity : AppCompatActivity() {
private lateinit var mapLoader: MapLoader
private lateinit var sceneView: MineSceneView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_map)
sceneView = findViewById(R.id.scene_view)
mapLoader = MapLoader(this)
loadMapFromAssets()
}
private fun loadMapFromAssets() {
lifecycleScope.launch {
try {
showLoadingDialog()
val map = mapLoader.loadFromAssets(
path = "maps/venue.json",
options = LoadOptions(
cacheEnabled = true,
validateSchema = true,
progressCallback = { progress -> updateLoadingProgress(progress) }
)
)
sceneView.setMap(map)
sceneView.apply {
focusOnFloor(map.getFloor(1))
enableUserLocation()
}
hideLoadingDialog()
showMapLoadedMessage()
} catch (e: MapLoadException) {
handleMapLoadError(e)
}
}
}
}
Depuis le stockage interne¶
private fun loadMapFromInternalStorage() {
lifecycleScope.launch {
try {
val file = File(filesDir, "downloaded_maps/venue_123.json")
if (!file.exists()) {
showError("Map file not found")
return@launch
}
val map = mapLoader.loadFromFile(
file = file,
options = LoadOptions(cacheEnabled = true, preloadModels = true)
)
sceneView.setMap(map)
} catch (e: Exception) {
Log.e("MapLoading", "Failed to load map", e)
showError("Failed to load map: ${e.message}")
}
}
}
Depuis le stockage externe¶
private fun loadMapFromExternalStorage() {
if (!hasStoragePermission()) {
requestStoragePermission()
return
}
lifecycleScope.launch {
try {
val file = File(getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), "maps/custom_venue.json")
val map = mapLoader.loadFromFile(file)
sceneView.setMap(map)
} catch (e: IOException) {
showError("Failed to read map file")
}
}
}
Organisation des assets
Structure conseillée :
assets/
maps/
venue_001.json
models/
floor_1.glb
floor_2.glb
images/
floor_1.png
floor_2.png
icons/
poi_markers.png
Chargement distant¶
Charger des cartes depuis des serveurs, API REST ou stockage cloud :
Via API REST¶
private fun loadMapFromAPI() {
lifecycleScope.launch {
try {
showLoadingDialog()
val map = mapLoader.loadFromUrl(
url = "https://api.example.com/venues/123/map",
options = LoadOptions(
headers = mapOf(
"Authorization" to "Bearer $authToken",
"Accept" to "application/json"
),
timeout = 30_000L,
cacheEnabled = true,
cacheExpiration = 24 * 60 * 60 * 1000L,
progressCallback = { progress -> updateProgress(progress) }
)
)
sceneView.setMap(map)
hideLoadingDialog()
} catch (e: NetworkException) {
handleNetworkError(e)
} catch (e: MapLoadException) {
handleMapLoadError(e)
}
}
}
Depuis AWS S3¶
private fun loadMapFromS3() {
lifecycleScope.launch {
try {
val s3Url = "https://my-bucket.s3.amazonaws.com/maps/venue_123.json"
val map = mapLoader.loadFromUrl(
url = s3Url,
options = LoadOptions(
cacheEnabled = true,
offlineMode = false,
headers = mapOf("x-amz-request-payer" to "requester")
)
)
sceneView.setMap(map)
} catch (e: Exception) {
Log.e("MapLoading", "Failed to load from S3", e)
}
}
}
Depuis Firebase Storage¶
private fun loadMapFromFirebase() {
val storage = FirebaseStorage.getInstance()
val mapRef = storage.reference.child("maps/venue_123.json")
lifecycleScope.launch {
try {
val url = mapRef.downloadUrl.await()
val map = mapLoader.loadFromUrl(url = url.toString(), options = LoadOptions(cacheEnabled = true))
sceneView.setMap(map)
} catch (e: Exception) {
Log.e("MapLoading", "Failed to load from Firebase", e)
}
}
}
Chargement progressif¶
Pour les grands lieux (multi-étages, modèles 3D détaillés), utilisez le chargement progressif :
private fun loadMapProgressively() {
lifecycleScope.launch {
try {
val mapMetadata = mapLoader.loadMetadata("maps/large_venue.json")
showVenueInfo(mapMetadata)
val firstFloor = mapLoader.loadFloor(
mapUrl = "maps/large_venue.json",
floorId = mapMetadata.defaultFloorId
)
sceneView.setMap(firstFloor)
loadRemainingFloorsInBackground(mapMetadata)
} catch (e: Exception) {
handleError(e)
}
}
}
private fun loadRemainingFloorsInBackground(metadata: MapMetadata) {
lifecycleScope.launch(Dispatchers.IO) {
metadata.floors.forEach { floorInfo ->
if (floorInfo.id != metadata.defaultFloorId) {
try {
val floor = mapLoader.loadFloor(
mapUrl = "maps/large_venue.json",
floorId = floorInfo.id
)
withContext(Dispatchers.Main) { sceneView.addFloor(floor) }
} catch (e: Exception) {
Log.w("MapLoading", "Failed to load floor ${floorInfo.id}", e)
}
}
}
}
}
Stratégie de cache¶
MINE fournit un cache intelligent pour la performance et l'offline :
Configuration du cache¶
val cacheConfig = MapCacheConfig(
maxSize = 500 * 1024 * 1024,
expirationTime = 7 * 24 * 60 * 60 * 1000L,
cacheLocation = CacheLocation.INTERNAL_STORAGE,
strategy = CacheStrategy.LRU
)
mapLoader.configurCache(cacheConfig)
Gestion manuelle du cache¶
mapLoader.clearCache(venueId = "venue_123")
mapLoader.clearAllCache()
val cacheInfo = mapLoader.getCacheInfo()
Log.d("Cache", "Size: ${cacheInfo.size}, Items: ${cacheInfo.itemCount}")
lifecycleScope.launch {
mapLoader.preloadToCache(url = "https://api.example.com/venues/123/map")
}
Mode hors ligne¶
val map = mapLoader.loadFromUrl(
url = "https://api.example.com/venues/123/map",
options = LoadOptions(
offlineMode = true,
fallbackToCache = true
)
)
Gestion multi-étages¶
Gérer efficacement les bâtiments à plusieurs niveaux :
Chargement multi-étages¶
private suspend fun loadMultiFloorVenue() {
val map = mapLoader.loadFromAssets("maps/multi_floor_venue.json")
val floors = map.getFloors()
floors.forEach { floor ->
Log.d("Floor", "Level ${floor.level}: ${floor.name}")
floor.model?.let { modelUrl -> sceneView.loadFloorModel(floor.id, modelUrl) }
}
sceneView.setActiveFloor(floors.first())
}
Changement d'étage¶
sceneView.switchToFloor(
floorId = "floor_2",
animated = true,
duration = 500L
)
sceneView.onFloorChanged = { floor ->
updateUI(floor)
loadFloorPOIs(floor)
}
Gestion des erreurs¶
private fun handleMapLoadError(exception: Exception) {
when (exception) {
is MapLoadException.InvalidFormat -> showError("Invalid map format. Please check the map file.")
is MapLoadException.NetworkError -> { showError("Network error. Please check your connection."); offerRetry() }
is MapLoadException.FileNotFound -> showError("Map file not found.")
is MapLoadException.ParseError -> { showError("Failed to parse map data: ${exception.message}"); Log.e("MapLoading", "Parse error", exception) }
is MapLoadException.ModelLoadError -> { showError("Failed to load 3D models."); switchTo2DMode() }
else -> { showError("Unexpected error: ${exception.message}"); Log.e("MapLoading", "Unexpected error", exception) }
}
}
Bonnes pratiques¶
Pratiques recommandées
- Charger en asynchrone (coroutines) ; éviter le thread principal
- Valider les données (schema) avant usage
- Gérer les états de chargement (Idle/Loading/Success/Error)
- Optimiser la taille des assets (Draco, LOD, textures adaptées)
Écueils courants
- Ne pas charger de grosses cartes sur le main thread
- Ne pas ignorer les erreurs réseau ou d'expiration de cache
- Toujours valider le format avant production
- Utiliser le chargement progressif pour les grands lieux
Optimisation des performances¶
Chargement paresseux des POI¶
sceneView.onCameraZoomChanged = { zoomLevel ->
if (zoomLevel > 0.7f) loadPOIsForCurrentView() else hidePOIs()
}
private fun loadPOIsForCurrentView() {
val visibleBounds = sceneView.getVisibleBounds()
val pois = currentMap.getPOIsInBounds(visibleBounds)
sceneView.showPOIs(pois)
}
Optimisation des modèles¶
val modelOptions = ModelLoadOptions(
enableLOD = true,
lodLevels = listOf(
LODLevel(distance = 0f, model = "high_detail.glb"),
LODLevel(distance = 50f, model = "medium_detail.glb"),
LODLevel(distance = 100f, model = "low_detail.glb")
)
)
mapLoader.loadWithOptions("map.json", modelOptions)
Dépannage¶
Carte ne se charge pas¶
val exists = try {
assets.open("maps/venue.json").close(); true
} catch (e: IOException) { false }
if (!exists) Log.e("MapLoading", "Map file not found in assets")
Chargement lent¶
- Activer le chargement progressif
- Réduire la complexité des modèles
- Compresser les textures
- Utiliser le cache
- Charger les POI à la demande
Erreurs mémoire (OOM)¶
val loadOptions = LoadOptions(
maxMemoryUsage = 200 * 1024 * 1024,
enableTextureCompression = true,
modelQuality = ModelQuality.MEDIUM
)