地图组件和ECS框架完成
This commit is contained in:
@@ -6,6 +6,12 @@ import '../providers/chat_provider.dart';
|
||||
import '../widgets/message_bubble.dart';
|
||||
import '../widgets/typing_indicator.dart';
|
||||
import '../widgets/chat_input.dart';
|
||||
import 'map_screen.dart'; // 导入地图界面
|
||||
|
||||
|
||||
// 定义一个 GlobalKey 来访问 _ChatScreenState
|
||||
final GlobalKey<_ChatScreenState> chatScreenStateKey = GlobalKey<_ChatScreenState>();
|
||||
|
||||
|
||||
class ChatScreen extends StatefulWidget {
|
||||
const ChatScreen({super.key});
|
||||
@@ -16,6 +22,13 @@ class ChatScreen extends StatefulWidget {
|
||||
|
||||
class _ChatScreenState extends State<ChatScreen> {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
bool _mapViewOpen = false; // 控制地图界面是否展开
|
||||
|
||||
void _toggleMapView() {
|
||||
setState(() {
|
||||
_mapViewOpen = !_mapViewOpen;
|
||||
});
|
||||
}
|
||||
|
||||
void _scrollToBottom() {
|
||||
if (_scrollController.hasClients) {
|
||||
@@ -62,18 +75,43 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
context.read<ChatProvider>().clearMessages();
|
||||
},
|
||||
),
|
||||
// 添加地图按钮
|
||||
CommandBarButton(
|
||||
icon: const Icon(FluentIcons.map_layers),
|
||||
label: const Text('Map'),
|
||||
onPressed: _toggleMapView, // 切换地图视图
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
content: Column(
|
||||
content: Row( // 修改为Row布局,实现左右分屏
|
||||
children: [
|
||||
Expanded(child: _buildMessageList()),
|
||||
_buildInputArea(),
|
||||
// 聊天界面部分,使用Expanded来正确分配空间
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: _buildChatContent(),
|
||||
),
|
||||
|
||||
// 地图界面部分,根据状态决定是否显示
|
||||
if (_mapViewOpen)
|
||||
Expanded(
|
||||
child: const MapUiPage(), // 使用现有的地图页面
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 提取聊天内容部分到单独的方法
|
||||
Widget _buildChatContent() {
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(child: _buildMessageList()),
|
||||
_buildInputArea(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMessageList() {
|
||||
return Consumer<ChatProvider>(
|
||||
builder: (context, chatProvider, child) {
|
||||
@@ -161,7 +199,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
color: theme.accentColor,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: const Icon(FluentIcons.robot, color: Colors.white, size: 40),
|
||||
child: Icon(FluentIcons.robot, color: Colors.white, size: 40),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text('Hello! I\'m your AI Assistant', style: theme.typography.title),
|
||||
@@ -220,4 +258,4 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
613
lib/screens/map_screen.dart
Normal file
613
lib/screens/map_screen.dart
Normal 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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user