Files
llm_chat/lib/screens/map_screen.dart

614 lines
19 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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();
});
}
}