From 2cf83149f02194a9acbde8dca3ee39899f5869a2 Mon Sep 17 00:00:00 2001 From: "torok.istvan" Date: Sat, 4 Jul 2026 23:36:01 +0200 Subject: [PATCH] =?UTF-8?q?Import=C3=A1lt=20r=C3=A9tegek=20st=C3=ADlus?= =?UTF-8?q?=C3=A1nak=20szerkeszt=C3=A9se?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/style_editable.dart | 10 +++ lib/models/imported_layer_meta.dart | 77 ++++++++++++------ .../controllers/map_survey_controller.dart | 3 +- lib/services/app_database.dart | 19 ++++- lib/services/layer_import_service.dart | 50 ++++++++++++ lib/widgets/map/all_layer_overlay.dart | 63 ++++++++------- lib/widgets/map/imported_layer_overlay.dart | 70 +++++++++++++++- lib/widgets/map_edit_tools/color_row.dart | 3 +- .../imported_layer_style_sheet.dart | 81 +++++++++++++++++++ .../map_edit_tools/opacity_slider.dart | 3 +- lib/widgets/map_edit_tools/stroke_slider.dart | 3 +- .../map_edit_tools/style_editable.dart | 23 ++++++ 12 files changed, 341 insertions(+), 64 deletions(-) create mode 100644 lib/core/style_editable.dart create mode 100644 lib/widgets/map_edit_tools/imported_layer_style_sheet.dart create mode 100644 lib/widgets/map_edit_tools/style_editable.dart diff --git a/lib/core/style_editable.dart b/lib/core/style_editable.dart new file mode 100644 index 0000000..34114be --- /dev/null +++ b/lib/core/style_editable.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +/// Közös interfész — MapSurveyController és LayerStyleSession is implementálja. +/// A ColorRow, OpacitySlider, StrokeSlider ezt várja (nem a teljes controllert). +abstract class StyleEditable { + Rx get activeEditColor; + RxDouble get activeEditOpacity; + RxDouble get activeEditStrokeWidth; +} diff --git a/lib/models/imported_layer_meta.dart b/lib/models/imported_layer_meta.dart index 289eea1..a4b92d6 100644 --- a/lib/models/imported_layer_meta.dart +++ b/lib/models/imported_layer_meta.dart @@ -1,4 +1,6 @@ +import 'package:flutter/material.dart'; import 'package:terepi_seged/enums/layer_import_source_type.dart'; +import 'package:terepi_seged/services/layer_import_service.dart'; class ImportedLayerMeta { final String id; @@ -10,18 +12,23 @@ class ImportedLayerMeta { final int? projectId; final DateTime importedAt; final DateTime? syncedAt; + final String? colorHex; + final double? opacity; + final double? strokeWidth; - const ImportedLayerMeta({ - required this.id, - required this.name, - required this.sourceType, - required this.localPath, - this.storagePath, - this.isVisible = true, - this.projectId, - required this.importedAt, - this.syncedAt, - }); + const ImportedLayerMeta( + {required this.id, + required this.name, + required this.sourceType, + required this.localPath, + this.storagePath, + this.isVisible = true, + this.projectId, + required this.importedAt, + this.syncedAt, + this.colorHex, + this.opacity, + this.strokeWidth}); bool get isSynced => storagePath != null; @@ -35,28 +42,36 @@ class ImportedLayerMeta { 'project_id': projectId, 'imported_at': importedAt.toIso8601String(), 'synced_at': syncedAt?.toIso8601String(), + if (colorHex != null) 'color_hex': colorHex, + if (opacity != null) 'opacity': opacity, + if (strokeWidth != null) 'stroke_width': strokeWidth }; factory ImportedLayerMeta.fromMap(Map m) => ImportedLayerMeta( - id: m['id'] as String, - name: m['name'] as String, - sourceType: - LayerImportSourceType.values.byName(m['source_type'] as String), - localPath: m['local_path'] as String, - storagePath: m['storage_path'] as String?, - isVisible: (m['is_visible'] as int) == 1, - projectId: m['project_id'] as int?, - importedAt: DateTime.parse(m['imported_at'] as String), - syncedAt: m['synced_at'] != null - ? DateTime.parse(m['synced_at'] as String) - : null, - ); + id: m['id'] as String, + name: m['name'] as String, + sourceType: + LayerImportSourceType.values.byName(m['source_type'] as String), + localPath: m['local_path'] as String, + storagePath: m['storage_path'] as String?, + isVisible: (m['is_visible'] as int) == 1, + projectId: m['project_id'] as int?, + importedAt: DateTime.parse(m['imported_at'] as String), + syncedAt: m['synced_at'] != null + ? DateTime.parse(m['synced_at'] as String) + : null, + colorHex: m['color_hex'] as String?, + opacity: (m['opacity'] as num?)?.toDouble(), + strokeWidth: (m['stroke_width'] as num?)?.toDouble()); ImportedLayerMeta copyWith({ bool? isVisible, String? storagePath, DateTime? syncedAt, + String? colorHex, + double? opacity, + double? strokeWidth, }) => ImportedLayerMeta( id: id, @@ -68,5 +83,19 @@ class ImportedLayerMeta { projectId: projectId, importedAt: importedAt, syncedAt: syncedAt ?? this.syncedAt, + colorHex: colorHex ?? this.colorHex, + opacity: opacity ?? this.opacity, + strokeWidth: strokeWidth ?? this.strokeWidth, ); + + ImportedLayerStyle? getStyle() { + if (colorHex == null) return null; + final hex = colorHex!.replaceAll('#', ''); + final color = Color(int.parse('FF$hex', radix: 16)); + + return ImportedLayerStyle( + color: color, + opacity: opacity ?? 0.35, + strokeWidth: strokeWidth ?? 2.5); + } } 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 890200f..e6e0a06 100644 --- a/lib/pages/map_survey/presentations/controllers/map_survey_controller.dart +++ b/lib/pages/map_survey/presentations/controllers/map_survey_controller.dart @@ -24,6 +24,7 @@ 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/core/style_editable.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'; @@ -56,7 +57,7 @@ import 'package:terepi_seged/widgets/shared_map_widgets.dart'; import '../views/measured_points_sheet.dart'; -class MapSurveyController extends GetxController { +class MapSurveyController extends GetxController implements StyleEditable { static MapSurveyController get to => Get.find(); // ── Függőségek (service-ek) ─────────────────────────────────────── diff --git a/lib/services/app_database.dart b/lib/services/app_database.dart index 35ceaed..32f05be 100644 --- a/lib/services/app_database.dart +++ b/lib/services/app_database.dart @@ -38,7 +38,7 @@ class AppDatabase { final path = p.join(dbDir.path, 'terepi_seged.db'); return openDatabase(path, - version: 1, + version: 2, onConfigure: (db) => db.execute('PRAGMA foreign_keys = ON'), onCreate: _onCreate, onUpgrade: _onUpgrade); @@ -202,6 +202,9 @@ class AppDatabase { source_type TEXT NOT NULL, local_path TEXT NOT NULL, storage_path TEXT, + color_hex TEXT, + opacity REAL, + stroke_width REAL, is_visible INTEGER NOT NULL DEFAULT 1, project_id INTEGER, imported_at TEXT NOT NULL, @@ -224,7 +227,19 @@ class AppDatabase { }); } - Future _onUpgrade(Database db, int oldVersion, int newVersion) async {} + Future _onUpgrade(Database db, int oldVersion, int newVersion) async { + if (oldVersion < 2) { + await db.execute(''' + ALTER TABLE imported_layers ADD COLUMN color_hex TEXT; + '''); + await db.execute(''' + ALTER TABLE imported_layers ADD COLUMN opacity REAL; + '''); + await db.execute(''' + ALTER TABLE imported_layers ADD COLUMN stroke_width REAL; + '''); + } + } // ── Projects CRUD ───────────────────────────────────────────────── diff --git a/lib/services/layer_import_service.dart b/lib/services/layer_import_service.dart index a0d789d..a568847 100644 --- a/lib/services/layer_import_service.dart +++ b/lib/services/layer_import_service.dart @@ -22,6 +22,29 @@ import '../controls/geojson_parser.dart'; import '../core/kml_parser.dart'; import '../services/project_service.dart'; +class ImportedLayerStyle { + final Color color; + final double opacity; + final double strokeWidth; + + const ImportedLayerStyle({ + required this.color, + this.opacity = 0.35, + this.strokeWidth = 2.5, + }); + + ImportedLayerStyle copyWith({ + Color? color, + double? opacity, + double? strokeWidth, + }) => + ImportedLayerStyle( + color: color ?? this.color, + opacity: opacity ?? this.opacity, + strokeWidth: strokeWidth ?? this.strokeWidth, + ); +} + class LayerImportService extends GetxService { static LayerImportService get to => Get.find(); @@ -40,6 +63,7 @@ class LayerImportService extends GetxService { String? _layerDir; static const _uuid = Uuid(); + final layerStyles = {}.obs; // ── Inicializálás ────────────────────────────────────────────────────────── @@ -137,6 +161,11 @@ class LayerImportService extends GetxService { final ext = meta.localPath.split('.').last; loaded.add(_parse(bytes, meta.name, meta.id, ext, isVisible: meta.isVisible)); + + final style = meta.getStyle(); + if (style != null) { + layerStyles[meta.id] = style; + } } catch (e) { debugPrint('Réteg betöltés hiba (${meta.name}): $e'); } @@ -271,4 +300,25 @@ class LayerImportService extends GetxService { 'kmz' => LayerImportSourceType.kmz, _ => LayerImportSourceType.geoJson, }; + + void setLayerStyle(String id, ImportedLayerStyle style) async { + layerStyles[id] = style; + layerStyles.refresh(); + layers.refresh(); + + final metas = await AppDatabase.instance.listImportedLayers(); + final meta = metas.firstWhereOrNull((m) => m.id == id); + + if (meta != null) { + final hex = + '#${style.color.value.toRadixString(16).padLeft(8, '0').substring(2).toUpperCase()}'; + + await AppDatabase.instance.updateImportedLayer(meta.copyWith( + colorHex: hex, + opacity: style.opacity, + strokeWidth: style.strokeWidth)); + } + } + + ImportedLayerStyle? getLayerStyle(String id) => layerStyles[id]; } diff --git a/lib/widgets/map/all_layer_overlay.dart b/lib/widgets/map/all_layer_overlay.dart index a5bc164..4d9b78e 100644 --- a/lib/widgets/map/all_layer_overlay.dart +++ b/lib/widgets/map/all_layer_overlay.dart @@ -117,39 +117,42 @@ class _GeometryLayerTile extends StatelessWidget { Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; - return ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 0), - leading: Container( - width: 36, - height: 36, - decoration: BoxDecoration( - color: (visible ? color : Colors.grey).withOpacity(0.12), - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: (visible ? color : Colors.grey).withOpacity(0.3), + return Material( + color: Colors.transparent, + child: ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 0), + leading: Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: (visible ? color : Colors.grey).withOpacity(0.12), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: (visible ? color : Colors.grey).withOpacity(0.3), + ), ), + child: Icon(icon, size: 18, color: visible ? color : Colors.grey), ), - child: Icon(icon, size: 18, color: visible ? color : Colors.grey), + title: Text(label, + style: TextStyle( + fontWeight: FontWeight.w500, + color: visible ? null : cs.onSurfaceVariant, + )), + subtitle: count > 0 + ? Text('$count objektum', style: const TextStyle(fontSize: 11)) + : Text('Nincs rögzített geometria', + style: TextStyle(fontSize: 11, color: cs.onSurfaceVariant)), + trailing: Switch( + value: visible, + onChanged: (_) => onToggle(), + activeColor: color, + trackColor: WidgetStateProperty.resolveWith((states) => + states.contains(WidgetState.selected) + ? color.withOpacity(0.3) + : null), + ), + onTap: onToggle, ), - title: Text(label, - style: TextStyle( - fontWeight: FontWeight.w500, - color: visible ? null : cs.onSurfaceVariant, - )), - subtitle: count > 0 - ? Text('$count objektum', style: const TextStyle(fontSize: 11)) - : Text('Nincs rögzített geometria', - style: TextStyle(fontSize: 11, color: cs.onSurfaceVariant)), - trailing: Switch( - value: visible, - onChanged: (_) => onToggle(), - activeColor: color, - trackColor: WidgetStateProperty.resolveWith((states) => - states.contains(WidgetState.selected) - ? color.withOpacity(0.3) - : null), - ), - onTap: onToggle, ); } } diff --git a/lib/widgets/map/imported_layer_overlay.dart b/lib/widgets/map/imported_layer_overlay.dart index 7fe1740..0423bad 100644 --- a/lib/widgets/map/imported_layer_overlay.dart +++ b/lib/widgets/map/imported_layer_overlay.dart @@ -10,6 +10,8 @@ import 'package:terepi_seged/enums/layer_import_source_type.dart'; import '../../models/imported_layer.dart'; import '../../services/layer_import_service.dart'; +import '../map_edit_tools/imported_layer_style_sheet.dart'; +import '../map_edit_tools/style_editable.dart'; // ════════════════════════════════════════════════════════════════════ // Térkép réteg — a flutter_map layers listájába kerül @@ -26,13 +28,39 @@ class ImportedLayerOverlay extends StatelessWidget { return Obx(() { final visible = LayerImportService.to.visibleLayers; + final styles = LayerImportService.to.layerStyles; if (visible.isEmpty) return const SizedBox.shrink(); - // Az összes látható réteg objektumait összegyűjtjük - final polylines = visible.expand((l) => l.polylines).toList(); - final polygons = visible.expand((l) => l.polygons).toList(); - final markers = visible.expand((l) => l.markers).toList(); + final polylines = []; + final polygons = []; + final markers = []; + for (final layer in visible) { + final style = styles[layer.id]; // ← override ha van + + if (style != null) { + // Stílussal felülírva + polylines.addAll(layer.polylines.map((p) => Polyline( + points: p.points, + color: style.color.withOpacity(0.85), + strokeWidth: style.strokeWidth, + ))); + polygons.addAll(layer.polygons.map((p) => Polygon( + points: p.points, + holePointsList: p.holePointsList, + color: style.color.withOpacity(style.opacity), + borderColor: style.color, + borderStrokeWidth: style.strokeWidth, + label: p.label, + ))); + markers.addAll(layer.markers); // marker szín nem változik egyszerűen + } else { + // Eredeti stílus + polylines.addAll(layer.polylines); + polygons.addAll(layer.polygons); + markers.addAll(layer.markers); + } + } return Stack(children: [ if (polygons.isNotEmpty) PolygonLayer(polygons: polygons), if (polylines.isNotEmpty) PolylineLayer(polylines: polylines), @@ -175,6 +203,10 @@ class _LayerTile extends StatelessWidget { constraints: const BoxConstraints(minWidth: 28, minHeight: 28), color: Colors.grey.shade500, ), + IconButton( + icon: const Icon(Icons.palette_outlined, size: 16), + onPressed: () => _openStyleSheet(context, layer), + tooltip: 'Stílus szerkesztése'), // Törlés IconButton( icon: const Icon(Icons.close, size: 16), @@ -188,6 +220,36 @@ class _LayerTile extends StatelessWidget { ); } + void _openStyleSheet(BuildContext context, ImportedLayer layer) { + final svc = LayerImportService.to; + final current = svc.getLayerStyle(layer.id); + final session = LayerStyleSession( + color: current?.color ?? const Color(0xFF1565C0), + opacity: current?.opacity ?? 0.35, + strokeWidth: current?.strokeWidth ?? 2.5, + ); + + Get.bottomSheet( + ImportedLayerStyleSheet( + layerName: layer.name, + session: session, + onSave: () => svc.setLayerStyle( + layer.id, + ImportedLayerStyle( + color: session.activeEditColor.value, + opacity: session.activeEditOpacity.value, + strokeWidth: session.activeEditStrokeWidth.value, + )), + onReset: () { + svc.layerStyles.remove(layer.id); + svc.layers.refresh(); + }, + ), + isScrollControlled: true, + backgroundColor: Colors.transparent, + ); + } + String _stats() { final parts = []; if (layer.markers.isNotEmpty) parts.add('${layer.markers.length} pont'); diff --git a/lib/widgets/map_edit_tools/color_row.dart b/lib/widgets/map_edit_tools/color_row.dart index 909049b..e81f971 100644 --- a/lib/widgets/map_edit_tools/color_row.dart +++ b/lib/widgets/map_edit_tools/color_row.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:terepi_seged/core/style_editable.dart'; import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart'; class ColorRow extends StatelessWidget { - final MapSurveyController ctrl; + final StyleEditable ctrl; final double circleSize; static const _palette = [ diff --git a/lib/widgets/map_edit_tools/imported_layer_style_sheet.dart b/lib/widgets/map_edit_tools/imported_layer_style_sheet.dart new file mode 100644 index 0000000..dea0a1a --- /dev/null +++ b/lib/widgets/map_edit_tools/imported_layer_style_sheet.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:terepi_seged/widgets/map_edit_tools/style_editable.dart'; + +import 'color_row.dart'; +import 'opacity_slider.dart'; +import 'stroke_slider.dart'; + +class ImportedLayerStyleSheet extends StatelessWidget { + final String layerName; + final LayerStyleSession session; // ← a közös interfész implementációja + final VoidCallback onSave; + final VoidCallback onReset; + + const ImportedLayerStyleSheet({ + required this.layerName, + required this.session, + required this.onSave, + required this.onReset, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.fromLTRB( + 20, 16, 20, MediaQuery.of(context).padding.bottom + 16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Handle + Center( + child: Container( + width: 40, + height: 4, + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(2)), + )), + + Text(layerName, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + overflow: TextOverflow.ellipsis), + const SizedBox(height: 20), + + // ── Meglévő widgetek — ismerős felület ────────────────── + ColorRow(ctrl: session), // ← ugyanaz mint a geometriáknál + const SizedBox(height: 18), + OpacitySlider(ctrl: session), + const SizedBox(height: 10), + StrokeSlider(ctrl: session), + const SizedBox(height: 24), + + // Gombok + Row(children: [ + OutlinedButton( + onPressed: () { + onReset(); + Get.back(); + }, + child: const Text('Visszaállítás'), + ), + const Spacer(), + FilledButton( + onPressed: () { + onSave(); + Get.back(); + }, + child: const Text('Alkalmaz'), + ), + ]), + ], + ), + ); + } +} diff --git a/lib/widgets/map_edit_tools/opacity_slider.dart b/lib/widgets/map_edit_tools/opacity_slider.dart index fe7e4b3..cbdee49 100644 --- a/lib/widgets/map_edit_tools/opacity_slider.dart +++ b/lib/widgets/map_edit_tools/opacity_slider.dart @@ -1,11 +1,12 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:terepi_seged/core/style_editable.dart'; import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart'; import 'labeled_slider.dart'; class OpacitySlider extends StatelessWidget { - final MapSurveyController ctrl; + final StyleEditable ctrl; const OpacitySlider({required this.ctrl}); @override diff --git a/lib/widgets/map_edit_tools/stroke_slider.dart b/lib/widgets/map_edit_tools/stroke_slider.dart index 6ed3c39..3b6e21a 100644 --- a/lib/widgets/map_edit_tools/stroke_slider.dart +++ b/lib/widgets/map_edit_tools/stroke_slider.dart @@ -1,11 +1,12 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:terepi_seged/core/style_editable.dart'; import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart'; import 'labeled_slider.dart'; class StrokeSlider extends StatelessWidget { - final MapSurveyController ctrl; + final StyleEditable ctrl; const StrokeSlider({required this.ctrl}); @override diff --git a/lib/widgets/map_edit_tools/style_editable.dart b/lib/widgets/map_edit_tools/style_editable.dart new file mode 100644 index 0000000..992bd2a --- /dev/null +++ b/lib/widgets/map_edit_tools/style_editable.dart @@ -0,0 +1,23 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:get/get_rx/src/rx_types/rx_types.dart'; +import 'package:terepi_seged/core/style_editable.dart'; + +class LayerStyleSession implements StyleEditable { + @override + Rx activeEditColor = Color(0xFF1565C0).obs; + @override + RxDouble activeEditOpacity = 0.35.obs; + @override + RxDouble activeEditStrokeWidth = 2.5.obs; + + LayerStyleSession( + {required Color color, + required double opacity, + required double strokeWidth}) { + activeEditColor.value = color; + activeEditOpacity.value = opacity; + activeEditStrokeWidth.value = strokeWidth; + } +}