103 lines
3.2 KiB
Dart
103 lines
3.2 KiB
Dart
import 'dart:async';
|
||
|
||
import 'package:flutter/widgets.dart';
|
||
import 'package:http/http.dart' as http;
|
||
|
||
/// 底图来源:中文用户用 ESRI,非中文用户用 Google
|
||
enum BaseMapProvider { esri, google }
|
||
|
||
class BaseMapConfig {
|
||
const BaseMapConfig({
|
||
required this.provider,
|
||
required this.maxNativeZoom,
|
||
this.urlTemplate,
|
||
this.googleLyr,
|
||
});
|
||
|
||
final BaseMapProvider provider;
|
||
|
||
/// 原生最大层级:超出后由 flutter_map 用低级别瓦片拉伸,避免整片空白
|
||
final int maxNativeZoom;
|
||
|
||
/// ESRI XYZ 模板(WGS84,与设备 GPS 同一坐标系)
|
||
final String? urlTemplate;
|
||
|
||
/// Google 瓦片图层:s=纯卫星 / h=地名 / y=卫星+地名
|
||
final String? googleLyr;
|
||
|
||
static const BaseMapConfig esri = BaseMapConfig(
|
||
provider: BaseMapProvider.esri,
|
||
maxNativeZoom: 19,
|
||
urlTemplate:
|
||
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
|
||
);
|
||
|
||
/// lyrs=y 让境外用户一张图同时拿到卫星影像 + 英文地名(hl=en)
|
||
static const BaseMapConfig google = BaseMapConfig(
|
||
provider: BaseMapProvider.google,
|
||
maxNativeZoom: 20,
|
||
googleLyr: 'y',
|
||
);
|
||
|
||
/// 规则:中文 → ESRI;其余语言 → Google
|
||
static BaseMapConfig of(Locale locale) =>
|
||
locale.languageCode == 'zh' ? esri : google;
|
||
}
|
||
|
||
/// Google 瓦片可达性探测:失败后允许定期重试,避免一次探测失败就永久降级
|
||
class GoogleTileAvailability {
|
||
/// 失败后的重试冷却时长:开/关 VPN、切换网络后无需重启 App
|
||
static const Duration retryAfter = Duration(seconds: 30);
|
||
|
||
static Future<bool>? _cached;
|
||
static bool? _lastResult;
|
||
static DateTime? _lastFailAt;
|
||
|
||
static Future<bool> reachable() {
|
||
final cached = _cached;
|
||
if (cached != null) {
|
||
final failedAt = _lastFailAt;
|
||
final shouldRetry = _lastResult == false &&
|
||
failedAt != null &&
|
||
DateTime.now().difference(failedAt) >= retryAfter;
|
||
// 继续复用同一个 Future 对象,避免 FutureBuilder 因 future 变化而反复重建
|
||
if (!shouldRetry) return cached;
|
||
}
|
||
final future = _probe();
|
||
_cached = future;
|
||
return future;
|
||
}
|
||
|
||
/// 手动重试(例如用户点「刷新地图」、App 从后台恢复)
|
||
static void reset() {
|
||
_cached = null;
|
||
_lastResult = null;
|
||
_lastFailAt = null;
|
||
}
|
||
|
||
static Future<bool> _probe() async {
|
||
final sw = Stopwatch()..start();
|
||
bool ok;
|
||
try {
|
||
final res = await http
|
||
.get(Uri.parse('https://mt0.google.com/vt/lyrs=s&hl=en&x=1&y=0&z=1'))
|
||
.timeout(const Duration(seconds: 3));
|
||
ok = res.statusCode == 200 && res.bodyBytes.isNotEmpty;
|
||
debugPrint(
|
||
'🗺️ [BaseMap] Google 探测: http=${res.statusCode} '
|
||
'bytes=${res.bodyBytes.length} ${sw.elapsedMilliseconds}ms '
|
||
'→ ${ok ? '可用,英文底图用 Google' : '不可用,回退 ESRI'}',
|
||
);
|
||
} catch (e) {
|
||
ok = false;
|
||
debugPrint(
|
||
'🗺️ [BaseMap] Google 探测异常: ${e.runtimeType} '
|
||
'${sw.elapsedMilliseconds}ms → 回退 ESRI',
|
||
);
|
||
}
|
||
_lastResult = ok;
|
||
_lastFailAt = ok ? null : DateTime.now();
|
||
return ok;
|
||
}
|
||
}
|