Flutter地图功能插件mapplet的使用

Flutter地图功能插件mapplet的使用

mapplet 是一个旨在简化离线地图存储的 Flutter 插件。它基于 flutter_map 包,并提供了易于使用的层来自动显示用户当前位置。

特性

  • 简单设计mapplet 的设计初衷是为了简化开发者的工作流程。
  • 区域存储:每个 Depot 可以包含多个区域。
  • 节省空间mapplet 自动检测并共享已存储的区域之间的图块,避免重复下载。
  • 并行下载:区域图块通过多线程下载,并批量写入内部数据库。
  • 快速学习曲线mapplet 仅暴露必要的 API,集成非常简单。
  • 当前位置层:提供了一个简单的层来自动显示用户的当前位置。

定义

Region(区域)

  • 定义:由 LatLngBounds 确定的区域。

Depot(仓库)

  • 定义:标识可以包含任意数量 Region 的存储。仓库内的图块在不同区域之间动态共享,以防止重复下载并节省空间。

主要功能

  • 存储和删除区域:每个 Depot 可以包含许多区域。
  • 节省空间:mapplet 检测并共享仓库内已存储区域之间的图块,避免重复下载。
  • 并行下载:区域图块通过多线程下载,并批量写入内部数据库。
  • 极速学习曲线:mapplet 仅暴露必要的 API,集成非常简单。
  • 当前位置层:提供了一个简单的层来自动显示用户的当前位置。

开始使用

初始化插件

首先,初始化插件并配置每个 Depot。建议使用 path_provider 包来获取适应不同平台的目录位置。

await Mapplet.initiate([
    DepotConfiguration(
        id: "default_depot",
        minZoom: 10,
        maxZoom: 16,
        directory: (await getApplicationDocumentsDirectory()).path,
        urlTemplate: "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
    ),
]);

使用

存储区域

首先,创建一个描述要存储区域的 LatLngBounds

var center = LatLng(46, 12);
var bounds = LatLngBoundsExtensions.fromDelta(center, 10);

然后,检索 Depot 实例并运行下载操作。

var depot = await Mapplet.depot("default_depot");

var depositOp = await depot.depositRegion("region_id", bounds);

// 监听取消事件
depositOp.onAbort.listen((_) async {
    // 处理取消操作
});

// 监听提交事件
depositOp.onCommit.listen((commitFuture) async {
    // 处理提交操作
});

// 开始下载操作
var stream = depositOp.fetch();

// 等待进度报告
await for (final progress in stream) {
    // 处理进度
    // 或者随时取消操作
    await depositOp.abort();
}

删除区域

var depot = await Mapplet.depot("default_depot");
await depot.dropRegion("region_id");

使用 TileProvider

var depot = await Mapplet.depot("default_depot");
var tileProvider = depot.getTileProvider();

额外信息

mappletextensions 模块中有一些有用的扩展方法,这些方法会自动导入。

当前位置层

mapplet 还包括一个有用的层来自动显示用户的当前位置。

LocationWatcherLayer(
    positionStream: _positionStream.stream,
    style: LocationWatcherLayerStyle(
        showAccuracyCircle: false,
        directionRadius: 64,
        directionAngle: 80,
        showDirection: widget.showDirectionSector,
        markerSize: const Size.square(20),
        directionColor: Colors.red,
        locationMarker: DecoratedBox(
            decoration: BoxDecoration(
                color: Colors.white,
                shape: BoxShape.circle,
            ),
            child: Padding(
                padding: const EdgeInsets.all(2),
                child: DecoratedBox(
                    decoration: BoxDecoration(
                        color: Colors.red,
                        shape: BoxShape.circle,
                    ),
                ),
            ),
        ),
    ),
)

示例代码

以下是一个完整的示例代码,展示了如何使用 mapplet 插件。

import 'package:flutter/material.dart';
import 'package:latlong2/latlong.dart';
import 'package:mapplet/mapplet.dart';
import 'package:path_provider/path_provider.dart';

