// 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 createState() => MapUiBodyState(); /// 静态方法,用于通过 GlobalKey 获取 MapUiBodyState 实例 static MapUiBodyState? of(BuildContext context) { final mapUiBody = context.findAncestorWidgetOfExactType(); if (mapUiBody?.key is GlobalKey) { final key = mapUiBody?.key as GlobalKey; return key.currentState; } return null; } } class MapUiBodyState extends State 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 = >[]; bool _customMarkerImageLoaded = false; // 圆形数据 final _circles = >[]; // 多边形数据(包括扇形和长方形) final _polygons = >[]; 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 _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(child: Text('Edit')), PopupMenuItem( onTap: () async { final isConfirmed = await _showConfirmationDialogDelete(index); if (isConfirmed) { _markerPositions.removeAt(index); setState(() {}); } }, child: const Text('Delete'), ), ], ); } Future _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 _onPanUpdate(DragUpdateDetails details, int index) async { final newPosition = await _toLngLat(details.globalPosition); _markerPositions[index] = newPosition; setState(() {}); } void _onTap(int index) { _showMarkerDetails(index); } Future _showConfirmationDialogDelete(int index) async { final isConfirmed = await showDialog( context: context, builder: (context) => AlertDialog( title: Text('Delete marker [$index]?'), actions: [ 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 _showConfirmationDialogMove() async { final isConfirmed = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Accept new position?'), actions: [ 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 _showMarkerDetails(int index) async { await showDialog( context: context, builder: (context) => AlertDialog( title: Text('Details marker with index: $index'), content: Text('Show here the details of Marker with index $index'), actions: [ 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? 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 = []; // 计算圆周上的点(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 = []; // 添加中心点 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(); }); } }