Kml, kmz és geojson import és térképi megjelnítésük.
This commit is contained in:
parent
0828630a5b
commit
7c29bc7ae5
272
lib/core/kml_parser.dart
Normal file
272
lib/core/kml_parser.dart
Normal file
@ -0,0 +1,272 @@
|
||||
// KML és KMZ → flutter_map objektumok
|
||||
//
|
||||
// FONTOS: KML koordináta sorrend: lon,lat,alt
|
||||
// Flutter LatLng(lat, lon) — tehát fordítva kell olvasni!
|
||||
//
|
||||
// KMZ = ZIP archív, benne doc.kml
|
||||
|
||||
import 'dart:typed_data';
|
||||
import 'package:archive/archive.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:terepi_seged/enums/layer_import_source_type.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:xml/xml.dart';
|
||||
|
||||
import '../models/imported_layer.dart';
|
||||
|
||||
class KmlParser {
|
||||
// Alapértelmezett stílusok — felülírhatók konstruktorban
|
||||
final Color defaultPolylineColor;
|
||||
final double defaultPolylineStroke;
|
||||
final Color defaultPolygonFillColor;
|
||||
final Color defaultPolygonBorderColor;
|
||||
final double defaultPolygonBorderStroke;
|
||||
final Color defaultMarkerColor;
|
||||
|
||||
const KmlParser({
|
||||
this.defaultPolylineColor = const Color(0xCC1565C0), // kék
|
||||
this.defaultPolylineStroke = 3.0,
|
||||
this.defaultPolygonFillColor = const Color(0x4D1565C0),
|
||||
this.defaultPolygonBorderColor = const Color(0xCC1565C0),
|
||||
this.defaultPolygonBorderStroke = 1.5,
|
||||
this.defaultMarkerColor = Colors.red,
|
||||
});
|
||||
|
||||
// ── KMZ (ZIP → KML) ──────────────────────────────────────────────
|
||||
|
||||
ImportedLayer parseKmz(Uint8List bytes, String fileName,
|
||||
{String? id, bool isVisible = true}) {
|
||||
final archive = ZipDecoder().decodeBytes(bytes);
|
||||
|
||||
ArchiveFile? kmlFile = archive.findFile('doc.kml');
|
||||
kmlFile ??= archive.files.firstWhere(
|
||||
(f) => f.name.toLowerCase().endsWith('.kml'),
|
||||
orElse: () => throw Exception('Nem található .kml fájl a KMZ-ben'),
|
||||
);
|
||||
|
||||
final kmlString = String.fromCharCodes(kmlFile.content as List<int>);
|
||||
return _parse(kmlString, fileName, LayerImportSourceType.kmz,
|
||||
id: id, isVisible: isVisible);
|
||||
}
|
||||
|
||||
// ── KML (szöveg) ──────────────────────────────────────────────────
|
||||
|
||||
ImportedLayer parseKml(String kmlString, String fileName,
|
||||
{String? id, bool isVisible = true}) =>
|
||||
_parse(kmlString, fileName, LayerImportSourceType.kml,
|
||||
id: id, isVisible: isVisible);
|
||||
|
||||
// ── Belső parse ───────────────────────────────────────────────────
|
||||
|
||||
ImportedLayer _parse(
|
||||
String kmlString, String fileName, LayerImportSourceType source,
|
||||
{String? id, bool isVisible = true}) {
|
||||
final doc = XmlDocument.parse(kmlString);
|
||||
final markers = <Marker>[];
|
||||
final polylines = <Polyline>[];
|
||||
final polygons = <Polygon>[];
|
||||
|
||||
for (final pm in doc.findAllElements('Placemark')) {
|
||||
final style = _style(pm);
|
||||
final label = pm.findElements('name').firstOrNull?.innerText.trim() ?? '';
|
||||
|
||||
// Point
|
||||
final point = pm.findElements('Point').firstOrNull;
|
||||
if (point != null) {
|
||||
final pt = _singleCoord(point);
|
||||
if (pt != null) {
|
||||
markers.add(Marker(
|
||||
point: pt,
|
||||
width: label.isNotEmpty ? 120 : 24,
|
||||
height: label.isNotEmpty ? 40 : 24,
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: _markerWidget(label, style.lineColor),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// LineString
|
||||
final line = pm.findElements('LineString').firstOrNull;
|
||||
if (line != null) {
|
||||
final pts = _coordList(line);
|
||||
if (pts.isNotEmpty) {
|
||||
polylines.add(Polyline(
|
||||
points: pts,
|
||||
color: style.lineColor,
|
||||
strokeWidth: style.lineWidth,
|
||||
borderColor: style.lineColor.withOpacity(0.3),
|
||||
borderStrokeWidth: 1.0,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Polygon
|
||||
final poly = pm.findElements('Polygon').firstOrNull;
|
||||
if (poly != null) {
|
||||
final rings = _polygonRings(poly);
|
||||
if (rings.isNotEmpty) {
|
||||
polygons.add(Polygon(
|
||||
points: rings[0],
|
||||
holePointsList: rings.length > 1 ? rings.sublist(1) : null,
|
||||
color: style.fillColor,
|
||||
borderColor: style.lineColor,
|
||||
borderStrokeWidth: style.lineWidth,
|
||||
label: label.isNotEmpty ? label : null,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// MultiGeometry — rekurzív feldolgozás az első egyszerű elemre
|
||||
final multi = pm.findElements('MultiGeometry').firstOrNull;
|
||||
if (multi != null) {
|
||||
for (final ls in multi.findElements('LineString')) {
|
||||
final pts = _coordList(ls);
|
||||
if (pts.isNotEmpty) {
|
||||
polylines.add(Polyline(
|
||||
points: pts,
|
||||
color: style.lineColor,
|
||||
strokeWidth: style.lineWidth,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ImportedLayer(
|
||||
id: id ?? const Uuid().v4(),
|
||||
isVisible: isVisible,
|
||||
name: fileName,
|
||||
sourceType: source,
|
||||
markers: markers,
|
||||
polylines: polylines,
|
||||
polygons: polygons,
|
||||
importedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
// ── Koordináta parserek ───────────────────────────────────────────
|
||||
|
||||
LatLng? _singleCoord(XmlElement geom) {
|
||||
final raw = geom.findElements('coordinates').firstOrNull?.innerText.trim();
|
||||
if (raw == null) return null;
|
||||
return _oneCoord(raw.split(RegExp(r'\s+')).first);
|
||||
}
|
||||
|
||||
List<LatLng> _coordList(XmlElement geom) {
|
||||
final raw = geom.findElements('coordinates').firstOrNull?.innerText.trim();
|
||||
if (raw == null) return [];
|
||||
return raw
|
||||
.split(RegExp(r'\s+'))
|
||||
.map(_oneCoord)
|
||||
.whereType<LatLng>()
|
||||
.toList();
|
||||
}
|
||||
|
||||
List<List<LatLng>> _polygonRings(XmlElement polygon) {
|
||||
final rings = <List<LatLng>>[];
|
||||
final outer = polygon
|
||||
.findElements('outerBoundaryIs')
|
||||
.firstOrNull
|
||||
?.findElements('LinearRing')
|
||||
.firstOrNull;
|
||||
if (outer != null) rings.add(_coordList(outer));
|
||||
for (final inner in polygon.findElements('innerBoundaryIs')) {
|
||||
final ring = inner.findElements('LinearRing').firstOrNull;
|
||||
if (ring != null) rings.add(_coordList(ring));
|
||||
}
|
||||
return rings.where((r) => r.isNotEmpty).toList();
|
||||
}
|
||||
|
||||
/// KML: lon,lat,alt → LatLng(lat, lon)
|
||||
LatLng? _oneCoord(String s) {
|
||||
final p = s.split(',');
|
||||
if (p.length < 2) return null;
|
||||
try {
|
||||
return LatLng(
|
||||
double.parse(p[1].trim()), // lat
|
||||
double.parse(p[0].trim()), // lon
|
||||
);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Stílus kiolvasás ──────────────────────────────────────────────
|
||||
|
||||
_Style _style(XmlElement pm) {
|
||||
final style = pm.findElements('Style').firstOrNull;
|
||||
if (style == null) return _Style.defaults(this);
|
||||
|
||||
Color lineColor = defaultPolylineColor;
|
||||
double lineWidth = defaultPolylineStroke;
|
||||
Color fillColor = defaultPolygonFillColor;
|
||||
|
||||
final ls = style.findElements('LineStyle').firstOrNull;
|
||||
if (ls != null) {
|
||||
final c = ls.findElements('color').firstOrNull?.innerText;
|
||||
if (c != null) lineColor = _kmlColor(c);
|
||||
final w = ls.findElements('width').firstOrNull?.innerText;
|
||||
if (w != null) lineWidth = double.tryParse(w) ?? lineWidth;
|
||||
}
|
||||
|
||||
final ps = style.findElements('PolyStyle').firstOrNull;
|
||||
if (ps != null) {
|
||||
final c = ps.findElements('color').firstOrNull?.innerText;
|
||||
if (c != null) fillColor = _kmlColor(c);
|
||||
}
|
||||
|
||||
return _Style(
|
||||
lineColor: lineColor, lineWidth: lineWidth, fillColor: fillColor);
|
||||
}
|
||||
|
||||
/// KML szín formátum: aabbggrr → Flutter Color(argb)
|
||||
Color _kmlColor(String s) {
|
||||
if (s.length != 8) return defaultPolylineColor;
|
||||
try {
|
||||
final a = int.parse(s.substring(0, 2), radix: 16);
|
||||
final b = int.parse(s.substring(2, 4), radix: 16);
|
||||
final g = int.parse(s.substring(4, 6), radix: 16);
|
||||
final r = int.parse(s.substring(6, 8), radix: 16);
|
||||
return Color.fromARGB(a, r, g, b);
|
||||
} catch (_) {
|
||||
return defaultPolylineColor;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Marker widget ─────────────────────────────────────────────────
|
||||
|
||||
Widget _markerWidget(String label, Color color) {
|
||||
if (label.isEmpty) {
|
||||
return Icon(Icons.location_pin, color: color, size: 24);
|
||||
}
|
||||
return Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2),
|
||||
decoration:
|
||||
BoxDecoration(color: color, borderRadius: BorderRadius.circular(4)),
|
||||
child: Text(label,
|
||||
style: const TextStyle(
|
||||
color: Colors.white, fontSize: 10, fontWeight: FontWeight.w600),
|
||||
overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
Icon(Icons.location_pin, color: color, size: 16),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
class _Style {
|
||||
final Color lineColor;
|
||||
final double lineWidth;
|
||||
final Color fillColor;
|
||||
const _Style(
|
||||
{required this.lineColor,
|
||||
required this.lineWidth,
|
||||
required this.fillColor});
|
||||
factory _Style.defaults(KmlParser p) => _Style(
|
||||
lineColor: p.defaultPolylineColor,
|
||||
lineWidth: p.defaultPolylineStroke,
|
||||
fillColor: p.defaultPolygonFillColor,
|
||||
);
|
||||
}
|
||||
1
lib/enums/layer_import_source_type.dart
Normal file
1
lib/enums/layer_import_source_type.dart
Normal file
@ -0,0 +1 @@
|
||||
enum LayerImportSourceType { geoJson, kml, kmz }
|
||||
@ -9,6 +9,7 @@ import 'package:terepi_seged/services/app_database.dart';
|
||||
import 'package:terepi_seged/services/coord_converter_service.dart';
|
||||
import 'package:terepi_seged/services/gnss/gnss_device_service.dart';
|
||||
import 'package:terepi_seged/services/gnss/gnss_service.dart';
|
||||
import 'package:terepi_seged/services/layer_import_service.dart';
|
||||
import 'package:terepi_seged/services/note_audio_service.dart';
|
||||
import 'package:terepi_seged/services/note_photo_service.dart';
|
||||
import 'package:terepi_seged/services/ntrip_service.dart';
|
||||
@ -34,6 +35,7 @@ Future<void> main() async {
|
||||
Get.put(TrackingController(), permanent: true);
|
||||
Get.put(NotePhotoService(), permanent: true);
|
||||
Get.put(NoteAudioService(), permanent: true);
|
||||
Get.put(LayerImportService(), permanent: true);
|
||||
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
96
lib/models/imported_layer.dart
Normal file
96
lib/models/imported_layer.dart
Normal file
@ -0,0 +1,96 @@
|
||||
// Importált réteg modellje.
|
||||
// A GeoJsonParser közvetlenül flutter_map objektumokat gyárt,
|
||||
// ezeket csomagoljuk egy kezelhető rétegbe.
|
||||
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:terepi_seged/enums/layer_import_source_type.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class ImportedLayer {
|
||||
final String id;
|
||||
final String name;
|
||||
final LayerImportSourceType sourceType;
|
||||
|
||||
// flutter_map objektumok — közvetlenül a réteg widgetekbe kerülnek
|
||||
final List<Marker> markers;
|
||||
final List<Polyline> polylines;
|
||||
final List<Polygon> polygons;
|
||||
|
||||
final bool isVisible;
|
||||
final DateTime importedAt;
|
||||
|
||||
const ImportedLayer({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.sourceType,
|
||||
required this.markers,
|
||||
required this.polylines,
|
||||
required this.polygons,
|
||||
this.isVisible = true,
|
||||
required this.importedAt,
|
||||
});
|
||||
|
||||
// ── Statisztikák ──────────────────────────────────────────────────
|
||||
|
||||
int get featureCount => markers.length + polylines.length + polygons.length;
|
||||
|
||||
bool get isEmpty => featureCount == 0;
|
||||
|
||||
// ── Bounding box — zoom a rétegre ─────────────────────────────────
|
||||
|
||||
LatLngBounds? get bounds {
|
||||
final points = <LatLng>[
|
||||
...markers.map((m) => m.point),
|
||||
...polylines.expand((p) => p.points),
|
||||
...polygons.expand((p) => p.points),
|
||||
];
|
||||
if (points.isEmpty) return null;
|
||||
|
||||
var minLat = points.first.latitude;
|
||||
var maxLat = points.first.latitude;
|
||||
var minLon = points.first.longitude;
|
||||
var maxLon = points.first.longitude;
|
||||
|
||||
for (final p in points) {
|
||||
if (p.latitude < minLat) minLat = p.latitude;
|
||||
if (p.latitude > maxLat) maxLat = p.latitude;
|
||||
if (p.longitude < minLon) minLon = p.longitude;
|
||||
if (p.longitude > maxLon) maxLon = p.longitude;
|
||||
}
|
||||
|
||||
return LatLngBounds(
|
||||
LatLng(minLat, minLon),
|
||||
LatLng(maxLat, maxLon),
|
||||
);
|
||||
}
|
||||
|
||||
ImportedLayer copyWith({bool? isVisible}) => ImportedLayer(
|
||||
id: id,
|
||||
name: name,
|
||||
sourceType: sourceType,
|
||||
markers: markers,
|
||||
polylines: polylines,
|
||||
polygons: polygons,
|
||||
isVisible: isVisible ?? this.isVisible,
|
||||
importedAt: importedAt,
|
||||
);
|
||||
|
||||
// ── Gyártó: GeoJsonParser kimenetéből ─────────────────────────────
|
||||
|
||||
static ImportedLayer fromGeoJsonParser({
|
||||
required dynamic parser, // GeoJsonParser
|
||||
required String name,
|
||||
LayerImportSourceType source = LayerImportSourceType.geoJson,
|
||||
}) {
|
||||
return ImportedLayer(
|
||||
id: const Uuid().v4(),
|
||||
name: name,
|
||||
sourceType: source,
|
||||
markers: List.from(parser.markers as List),
|
||||
polylines: List.from(parser.polylines as List),
|
||||
polygons: List.from(parser.polygons as List),
|
||||
importedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
}
|
||||
72
lib/models/imported_layer_meta.dart
Normal file
72
lib/models/imported_layer_meta.dart
Normal file
@ -0,0 +1,72 @@
|
||||
import 'package:terepi_seged/enums/layer_import_source_type.dart';
|
||||
|
||||
class ImportedLayerMeta {
|
||||
final String id;
|
||||
final String name;
|
||||
final LayerImportSourceType sourceType;
|
||||
final String localPath; // fájl elérési út
|
||||
final String? storagePath; // Supabase Storage (megosztáskor)
|
||||
final bool isVisible;
|
||||
final int? projectId;
|
||||
final DateTime importedAt;
|
||||
final DateTime? 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,
|
||||
});
|
||||
|
||||
bool get isSynced => storagePath != null;
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'source_type': sourceType.name,
|
||||
'local_path': localPath,
|
||||
'storage_path': storagePath,
|
||||
'is_visible': isVisible ? 1 : 0,
|
||||
'project_id': projectId,
|
||||
'imported_at': importedAt.toIso8601String(),
|
||||
'synced_at': syncedAt?.toIso8601String(),
|
||||
};
|
||||
|
||||
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,
|
||||
);
|
||||
|
||||
ImportedLayerMeta copyWith({
|
||||
bool? isVisible,
|
||||
String? storagePath,
|
||||
DateTime? syncedAt,
|
||||
}) =>
|
||||
ImportedLayerMeta(
|
||||
id: id,
|
||||
name: name,
|
||||
sourceType: sourceType,
|
||||
localPath: localPath,
|
||||
storagePath: storagePath ?? this.storagePath,
|
||||
isVisible: isVisible ?? this.isVisible,
|
||||
projectId: projectId,
|
||||
importedAt: importedAt,
|
||||
syncedAt: syncedAt ?? this.syncedAt,
|
||||
);
|
||||
}
|
||||
@ -43,6 +43,7 @@ import 'package:terepi_seged/services/gnss/gnss_device_service.dart';
|
||||
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/map_feature_save_sheet.dart';
|
||||
import 'package:terepi_seged/widgets/shared_map_widgets.dart';
|
||||
|
||||
@ -1623,4 +1624,82 @@ class MapSurveyController extends GetxController {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void openLayerPanel() {
|
||||
Get.bottomSheet(
|
||||
DraggableScrollableSheet(
|
||||
initialChildSize: 0.45,
|
||||
minChildSize: 0.3,
|
||||
maxChildSize: 0.85,
|
||||
snap: true,
|
||||
snapSizes: const [0.3, 0.45, 0.85],
|
||||
expand: false,
|
||||
builder: (_, scrollCtrl) => Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Get.theme.colorScheme.surface,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.15),
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, -4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: CustomScrollView(
|
||||
controller: scrollCtrl,
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Handle
|
||||
Center(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 10),
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Importált rétegek',
|
||||
style: TextStyle(
|
||||
fontSize: 18, fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 16),
|
||||
ImportLayerPanel(
|
||||
onFitBounds: fitImportedLayer,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
ignoreSafeArea: false,
|
||||
);
|
||||
}
|
||||
|
||||
void fitImportedLayer(LatLngBounds bounds) {
|
||||
_isMapProgrammaticMove = true;
|
||||
mapController.fitCamera(
|
||||
CameraFit.bounds(
|
||||
bounds: bounds,
|
||||
padding: const EdgeInsets.all(48),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ import 'package:terepi_seged/services/gnss/gnss_device_service.dart';
|
||||
import 'package:terepi_seged/services/gnss/gnss_service.dart';
|
||||
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_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';
|
||||
@ -74,16 +75,7 @@ class MapSurveyView extends GetView<MapSurveyController> {
|
||||
controller.clearNoteItemSelection();
|
||||
},
|
||||
layers: [
|
||||
Obx(() {
|
||||
final isGpsActive = GnssService.to.activeConnectionType.value !=
|
||||
GnssConnectionType.none;
|
||||
if (isGpsActive) {
|
||||
return MarkerLayer(
|
||||
markers: controller.currentLocationMarker.toList());
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
}),
|
||||
|
||||
const ImportedLayerOverlay(),
|
||||
// Track polyline
|
||||
Obx(() {
|
||||
final isTracking = TrackingController.to.isRecording.value;
|
||||
@ -166,6 +158,15 @@ class MapSurveyView extends GetView<MapSurveyController> {
|
||||
controller: controller.polygonEditorController,
|
||||
throttleDuration: Duration.zero);
|
||||
}),
|
||||
Obx(() {
|
||||
final isGpsActive = GnssService.to.activeConnectionType.value !=
|
||||
GnssConnectionType.none;
|
||||
if (isGpsActive) {
|
||||
return MarkerLayer(
|
||||
markers: controller.currentLocationMarker.toList());
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
}),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
|
||||
@ -4,6 +4,7 @@ import 'package:path_provider/path_provider.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:terepi_seged/enums/note_type.dart';
|
||||
import 'package:terepi_seged/models/imported_layer_meta.dart';
|
||||
import 'package:terepi_seged/models/note_item.dart';
|
||||
import 'package:terepi_seged/models/note_item_audio.dart';
|
||||
import 'package:terepi_seged/models/note_item_photo.dart';
|
||||
@ -190,6 +191,22 @@ class AppDatabase {
|
||||
'ON pending_points(sync_status)',
|
||||
);
|
||||
|
||||
await db.execute('''
|
||||
CREATE TABLE IF NOT EXISTS imported_layers (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
source_type TEXT NOT NULL,
|
||||
local_path TEXT NOT NULL,
|
||||
storage_path TEXT,
|
||||
is_visible INTEGER NOT NULL DEFAULT 1,
|
||||
project_id INTEGER,
|
||||
imported_at TEXT NOT NULL,
|
||||
synced_at TEXT
|
||||
)
|
||||
''');
|
||||
await db.execute(
|
||||
'CREATE INDEX idx_imp_layers_project ON imported_layers(project_id)');
|
||||
|
||||
// Alap projekt létrehozása az első indításhoz
|
||||
final now = DateTime.now().toIso8601String();
|
||||
await db.insert('projects', {
|
||||
@ -559,4 +576,33 @@ class AppDatabase {
|
||||
);
|
||||
return rows.map(NoteItemAudio.fromMap).toList();
|
||||
}
|
||||
|
||||
// ----------- Layer meta adatok
|
||||
Future<void> insertImportedLayer(ImportedLayerMeta meta) async {
|
||||
final db = await database;
|
||||
await db.insert('imported_layers', meta.toMap(),
|
||||
conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
}
|
||||
|
||||
Future<void> updateImportedLayer(ImportedLayerMeta meta) async {
|
||||
final db = await database;
|
||||
await db.update('imported_layers', meta.toMap(),
|
||||
where: 'id = ?', whereArgs: [meta.id]);
|
||||
}
|
||||
|
||||
Future<void> deleteImportedLayer(String id) async {
|
||||
final db = await database;
|
||||
await db.delete('imported_layers', where: 'id = ?', whereArgs: [id]);
|
||||
}
|
||||
|
||||
Future<List<ImportedLayerMeta>> listImportedLayers({int? projectId}) async {
|
||||
final db = await database;
|
||||
final rows = await db.query(
|
||||
'imported_layers',
|
||||
where: projectId != null ? 'project_id = ?' : null,
|
||||
whereArgs: projectId != null ? [projectId] : null,
|
||||
orderBy: 'imported_at DESC',
|
||||
);
|
||||
return rows.map(ImportedLayerMeta.fromMap).toList();
|
||||
}
|
||||
}
|
||||
|
||||
274
lib/services/layer_import_service.dart
Normal file
274
lib/services/layer_import_service.dart
Normal file
@ -0,0 +1,274 @@
|
||||
// Geo fájl import service — perzisztens verzió
|
||||
//
|
||||
// TÁRHELY STRATÉGIA:
|
||||
// Fájlrendszer → /files/layers/{id}.geojson|kml|kmz (nyers forrás)
|
||||
// SQLite → imported_layers tábla (metadata)
|
||||
// Supabase → Storage (megosztás, később)
|
||||
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:terepi_seged/enums/layer_import_source_type.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../models/imported_layer.dart';
|
||||
import '../models/imported_layer_meta.dart';
|
||||
import '../services/app_database.dart';
|
||||
import '../controls/geojson_parser.dart';
|
||||
import '../core/kml_parser.dart';
|
||||
import '../services/project_service.dart';
|
||||
|
||||
class LayerImportService extends GetxService {
|
||||
static LayerImportService get to => Get.find();
|
||||
|
||||
final layers = <ImportedLayer>[].obs;
|
||||
final isLoading = false.obs;
|
||||
final isSyncing = false.obs;
|
||||
final lastError = Rxn<String>();
|
||||
|
||||
final _kmlParser = const KmlParser(
|
||||
defaultPolylineColor: Color(0xCC1565C0),
|
||||
defaultPolylineStroke: 3.0,
|
||||
defaultPolygonFillColor: Color(0x4D1565C0),
|
||||
defaultPolygonBorderColor: Color(0xCC1565C0),
|
||||
defaultPolygonBorderStroke: 1.5,
|
||||
);
|
||||
|
||||
String? _layerDir;
|
||||
static const _uuid = Uuid();
|
||||
|
||||
// ── Inicializálás ──────────────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
Future<void> onReady() async {
|
||||
super.onReady();
|
||||
await _initLayerDir();
|
||||
await _loadPersistedLayers();
|
||||
}
|
||||
|
||||
Future<void> _initLayerDir() async {
|
||||
final ext = await getExternalStorageDirectory();
|
||||
final dir = Directory(p.join(ext!.path, 'layers'));
|
||||
if (!await dir.exists()) await dir.create(recursive: true);
|
||||
_layerDir = dir.path;
|
||||
}
|
||||
|
||||
// ── Fájl import ───────────────────────────────────────────────────────────
|
||||
|
||||
Future<ImportedLayer?> importFile() async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
lastError.value = null;
|
||||
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
// type: FileType.custom,
|
||||
// allowedExtensions: ['geojson', 'json', 'kml', 'kmz'],
|
||||
type: FileType.any,
|
||||
withData: true,
|
||||
);
|
||||
if (result == null || result.files.isEmpty) return null;
|
||||
|
||||
final file = result.files.first;
|
||||
final fileName = file.name;
|
||||
final ext = fileName.split('.').last.toLowerCase();
|
||||
final bytes = file.bytes!;
|
||||
final id = _uuid.v4();
|
||||
|
||||
if (!['geojson', 'json', 'kml', 'kmz'].contains(ext)) {
|
||||
throw Exception('Nem támogatott formátum: .$ext');
|
||||
}
|
||||
|
||||
// 1. Fájl mentése tartós tárhelyre
|
||||
final localPath = await _saveFile(id, ext, bytes);
|
||||
|
||||
// 2. Parse
|
||||
final layer = _parse(bytes, fileName, id, ext);
|
||||
if (layer.isEmpty) {
|
||||
await File(localPath).delete();
|
||||
throw Exception('Nem tartalmaz feldolgozható geometriát.');
|
||||
}
|
||||
|
||||
// 3. SQLite metadata
|
||||
final meta = ImportedLayerMeta(
|
||||
id: id,
|
||||
name: fileName,
|
||||
sourceType: _sourceType(ext),
|
||||
localPath: localPath,
|
||||
isVisible: true,
|
||||
projectId: ProjectService.to.activeProjectId,
|
||||
importedAt: DateTime.now(),
|
||||
);
|
||||
await AppDatabase.instance.insertImportedLayer(meta);
|
||||
|
||||
layers.add(layer);
|
||||
return layer;
|
||||
} catch (e) {
|
||||
lastError.value = e.toString();
|
||||
Get.snackbar('Import hiba', e.toString(),
|
||||
snackPosition: SnackPosition.BOTTOM);
|
||||
return null;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Perzisztens rétegek betöltése induláskor ──────────────────────────────
|
||||
|
||||
Future<void> _loadPersistedLayers() async {
|
||||
try {
|
||||
final projectId = ProjectService.to.activeProjectId;
|
||||
final metas =
|
||||
await AppDatabase.instance.listImportedLayers(projectId: projectId);
|
||||
|
||||
final loaded = <ImportedLayer>[];
|
||||
for (final meta in metas) {
|
||||
final file = File(meta.localPath);
|
||||
if (!await file.exists()) {
|
||||
// Fájl törlődött — SQLite rekord is törlendő
|
||||
await AppDatabase.instance.deleteImportedLayer(meta.id);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
final bytes = await file.readAsBytes();
|
||||
final ext = meta.localPath.split('.').last;
|
||||
loaded.add(_parse(bytes, meta.name, meta.id, ext,
|
||||
isVisible: meta.isVisible));
|
||||
} catch (e) {
|
||||
debugPrint('Réteg betöltés hiba (${meta.name}): $e');
|
||||
}
|
||||
}
|
||||
layers.assignAll(loaded);
|
||||
} catch (e) {
|
||||
debugPrint('Perzisztens rétegek betöltési hiba: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Réteg kezelés ─────────────────────────────────────────────────────────
|
||||
|
||||
Future<void> toggleLayer(String id) async {
|
||||
final idx = layers.indexWhere((l) => l.id == id);
|
||||
if (idx < 0) return;
|
||||
final newVisible = !layers[idx].isVisible;
|
||||
layers[idx] = layers[idx].copyWith(isVisible: newVisible);
|
||||
layers.refresh();
|
||||
|
||||
// SQLite szinkron
|
||||
final metas = await AppDatabase.instance.listImportedLayers();
|
||||
final meta = metas.firstWhereOrNull((m) => m.id == id);
|
||||
if (meta != null) {
|
||||
await AppDatabase.instance
|
||||
.updateImportedLayer(meta.copyWith(isVisible: newVisible));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> removeLayer(String id) async {
|
||||
final metas = await AppDatabase.instance.listImportedLayers();
|
||||
final meta = metas.firstWhereOrNull((m) => m.id == id);
|
||||
if (meta != null) {
|
||||
final f = File(meta.localPath);
|
||||
if (await f.exists()) await f.delete();
|
||||
await AppDatabase.instance.deleteImportedLayer(id);
|
||||
}
|
||||
layers.removeWhere((l) => l.id == id);
|
||||
}
|
||||
|
||||
Future<void> removeAll() async {
|
||||
for (final l in List.from(layers)) {
|
||||
await removeLayer(l.id);
|
||||
}
|
||||
}
|
||||
|
||||
List<ImportedLayer> get visibleLayers =>
|
||||
layers.where((l) => l.isVisible).toList();
|
||||
|
||||
// ── Supabase megosztás (előkészítve) ──────────────────────────────────────
|
||||
//
|
||||
// Implementáció a megosztási feladatnál:
|
||||
//
|
||||
// Future<void> syncLayerToSupabase(String id) async {
|
||||
// final meta = ...
|
||||
// final bytes = await File(meta.localPath).readAsBytes();
|
||||
// final ext = meta.localPath.split('.').last;
|
||||
// final path = 'layers/$projectUuid/${meta.id}.$ext';
|
||||
//
|
||||
// await supabase.storage.from('geo_layers').uploadBinary(path, bytes);
|
||||
//
|
||||
// await AppDatabase.instance.updateImportedLayer(
|
||||
// meta.copyWith(storagePath: path, syncedAt: DateTime.now()));
|
||||
//
|
||||
// await supabase.from('terepi_seged_shared_layers').upsert({
|
||||
// 'id': meta.id, 'name': meta.name, 'project_uuid': projectUuid,
|
||||
// 'storage_path': path, 'source_type': meta.sourceType.name,
|
||||
// 'created_at': DateTime.now().toIso8601String(),
|
||||
// });
|
||||
// }
|
||||
|
||||
// ── Belső segédek ─────────────────────────────────────────────────────────
|
||||
|
||||
Future<String> _saveFile(String id, String ext, Uint8List bytes) async {
|
||||
final path = p.join(_layerDir!, '$id.$ext');
|
||||
await File(path).writeAsBytes(bytes);
|
||||
return path;
|
||||
}
|
||||
|
||||
ImportedLayer _parse(Uint8List bytes, String name, String id, String ext,
|
||||
{bool isVisible = true}) {
|
||||
switch (ext.toLowerCase()) {
|
||||
case 'geojson':
|
||||
case 'json':
|
||||
return _parseGeoJson(String.fromCharCodes(bytes), name, id,
|
||||
isVisible: isVisible);
|
||||
case 'kml':
|
||||
return _kmlParser.parseKml(String.fromCharCodes(bytes), name,
|
||||
id: id, isVisible: isVisible);
|
||||
case 'kmz':
|
||||
return _kmlParser.parseKmz(bytes, name, id: id, isVisible: isVisible);
|
||||
default:
|
||||
throw Exception('Nem támogatott formátum: .$ext');
|
||||
}
|
||||
}
|
||||
|
||||
ImportedLayer _parseGeoJson(String content, String name, String id,
|
||||
{bool isVisible = true}) {
|
||||
final parser = GeoJsonParser(
|
||||
defaultMarkerColor: Colors.red.withOpacity(0.9),
|
||||
defaultMarkerIcon: Icons.location_pin,
|
||||
defaultPolylineColor: const Color(0xCC1565C0),
|
||||
defaultPolylineStroke: 3.0,
|
||||
defaultPolygonFillColor: const Color(0x4D1565C0),
|
||||
defaultPolygonBorderColor: const Color(0xCC1565C0),
|
||||
defaultPolygonBorderStroke: 1.5,
|
||||
defaultPolygonIsFilled: true,
|
||||
onMarkerTapCallback: (props) {
|
||||
final label = props['name'] ?? props['title'] ?? '';
|
||||
if (label.toString().isNotEmpty) {
|
||||
Get.snackbar(label.toString(), props['description']?.toString() ?? '',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
duration: const Duration(seconds: 3));
|
||||
}
|
||||
},
|
||||
);
|
||||
parser.parseGeoJsonAsString(content);
|
||||
|
||||
return ImportedLayer(
|
||||
id: id,
|
||||
name: name,
|
||||
sourceType: LayerImportSourceType.geoJson,
|
||||
markers: parser.markers,
|
||||
polylines: parser.polylines,
|
||||
polygons: parser.polygons,
|
||||
isVisible: isVisible,
|
||||
importedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
LayerImportSourceType _sourceType(String ext) => switch (ext.toLowerCase()) {
|
||||
'kml' => LayerImportSourceType.kml,
|
||||
'kmz' => LayerImportSourceType.kmz,
|
||||
_ => LayerImportSourceType.geoJson,
|
||||
};
|
||||
}
|
||||
@ -87,7 +87,35 @@ class AppDrawer extends StatelessWidget {
|
||||
);
|
||||
}),
|
||||
|
||||
const Divider(height: 24),
|
||||
//const Divider(height: 24),
|
||||
const Divider(height: 12),
|
||||
|
||||
// ----------- További funkciók
|
||||
const _SectionLabel('Funkciók'),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.message_outlined),
|
||||
title: const Text('Üzenetek'),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
// Get.to(() => const NtripSettingsView());
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.phone_outlined),
|
||||
title: const Text('Kapcsolatok'),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
// Get.to(() => const NtripSettingsView());
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.data_exploration_outlined),
|
||||
title: const Text('Mérés'),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
// Get.to(() => const NtripSettingsView());
|
||||
},
|
||||
),
|
||||
|
||||
// ── 3. Beállítások ─────────────────────────────────
|
||||
const _SectionLabel('Beállítások'),
|
||||
|
||||
@ -156,7 +156,11 @@ class ShellMapAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
PopupMenuButton(
|
||||
tooltip: 'További funkciók',
|
||||
icon: const Icon(Icons.more_vert),
|
||||
onSelected: null,
|
||||
onSelected: (value) {
|
||||
if (value == 1) {
|
||||
controller.openLayerPanel();
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => const [
|
||||
PopupMenuItem(
|
||||
value: 1,
|
||||
|
||||
200
lib/widgets/map/imported_layer_overlay.dart
Normal file
200
lib/widgets/map/imported_layer_overlay.dart
Normal file
@ -0,0 +1,200 @@
|
||||
// Térkép overlay és panel widget az importált rétegekhez.
|
||||
// Az ImportedLayer már kész flutter_map objektumokat tartalmaz —
|
||||
// itt csak megjelenítjük őket.
|
||||
|
||||
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/layer_import_source_type.dart';
|
||||
|
||||
import '../../models/imported_layer.dart';
|
||||
import '../../services/layer_import_service.dart';
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// Térkép réteg — a flutter_map layers listájába kerül
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
class ImportedLayerOverlay extends StatelessWidget {
|
||||
const ImportedLayerOverlay({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!Get.isRegistered<LayerImportService>()) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Obx(() {
|
||||
final visible = LayerImportService.to.visibleLayers;
|
||||
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();
|
||||
|
||||
return Stack(children: [
|
||||
if (polygons.isNotEmpty) PolygonLayer(polygons: polygons),
|
||||
if (polylines.isNotEmpty) PolylineLayer(polylines: polylines),
|
||||
if (markers.isNotEmpty) MarkerLayer(markers: markers),
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// Réteg panel — import gomb + betöltött rétegek listája
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
class ImportLayerPanel extends StatelessWidget {
|
||||
/// Ha meg van adva, a rétegre zoom gomb ezt hívja
|
||||
final void Function(LatLngBounds bounds)? onFitBounds;
|
||||
|
||||
const ImportLayerPanel({super.key, this.onFitBounds});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final svc = LayerImportService.to;
|
||||
|
||||
return Obx(() => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Import gomb
|
||||
_ImportButton(svc: svc),
|
||||
|
||||
// Réteg lista
|
||||
if (svc.layers.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
const Divider(height: 1),
|
||||
const SizedBox(height: 8),
|
||||
...svc.layers.map((layer) => _LayerTile(
|
||||
layer: layer,
|
||||
onToggle: () => svc.toggleLayer(layer.id),
|
||||
onRemove: () => svc.removeLayer(layer.id),
|
||||
onZoomTo: onFitBounds != null
|
||||
? () {
|
||||
final b = layer.bounds;
|
||||
if (b != null) onFitBounds!(b);
|
||||
}
|
||||
: null,
|
||||
)),
|
||||
],
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Import gomb ─────────────────────────────────────────────────────────────
|
||||
|
||||
class _ImportButton extends StatelessWidget {
|
||||
final LayerImportService svc;
|
||||
const _ImportButton({required this.svc});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(() => svc.isLoading.value
|
||||
? const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2)),
|
||||
SizedBox(width: 8),
|
||||
Text('Betöltés...', style: TextStyle(fontSize: 13)),
|
||||
]),
|
||||
)
|
||||
: OutlinedButton.icon(
|
||||
onPressed: () => svc.importFile(),
|
||||
icon: const Icon(Icons.file_open_outlined, size: 18),
|
||||
label: const Text('GeoJSON / KML / KMZ'),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Egy réteg sor ───────────────────────────────────────────────────────────
|
||||
|
||||
class _LayerTile extends StatelessWidget {
|
||||
final ImportedLayer layer;
|
||||
final VoidCallback onToggle;
|
||||
final VoidCallback onRemove;
|
||||
final VoidCallback? onZoomTo;
|
||||
|
||||
const _LayerTile({
|
||||
required this.layer,
|
||||
required this.onToggle,
|
||||
required this.onRemove,
|
||||
this.onZoomTo,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final icon = switch (layer.sourceType) {
|
||||
LayerImportSourceType.geoJson => Icons.data_object,
|
||||
LayerImportSourceType.kml => Icons.map_outlined,
|
||||
LayerImportSourceType.kmz => Icons.folder_zip_outlined,
|
||||
};
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 6),
|
||||
child: Row(children: [
|
||||
// Láthatóság
|
||||
Transform.scale(
|
||||
scale: 0.85,
|
||||
child: Switch.adaptive(
|
||||
value: layer.isVisible,
|
||||
onChanged: (_) => onToggle(),
|
||||
),
|
||||
),
|
||||
// Ikon
|
||||
Icon(icon, size: 15, color: Colors.grey.shade500),
|
||||
const SizedBox(width: 6),
|
||||
// Név + statisztika
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(layer.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 13, fontWeight: FontWeight.w500),
|
||||
overflow: TextOverflow.ellipsis),
|
||||
Text(_stats(),
|
||||
style: TextStyle(fontSize: 10, color: Colors.grey.shade500)),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Zoom gomb
|
||||
if (onZoomTo != null)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.fit_screen, size: 16),
|
||||
onPressed: onZoomTo,
|
||||
tooltip: 'Ráközelítés',
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(minWidth: 28, minHeight: 28),
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
// Törlés
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, size: 16),
|
||||
onPressed: onRemove,
|
||||
tooltip: 'Eltávolítás',
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(minWidth: 28, minHeight: 28),
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
String _stats() {
|
||||
final parts = <String>[];
|
||||
if (layer.markers.isNotEmpty) parts.add('${layer.markers.length} pont');
|
||||
if (layer.polylines.isNotEmpty)
|
||||
parts.add('${layer.polylines.length} vonal');
|
||||
if (layer.polygons.isNotEmpty)
|
||||
parts.add('${layer.polygons.length} terület');
|
||||
return parts.isEmpty ? 'Üres réteg' : parts.join(' · ');
|
||||
}
|
||||
}
|
||||
@ -79,7 +79,12 @@ class MapModeMenuAnchor extends StatelessWidget {
|
||||
fontSize: 18, fontWeight: FontWeight.w600)),
|
||||
const Icon(Icons.arrow_drop_down, size: 22),
|
||||
]),
|
||||
Text(p == null ? 'Projekt' : p.name,
|
||||
Text(
|
||||
p == null
|
||||
? 'Projekt'
|
||||
: (p.name.length > 18
|
||||
? '${p.name.substring(0, 18)}...'
|
||||
: p.name),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontStyle: FontStyle.italic,
|
||||
|
||||
@ -75,6 +75,8 @@ dependencies:
|
||||
image_picker: ^1.2.2
|
||||
record: ^7.1.0
|
||||
audioplayers: ^6.7.1
|
||||
archive: ^4.0.9
|
||||
xml: ^7.0.1
|
||||
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
Loading…
Reference in New Issue
Block a user