Future<void> main() async {
  await Mapplet.initialize([
    DepotConfiguration(
      id: "default_depot",
      urlTemplate: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
      directory: (await getApplicationDocumentsDirectory()).path,
      minZoom: 10,
      maxZoom: 16,
    ),
  ]);
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "Mapplet",
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: "Mapplet"),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  final String title;

  [@override](/user/override)
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final Depot _depot = Mapplet.depot("default_depot");
  DepotStats? _stats;
  FetchOperation? _fetchOp;
  FetchProgress? _progressFirst;
  FetchProgress? _progressSecond;
  bool _committingFirst = false;
  bool _committingSecond = false;

  Widget _buildStats() {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceAround,
      children: [
        Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            Wrap(
              crossAxisAlignment: WrapCrossAlignment.center,
              spacing: 8,
              children: [
                const Icon(Icons.storage_rounded),
                Text(
                  _stats != null ? "${_stats!.byteSize.byteToMib().toStringAsFixed(1)} MiB" : "...",
                  style: Theme.of(context).textTheme.titleMedium,
                ),
              ],
            ),
            const SizedBox(height: 4),
            Text(
              "occupied",
              style: Theme.of(context).textTheme.bodySmall,
            ),
          ],
        ),
        Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            Wrap(
              crossAxisAlignment: WrapCrossAlignment.center,
              spacing: 8,
              children: [
                const Icon(Icons.landscape_rounded),
                Text(
                  _stats != null ? _stats!.regionCount.toString() : "...",
                  style: Theme.of(context).textTheme.titleMedium,
                ),
              ],
            ),
            const SizedBox(height: 4),
            Text(
              "regions",
              style: Theme.of(context).textTheme.bodySmall,
            ),
          ],
        ),
        Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            Wrap(
              crossAxisAlignment: WrapCrossAlignment.center,
              spacing: 8,
              children: [
                const Icon(Icons.square_rounded),
                Text(
                  _stats != null ? _stats!.tilesCount.toString() : "...",
                  style: Theme.of(context).textTheme.titleMedium,
                ),
              ],
            ),
            const SizedBox(height: 4),
            Text(
              "tiles",
              style: Theme.of(context).textTheme.bodySmall,
            ),
          ],
        ),
      ],
    );
  }

  Iterable<Widget> _buildFirstRegion() sync* {
    yield const Text(
      "First region",
      style: TextStyle(fontSize: 20),
    );
    yield Padding(
      padding: const EdgeInsets.all(8),
      child: LinearProgressIndicator(
        value: _progressFirst?.progress ?? 0,
      ),
    );

    yield Padding(
      padding: const EdgeInsets.all(16),
      child: SizedBox(
        height: 16,
        width: 16,
        child: _committingFirst ? const CircularProgressIndicator() : null,
      ),
    );
    yield Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Expanded(
          child: Padding(
            padding: const EdgeInsets.symmetric(horizontal: 8),
            child: ElevatedButton.icon(
              onPressed: _committingSecond || _committingFirst
                  ? null
                  : () async {
                      _fetchOp = _depot.depositRegion("region", LatLngBoundsExtensions.fromDelta(const LatLng(46, 12), 2.5));
                      _fetchOp!.onAbort.listen((_) {
                        debugPrint("operation aborted");
                        setState(() => _committingFirst = false);
                      });
                      _fetchOp!.onCommit.listen((commitFuture) async {
                        setState(() => _committingFirst = true);
                        await commitFuture;
                        debugPrint("operation committed");
                        var stats = await _depot.getStats();
                        setState(() {
                          _committingFirst = false;
                          _stats = stats;
                        });
                      });
                      await for (final progress in _fetchOp!.fetch()) {
                        setState(() => _progressFirst = progress);
                      }
                    },
              icon: const Icon(Icons.download_rounded),
              label: const Text("Download region"),
            ),
          ),
        ),
        Expanded(
          child: Padding(
            padding: const EdgeInsets.symmetric(horizontal: 8),
            child: ElevatedButton.icon(
              onPressed: _committingSecond || _committingFirst
                  ? null
                  : () async {
                      await _depot.dropRegion("region");
                      var stats = await _depot.getStats();
                      setState(() => _stats = stats);
                    },
              icon: const Icon(Icons.delete_rounded),
              label: const Text("Delete region"),
            ),
          ),
        ),
      ],
    );
  }

  Iterable<Widget> _buildSecondRegion() sync* {
    yield const Text(
      "Second region",
      style: TextStyle(fontSize: 20),
    );
    yield Padding(
      padding: const EdgeInsets.all(8),
      child: LinearProgressIndicator(
        value: _progressSecond?.progress ?? 0,
      ),
    );
    yield Padding(
      padding: const EdgeInsets.all(16),
      child: SizedBox(
        height: 16,
        width: 16,
        child: _committingSecond ? const CircularProgressIndicator() : null,
      ),
    );
    yield Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Expanded(
          child: Padding(
            padding: const EdgeInsets.symmetric(horizontal: 8),
            child: ElevatedButton.icon(
              onPressed: _committingSecond || _committingFirst
                  ? null
                  : () async {
                      _fetchOp = _depot.depositRegion("region_1", LatLngBoundsExtensions.fromDelta(const LatLng(46, 12), 5));
                      _fetchOp!.onAbort.listen((_) {
                        debugPrint("operation aborted");
                        setState(() => _committingSecond = false);
                      });
                      _fetchOp!.onCommit.listen((commitFuture) async {
                        setState(() => _committingSecond = true);
                        await commitFuture;
                        debugPrint("operation committed");
                        var stats = await _depot.getStats();
                        setState(() {
                          _committingSecond = false;
                          _stats = stats;
                        });
                      });
                      await for (final progress in _fetchOp!.fetch()) {
                        setState(() => _progressSecond = progress);
                      }
                    },
              icon: const Icon(Icons.download_rounded),
              label: const Text("Download region"),
            ),
          ),
        ),
        Expanded(
          child: Padding(
            padding: const EdgeInsets.symmetric(horizontal: 8),
            child: ElevatedButton.icon(
              onPressed: _committingSecond || _committingFirst
                  ? null
                  : () async {
                      await _depot.dropRegion("region_1");
                      var stats = await _depot.getStats();
                      setState(() => _stats = stats);
                    },
              icon: const Icon(Icons.delete_rounded),
              label: const Text("Delete region"),
            ),
          ),
        ),
      ],
    );
    yield const Text(
      "The second region is centered at the same spot of the first region but expands for double the area.\nMapplet detects the overlap and skips the fetch procedure on common tiles",
      textAlign: TextAlign.center,
      style: TextStyle(fontSize: 18),
    );
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        centerTitle: true,
        title: Text(widget.title),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.start,
            children: [
              _buildStats(),
              const Divider(height: 32),
              ..._buildFirstRegion(),
              Padding(
                padding: const EdgeInsets.symmetric(vertical: 32),
                child: Container(height: 8, color: Colors.grey),
              ),
              ..._buildSecondRegion(),
              const Divider(),
            ],
          ),
        ),
      ),
    );
  }
}

