地图组件和ECS框架完成
This commit is contained in:
90
lib/ecs/component.dart
Normal file
90
lib/ecs/component.dart
Normal file
@@ -0,0 +1,90 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'entity.dart';
|
||||
|
||||
/// 组件基类 - 代表实体的属性
|
||||
abstract class Component {
|
||||
final String id;
|
||||
Entity? entity;
|
||||
|
||||
Component({String? id}) : id = id ?? "${DateTime.now().millisecondsSinceEpoch}";
|
||||
}
|
||||
|
||||
/// 标绘组件 - 用于在地图上绘制图形
|
||||
class PlotComponent extends Component {
|
||||
final String plotType;
|
||||
final List<LatLng> coordinates;
|
||||
final Color color;
|
||||
final double strokeWidth;
|
||||
|
||||
PlotComponent({
|
||||
super.id,
|
||||
required this.plotType,
|
||||
required this.coordinates,
|
||||
this.color = Colors.red,
|
||||
this.strokeWidth = 2.0,
|
||||
});
|
||||
}
|
||||
|
||||
/// 图标组件 - 用于在地图上显示图标
|
||||
class IconComponent extends Component {
|
||||
final String iconPath;
|
||||
LatLng position;
|
||||
final double size;
|
||||
|
||||
IconComponent({
|
||||
super.id,
|
||||
required this.iconPath,
|
||||
required this.position,
|
||||
this.size = 30.0,
|
||||
});
|
||||
}
|
||||
|
||||
/// 拖尾组件 - 用于显示移动实体的轨迹
|
||||
class TrailComponent extends Component {
|
||||
final List<LatLng> trailPoints;
|
||||
final Color trailColor;
|
||||
final double maxLength;
|
||||
|
||||
TrailComponent({
|
||||
super.id,
|
||||
List<LatLng>? trailPoints,
|
||||
this.trailColor = Colors.blue,
|
||||
this.maxLength = 20.0,
|
||||
}) : trailPoints = trailPoints ?? [];
|
||||
|
||||
void addPoint(LatLng point) {
|
||||
trailPoints.add(point);
|
||||
if (trailPoints.length > maxLength) {
|
||||
trailPoints.removeAt(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 音频组件 - 用于播放与实体相关的音频
|
||||
class AudioComponent extends Component {
|
||||
final String audioPath;
|
||||
final bool loop;
|
||||
final double volume;
|
||||
|
||||
AudioComponent({
|
||||
super.id,
|
||||
required this.audioPath,
|
||||
this.loop = false,
|
||||
this.volume = 1.0,
|
||||
});
|
||||
}
|
||||
|
||||
/// 移动组件 - 用于控制实体的移动
|
||||
class MovementComponent extends Component {
|
||||
LatLng position;
|
||||
LatLng? target;
|
||||
double speed;
|
||||
|
||||
MovementComponent({
|
||||
super.id,
|
||||
required this.position,
|
||||
this.target,
|
||||
this.speed = 1.0,
|
||||
});
|
||||
}
|
||||
39
lib/ecs/entity.dart
Normal file
39
lib/ecs/entity.dart
Normal file
@@ -0,0 +1,39 @@
|
||||
import 'component.dart';
|
||||
/// 实体ID生成器
|
||||
class EntityIdGenerator {
|
||||
static int _id = 0;
|
||||
static int nextId() => _id++;
|
||||
}
|
||||
|
||||
/// 实体类 - 代表地图上的一个对象
|
||||
class Entity {
|
||||
final int id;
|
||||
final String name;
|
||||
int entityType;
|
||||
final Map<String, Component> _components = {};
|
||||
|
||||
Entity({int? id, required this.name, this.entityType = 0}) : id = id ?? EntityIdGenerator.nextId();
|
||||
|
||||
/// 添加组件
|
||||
void addComponent<T extends Component>(T component) {
|
||||
_components[T.toString()] = component;
|
||||
}
|
||||
|
||||
/// 获取组件
|
||||
T? getComponent<T extends Component>() {
|
||||
return _components[T.toString()] as T?;
|
||||
}
|
||||
|
||||
/// 检查是否有特定类型的组件
|
||||
bool hasComponent<T extends Component>() {
|
||||
return _components.containsKey(T.toString());
|
||||
}
|
||||
|
||||
/// 移除组件
|
||||
void removeComponent<T extends Component>() {
|
||||
_components.remove(T.toString());
|
||||
}
|
||||
|
||||
/// 获取所有组件
|
||||
Map<String, Component> get components => _components;
|
||||
}
|
||||
389
lib/ecs/system.dart
Normal file
389
lib/ecs/system.dart
Normal file
@@ -0,0 +1,389 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:maplibre/maplibre.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart' as gl;
|
||||
import '../screens/map_screen.dart';
|
||||
import 'entity.dart';
|
||||
import 'component.dart';
|
||||
|
||||
/// 系统基类 - 用于处理特定类型的组件
|
||||
abstract class System {
|
||||
void update(List<Entity> entities, Duration deltaTime);
|
||||
}
|
||||
|
||||
/// 地图系统 - 处理地图相关的实体和组件
|
||||
class MapSystem implements System {
|
||||
gl.MapLibreMapController? mapController;
|
||||
MapUiBodyState? mapUiBodyState;
|
||||
final List<String> _plotIds = [];
|
||||
final List<String> _trailIds = [];
|
||||
final List<String> _iconIds = [];
|
||||
|
||||
// 单例实例
|
||||
static MapSystem? _instance;
|
||||
|
||||
// 工厂构造函数,实现单例
|
||||
factory MapSystem({gl.MapLibreMapController? mapController, MapUiBodyState? mapUiBodyState}) {
|
||||
_instance ??= MapSystem._internal(mapController: mapController, mapUiBodyState: mapUiBodyState);
|
||||
if (mapController != null) _instance!.mapController = mapController;
|
||||
if (mapUiBodyState != null) _instance!.mapUiBodyState = mapUiBodyState;
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
// 私有构造函数
|
||||
MapSystem._internal({this.mapController, this.mapUiBodyState});
|
||||
|
||||
/// 获取单例实例
|
||||
static MapSystem get instance {
|
||||
_instance ??= MapSystem._internal();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
@override
|
||||
void update(List<Entity> entities, Duration deltaTime) {
|
||||
// 更新地图上的标绘
|
||||
for (var entity in entities) {
|
||||
if (entity.hasComponent<PlotComponent>()) {
|
||||
_updatePlot(entity.getComponent<PlotComponent>()!);
|
||||
}
|
||||
|
||||
if (entity.hasComponent<IconComponent>()) {
|
||||
_updateIcon(entity.getComponent<IconComponent>()!);
|
||||
}
|
||||
|
||||
if (entity.hasComponent<TrailComponent>()) {
|
||||
_updateTrail(entity.getComponent<TrailComponent>()!);
|
||||
}
|
||||
|
||||
if (entity.hasComponent<MovementComponent>()) {
|
||||
_updateMovement(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _updatePlot(PlotComponent plot) {
|
||||
// 实现标绘更新逻辑
|
||||
// 这里可以根据plotType绘制不同类型的图形
|
||||
if (plot.coordinates.length >= 2) {
|
||||
// 示例:绘制线段
|
||||
// mapController?.addLine() // 这里需要根据实际API实现
|
||||
}
|
||||
}
|
||||
|
||||
void _updateIcon(IconComponent icon) {
|
||||
// 实现图标更新逻辑
|
||||
// mapController?.addMarker() // 这里需要根据实际API实现
|
||||
}
|
||||
|
||||
void _updateTrail(TrailComponent trail) {
|
||||
// 实现拖尾更新逻辑
|
||||
// 绘制轨迹线
|
||||
}
|
||||
|
||||
void _updateMovement(Entity entity) {
|
||||
var movement = entity.getComponent<MovementComponent>();
|
||||
var plot = entity.getComponent<PlotComponent>();
|
||||
var icon = entity.getComponent<IconComponent>();
|
||||
|
||||
if (movement != null && (plot != null || icon != null)) {
|
||||
if (movement.target != null) {
|
||||
// 简单的移动逻辑 - 向目标点移动
|
||||
double dx = movement.target!.longitude - movement.position.longitude;
|
||||
double dy = movement.target!.latitude - movement.position.latitude;
|
||||
double distance = sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance > 0.0001) { // 如果距离目标足够近就停止
|
||||
double ratio = movement.speed * 0.0001 / distance;
|
||||
movement.position = gl.LatLng(
|
||||
movement.position.latitude + dy * ratio,
|
||||
movement.position.longitude + dx * ratio,
|
||||
);
|
||||
|
||||
// 更新图标位置
|
||||
if (icon != null) {
|
||||
icon.position = movement.position;
|
||||
}
|
||||
|
||||
// 如果有拖尾组件,添加新点
|
||||
var trail = entity.getComponent<TrailComponent>();
|
||||
if (trail != null) {
|
||||
trail.addPoint(movement.position);
|
||||
}
|
||||
} else {
|
||||
movement.target = null; // 到达目标
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 便捷方法:通过 mapUiBodyState 调用地图接口 ====================
|
||||
|
||||
/// 添加自定义图片标记
|
||||
void addCustomMarker(Geographic position, {Map<String, dynamic>? properties}) {
|
||||
mapUiBodyState?.addCustomMarker(position, properties: properties);
|
||||
}
|
||||
|
||||
/// 添加圆形
|
||||
void addCircle(
|
||||
Geographic position, {
|
||||
double radius = 20,
|
||||
Color color = Colors.orange,
|
||||
Color strokeColor = Colors.red,
|
||||
double strokeWidth = 2,
|
||||
}) {
|
||||
mapUiBodyState?.addCircle(
|
||||
position,
|
||||
radius: radius,
|
||||
color: color,
|
||||
strokeWidth: strokeWidth,
|
||||
);
|
||||
}
|
||||
|
||||
/// 添加扇形
|
||||
/// [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,
|
||||
}) {
|
||||
mapUiBodyState?.addSector(
|
||||
center,
|
||||
radius: radius,
|
||||
startAngle: startAngle,
|
||||
endAngle: endAngle,
|
||||
segments: segments,
|
||||
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,
|
||||
}) {
|
||||
mapUiBodyState?.addRectangle(
|
||||
center,
|
||||
width: width,
|
||||
height: height,
|
||||
color: color,
|
||||
outlineColor: outlineColor,
|
||||
);
|
||||
}
|
||||
|
||||
/// 清除所有自定义标记
|
||||
void clearCustomMarkers() {
|
||||
mapUiBodyState?.clearCustomMarkers();
|
||||
}
|
||||
|
||||
/// 清除所有圆形
|
||||
void clearCircles() {
|
||||
mapUiBodyState?.clearCircles();
|
||||
}
|
||||
|
||||
/// 清除所有多边形(包括扇形和长方形)
|
||||
void clearPolygons() {
|
||||
mapUiBodyState?.clearPolygons();
|
||||
}
|
||||
|
||||
/// 清除所有自定义图层
|
||||
void clearAllCustomLayers() {
|
||||
mapUiBodyState?.clearAllCustomLayers();
|
||||
}
|
||||
|
||||
/// 添加固定大小的圆形(不随地图缩放)
|
||||
void addFixedCircle(
|
||||
Geographic position, {
|
||||
double radius = 20,
|
||||
Color color = Colors.orange,
|
||||
Color strokeColor = Colors.red,
|
||||
double strokeWidth = 2,
|
||||
}) {
|
||||
mapUiBodyState?.addFixedCircle(
|
||||
position,
|
||||
radius: radius,
|
||||
color: color,
|
||||
outlineColor: strokeColor,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 音频系统 - 处理音频相关的实体和组件
|
||||
class AudioSystem implements System {
|
||||
@override
|
||||
void update(List<Entity> entities, Duration deltaTime) {
|
||||
for (var entity in entities) {
|
||||
if (entity.hasComponent<AudioComponent>()) {
|
||||
var audioComponent = entity.getComponent<AudioComponent>()!;
|
||||
_playAudio(audioComponent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _playAudio(AudioComponent audio) {
|
||||
// 实现音频播放逻辑
|
||||
// 这里可以使用Flutter的音频插件如audioplayers
|
||||
print("Playing audio: ${audio.audioPath}");
|
||||
}
|
||||
}
|
||||
|
||||
/// 组件系统 - 管理各种组件的交互
|
||||
class ComponentSystem implements System {
|
||||
@override
|
||||
void update(List<Entity> entities, Duration deltaTime) {
|
||||
// 处理组件间的交互
|
||||
for (var entity in entities) {
|
||||
// 可以根据需要实现组件交互逻辑
|
||||
if (entity.hasComponent<PlotComponent>() && entity.hasComponent<IconComponent>()) {
|
||||
// 当一个实体同时有标绘和图标组件时的特殊处理
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ECS管理器 - 管理实体、系统和整个ECS框架
|
||||
class ECSManager {
|
||||
static ECSManager? _instance;
|
||||
|
||||
factory ECSManager() {
|
||||
_instance ??= ECSManager._internal();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
ECSManager._internal();
|
||||
|
||||
/// 获取单例实例
|
||||
static ECSManager get instance {
|
||||
_instance ??= ECSManager._internal();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
final List<Entity> _entities = [];
|
||||
final List<System> _systems = [];
|
||||
final Queue<Entity> _entitiesToAdd = Queue<Entity>();
|
||||
final Queue<Entity> _entitiesToRemove = Queue<Entity>();
|
||||
|
||||
void addEntity(Entity entity) {
|
||||
_entitiesToAdd.add(entity);
|
||||
}
|
||||
|
||||
void removeEntity(Entity entity) {
|
||||
_entitiesToRemove.add(entity);
|
||||
}
|
||||
|
||||
void addSystem(System system) {
|
||||
_systems.add(system);
|
||||
}
|
||||
|
||||
void removeSystem(System system) {
|
||||
_systems.remove(system);
|
||||
}
|
||||
|
||||
void update(Duration deltaTime) {
|
||||
// 添加新实体
|
||||
while (_entitiesToAdd.isNotEmpty) {
|
||||
_entities.add(_entitiesToAdd.removeFirst());
|
||||
}
|
||||
|
||||
// 更新所有系统
|
||||
for (var system in _systems) {
|
||||
system.update(_entities, deltaTime);
|
||||
}
|
||||
|
||||
// 移除实体
|
||||
while (_entitiesToRemove.isNotEmpty) {
|
||||
var entity = _entitiesToRemove.removeFirst();
|
||||
_entities.remove(entity);
|
||||
}
|
||||
}
|
||||
|
||||
List<Entity> get entities => _entities;
|
||||
|
||||
/// 工厂方法:创建一个带位置的实体
|
||||
Entity createEntityAtPosition({
|
||||
required String name,
|
||||
required gl.LatLng position,
|
||||
String? plotType,
|
||||
Color plotColor = Colors.red,
|
||||
double strokeWidth = 2.0,
|
||||
String? iconPath,
|
||||
double iconSize = 30.0,
|
||||
bool hasTrail = false,
|
||||
Color trailColor = Colors.blue,
|
||||
double trailMaxLength = 20.0,
|
||||
bool hasMovement = false,
|
||||
gl.LatLng? movementTarget,
|
||||
double movementSpeed = 1.0,
|
||||
String? audioPath,
|
||||
bool audioLoop = false,
|
||||
double audioVolume = 1.0,
|
||||
}) {
|
||||
// 创建实体
|
||||
final entity = Entity(name: name);
|
||||
|
||||
// 添加位置相关的组件
|
||||
if (plotType != null) {
|
||||
entity.addComponent(PlotComponent(
|
||||
plotType: plotType,
|
||||
coordinates: [position],
|
||||
color: plotColor,
|
||||
strokeWidth: strokeWidth,
|
||||
));
|
||||
}
|
||||
|
||||
if (iconPath != null) {
|
||||
entity.addComponent(IconComponent(
|
||||
iconPath: iconPath,
|
||||
position: position,
|
||||
size: iconSize,
|
||||
));
|
||||
}
|
||||
|
||||
if (hasTrail) {
|
||||
entity.addComponent(TrailComponent(
|
||||
trailColor: trailColor,
|
||||
maxLength: trailMaxLength,
|
||||
));
|
||||
}
|
||||
|
||||
if (hasMovement) {
|
||||
entity.addComponent(MovementComponent(
|
||||
position: position,
|
||||
target: movementTarget,
|
||||
speed: movementSpeed,
|
||||
));
|
||||
}
|
||||
|
||||
if (audioPath != null) {
|
||||
entity.addComponent(AudioComponent(
|
||||
audioPath: audioPath,
|
||||
loop: audioLoop,
|
||||
volume: audioVolume,
|
||||
));
|
||||
}
|
||||
|
||||
// 添加到管理器
|
||||
addEntity(entity);
|
||||
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
174
lib/utils/map_styles.dart
Normal file
174
lib/utils/map_styles.dart
Normal file
@@ -0,0 +1,174 @@
|
||||
enum MapStyles {
|
||||
openMapTilesLiberty(
|
||||
name: 'OpenMapTiles Liberty',
|
||||
uri: 'https://tiles.openfreemap.org/styles/liberty',
|
||||
),
|
||||
openMapTilesBright(
|
||||
name: 'OpenMapTiles Bright',
|
||||
uri: 'https://tiles.openfreemap.org/styles/bright',
|
||||
),
|
||||
openMapTilesPositron(
|
||||
name: 'OpenMapTiles Positron',
|
||||
uri: 'https://tiles.openfreemap.org/styles/positron',
|
||||
),
|
||||
openMapTilesStreets(
|
||||
name: 'OpenMapTiles Streets',
|
||||
uri:
|
||||
'https://api.maptiler.com/maps/streets-v2/style.json?key=$_maptilerKey',
|
||||
),
|
||||
stadiaMapsAlidadeSmooth(
|
||||
name: 'Alidade Smooth',
|
||||
uri:
|
||||
'https://tiles.stadiamaps.com/styles/alidade_smooth.json?api_key=$_stadiamapsKey',
|
||||
),
|
||||
stadiaMapsAlidadeSmoothDark(
|
||||
name: 'Alidade Smooth Dark',
|
||||
uri:
|
||||
'https://tiles.stadiamaps.com/styles/alidade_smooth_dark.json?api_key=$_stadiamapsKey',
|
||||
),
|
||||
versaTilesColorful(
|
||||
name: 'VersaTiles Colorful',
|
||||
uri: 'https://tiles.versatiles.org/assets/styles/colorful/style.json',
|
||||
),
|
||||
versaTilesGraybeard(
|
||||
name: 'VersaTiles Graybeard',
|
||||
uri: 'https://tiles.versatiles.org/assets/styles/graybeard/style.json',
|
||||
),
|
||||
versaTilesEclipse(
|
||||
name: 'VersaTiles Eclipse',
|
||||
uri: 'https://tiles.versatiles.org/assets/styles/eclipse/style.json',
|
||||
),
|
||||
versaTilesNeutrino(
|
||||
name: 'VersaTiles Neutrino',
|
||||
uri: 'https://tiles.versatiles.org/assets/styles/neutrino/style.json',
|
||||
),
|
||||
protomapsLight(
|
||||
name: 'Protomaps Light',
|
||||
uri: 'https://api.protomaps.com/styles/v2/light.json?key=$_protomapsKey',
|
||||
),
|
||||
protomapsLightPmTiles(
|
||||
name: 'Protomaps Light (PMTiles)',
|
||||
uri: 'assets/styles/protomaps-light.json',
|
||||
),
|
||||
protomapsDark(
|
||||
name: 'Protomaps Dark',
|
||||
uri: 'https://api.protomaps.com/styles/v2/dark.json?key=$_protomapsKey',
|
||||
),
|
||||
protomapsWhite(
|
||||
name: 'Protomaps White',
|
||||
uri: 'https://api.protomaps.com/styles/v2/white.json?key=$_protomapsKey',
|
||||
),
|
||||
// mapboxStreets(
|
||||
// name: 'Mapbox Streets (legacy)',
|
||||
// uri: 'mapbox://styles/mapbox/streets-v12?access_token=$_mapboxKey',
|
||||
// ),
|
||||
// mapboxOutdoor(
|
||||
// name: 'Mapbox Outdoors (legacy)',
|
||||
// uri: 'mapbox://styles/mapbox/outdoors-v12?access_token=$_mapboxKey',
|
||||
// ),
|
||||
// mapboxLight(
|
||||
// name: 'Mapbox Light (legacy)',
|
||||
// uri: 'mapbox://styles/mapbox/light-v11?access_token=$_mapboxKey',
|
||||
// ),
|
||||
// mapboxDark(
|
||||
// name: 'Mapbox Dark (legacy)',
|
||||
// uri: 'mapbox://styles/mapbox/dark-v11?access_token=$_mapboxKey',
|
||||
// ),
|
||||
// mapboxSatellite(
|
||||
// name: 'Mapbox Satellite (legacy)',
|
||||
// uri: 'mapbox://styles/mapbox/satellite-v9?access_token=$_mapboxKey',
|
||||
// ),
|
||||
// mapboxSatelliteStreets(
|
||||
// name: 'Mapbox Satellite Streets (legacy)',
|
||||
// uri:
|
||||
// 'mapbox://styles/mapbox/satellite-streets-v12?access_token=$_mapboxKey',
|
||||
// ),
|
||||
maplibreWorld(
|
||||
name: 'MapLibre World',
|
||||
uri: 'https://demotiles.maplibre.org/style.json',
|
||||
),
|
||||
maplibreDebug(
|
||||
name: 'MapLibre Debug',
|
||||
uri: 'https://demotiles.maplibre.org/debug-tiles/style.json',
|
||||
),
|
||||
translucent(name: 'Translucent', uri: 'assets/styles/translucent_style.json'),
|
||||
countries(
|
||||
name: 'Countries',
|
||||
uri: '''
|
||||
{
|
||||
"version": 8,
|
||||
"name": "MapLibre",
|
||||
"center": [17.65431710431244, 32.954120326746775],
|
||||
"zoom": 0.8619833357855968,
|
||||
"bearing": 0,
|
||||
"pitch": 0,
|
||||
"sources": {
|
||||
"maplibre": {
|
||||
"url": "https://demotiles.maplibre.org/tiles/tiles.json",
|
||||
"type": "vector"
|
||||
}
|
||||
},
|
||||
"glyphs": "https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf",
|
||||
"layers": [
|
||||
{
|
||||
"id": "background",
|
||||
"type": "background",
|
||||
"maxzoom": 24,
|
||||
"filter": ["all"],
|
||||
"paint": {"background-color": "#D8F2FF"}
|
||||
},
|
||||
{
|
||||
"id": "countries-fill",
|
||||
"type": "fill",
|
||||
"source": "maplibre",
|
||||
"source-layer": "countries",
|
||||
"maxzoom": 24,
|
||||
"filter": ["all"],
|
||||
"paint": {"fill-color": "#FFFFFF"}
|
||||
},
|
||||
{
|
||||
"id": "countries-boundary",
|
||||
"type": "line",
|
||||
"source": "maplibre",
|
||||
"source-layer": "countries",
|
||||
"maxzoom": 24,
|
||||
"layout": {
|
||||
"line-cap": "round",
|
||||
"line-join": "round",
|
||||
"visibility": "visible"
|
||||
},
|
||||
"paint": {
|
||||
"line-color": "#198EC8",
|
||||
"line-width": {"stops": [[1, 1], [6, 2], [14, 6], [22, 12]]},
|
||||
"line-opacity": {"stops": [[3, 0.5], [6, 1]]}
|
||||
}
|
||||
}
|
||||
],
|
||||
"id": "43f36e14-e3f5-43c1-84c0-50a9c80dc5c7"
|
||||
}
|
||||
''',
|
||||
);
|
||||
|
||||
const MapStyles({required this.name, required this.uri});
|
||||
|
||||
final String name;
|
||||
final String uri;
|
||||
}
|
||||
|
||||
/// **Use your own key for your project!**
|
||||
/// This key will be rotated occasionally.
|
||||
///
|
||||
/// https://cloud.maptiler.com/account/keys/
|
||||
const _maptilerKey = 'OPCgnZ51sHETbEQ4wnkd';
|
||||
|
||||
/// **Use your own key for your project!**
|
||||
/// This key will be rotated occasionally.
|
||||
///
|
||||
/// https://protomaps.com/account
|
||||
const _protomapsKey = 'a6f9aebb3965458c';
|
||||
|
||||
/// **Use your own key for your project!**
|
||||
/// This key will be rotated occasionally.
|
||||
///
|
||||
/// https://client.stadiamaps.com/dashboard
|
||||
const _stadiamapsKey = '0d5e614b-aaf7-4bfd-9bf3-1f7c3062248e';
|
||||
35
lib/utils/style_dropdown.dart
Normal file
35
lib/utils/style_dropdown.dart
Normal file
@@ -0,0 +1,35 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '/utils/map_styles.dart';
|
||||
|
||||
class StyleDropdown extends StatefulWidget {
|
||||
const StyleDropdown({required this.onChanged, super.key});
|
||||
|
||||
final void Function(MapStyles style) onChanged;
|
||||
|
||||
static MapStyles get initStyle => MapStyles.openMapTilesLiberty;
|
||||
|
||||
@override
|
||||
State<StyleDropdown> createState() => _StyleDropdownState();
|
||||
}
|
||||
|
||||
class _StyleDropdownState extends State<StyleDropdown> {
|
||||
late MapStyles _selectedStyle = StyleDropdown.initStyle;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: DropdownButton<MapStyles>(
|
||||
value: _selectedStyle,
|
||||
items: MapStyles.values
|
||||
.map((e) => DropdownMenuItem(value: e, child: Text(e.name)))
|
||||
.toList(growable: false),
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
setState(() => _selectedStyle = value);
|
||||
widget.onChanged(value);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user