Importált rétegek stílusának szerkesztése

This commit is contained in:
2026-07-04 23:36:01 +02:00
parent 077b40967c
commit 2cf83149f0
12 changed files with 341 additions and 64 deletions
+10
View File
@@ -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<Color> get activeEditColor;
RxDouble get activeEditOpacity;
RxDouble get activeEditStrokeWidth;
}
+53 -24
View File
@@ -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<String, dynamic> 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);
}
}
@@ -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) ───────────────────────────────────────
+17 -2
View File
@@ -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<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {}
Future<void> _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 ─────────────────────────────────────────────────
+50
View File
@@ -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 = <String, ImportedLayerStyle>{}.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];
}
+33 -30
View File
@@ -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,
);
}
}
+66 -4
View File
@@ -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 = <Polyline>[];
final polygons = <Polygon>[];
final markers = <Marker>[];
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 = <String>[];
if (layer.markers.isNotEmpty) parts.add('${layer.markers.length} pont');
+2 -1
View File
@@ -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 = [
@@ -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'),
),
]),
],
),
);
}
}
@@ -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
@@ -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
@@ -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<Color> 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;
}
}