Skip to main content

junny_map 0.2.1 copy "junny_map: ^0.2.1" to clipboard
junny_map: ^0.2.1 copied to clipboard

PlatformAndroid

Shared Tianditu map, geocoding, POI search, and geolocator services for Flutter applications.

junny_map #

junny_map 是供多个 Flutter 项目共用的天地图、OpenFreeMap、OSM 与设备定位基础库。

当前版本提供:

  • 天地图 WMTS 矢量、影像、地形和注记瓦片;
  • 基于 MapLibre 的 OpenFreeMap 矢量地图,默认使用彩色的 Liberty 样式;
  • OpenStreetMap 栅格地图及跨镜像备用瓦片配置;
  • flutter_map 地图组件、点击、相机事件、Marker、折线和多边形;
  • 天地图 Search V2.0 普通、视野和周边 POI 搜索;
  • 天地图正向和逆地理编码客户端;
  • geolocator 单次定位、连续定位、权限和系统服务状态;
  • 带坐标系、精度、时间戳和模拟位置标记的统一位置模型;
  • WGS84、CGCS2000、GCJ02、BD09 标识及常用转换。

使用 #

通过 Dart Pub 添加依赖:

dependencies:
  junny_map: ^0.1.0

库内公共地图控件使用 Flutter 原生 Localizations 机制,默认支持简体中文和英文。 宿主应用应注册库的 Delegate;语言会由 MaterialApp 根据设备 Locale 自动选择:

MaterialApp(
  localizationsDelegates: <LocalizationsDelegate<dynamic>>[
    JunnyMapLocalizations.delegate,
    ...GlobalMaterialLocalizations.delegates,
  ],
  supportedLocales: const <Locale>[Locale('zh'), Locale('en')],
)

库只负责通用地图控件、定位权限操作和底图状态文案。搜索业务状态、POI 选择流程、 天地图 Key 配置和具体页面标题应由调用方维护,并根据业务需要覆盖文案。

生产边界 #

本库提供地图渲染、GCJ02 坐标、定位、搜索、逆地理编码和选点能力。宿主项目负责鉴权、 业务规则、数据提交和页面流程。

Android 页面只保留一个 MapLibre 原生地图实例。JunnyOpenFreeMap 默认启用纹理模式, 保证 Flutter 的 AppBar、搜索框和地图操作按钮正常显示;底图切换时销毁旧实例,避免 多个原生地图同时占用资源。

天地图 Key 可以直连配置,也可以通过 apiBaseUrl、tileBaseUrl 接入自有代理。定位权限、 定位精度阈值、模拟位置处理和失败后的页面降级由宿主项目配置。

库内 API 的所有经纬度输入和输出统一以 GCJ02 表示;WGS84 只用于系统定位和部分底图 的内部渲染适配。其他坐标系应通过 CoordinateTransformer 显式转换,不能根据数值猜测。 定位质量阈值、模拟位置处理、权限申请时机和数据写入由宿主业务决定。

仓库内包含一个可独立运行的测试项目。配置天地图 Key 后运行:

cd example
flutter pub get
flutter run

天地图 Key 配置在 example/env/test.env,应用启动时会自动读取,该文件已加入 Git 忽略,直接编辑即可。 不配置 Key 时,示例直接使用 OpenFreeMap 底图和设备定位;天地图瓦片、POI 搜索与 逆地理编码仍需要 Key。天地图瓦片错误达到阈值时,示例会自动切换到 OpenFreeMap 兜底地图; geolocator 不需要天地图 Key。

示例页定位后使用库内统一的 GCJ02 坐标显示位置,并展示系统返回的水平精度和模拟位置 标记。外部服务如果使用其他坐标系,只在服务边界转换,不会改变定位和业务模型中的坐标。

天地图生产接入 #

TdtServiceOptions 将 REST 和 WMTS 分成 apiBaseUrl、tileBaseUrl 两个边界。客户端 会把 REST 路径拼成 ${apiBaseUrl}/v2/search 或 ${apiBaseUrl}/geocoder,把瓦片路径 拼成 ${tileBaseUrl}/vec_w/wmts 等 WMTS 路径;基地址不能带 query 或 fragment。

官方直连模式 #

