MobilApp/lib/core/geopackage_exporter.dart

551 lines
19 KiB
Dart
Raw Permalink Normal View History

// GeoPackage (.gpkg) export — OGC szabvány, QGIS-kompatibilis
//
// Tartalom:
// - note_items_point : rögzített pontok
// - note_items_line : rögzített vonalak
// - note_items_polygon : rögzített területek
// - measured_points : bemért pontok
// - tracks : GPS nyomvonalak
//
// Kimenet: ZIP archív (.gpkg + médiafájlok)
import 'dart:io';
import 'dart:typed_data';
import 'package:archive/archive_io.dart';
import 'package:flutter/foundation.dart';
import 'package:intl/intl.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
import 'package:sqflite/sqflite.dart';
import 'package:terepi_seged/models/measured_point.dart';
import 'package:terepi_seged/services/app_database.dart';
import 'package:terepi_seged/services/project_service.dart';
import '../models/note_item.dart';
import '../enums/note_type.dart';
class GeoPackageExporter {
// ── Publikus belépési pont ────────────────────────────────────────
Future<void> exportProject() async {
final project = ProjectService.to.activeProject.value;
final projectId = ProjectService.to.activeProjectId;
final name = project?.name ?? 'projekt';
final ts = DateFormat('yyyyMMdd_HHmm').format(DateTime.now());
final tmpDir = await getTemporaryDirectory();
final workDir = Directory(p.join(tmpDir.path, 'gpkg_export_$ts'));
await workDir.create(recursive: true);
try {
// 1. GeoPackage létrehozása
final gpkgPath = p.join(workDir.path, '$name.gpkg');
await _buildGpkg(gpkgPath, projectId);
// 2. Médiafájlok összegyűjtése
final mediaDir = Directory(p.join(workDir.path, 'media'));
await _collectMedia(projectId, mediaDir);
// 3. ZIP csomagolás
final zipPath = p.join(tmpDir.path, '${name}_$ts.zip');
await _packZip(workDir, zipPath);
// 4. Megosztás
await SharePlus.instance.share(ShareParams(
files: [XFile(zipPath, mimeType: 'application/zip')],
subject: 'GeoPackage export — $name',
));
} finally {
// Ideiglenes munkamappa törlése
await workDir.delete(recursive: true).catchError((_) {});
}
}
// ── GeoPackage (.gpkg) létrehozása ───────────────────────────────
Future<void> _buildGpkg(String path, int? projectId) async {
final db = await openDatabase(path);
try {
await _initGpkg(db);
await _exportNoteItems(db, projectId);
await _exportMeasuredPoints(db, projectId);
await _exportTracks(db, projectId);
} finally {
await db.close();
}
}
// ── GeoPackage kötelező táblák ────────────────────────────────────
Future<void> _initGpkg(Database db) async {
// GeoPackage magic numbers
await db.execute('PRAGMA application_id = 1196444487');
await db.execute('PRAGMA user_version = 10200');
// Spatial reference systems
await db.execute('''
CREATE TABLE gpkg_spatial_ref_sys (
srs_name TEXT NOT NULL,
srs_id INTEGER NOT NULL PRIMARY KEY,
organization TEXT NOT NULL,
organization_coordsys_id INTEGER NOT NULL,
definition TEXT NOT NULL,
description TEXT
)''');
// WGS84 bejegyzés
await db.insert('gpkg_spatial_ref_sys', {
'srs_name': 'WGS 84 geographic 2D',
'srs_id': 4326,
'organization': 'EPSG',
'organization_coordsys_id': 4326,
'definition': 'GEOGCS["WGS 84",DATUM["WGS_1984",'
'SPHEROID["WGS 84",6378137,298.257223563]],'
'PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]]',
'description': 'longitude/latitude coordinates in decimal degrees',
});
// Undefined srs
await db.insert('gpkg_spatial_ref_sys', {
'srs_name': 'Undefined Cartesian SRS',
'srs_id': -1,
'organization': 'NONE',
'organization_coordsys_id': -1,
'definition': 'undefined',
});
await db.insert('gpkg_spatial_ref_sys', {
'srs_name': 'Undefined geographic SRS',
'srs_id': 0,
'organization': 'NONE',
'organization_coordsys_id': 0,
'definition': 'undefined',
});
// Contents tábla
await db.execute('''
CREATE TABLE gpkg_contents (
table_name TEXT NOT NULL PRIMARY KEY,
data_type TEXT NOT NULL,
identifier TEXT,
description TEXT DEFAULT '',
last_change DATETIME NOT NULL
DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', CURRENT_TIMESTAMP)),
min_x REAL, min_y REAL, max_x REAL, max_y REAL,
srs_id INTEGER,
CONSTRAINT fk_gc_r_srs_id
FOREIGN KEY (srs_id) REFERENCES gpkg_spatial_ref_sys(srs_id)
)''');
// Geometry columns tábla
await db.execute('''
CREATE TABLE gpkg_geometry_columns (
table_name TEXT NOT NULL,
column_name TEXT NOT NULL,
geometry_type_name TEXT NOT NULL,
srs_id INTEGER NOT NULL,
z TINYINT NOT NULL,
m TINYINT NOT NULL,
CONSTRAINT pk_geom_cols PRIMARY KEY (table_name, column_name),
CONSTRAINT fk_gc_tn
FOREIGN KEY (table_name) REFERENCES gpkg_contents(table_name),
CONSTRAINT fk_gc_srs
FOREIGN KEY (srs_id) REFERENCES gpkg_spatial_ref_sys(srs_id)
)''');
}
// ── Feature tábla regisztrálása ───────────────────────────────────
Future<void> _registerLayer(
Database db, {
required String tableName,
required String geomType, // 'POINT', 'LINESTRING', 'POLYGON'
required String identifier,
List<double>? extent, // [minX, minY, maxX, maxY]
}) async {
await db.insert('gpkg_contents', {
'table_name': tableName,
'data_type': 'features',
'identifier': identifier,
'description': '',
'last_change': DateTime.now().toUtc().toIso8601String(),
'min_x': extent?[0],
'min_y': extent?[1],
'max_x': extent?[2],
'max_y': extent?[3],
'srs_id': 4326,
});
await db.insert('gpkg_geometry_columns', {
'table_name': tableName,
'column_name': 'geom',
'geometry_type_name': geomType,
'srs_id': 4326,
'z': 0,
'm': 0,
});
}
// ── NoteItem exportok ─────────────────────────────────────────────
Future<void> _exportNoteItems(Database db, int? projectId) async {
final items = await AppDatabase.instance.listNoteItems(projectId);
final points = items.where((i) => i.type == NoteType.point).toList();
final lines = items.where((i) => i.type == NoteType.line).toList();
final polygons = items.where((i) => i.type == NoteType.polygon).toList();
if (points.isNotEmpty) await _exportPoints(db, points);
if (lines.isNotEmpty) await _exportLines(db, lines);
if (polygons.isNotEmpty) await _exportPolygons(db, polygons);
}
Future<void> _exportPoints(Database db, List<NoteItem> items) async {
await db.execute('''
CREATE TABLE note_items_point (
id INTEGER PRIMARY KEY,
geom BLOB NOT NULL,
label TEXT,
color_hex TEXT,
created_at TEXT,
photo_count INTEGER DEFAULT 0,
audio_count INTEGER DEFAULT 0
)''');
await _registerLayer(db,
tableName: 'note_items_point',
geomType: 'POINT',
identifier: 'Rögzített pontok',
extent: _extent(items));
for (final item in items) {
final pt = item.points.first;
final pc = await AppDatabase.instance.countNotePhotos(item.id!);
final ac = await AppDatabase.instance.countNoteAudios(item.id!);
await db.insert('note_items_point', {
'id': item.id,
'geom': _gpkgPoint(pt.longitude, pt.latitude),
'label': item.label,
'color_hex':
'#${item.color.value.toRadixString(16).padLeft(8, '0').substring(2)}',
'created_at': item.createdAt.toIso8601String(),
'photo_count': pc,
'audio_count': ac,
});
}
}
Future<void> _exportLines(Database db, List<NoteItem> items) async {
await db.execute('''
CREATE TABLE note_items_line (
id INTEGER PRIMARY KEY,
geom BLOB NOT NULL,
label TEXT,
color_hex TEXT,
stroke_width REAL,
point_count INTEGER,
created_at TEXT,
photo_count INTEGER DEFAULT 0,
audio_count INTEGER DEFAULT 0
)''');
await _registerLayer(db,
tableName: 'note_items_line',
geomType: 'LINESTRING',
identifier: 'Rögzített vonalak',
extent: _extent(items));
for (final item in items) {
final pc = await AppDatabase.instance.countNotePhotos(item.id!);
final ac = await AppDatabase.instance.countNoteAudios(item.id!);
await db.insert('note_items_line', {
'id': item.id,
'geom': _gpkgLineString(item.points),
'label': item.label,
'color_hex':
'#${item.color.value.toRadixString(16).padLeft(8, '0').substring(2)}',
'stroke_width': item.strokeWidth,
'point_count': item.points.length,
'created_at': item.createdAt.toIso8601String(),
'photo_count': pc,
'audio_count': ac,
});
}
}
Future<void> _exportPolygons(Database db, List<NoteItem> items) async {
await db.execute('''
CREATE TABLE note_items_polygon (
id INTEGER PRIMARY KEY,
geom BLOB NOT NULL,
label TEXT,
color_hex TEXT,
opacity REAL,
stroke_width REAL,
point_count INTEGER,
created_at TEXT,
photo_count INTEGER DEFAULT 0,
audio_count INTEGER DEFAULT 0
)''');
await _registerLayer(db,
tableName: 'note_items_polygon',
geomType: 'POLYGON',
identifier: 'Rögzített területek',
extent: _extent(items));
for (final item in items) {
final pc = await AppDatabase.instance.countNotePhotos(item.id!);
final ac = await AppDatabase.instance.countNoteAudios(item.id!);
await db.insert('note_items_polygon', {
'id': item.id,
'geom': _gpkgPolygon(item.points),
'label': item.label,
'color_hex':
'#${item.color.value.toRadixString(16).padLeft(8, '0').substring(2)}',
'opacity': item.opacity,
'stroke_width': item.strokeWidth,
'point_count': item.points.length,
'created_at': item.createdAt.toIso8601String(),
'photo_count': pc,
'audio_count': ac,
});
}
}
// ── Bemért pontok exportja ────────────────────────────────────────
Future<void> _exportMeasuredPoints(Database db, int? projectId) async {
final points = projectId != null
? await AppDatabase.instance.listMeasuredPoints(projectId)
: <MeasuredPoint>[];
if (points.isEmpty) return;
await db.execute('''
CREATE TABLE measured_points (
id INTEGER PRIMARY KEY,
geom BLOB NOT NULL,
name TEXT,
eov_y REAL,
eov_x REAL,
eov_z REAL,
accuracy REAL,
fix_quality INTEGER,
timestamp TEXT,
note TEXT
)''');
await _registerLayer(db,
tableName: 'measured_points',
geomType: 'POINT',
identifier: 'Bemért pontok');
for (final pt in points) {
if (pt.latitude == null || pt.longitude == null) continue;
await db.insert('measured_points', {
'id': pt.id,
'geom': _gpkgPoint(pt.longitude!, pt.latitude!),
'name': pt.name,
'eov_y': pt.eovY,
'eov_x': pt.eovX,
'eov_z': pt.eovZ,
'accuracy': pt.accuracy,
'fix_quality': pt.fixQuality,
'timestamp': pt.timestamp.toIso8601String(),
'note': pt.note,
});
}
}
// ── Track exportja ────────────────────────────────────────────────
Future<void> _exportTracks(Database db, int? projectId) async {
final tracks = await AppDatabase.instance.listTracks();
final filtered = projectId != null
? tracks.where((t) => t.projectId == projectId).toList()
: tracks;
if (filtered.isEmpty) return;
await db.execute('''
CREATE TABLE tracks (
id INTEGER PRIMARY KEY,
geom BLOB NOT NULL,
name TEXT,
distance_meters REAL,
start_time TEXT,
end_time TEXT,
point_count INTEGER
)''');
await _registerLayer(db,
tableName: 'tracks',
geomType: 'LINESTRING',
identifier: 'GPS nyomvonalak');
for (final track in filtered) {
final pts = await AppDatabase.instance.getLatLons(track.id!);
if (pts.length < 2) continue;
// Egyszerű record list → LatLng-szerű párok
final coords = pts.map((p) => (lon: p.lon, lat: p.lat)).toList();
await db.insert('tracks', {
'id': track.id,
'geom': _gpkgLineStringFromRecords(coords),
'name': track.name,
'distance_meters': track.distanceMeters,
'start_time': track.startTime.toIso8601String(),
'end_time': track.endTime?.toIso8601String(),
'point_count': pts.length,
});
}
}
// ── Médiafájlok másolása ──────────────────────────────────────────
Future<void> _collectMedia(int? projectId, Directory mediaDir) async {
if (projectId == null) return;
final photos = Directory(p.join(mediaDir.path, 'photos'));
final audios = Directory(p.join(mediaDir.path, 'audio'));
await photos.create(recursive: true);
await audios.create(recursive: true);
final items = await AppDatabase.instance.listNoteItems(projectId);
for (final item in items) {
// Fotók
final photoList = await AppDatabase.instance.listNotePhotos(item.id!);
for (final photo in photoList) {
final src = File(photo.localPath);
if (await src.exists()) {
final dst = File(p.join(photos.path, p.basename(photo.localPath)));
await src.copy(dst.path);
}
}
// Hangok
final audioList = await AppDatabase.instance.listNoteAudios(item.id!);
for (final audio in audioList) {
final src = File(audio.localPath);
if (await src.exists()) {
final dst = File(p.join(audios.path, p.basename(audio.localPath)));
await src.copy(dst.path);
}
}
}
}
// ── ZIP csomagolás ────────────────────────────────────────────────
Future<void> _packZip(Directory workDir, String zipPath) async {
final archive = Archive();
for (final entity in workDir.listSync(recursive: true)) {
if (entity is! File) continue;
final bytes = entity.readAsBytesSync();
final rel = p.relative(entity.path, from: workDir.path);
final archiveName = rel.replaceAll('\\', '/'); // Windows path fix
archive.addFile(ArchiveFile(archiveName, bytes.length, bytes));
}
final encoded = ZipEncoder().encode(archive);
if (encoded == null || encoded.isEmpty) {
throw Exception(
'ZIP encoding sikertelen — nincsenek exportálható adatok');
}
await File(zipPath).writeAsBytes(Uint8List.fromList(encoded));
}
// ── WKB/GeoPackage bináris kódolás ───────────────────────────────
//
// GPKG binary header (8 byte):
// 0x47 0x50 'GP' magic
// 0x00 version 0
// 0x01 flags: little-endian, no envelope
// srs_id (int32) 4326 (WGS84)
// + WKB geometry
Uint8List _gpkgPoint(double lon, double lat) {
final b = ByteData(8 + 21); // header + wkb point
_writeGpkgHeader(b, 0);
b.setUint8(8, 1); // WKB: little endian
b.setUint32(9, 1, Endian.little); // type: Point
b.setFloat64(13, lon, Endian.little);
b.setFloat64(21, lat, Endian.little);
return b.buffer.asUint8List();
}
Uint8List _gpkgLineString(List<dynamic> points) {
final n = points.length;
final b = ByteData(8 + 9 + n * 16);
_writeGpkgHeader(b, 0);
int o = 8;
b.setUint8(o++, 1);
b.setUint32(o, 2, Endian.little);
o += 4; // type: LineString
b.setUint32(o, n, Endian.little);
o += 4;
for (final pt in points) {
b.setFloat64(o, pt.longitude, Endian.little);
o += 8;
b.setFloat64(o, pt.latitude, Endian.little);
o += 8;
}
return b.buffer.asUint8List();
}
Uint8List _gpkgLineStringFromRecords(
List<({double lon, double lat})> coords) {
final n = coords.length;
final b = ByteData(8 + 9 + n * 16);
_writeGpkgHeader(b, 0);
int o = 8;
b.setUint8(o++, 1);
b.setUint32(o, 2, Endian.little);
o += 4;
b.setUint32(o, n, Endian.little);
o += 4;
for (final c in coords) {
b.setFloat64(o, c.lon, Endian.little);
o += 8;
b.setFloat64(o, c.lat, Endian.little);
o += 8;
}
return b.buffer.asUint8List();
}
Uint8List _gpkgPolygon(List<dynamic> ring) {
final n = ring.length;
final b = ByteData(8 + 9 + 4 + n * 16);
_writeGpkgHeader(b, 0);
int o = 8;
b.setUint8(o++, 1);
b.setUint32(o, 3, Endian.little);
o += 4; // type: Polygon
b.setUint32(o, 1, Endian.little);
o += 4; // 1 ring
b.setUint32(o, n, Endian.little);
o += 4;
for (final pt in ring) {
b.setFloat64(o, pt.longitude, Endian.little);
o += 8;
b.setFloat64(o, pt.latitude, Endian.little);
o += 8;
}
return b.buffer.asUint8List();
}
void _writeGpkgHeader(ByteData b, int offset) {
b.setUint8(offset, 0x47); // 'G'
b.setUint8(offset + 1, 0x50); // 'P'
b.setUint8(offset + 2, 0x00); // version
b.setUint8(offset + 3, 0x01); // flags: little-endian, no envelope
b.setInt32(offset + 4, 4326, Endian.little); // WGS84
}
// ── Segédek ───────────────────────────────────────────────────────
/// Befoglaló téglalap az összes NoteItem pontjaiból
List<double>? _extent(List<NoteItem> items) {
final pts = items.expand((i) => i.points).toList();
if (pts.isEmpty) return null;
return [
pts.map((p) => p.longitude).reduce((a, b) => a < b ? a : b),
pts.map((p) => p.latitude).reduce((a, b) => a < b ? a : b),
pts.map((p) => p.longitude).reduce((a, b) => a > b ? a : b),
pts.map((p) => p.latitude).reduce((a, b) => a > b ? a : b),
];
}
}