mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
feat: MeshCore SAR - Flutter BLE mesh radio companion app
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
273
lib/services/mbtiles_service.dart
Normal file
273
lib/services/mbtiles_service.dart
Normal file
@@ -0,0 +1,273 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:mbtiles/mbtiles.dart';
|
||||
|
||||
/// Metadata information extracted from an MBTiles file
|
||||
class MbtilesMetadata {
|
||||
final String name;
|
||||
final String? description;
|
||||
final String? version;
|
||||
final String? attribution;
|
||||
final String? bounds; // "minLon,minLat,maxLon,maxLat"
|
||||
final String? center; // "lon,lat,zoom"
|
||||
final int? minZoom;
|
||||
final int? maxZoom;
|
||||
final String? format; // "pbf", "png", "jpg", etc.
|
||||
final String? type; // "overlay", "baselayer"
|
||||
final String? json; // Additional metadata JSON
|
||||
final File file;
|
||||
final int fileSize;
|
||||
|
||||
const MbtilesMetadata({
|
||||
required this.name,
|
||||
this.description,
|
||||
this.version,
|
||||
this.attribution,
|
||||
this.bounds,
|
||||
this.center,
|
||||
this.minZoom,
|
||||
this.maxZoom,
|
||||
this.format,
|
||||
this.type,
|
||||
this.json,
|
||||
required this.file,
|
||||
required this.fileSize,
|
||||
});
|
||||
|
||||
/// Check if this is a vector tile MBTiles file
|
||||
bool get isVector => format == 'pbf' || format == 'mvt';
|
||||
|
||||
/// Parse bounds string into [minLon, minLat, maxLon, maxLat]
|
||||
List<double>? get boundsCoordinates {
|
||||
if (bounds == null) return null;
|
||||
try {
|
||||
final parts = bounds!.split(',');
|
||||
if (parts.length != 4) return null;
|
||||
return parts.map((s) => double.parse(s.trim())).toList();
|
||||
} catch (e) {
|
||||
debugPrint('Error parsing bounds: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse center string into [lon, lat, zoom]
|
||||
List<double>? get centerCoordinates {
|
||||
if (center == null) return null;
|
||||
try {
|
||||
final parts = center!.split(',');
|
||||
if (parts.length < 2) return null;
|
||||
return parts.map((s) => double.parse(s.trim())).toList();
|
||||
} catch (e) {
|
||||
debugPrint('Error parsing center: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get file size in human-readable format
|
||||
String get fileSizeFormatted {
|
||||
if (fileSize < 1024) {
|
||||
return '$fileSize B';
|
||||
} else if (fileSize < 1024 * 1024) {
|
||||
return '${(fileSize / 1024).toStringAsFixed(1)} KB';
|
||||
} else if (fileSize < 1024 * 1024 * 1024) {
|
||||
return '${(fileSize / (1024 * 1024)).toStringAsFixed(1)} MB';
|
||||
} else {
|
||||
return '${(fileSize / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Service for managing MBTiles files for offline vector maps
|
||||
class MbtilesService {
|
||||
static const String _mbtilesDirectory = 'offline_maps';
|
||||
|
||||
/// Get the directory where MBTiles files are stored
|
||||
Future<Directory> getMbtilesDirectory() async {
|
||||
final appDocDir = await getApplicationDocumentsDirectory();
|
||||
final mbtilesDir = Directory('${appDocDir.path}/$_mbtilesDirectory');
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
if (!await mbtilesDir.exists()) {
|
||||
await mbtilesDir.create(recursive: true);
|
||||
}
|
||||
|
||||
return mbtilesDir;
|
||||
}
|
||||
|
||||
/// List all MBTiles files in the offline maps directory
|
||||
Future<List<File>> listMbtilesFiles() async {
|
||||
final dir = await getMbtilesDirectory();
|
||||
|
||||
try {
|
||||
final files = await dir
|
||||
.list()
|
||||
.where((entity) => entity is File && entity.path.endsWith('.mbtiles'))
|
||||
.map((entity) => entity as File)
|
||||
.toList();
|
||||
|
||||
return files;
|
||||
} catch (e) {
|
||||
debugPrint('Error listing MBTiles files: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Get metadata from an MBTiles file
|
||||
Future<MbtilesMetadata?> getMetadata(File file) async {
|
||||
try {
|
||||
// Check if file exists
|
||||
if (!await file.exists()) {
|
||||
debugPrint('MBTiles file does not exist: ${file.path}');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get file size
|
||||
final fileSize = await file.length();
|
||||
|
||||
// Open MBTiles file
|
||||
final mbtiles = MbTiles(mbtilesPath: file.path);
|
||||
|
||||
// Get metadata from MBTiles
|
||||
final metadata = mbtiles.getMetadata();
|
||||
|
||||
// Convert bounds object to string if available
|
||||
String? boundsStr;
|
||||
if (metadata.bounds != null) {
|
||||
boundsStr = metadata.bounds.toString();
|
||||
}
|
||||
|
||||
return MbtilesMetadata(
|
||||
name: metadata.name,
|
||||
description: metadata.description,
|
||||
version: metadata.version?.toString(),
|
||||
attribution: null, // Not available in new API
|
||||
bounds: boundsStr,
|
||||
center: null, // Not available in new API
|
||||
minZoom: metadata.minZoom?.toInt(),
|
||||
maxZoom: metadata.maxZoom?.toInt(),
|
||||
format: metadata.format,
|
||||
type: metadata.type?.name,
|
||||
json: null, // Not available in new API
|
||||
file: file,
|
||||
fileSize: fileSize,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('Error reading MBTiles metadata from ${file.path}: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get metadata for all MBTiles files
|
||||
Future<List<MbtilesMetadata>> getAllMetadata() async {
|
||||
final files = await listMbtilesFiles();
|
||||
final metadataList = <MbtilesMetadata>[];
|
||||
|
||||
for (final file in files) {
|
||||
final metadata = await getMetadata(file);
|
||||
if (metadata != null) {
|
||||
metadataList.add(metadata);
|
||||
}
|
||||
}
|
||||
|
||||
return metadataList;
|
||||
}
|
||||
|
||||
/// Import an MBTiles file from an external location
|
||||
Future<File?> importMbtilesFile(String sourcePath) async {
|
||||
try {
|
||||
final sourceFile = File(sourcePath);
|
||||
|
||||
// Verify source file exists
|
||||
if (!await sourceFile.exists()) {
|
||||
debugPrint('Source file does not exist: $sourcePath');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get destination directory
|
||||
final destDir = await getMbtilesDirectory();
|
||||
final fileName = _getFileName(sourceFile);
|
||||
final destPath = '${destDir.path}/$fileName';
|
||||
|
||||
// Copy file to destination
|
||||
final destFile = await sourceFile.copy(destPath);
|
||||
debugPrint('Imported MBTiles file to: $destPath');
|
||||
|
||||
return destFile;
|
||||
} catch (e) {
|
||||
debugPrint('Error importing MBTiles file: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete an MBTiles file
|
||||
Future<bool> deleteMbtilesFile(File file) async {
|
||||
try {
|
||||
if (await file.exists()) {
|
||||
await file.delete();
|
||||
debugPrint('Deleted MBTiles file: ${file.path}');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
debugPrint('Error deleting MBTiles file: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if data in MBTiles is gzip compressed
|
||||
Future<bool> isGzipCompressed(File file) async {
|
||||
try {
|
||||
// Open MBTiles and check a sample tile
|
||||
final mbtiles = MbTiles(mbtilesPath: file.path);
|
||||
|
||||
// Try to get metadata to check for compression hints
|
||||
final metadata = mbtiles.getMetadata();
|
||||
final format = metadata.format;
|
||||
|
||||
// For Geofabrik files, format is 'pbf' and data is gzipped
|
||||
// We can infer this from common patterns, but ideally we'd check actual tile data
|
||||
if (format == 'pbf') {
|
||||
// Geofabrik MBTiles are typically gzipped
|
||||
// Could also check tile data headers, but this is a reasonable heuristic
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (e) {
|
||||
debugPrint('Error checking gzip compression: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine the vector tile schema from metadata
|
||||
String? getVectorSchema(MbtilesMetadata metadata) {
|
||||
// Try to infer schema from metadata
|
||||
final json = metadata.json;
|
||||
if (json != null) {
|
||||
if (json.contains('shortbread')) {
|
||||
return 'shortbread';
|
||||
} else if (json.contains('openmaptiles')) {
|
||||
return 'openmaptiles';
|
||||
}
|
||||
}
|
||||
|
||||
// Check description
|
||||
final description = metadata.description?.toLowerCase();
|
||||
if (description != null) {
|
||||
if (description.contains('shortbread')) {
|
||||
return 'shortbread';
|
||||
} else if (description.contains('openmaptiles')) {
|
||||
return 'openmaptiles';
|
||||
}
|
||||
}
|
||||
|
||||
// Default to unknown
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Helper: Get file name from path
|
||||
String _getFileName(File file) {
|
||||
return file.path.split(Platform.pathSeparator).last;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user