diff --git a/lib/core/geopackage_exporter.dart b/lib/core/geopackage_exporter.dart new file mode 100644 index 0000000..8172d20 --- /dev/null +++ b/lib/core/geopackage_exporter.dart @@ -0,0 +1,550 @@ +// 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 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 _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 _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 _registerLayer( + Database db, { + required String tableName, + required String geomType, // 'POINT', 'LINESTRING', 'POLYGON' + required String identifier, + List? 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 _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 _exportPoints(Database db, List 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 _exportLines(Database db, List 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 _exportPolygons(Database db, List 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 _exportMeasuredPoints(Database db, int? projectId) async { + final points = projectId != null + ? await AppDatabase.instance.listMeasuredPoints(projectId) + : []; + 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 _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 _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 _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 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 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? _extent(List 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), + ]; + } +} diff --git a/lib/pages/map_survey/presentations/controllers/map_survey_controller.dart b/lib/pages/map_survey/presentations/controllers/map_survey_controller.dart index abac060..345e0a7 100644 --- a/lib/pages/map_survey/presentations/controllers/map_survey_controller.dart +++ b/lib/pages/map_survey/presentations/controllers/map_survey_controller.dart @@ -23,6 +23,7 @@ import 'package:terepi_seged/controls/geoid_grid.dart'; import 'package:terepi_seged/controls/wgs84_coordinate_formatter.dart'; import 'package:terepi_seged/core/geometry_measure.dart'; import 'package:terepi_seged/core/geometry_measure_formatter.dart'; +import 'package:terepi_seged/core/geopackage_exporter.dart'; import 'package:terepi_seged/enums/map_edit_tool.dart'; import 'package:terepi_seged/enums/map_survey_mode.dart'; import 'package:terepi_seged/enums/note_type.dart'; @@ -45,6 +46,7 @@ import 'package:terepi_seged/services/gnss/gnss_service.dart'; import 'package:terepi_seged/services/ntrip_service.dart'; import 'package:terepi_seged/services/project_service.dart'; import 'package:terepi_seged/widgets/map/imported_layer_overlay.dart'; +import 'package:terepi_seged/widgets/map_edit_tools/color_row.dart'; import 'package:terepi_seged/widgets/map_edit_tools/map_feature_save_sheet.dart'; import 'package:terepi_seged/widgets/map_edit_tools/note_item_list_sheet.dart'; import 'package:terepi_seged/widgets/shared_map_widgets.dart'; @@ -142,6 +144,9 @@ class MapSurveyController extends GetxController { final pointsToMeasureLabel = [].obs; final pointsToMeasureDropDownMenuItem = >[].obs; + final allNoteItems = [].obs; + final showGeometryLabels = false.obs; + // ── Pont adatok ─────────────────────────────────────────────────── final RxList pointsToMeasure = [].obs; final RxList pointWithDescriptionList = @@ -1251,6 +1256,8 @@ class MapSurveyController extends GetxController { final projectId = ProjectService.to.activeProject.value?.id; final items = await AppDatabase.instance.listNoteItems(projectId); + allNoteItems.assignAll(items); + // Listák resetelése pointNotes.clear(); polylineNotes.clear(); @@ -1341,6 +1348,9 @@ class MapSurveyController extends GetxController { ); await updateNoteItem(updated); + + allNoteItems.removeWhere((i) => i.id == id); + allNoteItems.add(updated); editingNoteItemId = null; activeEditTool.value = MapEditTool.none; draftAreaSquareMeters.value = 0.0; @@ -1413,6 +1423,7 @@ class MapSurveyController extends GetxController { ).then((saved) { if (saved == null) return; pointNotes.add(_markerFromNoteItem(saved)); + allNoteItems.add(saved); }); activeEditTool.value = MapEditTool.none; } @@ -1510,6 +1521,7 @@ class MapSurveyController extends GetxController { } Future deleteNoteItem(NoteItem item) async { + allNoteItems.removeWhere((i) => i.id == item.id); await AppDatabase.instance.deleteNoteItem(item.id!); switch (item.type) { @@ -1634,6 +1646,7 @@ class MapSurveyController extends GetxController { ); if (saved != null) { polylineNotes.add(saved.toPolyline()); + allNoteItems.add(saved); } polygonEditorController.clear(); activeEditTool.value = MapEditTool.none; @@ -1796,15 +1809,23 @@ class MapSurveyController extends GetxController { Get.dialog( AlertDialog( title: const Text('Pont mentése'), - content: TextField( - controller: labelCtrl, - autofocus: true, - decoration: const InputDecoration( - hintText: 'Pont neve (opcionális)', - border: OutlineInputBorder(), - ), - textCapitalization: TextCapitalization.sentences, - onSubmitted: (_) => _doSavePoint(point, labelCtrl.text), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ColorRow(ctrl: this, circleSize: 34), + SizedBox(height: 8), + TextField( + controller: labelCtrl, + autofocus: true, + decoration: const InputDecoration( + hintText: 'Pont neve (opcionális)', + border: OutlineInputBorder(), + ), + textCapitalization: TextCapitalization.sentences, + onSubmitted: (_) => _doSavePoint(point, labelCtrl.text), + ), + ], ), actions: [ TextButton( @@ -1853,4 +1874,45 @@ class MapSurveyController extends GetxController { ignoreSafeArea: false, ); } + + void toggleGeometryLabels() => + showGeometryLabels.value = !showGeometryLabels.value; + + Future exportProject() async { + Get.dialog( + Center( + child: Material( + borderRadius: BorderRadius.circular(14), + child: Container( + padding: const EdgeInsets.all(28), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(14), + ), + child: const Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text( + 'GeoPackage generálása...', + style: TextStyle(fontSize: 16), + ), + ], + ), + ), + ), + ), + barrierDismissible: false, + ); + + try { + await GeoPackageExporter().exportProject(); + } catch (e) { + Get.snackbar('Export hiba', e.toString(), + snackPosition: SnackPosition.BOTTOM); + } finally { + Get.back(); // ← dialóg bezárása, akár sikerült akár nem + } + } } diff --git a/lib/pages/map_survey/presentations/views/map_survey_view.dart b/lib/pages/map_survey/presentations/views/map_survey_view.dart index 0afa66d..44d4ee8 100644 --- a/lib/pages/map_survey/presentations/views/map_survey_view.dart +++ b/lib/pages/map_survey/presentations/views/map_survey_view.dart @@ -15,6 +15,7 @@ import 'package:terepi_seged/utils/rive_utils.dart'; import 'package:terepi_seged/widgets/coordinate_panel.dart'; import 'package:terepi_seged/widgets/map/imported_layer_overlay.dart'; import 'package:terepi_seged/widgets/map/measure_bottom_panel.dart'; +import 'package:terepi_seged/widgets/map/note_item_label_layer.dart'; import 'package:terepi_seged/widgets/map_bottom_panel.dart'; import 'package:terepi_seged/widgets/map_edit_tools/map_edit_drawing_toolbar.dart'; import 'package:terepi_seged/widgets/map_edit_tools/map_edit_toolbar.dart'; @@ -171,6 +172,9 @@ class MapSurveyView extends GetView { controller: controller.polygonEditorController, throttleDuration: Duration.zero); }), + NoteItemLabelLayer( + controller: controller, + ), Obx(() { final isGpsActive = GnssService.to.activeConnectionType.value != GnssConnectionType.none; diff --git a/lib/widgets/app_drawer.dart b/lib/widgets/app_drawer.dart index 90748ee..e7af60e 100644 --- a/lib/widgets/app_drawer.dart +++ b/lib/widgets/app_drawer.dart @@ -252,7 +252,9 @@ class _DrawerFooter extends StatelessWidget { FutureBuilder( future: PackageInfo.fromPlatform(), builder: (_, snap) => Text( - snap.hasData ? 'v${snap.data!.version}' : '', + snap.hasData + ? 'v${snap.data!.version}+${snap.data!.buildNumber}' + : '', style: TextStyle( fontSize: 11, color: Colors.grey.shade500, diff --git a/lib/widgets/appbar/shell_map_appbar.dart b/lib/widgets/appbar/shell_map_appbar.dart index 61f8a66..f5ae46d 100644 --- a/lib/widgets/appbar/shell_map_appbar.dart +++ b/lib/widgets/appbar/shell_map_appbar.dart @@ -153,7 +153,7 @@ class ShellMapAppBar extends StatelessWidget implements PreferredSizeWidget { controller: TrackingController.to, onTap: () => _openTrackingSheet(context), ), - PopupMenuButton( + PopupMenuButton( tooltip: 'További funkciók', icon: const Icon(Icons.more_vert), onSelected: (value) { @@ -163,16 +163,48 @@ class ShellMapAppBar extends StatelessWidget implements PreferredSizeWidget { if (value == 0) { controller.openNoteItemList(); } + if (value == 2) { + controller.toggleGeometryLabels(); + } + if (value == 3) { + controller.exportProject(); + } }, - itemBuilder: (context) => const [ + itemBuilder: (context) => [ PopupMenuItem( value: 0, - child: Text('Feljegyzések'), + child: ListTile( + leading: Icon(Icons.line_axis), + title: Text('Geometriák'), + dense: true), + ), + PopupMenuItem( + value: 2, + child: ListTile( + leading: Icon( + controller.showGeometryLabels.value + ? Icons.label + : Icons.label_off_outlined, + color: controller.showGeometryLabels.value + ? Theme.of(context).colorScheme.primary + : null), + title: Text(controller.showGeometryLabels.value + ? "Feliartok elrejtése" + : 'Feliratok megjelenítése'), + ), ), PopupMenuItem( value: 1, child: Text('Rétegek'), ), + PopupMenuDivider(), + PopupMenuItem( + value: 3, + child: ListTile( + leading: const Icon(Icons.archive_outlined), + title: const Text('Projekt exportálása'), + dense: true, + )) ]) ], bottom: PreferredSize( diff --git a/lib/widgets/map/note_item_label_layer.dart b/lib/widgets/map/note_item_label_layer.dart new file mode 100644 index 0000000..5996673 --- /dev/null +++ b/lib/widgets/map/note_item_label_layer.dart @@ -0,0 +1,116 @@ +// Szöveges feliratok a rögzített pontokhoz és vonalakhoz. +// Poligonoknál a Polygon.label-t a view kezeli (flutter_map beépített). + +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:get/get.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:terepi_seged/enums/map_survey_mode.dart'; + +import '../../enums/note_type.dart'; +import '../../pages/map_survey/presentations/controllers/map_survey_controller.dart'; + +class NoteItemLabelLayer extends StatelessWidget { + final MapSurveyController controller; + const NoteItemLabelLayer({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + return Obx(() { + if (!controller.showGeometryLabels.value) return const SizedBox.shrink(); + if (controller.mode.value != MapSurveyMode.fieldWalk) + return const SizedBox.shrink(); + + final markers = []; + + for (final item in controller.allNoteItems) { + if (item.label.isEmpty) continue; + + switch (item.type) { + // ── Pont — felirat a marker felett ────────────────────── + case NoteType.point: + markers.add(Marker( + point: item.points.first, + width: 110, + height: 48, // 22 szöveg + 22 pont marker helye + alignment: Alignment.bottomCenter, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _LabelBubble(label: item.label, color: item.color), + //const SizedBox(height: 4), // pont marker fölé kerül + ], + ), + )); + + // ── Vonal — felirat a felezőponton ────────────────────── + case NoteType.line: + if (item.points.isEmpty) continue; + final mid = _midpoint(item.points); + markers.add(Marker( + point: mid, + width: 120, + height: 24, + alignment: Alignment.center, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _LabelBubble(label: item.label, color: item.color), + ], + ), + )); + + case NoteType.polygon: + break; // poligonnál a Polygon.label kezeli a view-ban + } + } + + if (markers.isEmpty) return const SizedBox.shrink(); + return MarkerLayer(markers: markers); + }); + } + + /// Vonal felezőpontja + LatLng _midpoint(List points) { + final mid = points[points.length ~/ 2]; + return mid; + } +} + +// ─── Felirat buborék ───────────────────────────────────────────────────────── + +class _LabelBubble extends StatelessWidget { + final String label; + final Color color; + const _LabelBubble({required this.label, required this.color}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.92), + borderRadius: BorderRadius.circular(5), + border: Border(left: BorderSide(color: color, width: 3)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.18), + blurRadius: 4, + offset: const Offset(0, 1), + ), + ], + ), + child: Text( + label, + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Colors.black87, + height: 1.2, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ); + } +} diff --git a/lib/widgets/map_edit_tools/color_row.dart b/lib/widgets/map_edit_tools/color_row.dart index 0f9e686..909049b 100644 --- a/lib/widgets/map_edit_tools/color_row.dart +++ b/lib/widgets/map_edit_tools/color_row.dart @@ -4,6 +4,8 @@ import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_surv class ColorRow extends StatelessWidget { final MapSurveyController ctrl; + final double circleSize; + static const _palette = [ Color(0xFF6C63FF), Color(0xFFE74C3C), @@ -13,7 +15,7 @@ class ColorRow extends StatelessWidget { Color(0xFF3498DB), Color(0xFF8BC34A), ]; - const ColorRow({required this.ctrl}); + const ColorRow({required this.ctrl, this.circleSize = 40}); @override Widget build(BuildContext context) { @@ -31,8 +33,8 @@ class ColorRow extends StatelessWidget { onTap: () => ctrl.activeEditColor.value = color, child: AnimatedContainer( duration: const Duration(milliseconds: 150), - width: 40, - height: 40, + width: circleSize, + height: circleSize, decoration: BoxDecoration( color: color, shape: BoxShape.circle,