MobilApp/lib/core/geopackage_exporter.dart

551 lines
19 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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),
];
}
}