更多关于Flutter地图功能插件mapplet的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter地图功能插件mapplet的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


在Flutter中,mapplet 是一个用于集成地图功能的强大插件。它允许开发者在应用中嵌入交互式地图,支持多种功能如定位、标记、路线规划等。以下是一个使用 mapplet 插件的基本示例代码,展示了如何在 Flutter 应用中实现地图功能。

首先,确保在你的 pubspec.yaml 文件中添加 mapplet 依赖项:

dependencies:
  flutter:
    sdk: flutter
  mapplet: ^最新版本号  # 请替换为实际可用的最新版本号

然后,运行 flutter pub get 来获取依赖项。

接下来,在你的 Flutter 项目中,创建一个显示地图的页面。以下是一个简单的示例代码:

import 'package:flutter/material.dart';
import 'package:mapplet/mapplet.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MapScreen(),
    );
  }
}

class MapScreen extends StatefulWidget {
  @override
  _MapScreenState createState() => _MapScreenState();
}

class _MapScreenState extends State<MapScreen> {
  MappletController? _mapController;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Mapplet Demo'),
      ),
      body: MappletMap(
        apiKey: '你的Mapplet API密钥',  // 请替换为你的实际API密钥
        onMapCreated: (MappletController mapController) {
          _mapController = mapController;
          // 可以在这里进行其他地图初始化操作,比如设置初始中心点
          _mapController?.moveCamera(CameraUpdateFactory.newLatLng(LatLng(37.7749, -122.4194))); // 旧金山
        },
        myLocationEnabled: true,
        myLocationButtonEnabled: true,
        zoomGesturesEnabled: true,
        scrollGesturesEnabled: true,
        tiltGesturesEnabled: true,
        rotateGesturesEnabled: true,
        compassEnabled: true,
        markers: [
          MappletMarker(
            position: LatLng(37.7749, -122.4194), // 旧金山
            infoWindow: MappletInfoWindow(
              title: '旧金山',
              snippet: '美国加利福尼亚州的城市',
            ),
            icon: MappletBitmapDescriptorFactory.defaultMarker(
              color: Colors.red,
            ),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          if (_mapController != null) {
            _mapController?.showMyLocation();
          }
        },
        tooltip: '显示我的位置',
        child: Icon(Icons.location_on),
      ),
    );
  }
}

关键点解释:

  1. 依赖项添加:在 pubspec.yaml 中添加 mapplet 依赖项。
  2. API 密钥:在 MappletMap 组件中,你需要提供 apiKey,这是从 Mapplet 平台获取的 API 密钥。
  3. 地图控制器:通过 onMapCreated 回调获取 MappletController 实例,用于控制地图的各种操作,如移动相机、添加标记等。
  4. 标记:在 markers 属性中,你可以添加多个 MappletMarker,每个标记都包含位置、信息窗口和图标。
  5. 功能按钮:通过 floatingActionButton 添加一个浮动操作按钮,用于显示当前位置。

这个示例代码展示了如何在 Flutter 应用中集成 mapplet 插件,并展示了基本的地图功能。你可以根据需求进一步扩展,如添加更多的标记、自定义图标、处理地图点击事件等。

回到顶部