官方直连时,includeRestApiKey 和 includeTileApiKey 保持默认的 true,REST 与瓦片 请求都会携带 tk。Android/iOS 必须使用天地图控制台允许原生服务请求的 Key(通常是 服务端或移动端 Key);浏览器/JS API Key 不能直接替代原生 Key。Key 即使通过 --dart-define 注入,仍会随应用发布到客户端;需要集中管理时建议使用自有代理。

final TdtServiceOptions services = TdtServiceOptions(
  apiKey: const String.fromEnvironment('TIANDITU_KEY'),
  apiBaseUrl: 'https://api.tianditu.gov.cn',
  tileBaseUrl: 'https://{s}.tianditu.gov.cn',
  serviceCoordinateSystem: CoordinateSystem.gcj02,
);

构建时通过 --dart-define=TIANDITU_KEY=... 提供值,不要把真实 Key 提交到仓库。

纯代理模式 #

不希望 REST 或瓦片 Key 出现在客户端请求中时,REST 和瓦片都指向宿主的 HTTPS 代理, 并同时关闭两个 Key 参数。下面配置会生成以下代理请求:

  • REST:https://maps.example.com/tianditu/rest/v2/search 和 https://maps.example.com/tianditu/rest/geocoder;
  • 瓦片:https://maps.example.com/tianditu/tiles/vec_w/wmts,以及影像、地形或注记 对应的同构路径。
final TdtServiceOptions services = TdtServiceOptions(
  // 两个 include*ApiKey 都为 false 时可以使用空字符串。
  apiKey: '',
  apiBaseUrl: 'https://maps.example.com/tianditu/rest',
  tileBaseUrl: 'https://maps.example.com/tianditu/tiles',
  includeRestApiKey: false,
  includeTileApiKey: false,
  serviceCoordinateSystem: CoordinateSystem.gcj02,
);

代理应在服务端保存天地图 Key,校验宿主自己的身份和权限,按需限流,并转发原库需要 的 REST JSON 或 WMTS 图片响应;不要要求客户端再次传入 tk,也不要把官方 Key 放进 代理 URL 的 query。代理返回的 Search V2.0 和地理编码响应结构需要与天地图兼容,或由 代理完成等价转换。若代理使用子域名负载均衡,可将 tileBaseUrl 写为 https://maps.example.com/tianditu/tiles/{s},实际的 t0 路径会位于 .../tiles/t0/vec_w/wmts。

搜索和逆地理客户端的请求都接受可选的 Dio CancelToken;JunnyMapPickerController 会取消自己创建的旧 REST 请求,并用请求代次忽略迟到响应。宿主直接使用客户端时, 也应在页面销毁、查询替换或用户取消时管理 Token 和客户端生命周期。

创建服务配置:

final TdtServiceOptions services = TdtServiceOptions(
  apiKey: const String.fromEnvironment('TIANDITU_KEY'),
  // 仅用于天地图 REST 服务边界;客户端返回值仍统一为 GCJ02。
  serviceCoordinateSystem: CoordinateSystem.gcj02,
);

显示地图:

JunnyTdtMap(
  services: services,
  initialCenter: const LatLng(30.5928, 114.3055),
  initialZoom: 13,
  markers: <Marker>[
    Marker(
      point: const LatLng(30.5928, 114.3055),
      width: 40,
      height: 40,
      child: const Icon(Icons.location_on),
    ),
  ],
)

地图定位由库内 JunnyMapLocationController 管理,Demo 或业务页面无需自行订阅 LocationProvider.watchLocation。控制器也可以脱离地图单独使用;传入地图后,地图会自动 渲染当前位置 marker,并在 isFollowing 为 true 时跟随最新位置移动相机:

final JunnyMapLocationController locationController =
    JunnyMapLocationController(provider: const GeolocatorLocationProvider());

JunnyTdtMap(
  services: services,
  initialCenter: const LatLng(30.5928, 114.3055),
  locationController: locationController,
)

await locationController.locate(requestPermission: true);

项目内部定位模型统一使用 GCJ02。设备定位和 OpenFreeMap 的 WGS84 适配只在对应消费边界 执行一次;地图控制器负责连续定位、跟随状态和订阅生命周期。

使用 OpenFreeMap 兜底地图:

