地图组件和ECS框架完成

This commit is contained in:
2026-01-13 15:39:45 +08:00
parent 9d45d4c726
commit 566ec47a73
24 changed files with 17072 additions and 66 deletions

613
lib/screens/map_screen.dart Normal file
View File

@@ -0,0 +1,613 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as Math;
import 'package:flutter/material.dart';
import 'package:maplibre/maplibre.dart';
import 'package:maplibre_gl/maplibre_gl.dart' as gl;
import '../ecs/system.dart';
abstract class ExamplePage extends StatelessWidget {
const ExamplePage(
this.leading,
this.title, {
this.needsLocationPermission = true,
super.key,
});
final Widget leading;
final String title;
final bool needsLocationPermission;
}
class MapUiPage extends ExamplePage {
const MapUiPage({super.key}) : super(const Icon(Icons.map), '态势');
@override
Widget build(BuildContext context) {
return const MapUiBody();
}
}
class MapUiBody extends StatefulWidget {
const MapUiBody({super.key});
@override
State<MapUiBody> createState() => MapUiBodyState();
/// 静态方法,用于通过 GlobalKey 获取 MapUiBodyState 实例
static MapUiBodyState? of(BuildContext context) {
final mapUiBody = context.findAncestorWidgetOfExactType<MapUiBody>();
if (mapUiBody?.key is GlobalKey<MapUiBodyState>) {
final key = mapUiBody?.key as GlobalKey<MapUiBodyState>;
return key.currentState;
}
return null;
}
}
class MapUiBodyState extends State<MapUiBody> with TickerProviderStateMixin {
late final MapController _controller;
final GlobalKey _mapKey = GlobalKey();
// MapSystem 实例,用于通过系统调用地图接口
late final MapSystem mapSystem;
final _markerPositions = [
const Geographic(lon: -10, lat: 0),
const Geographic(lon: -5, lat: 0),
const Geographic(lon: 0, lat: 0),
const Geographic(lon: 5, lat: 0),
];
// 自定义图片标记数据
final _customMarkers = <Feature<Point>>[];
bool _customMarkerImageLoaded = false;
// 圆形数据
final _circles = <Feature<Point>>[];
// 多边形数据(包括扇形和长方形)
final _polygons = <Feature<Polygon>>[];
Geographic? _originalPosition;
MapGestures _mapGestures = const MapGestures.all();
@override
void initState() {
super.initState();
// 初始化 MapSystem并将 this 传递给它
mapSystem = MapSystem(mapUiBodyState: this);
}
@override
Widget build(BuildContext context) {
return Scaffold(
//appBar: AppBar(title: const Text('Interactive Widget Layer')),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.only(left: 16, bottom: 8),
),
Expanded(
child: MapLibreMap(
key: _mapKey,
options: MapOptions(
initZoom: 3,
initCenter: const Geographic(lon: 0, lat: 0),
initStyle: "assets/osm_style.json",
gestures: _mapGestures,
),
onMapCreated: (controller) => _controller = controller,
onEvent: (event) async {
if (event is MapEventStyleLoaded) {
// 加载自定义标记图片
await event.style.addImageFromIconData(
id: 'custom-marker',
iconData: Icons.location_on,
color: Colors.blue,
);
setState(() {
_customMarkerImageLoaded = true;
});
} else if (event is MapEventLongClick) {
final position = event.point;
_markerPositions.add(position);
setState(() {});
}
},
layers: [
// 自定义图片标记层
MarkerLayer(
points: _customMarkers.where((marker) =>
marker.properties['type'] != 'fixed_circle'
).toList(), // 过滤掉固定圆形
iconImage: _customMarkerImageLoaded ? 'custom-marker' : null,
iconSize: 0.15,
iconAnchor: IconAnchor.bottom,
),
// 圆形层
CircleLayer(
points: _circles,
color: Colors.orange.withValues(alpha: 0.5),
radius: 20,
strokeColor: Colors.red,
strokeWidth: 2,
),
// 多边形层(扇形、长方形和圆形)
PolygonLayer(
polygons: _polygons,
color: Colors.lightBlueAccent.withValues(alpha: 0.6),
outlineColor: Colors.blue,
),
],
children: [
WidgetLayer(
allowInteraction: true,
markers: List.generate(
_markerPositions.length,
(index) => Marker(
size: const Size.square(50),
point: _markerPositions[index],
child: GestureDetector(
onTap: () => _onTap(index),
onLongPressStart: (details) =>
_onLongPress(index, details),
onPanStart: (details) => _onLongPanStart(details, index),
onPanUpdate: (details) async =>
_onPanUpdate(details, index),
onPanEnd: (details) async => _onPanEnd(details, index),
child: const Icon(
Icons.location_on,
color: Colors.red,
size: 50,
),
),
alignment: Alignment.bottomCenter,
),
),
),
// 添加固定圆形的Widget
WidgetLayer(
allowInteraction: false, // 固定圆形不需要交互
markers: _customMarkers
.where((marker) => marker.properties['type'] == 'fixed_circle')
.map((marker) {
final pointGeometry = marker.geometry as Point;
// 使用Point的position属性获取坐标
final geographic = Geographic(
lon: pointGeometry.position.x,
lat: pointGeometry.position.y,
);
return Marker(
size: Size.fromRadius(marker.properties['radius']?.toDouble() ?? 20.0),
point: geographic,
child: IgnorePointer( // 忽略指针事件,确保不会干扰地图手势
child: Container(
width: (marker.properties['radius']?.toDouble() ?? 20.0) * 2,
height: (marker.properties['radius']?.toDouble() ?? 20.0) * 2,
decoration: BoxDecoration(
color: marker.properties['color'] ?? Colors.orange,
shape: BoxShape.circle,
border: Border.all(
color: marker.properties['strokeColor'] ?? Colors.red,
width: marker.properties['strokeWidth']?.toDouble() ?? 2.0,
),
),
),
),
alignment: Alignment.center,
);
})
.toList(),
),
// display the UI widgets above the widget markers.
const MapScalebar(),
const SourceAttribution(),
const MapControlButtons(),
const MapCompass(),
],
),
),
],
),
);
}
Future<Geographic> _toLngLat(Offset eventOffset) async {
final mapRenderBox =
_mapKey.currentContext?.findRenderObject() as RenderBox?;
assert(mapRenderBox != null, 'RenderBox of Map should never be null');
final mapOffset = mapRenderBox!.localToGlobal(Offset.zero);
final offset = Offset(
eventOffset.dx - mapOffset.dx,
eventOffset.dy - mapOffset.dy,
);
return _controller.toLngLat(offset);
}
void _onLongPress(int index, LongPressStartDetails details) {
final offset = details.globalPosition;
showMenu(
context: context,
position: RelativeRect.fromLTRB(
offset.dx,
offset.dy,
MediaQuery.of(context).size.width - offset.dx,
MediaQuery.of(context).size.height - offset.dy,
),
items: [
const PopupMenuItem<void>(child: Text('Edit')),
PopupMenuItem<void>(
onTap: () async {
final isConfirmed = await _showConfirmationDialogDelete(index);
if (isConfirmed) {
_markerPositions.removeAt(index);
setState(() {});
}
},
child: const Text('Delete'),
),
],
);
}
Future<void> _onPanEnd(DragEndDetails details, int index) async {
final isAccepted = await _showConfirmationDialogMove();
if (!isAccepted) {
_markerPositions[index] = _originalPosition!;
} else {
final newPosition = await _toLngLat(details.globalPosition);
_markerPositions[index] = newPosition;
}
_originalPosition = null;
setState(() {
_mapGestures = const MapGestures.all();
});
}
void _onLongPanStart(DragStartDetails details, int index) {
// Keep original position in case of discarded move
_originalPosition = Geographic.from(_markerPositions[index]);
setState(() {
// Disable camera panning while a marker gets moved.
_mapGestures = const MapGestures.all(pan: false);
});
}
Future<void> _onPanUpdate(DragUpdateDetails details, int index) async {
final newPosition = await _toLngLat(details.globalPosition);
_markerPositions[index] = newPosition;
setState(() {});
}
void _onTap(int index) {
_showMarkerDetails(index);
}
Future<bool> _showConfirmationDialogDelete(int index) async {
final isConfirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text('Delete marker [$index]?'),
actions: <Widget>[
TextButton(
style: TextButton.styleFrom(
textStyle: Theme.of(context).textTheme.labelLarge,
),
child: const Text('Cancel'),
onPressed: () {
Navigator.of(context).pop(false);
},
),
TextButton(
style: TextButton.styleFrom(
textStyle: Theme.of(context).textTheme.labelLarge,
),
child: const Text('Delete'),
onPressed: () {
Navigator.of(context).pop(true);
},
),
],
),
);
return isConfirmed ?? false;
}
Future<bool> _showConfirmationDialogMove() async {
final isConfirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Accept new position?'),
actions: <Widget>[
TextButton(
style: TextButton.styleFrom(
textStyle: Theme.of(context).textTheme.labelLarge,
),
child: const Text('Discard'),
onPressed: () {
Navigator.of(context).pop(false);
},
),
TextButton(
style: TextButton.styleFrom(
textStyle: Theme.of(context).textTheme.labelLarge,
),
child: const Text('Accept'),
onPressed: () {
Navigator.of(context).pop(true);
},
),
],
),
);
return isConfirmed ?? false;
}
Future<void> _showMarkerDetails(int index) async {
await showDialog<void>(
context: context,
builder: (context) => AlertDialog(
title: Text('Details marker with index: $index'),
content: Text('Show here the details of Marker with index $index'),
actions: <Widget>[
TextButton(
style: TextButton.styleFrom(
textStyle: Theme.of(context).textTheme.labelLarge,
),
child: const Text('Cancel'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
),
);
return;
}
// ==================== 公共接口方法 ====================
/// 添加自定义图片标记
///
/// [position] 标记的地理位置
/// [properties] 可选的属性数据
void addCustomMarker(Geographic position, {Map<String, dynamic>? properties}) {
setState(() {
_customMarkers.add(Feature(
geometry: Point(position),
properties: properties,
));
});
}
/// 添加圆形
///
/// [position] 圆心的地理位置
/// [radius] 圆的半径(单位:像素)
/// [color] 填充颜色
/// [strokeColor] 边框颜色
/// [strokeWidth] 边框宽度
void addCircle(
Geographic position, {
double radius = 20,
Color color = Colors.orange,
Color strokeColor = Colors.red,
double strokeWidth = 2,
}) {
setState(() {
_circles.add(Feature(
geometry: Point(position),
properties: {
'radius': radius,
'color': color,
'strokeColor': strokeColor,
'strokeWidth': strokeWidth,
},
));
});
}
/// 添加固定大小的圆形(不随地图缩放)
///
/// [center] 圆心的地理位置
/// [radius] 圆的半径(单位:米)
/// [color] 填充颜色
/// [outlineColor] 边框颜色
void addFixedCircle(
Geographic center, {
required double radius,
Color color = Colors.orange,
Color outlineColor = Colors.blue,
}) {
// 将半径从米转换为经纬度(度)
final radiusInDegrees = _metersToDegrees(center, radius);
final points = <Geographic>[];
// 计算圆周上的点360度
for (int i = 0; i <= 36; i++) { // 使用36个点来构成圆形
final angle = (360 * i / 36) * 3.14159265359 / 180; // 角度转弧度
// 将角度转换为经纬度偏移
final latOffset = radiusInDegrees * Math.sin(angle);
final lonOffset = radiusInDegrees * Math.cos(angle);
points.add(Geographic(
lon: center.lon + lonOffset,
lat: center.lat + latOffset,
));
}
setState(() {
_polygons.add(Feature(
geometry: Polygon.from([points]),
properties: {
'color': color,
'outlineColor': outlineColor,
'type': 'fixed_circle_geo', // 标记为地理圆形
},
));
});
}
/// 添加扇形
///
/// [center] 扇形中心的地理位置
/// [radius] 扇形的半径(单位:米)
/// [startAngle] 起始角度单位0度表示正东方向
/// [endAngle] 结束角度(单位:度)
/// [segments] 扇形的分段数,值越大越平滑
/// [color] 填充颜色
/// [outlineColor] 边框颜色
void addSector(
Geographic center, {
required double radius,
required double startAngle,
required double endAngle,
int segments = 36,
Color color = Colors.lightBlueAccent,
Color outlineColor = Colors.blue,
}) {
// 将半径从米转换为经纬度(度)
final radiusInDegrees = _metersToDegrees(center, radius);
final points = <Geographic>[];
// 添加中心点
points.add(center);
// 计算扇形的弧线上的点
for (int i = 0; i <= segments; i++) {
final angle = startAngle + (endAngle - startAngle) * i / segments;
final angleRad = angle * 3.14159265359 / 180;
// 将角度转换为经纬度偏移
final latOffset = radiusInDegrees * Math.sin(angleRad);
final lonOffset = radiusInDegrees * Math.cos(angleRad);
points.add(Geographic(
lon: center.lon + lonOffset,
lat: center.lat + latOffset,
));
}
setState(() {
_polygons.add(Feature(
geometry: Polygon.from([points]),
properties: {
'color': color,
'outlineColor': outlineColor,
},
));
});
}
/// 添加长方形
///
/// [center] 长方形中心的地理位置
/// [width] 长方形的宽度(单位:米)
/// [height] 长方形的高度(单位:米)
/// [color] 填充颜色
/// [outlineColor] 边框颜色
void addRectangle(
Geographic center, {
required double width,
required double height,
Color color = Colors.lightBlueAccent,
Color outlineColor = Colors.blue,
}) {
// 将宽高从米转换为经纬度(度)
final widthInDegrees = _metersToDegrees(center, width / 2);
final heightInDegrees = _metersToDegrees(center, height / 2);
final points = [
Geographic(lon: center.lon - widthInDegrees, lat: center.lat - heightInDegrees),
Geographic(lon: center.lon + widthInDegrees, lat: center.lat - heightInDegrees),
Geographic(lon: center.lon + widthInDegrees, lat: center.lat + heightInDegrees),
Geographic(lon: center.lon - widthInDegrees, lat: center.lat + heightInDegrees),
Geographic(lon: center.lon - widthInDegrees, lat: center.lat - heightInDegrees), // 闭合多边形
];
setState(() {
_polygons.add(Feature(
geometry: Polygon.from([points]),
properties: {
'color': color,
'outlineColor': outlineColor,
},
));
});
}
/// 将距离(米)转换为经纬度(度)
/// 此方法使用近似计算,适用于小范围区域
double _metersToDegrees(Geographic center, double meters) {
// 地球半径约为6378137米
const earthRadius = 6378137.0;
// 将米转换为度
// 1度大约等于111320米在赤道附近
// 但需要考虑纬度的影响,因为经度的距离会随纬度变化
final latCircumference = 2 * Math.pi * earthRadius; // 纬线周长
final latFactor = meters / latCircumference * 360; // 纬度方向转换因子
// 经度方向需要考虑纬度的影响
final lonCircumference = 2 * Math.pi * earthRadius * Math.cos(center.lat * Math.pi / 180); // 经线周长
final lonFactor = meters / lonCircumference * 360; // 经度方向转换因子
// 返回平均值,对于小范围区域,这个近似是足够的
return (latFactor + lonFactor) / 2;
}
/// 清除所有自定义标记
void clearCustomMarkers() {
setState(() {
_customMarkers.clear();
});
}
/// 清除所有圆形
void clearCircles() {
setState(() {
_circles.clear();
});
}
/// 清除所有多边形(包括扇形和长方形)
void clearPolygons() {
setState(() {
_polygons.clear();
});
}
/// 清除所有自定义图层
void clearAllCustomLayers() {
setState(() {
_customMarkers.clear();
_circles.clear();
_polygons.clear();
});
}
}