You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

218 lines
6.2 KiB

import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'dart:math' as math;
import 'package:amap_flutter/amap_flutter.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:http/http.dart' as http;
class ChatNavigationCard extends StatefulWidget {
final String content;
final Map? data;
final bool isDarkMode;
const ChatNavigationCard(this.data, this.content, this.isDarkMode,
{super.key});
@override
State<ChatNavigationCard> createState() => _ChatNavigationCardState();
}
class _ChatNavigationCardState extends State<ChatNavigationCard> {
final String apiKey = '8bde023b7e5e00f321ea17929f10fbae';
LatLng startPoint = LatLng(116.397428, 39.90923); // 起点坐标
LatLng endPoint = LatLng(116.397026, 39.903303); // 终点坐标
final _amapFlutter = AmapFlutter();
StreamSubscription? _locationStream;
String _mapUrl = '';
@override
void initState() {
super.initState();
_getLocation();
}
@override
void dispose() {
_amapFlutter.dispose();
_locationStream?.cancel();
super.dispose();
}
void _getLocation() async {
List endPointList = widget.data?['destination'].split(',');
endPoint = LatLng(
double.parse(endPointList[0]),
double.parse(endPointList[1]),
);
await _amapFlutter.initLocation();
_locationStream = _amapFlutter.locationStream().listen((data) async {
startPoint = LatLng(
data['longitude'],
data['latitude'],
);
_mapUrl = await _getMapUrl();
_amapFlutter.stopLocation();
setState(() {});
});
_amapFlutter.startLocation();
}
Future<String> _getMapUrl() async {
// 1. 获取路径数据
final route = await _fetchRouteData();
// 2. 解析并简化路径
List<LatLng> simplified = _simplifyPath(route, epsilon: 0.0001);
// 3. 计算地图参数
final zoom = _calculateAutoZoom(simplified);
// 4. 生成静态地图URL
return _buildStaticMapUrl(simplified, zoom);
}
Future<List<LatLng>> _fetchRouteData() async {
final url = 'https://restapi.amap.com/v3/direction/driving?'
'origin=${startPoint.lng},${startPoint.lat}&'
'destination=${endPoint.lng},${endPoint.lat}&'
'key=$apiKey';
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
final data = json.decode(response.body);
return _parsePolyline(data['route']['paths'][0]['steps']);
}
throw Exception('Failed to load route');
}
List<LatLng> _parsePolyline(List<dynamic> steps) {
final List<LatLng> path = [];
for (final step in steps) {
final points = (step['polyline'] as String).split(';');
for (final point in points) {
final coords = point.split(',').map(double.parse).toList();
path.add(LatLng(coords[0], coords[1]));
}
}
return path;
}
List<LatLng> _simplifyPath(List<LatLng> points, {double epsilon = 0.0001}) {
return douglasPeucker(points, epsilon);
}
int _calculateAutoZoom(List<LatLng> points,
{int mapWidth = 1024, int mapHeight = 512}) {
if (points.isEmpty) return 15; // 默认缩放级别
// 1. 计算坐标范围
double minLng = double.infinity;
double maxLng = -double.infinity;
double minLat = double.infinity;
double maxLat = -double.infinity;
for (final p in points) {
final lng = p.lng;
final lat = p.lat;
minLng = math.min(minLng, lng);
maxLng = math.max(maxLng, lng);
minLat = math.min(minLat, lat);
maxLat = math.max(maxLat, lat);
}
// 3. 计算缩放级别(基于Leaflet算法改进)
const worldWidth = 256; // 高德瓦片尺寸
const worldHeight = 256;
const zoomMax = 18; // 高德最大缩放级别
double latRad(double lat) {
final sin = math.sin(lat * math.pi / 180);
return math.log((1 + sin) / (1 - sin)) / 2;
}
final latFraction = (latRad(maxLat) - latRad(minLat)) / math.pi;
final lngDiff = maxLng - minLng;
final lngFraction = ((lngDiff < 0) ? (lngDiff + 360) : lngDiff) / 360;
final latZoom =
math.log((mapHeight / worldHeight) / latFraction) / math.ln2;
final lngZoom = math.log((mapWidth / worldWidth) / lngFraction) / math.ln2;
return math.min(math.min(latZoom, lngZoom).floor(), zoomMax);
}
String _buildStaticMapUrl(List<LatLng> path, int zoom) {
final pathParam = path.map((p) => '${p.lng},${p.lat}').join(';');
return 'https://restapi.amap.com/v3/staticmap?'
'zoom=$zoom&'
'size=1024*512&'
'paths=10,0x0000ff,1,,:$pathParam&'
'markers=mid,,A:${startPoint.lng},${startPoint.lat}|'
'mid,,B:${endPoint.lng},${endPoint.lat}&'
'key=$apiKey';
}
List<LatLng> douglasPeucker(List<LatLng> points, double epsilon) {
if (points.length < 3) return points;
double maxDistance = 0;
int index = 0;
for (int i = 1; i < points.length - 1; i++) {
double d = _perpendicularDistance(points[i], points.first, points.last);
if (d > maxDistance) {
index = i;
maxDistance = d;
}
}
if (maxDistance > epsilon) {
return [
...douglasPeucker(points.sublist(0, index + 1), epsilon),
...douglasPeucker(points.sublist(index), epsilon).skip(1)
];
}
return [points.first, points.last];
}
double _perpendicularDistance(LatLng point, LatLng start, LatLng end) {
final area = (end.lng - start.lng) * (point.lat - start.lat) -
(end.lat - start.lat) * (point.lng - start.lng);
final length =
sqrt(pow(end.lng - start.lng, 2) + pow(end.lat - start.lat, 2));
return area.abs() / length;
}
@override
Widget build(BuildContext context) {
return Column(
children: [
_mapUrl.isNotEmpty
? Image.network(_mapUrl)
: const CircularProgressIndicator(),
10.verticalSpace,
Text(
widget.content,
style: TextStyle(
color: widget.isDarkMode ? Colors.white : Colors.black,
fontSize: 16.sp,
height: 1.5,
),
),
],
);
}
}
class LatLng {
final double lat;
final double lng;
LatLng(this.lng, this.lat); // 高德使用经度在前纬度在后的格式
}