JunnyOpenFreeMap(
  initialCenter: const GeoCoordinate(
    latitude: 30.5928,
    longitude: 114.3055,
    system: CoordinateSystem.gcj02,
  ),
  style: JunnyOpenFreeMapStyle.liberty,
)

OpenFreeMap 不需要 Key,适合无天地图 Key 时作为示例和备用底图。需要固定样式或稳定 访问时,使用 styleUrl 配置自有的 MapLibre Style JSON。

需要稳定容量时,宿主可以自托管 MapLibre Style JSON,并把其中的矢量瓦片 source、字体 glyph、sprite 等资源一起部署或代理,再通过 styleUrl 指向设备可访问的 HTTPS 地址:

JunnyOpenFreeMap(
  initialCenter: const GeoCoordinate(
    latitude: 30.5928,
    longitude: 114.3055,
    system: CoordinateSystem.gcj02,
  ),
  styleUrl: 'https://maps.example.com/styles/production.json',
  showAttribution: true,
  androidTextureMode: true,
)

传入 styleUrl 后会覆盖内置样式地址。

如果部署网络可以访问 CARTO,可使用 Voyager 样式并自动回退到 OSM:

JunnyOsmMap(
  initialCenter: const LatLng(30.5928, 114.3055),
  tileLayerOptions: const JunnyOsmTileLayerOptions.carto(
    userAgentPackageName: 'com.example.app',
  ),
)

在部署网络无法直连 CARTO 时,将 urlTemplate 改为自有瓦片代理地址。

OSM 栅格服务边界 #

JunnyOsmMap 默认显示归属标识。接入宿主时将 JunnyOsmTileLayerOptions.userAgentPackageName 改为实际应用包名;如请求经过代理, 在代理侧统一处理 User-Agent。

JunnyOsmMap(
  initialCenter: const LatLng(30.5928, 114.3055),
  tileLayerOptions: const JunnyOsmTileLayerOptions(
    userAgentPackageName: 'com.example.attendance',
  ),
  showAttribution: true,
  attributionText: '© OpenStreetMap contributors',
)

默认主源是 tile.openstreetmap.de,备用源是 tile.openstreetmap.jp。宿主可以通过 urlTemplate、fallbackUrl 和 requestTimeout 配置代理、备用地址和单瓦片超时。

搜索和逆地理编码:

final TdtSearchClient searchClient = TdtSearchClient(options: services);
final TdtSearchResult result = await searchClient.searchNearby(
  keyword: '医院',
  center: const GeoCoordinate(
    latitude: 30.5928,
    longitude: 114.3055,
    system: CoordinateSystem.gcj02,
  ),
);

final TdtGeocodingClient geocodingClient =
    TdtGeocodingClient(options: services);
final TdtReverseGeocodeResult address = await geocodingClient.reverseGeocode(
  const GeoCoordinate(
    latitude: 30.5928,
    longitude: 114.3055,
    system: CoordinateSystem.gcj02,
  ),
);

设备定位:

final LocationProvider locationProvider =
    const GeolocatorLocationProvider();
final LocationSnapshot snapshot =
  await locationProvider.getCurrentLocation(
      requestPermission: true,
      settings: const LocationRequestSettings(
        timeLimit: Duration(seconds: 15),
      ),
    );

timeLimit 建议由调用方为单次定位设置有限值;超时只表示设备没有在期限内返回新的 位置,不会自动使用过期的 last-known location。部分 Android 设备如果 fused provider 不可用,库会自动回退到系统 NETWORK_PROVIDER,继续尝试 Wi-Fi/基站定位。可以将 forceAndroidLocationManager 设为 true 进行兼容性测试;网络定位回退的等待时间和 最大缓存年龄也可以通过 networkLocationTimeLimit、maximumLocationAge 配置。

生产定位或轨迹流应显式配置位置质量策略。maximumAccuracyMeters 同时约束单次定位 和连续定位更新;rejectMockedLocation 会拒绝平台标记为模拟位置的样本,并以 LocationProviderErrorCode.insufficientAccuracy 或 mockedLocation 暴露稳定错误。默认 值分别是不限制精度和不拒绝模拟位置,以保持兼容;平台的模拟位置标记不是完整的反作弊 能力,是否允许、如何提示和如何在服务端复核由宿主决定。

const LocationRequestSettings attendanceLocationSettings =
    LocationRequestSettings(
      accuracy: LocationAccuracyPreset.high,
      timeLimit: Duration(seconds: 15),
      maximumLocationAge: Duration(minutes: 2),
      maximumAccuracyMeters: 100,
      rejectMockedLocation: true,
    );

final LocationSnapshot punchLocation =
    await locationProvider.getCurrentLocation(
      requestPermission: true,
      settings: attendanceLocationSettings,
    );
final Stream<LocationSnapshot> trackingLocations =
    locationProvider.watchLocation(settings: attendanceLocationSettings);

宿主配置 #

geolocator 不能替宿主项目声明系统权限。Android 至少需要:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

后台定位还需要按 Android 版本配置 ACCESS_BACKGROUND_LOCATION、定位类型的前台服务 和用户可见通知,并符合应用商店政策;只做前台定位不应为了复用配置而默认申请后台权限。 iOS 至少需要 NSLocationWhenInUseUsageDescription;后台定位还需要 Background Modes 的 Location updates 和对应授权。权限弹窗的展示时机由宿主控制:picker 的 locate 默认 不申请权限,用户明确触发定位时再传 requestPermission: true。

天地图 Key、服务代理、权限声明、定位失败提示和请求重试由宿主项目负责。需要隐藏 Key 时使用上面的纯代理模式。

坐标约定 #

库内业务、定位、搜索、逆地理编码和地图交互统一使用 GCJ02。 EPSG:900913 只是天地图 Web Mercator 瓦片矩阵,不是业务坐标。 TdtServiceOptions.serviceCoordinateSystem 只描述 REST 服务边界坐标,发送请求前转换, 解析响应后转换回 GCJ02。

选点控制器 #

JunnyMapPickerController 只编排状态,不创建页面或地图 Widget。宿主把输入框、地图 点击、地图视野和按钮事件转发给它,并监听 state、searchResults、selection 和 error 构建自己的 UI。控制器支持防抖搜索、视野搜索、POI 选择、点击逆地理编码、单次 定位、cancel、clear、clearSearch 和 clearSelection;旧请求完成后不会覆盖新请求 的状态。

final TdtSearchClient searchClient = TdtSearchClient(options: services);
final TdtGeocodingClient geocodingClient = TdtGeocodingClient(options: services);
final JunnyMapPickerController picker = JunnyMapPickerController(
  searchClient: searchClient,
  geocodingClient: geocodingClient,
  locationProvider: const GeolocatorLocationProvider(),
);

await picker.searchInView(
  '医院',
  bounds: const GeoBounds(
    southwest: GeoCoordinate(
      latitude: 30.3,
      longitude: 114.0,
      system: CoordinateSystem.gcj02,
    ),
    northeast: GeoCoordinate(
      latitude: 30.9,
      longitude: 114.6,
      system: CoordinateSystem.gcj02,
    ),
  ),
);
if (picker.searchResults.isNotEmpty) {
  await picker.selectPoi(picker.searchResults.first);
}
await picker.selectCoordinate(
  const GeoCoordinate(
    latitude: 30.5928,
    longitude: 114.3055,
    system: CoordinateSystem.gcj02,
  ),
);
await picker.locate(
  requestPermission: true,
  settings: attendanceLocationSettings,
);

final JunnyMapPickerSelection? selection = picker.selection;
final Map<String, Object?>? selectionPayload = selection?.toJson();

JunnyMapPickerSelection 构造和控制器结果都把坐标归一化为 GCJ02。 toJson(outputSystem: ...) 可以按需要输出指定坐标系:

字段 含义
latitude / longitude 按 outputSystem 输出
coordinateSystem 经纬度使用的坐标系
address 地址文本;没有值时为 ''
poi POI 名称;没有值时为 ''
originalLatitude / originalLongitude 内部 GCJ02 坐标
originalCoordinateSystem 内部坐标系,固定为 GCJ02
0
likes
140
points
115
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Shared Tianditu map, geocoding, POI search, and geolocator services for Flutter applications.

Repository (GitHub)

License

MIT (license)

Dependencies

dio, flutter, flutter_map, geolocator, http, latlong2, maplibre

More

Packages that depend on junny_map

Packages that implement junny_map