add google_fonts dependency, implement feature inspection on map, and enhance tile cache management
This commit is contained in:
parent
dc22ce2f46
commit
31a91a054a
7 changed files with 1536 additions and 431 deletions
26
lib/pages/map/constants.dart
Normal file
26
lib/pages/map/constants.dart
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
final Color kMapBackgroundColor = Color(0xFFA7BFDB);
|
||||
|
||||
final Color kMapLanduseColor = Color(0xFFE500FE);
|
||||
final Color kMapGrassColor = Color(0xFFA2CE83);
|
||||
final Color kMapWaterColor = Color(0xFF74B8ED);
|
||||
final Color kMapBuildingFillColor = Color(0xFFFED000);
|
||||
final Color kMapBuildingOutlineColor = Color(0xFFE7B600);
|
||||
|
||||
final Color kMapRoadFillColor = Color(0xFFFEFEFF);
|
||||
final Color kMapPathFillColour = Color(0xFFD9E0F0);
|
||||
|
||||
final Color kMapDebugTileBoundaryColor = Color(0xCCFF2D55);
|
||||
|
||||
final Color kMapPlaceLabelColor = Color(0xFF1F2636);
|
||||
final Color kMapPlaceLabelHaloColor = Color(0xF7FFFFFF);
|
||||
|
||||
final Color kMapRoadLabelColor = Color(0xFF56657A);
|
||||
final Color kMapRoadLabelHaloColor = Color(0xF2FFFFFF);
|
||||
|
||||
final Color kMapRailFillColor = Color(0xFF032D51);
|
||||
final Color kMapRailUnderlayColor = Color(0xFF8A97A8);
|
||||
|
||||
final Color kMapPoiLabelColor = Color(0xFF032D51);
|
||||
final Color kMapPoiLabelHaloColor = Color(0xF2FFFFFF);
|
||||
|
|
@ -14,6 +14,7 @@ import 'package:flutter/scheduler.dart';
|
|||
import 'package:go_router/go_router.dart';
|
||||
import 'package:latlong2/latlong.dart' show LatLng;
|
||||
import 'package:rra_app/pages/map/routing/osrm_routing_service.dart';
|
||||
import 'package:rra_app/pages/map/vector/mapbox_vector_tile_cache.dart';
|
||||
import 'package:rra_app/pages/map/vector/tile_pyramid_cache.dart';
|
||||
import 'package:rra_app/pages/map/vector/tile_pyramid_painter.dart';
|
||||
import 'package:rra_app/pages/map/widgets/toolbar.dart';
|
||||
|
|
@ -72,10 +73,12 @@ class _MapPageState extends State<MapPage> with TickerProviderStateMixin {
|
|||
|
||||
bool _isAddPointArmed = false;
|
||||
bool _isResolvingRoute = false;
|
||||
bool _isInspectingFeature = false;
|
||||
String? _routeError;
|
||||
int _routeRequestId = 0;
|
||||
int? _draggingInsertPointIndex;
|
||||
Timer? _interactionIdleTimer;
|
||||
MapDebugHit? _debugFeatureHit;
|
||||
|
||||
late final TilePyramidPainter _pyramidPainter;
|
||||
|
||||
|
|
@ -103,12 +106,25 @@ class _MapPageState extends State<MapPage> with TickerProviderStateMixin {
|
|||
void dispose() {
|
||||
_zoomTicker.dispose();
|
||||
_pyramidPainter.dispose();
|
||||
MapboxVectorTileCache.clear();
|
||||
TilePyramidCache.clear();
|
||||
SchedulerBinding.instance.removeTimingsCallback(_onFrameTimings);
|
||||
_interactionIdleTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void reassemble() {
|
||||
super.reassemble();
|
||||
MapboxVectorTileCache.clear();
|
||||
TilePyramidCache.clear();
|
||||
_pyramidPainter.markDirty();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_fallbackTileZoom = null;
|
||||
});
|
||||
}
|
||||
|
||||
void _onFrameTimings(List<FrameTiming> timings) {
|
||||
for (final timing in timings) {
|
||||
_frameMsWindow.add(timing.totalSpan.inMicroseconds / 1000.0);
|
||||
|
|
@ -256,8 +272,10 @@ class _MapPageState extends State<MapPage> with TickerProviderStateMixin {
|
|||
|
||||
int _idealTileZoom(double zoomValue, {required double devicePixelRatio}) {
|
||||
final ratio = devicePixelRatio.clamp(1.0, 4.0).toDouble();
|
||||
final adjusted = zoomValue + (math.log(ratio / 2.0) / math.ln2);
|
||||
return adjusted.floor().clamp(_minTileZoom, _maxTileZoom);
|
||||
// Bias toward higher-detail tiles on high-DPI screens so the visible
|
||||
// raster layer matches the viewport density more closely.
|
||||
final adjusted = zoomValue + (math.log(ratio) / math.ln2);
|
||||
return adjusted.round().clamp(_minTileZoom, _maxTileZoom);
|
||||
}
|
||||
|
||||
void _setZoomAroundFocal(double requestedZoom, Offset focal) {
|
||||
|
|
@ -417,6 +435,20 @@ class _MapPageState extends State<MapPage> with TickerProviderStateMixin {
|
|||
_resolveRoute();
|
||||
}
|
||||
|
||||
Future<void> _inspectFeatureAt(Offset localPosition) async {
|
||||
setState(() {
|
||||
_isInspectingFeature = true;
|
||||
});
|
||||
|
||||
final hit = await _pyramidPainter.inspectFeatureAt(localPosition);
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() {
|
||||
_debugFeatureHit = hit;
|
||||
_isInspectingFeature = false;
|
||||
});
|
||||
}
|
||||
|
||||
List<Widget> _buildPointWidgets() {
|
||||
final widgets = <Widget>[];
|
||||
for (var i = 0; i < _points.length; i++) {
|
||||
|
|
@ -572,11 +604,12 @@ class _MapPageState extends State<MapPage> with TickerProviderStateMixin {
|
|||
_queuePan(details.delta);
|
||||
},
|
||||
onTapUp: (details) {
|
||||
if (!_isAddPointArmed ||
|
||||
_draggingInsertPointIndex != null) {
|
||||
if (_draggingInsertPointIndex != null) return;
|
||||
if (_isAddPointArmed) {
|
||||
_addPoint(_screenToLatLng(details.localPosition));
|
||||
return;
|
||||
}
|
||||
_addPoint(_screenToLatLng(details.localPosition));
|
||||
_inspectFeatureAt(details.localPosition);
|
||||
},
|
||||
child: ClipRect(
|
||||
child: Stack(
|
||||
|
|
@ -658,11 +691,22 @@ class _MapPageState extends State<MapPage> with TickerProviderStateMixin {
|
|||
alignment: Alignment.topRight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: IgnorePointer(
|
||||
child: _PerformanceHud(
|
||||
fps: _displayFps,
|
||||
frameMs: _displayFrameMs,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
IgnorePointer(
|
||||
child: _PerformanceHud(
|
||||
fps: _displayFps,
|
||||
frameMs: _displayFrameMs,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_DebugInspectPanel(
|
||||
hit: _debugFeatureHit,
|
||||
isInspecting: _isInspectingFeature,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -750,6 +794,83 @@ class _HudCornerNotice extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class _DebugInspectPanel extends StatelessWidget {
|
||||
const _DebugInspectPanel({required this.hit, required this.isInspecting});
|
||||
|
||||
final MapDebugHit? hit;
|
||||
final bool isInspecting;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!isInspecting && hit == null) return const SizedBox.shrink();
|
||||
|
||||
final rows = <String>[
|
||||
if (hit != null) 'layer: ${hit!.layerName}',
|
||||
if (hit != null) 'type: ${hit!.geometryType.name}',
|
||||
if (hit != null) 'tile: z${hit!.tileZ}/${hit!.tileX}/${hit!.tileY}',
|
||||
if (hit != null) 'distance: ${hit!.distance.toStringAsFixed(1)}',
|
||||
if (hit != null)
|
||||
...hit!.properties.entries.map(
|
||||
(entry) => '${entry.key}: ${entry.value}',
|
||||
),
|
||||
];
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 320),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.94),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: const Color(0x22000000)),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x22000000),
|
||||
blurRadius: 10,
|
||||
spreadRadius: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: DefaultTextStyle(
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
height: 1.35,
|
||||
color: Color(0xFF1F2937),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
isInspecting ? 'Inspecting map...' : 'Map Inspect',
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Color(0xFF111827),
|
||||
),
|
||||
),
|
||||
if (!isInspecting && hit == null)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 6),
|
||||
child: Text('Click the map to inspect a feature.'),
|
||||
),
|
||||
if (rows.isNotEmpty) const SizedBox(height: 6),
|
||||
for (final row in rows)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 2),
|
||||
child: Text(row),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PerformanceHud extends StatelessWidget {
|
||||
const _PerformanceHud({required this.fps, required this.frameMs});
|
||||
|
||||
|
|
|
|||
|
|
@ -68,4 +68,9 @@ class MapboxVectorTileCache {
|
|||
_cache.remove(_cache.keys.first);
|
||||
}
|
||||
}
|
||||
|
||||
static void clear() {
|
||||
_cache.clear();
|
||||
_inFlight.clear();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,18 +17,13 @@ class _TileCoord {
|
|||
}
|
||||
|
||||
class CachedTile {
|
||||
CachedTile(this.image, this.zoomBucket);
|
||||
CachedTile(this.image);
|
||||
|
||||
final ui.Image image;
|
||||
final int zoomBucket;
|
||||
}
|
||||
|
||||
// Static tile pyramid image cache. Stores rasterised ui.Image per (z, x, y).
|
||||
// Each entry also records the zoom bucket it was rendered at — callers can
|
||||
// check whether a re-render is warranted, but the old image stays as a
|
||||
// fallback until the new one arrives.
|
||||
//
|
||||
// Ancestor lookup (for fallback blitting) just calls peek() at a coarser z.
|
||||
// Static tile pyramid image cache. Stores one rasterised ui.Image per
|
||||
// deterministic pyramid tile coordinate (z, x, y).
|
||||
class TilePyramidCache {
|
||||
TilePyramidCache._();
|
||||
|
||||
|
|
@ -67,7 +62,6 @@ class TilePyramidCache {
|
|||
int z,
|
||||
int x,
|
||||
int y,
|
||||
int zoomBucket,
|
||||
Future<ui.Image?> Function() render,
|
||||
void Function() onReady,
|
||||
) {
|
||||
|
|
@ -77,14 +71,7 @@ class TilePyramidCache {
|
|||
if (filter != null && !filter(z, x, y)) return;
|
||||
_inFlight.add(coord);
|
||||
|
||||
_queue.add(
|
||||
_Queued(
|
||||
coord: coord,
|
||||
zoomBucket: zoomBucket,
|
||||
render: render,
|
||||
onReady: onReady,
|
||||
),
|
||||
);
|
||||
_queue.add(_Queued(coord: coord, render: render, onReady: onReady));
|
||||
_drain();
|
||||
}
|
||||
|
||||
|
|
@ -118,7 +105,7 @@ class TilePyramidCache {
|
|||
existing.image.dispose();
|
||||
}
|
||||
|
||||
_cache[item.coord] = CachedTile(image, item.zoomBucket);
|
||||
_cache[item.coord] = CachedTile(image);
|
||||
_approxBytes += _estimateBytes(image);
|
||||
_trim();
|
||||
|
||||
|
|
@ -176,13 +163,11 @@ class TilePyramidCache {
|
|||
class _Queued {
|
||||
const _Queued({
|
||||
required this.coord,
|
||||
required this.zoomBucket,
|
||||
required this.render,
|
||||
required this.onReady,
|
||||
});
|
||||
|
||||
final _TileCoord coord;
|
||||
final int zoomBucket;
|
||||
final Future<ui.Image?> Function() render;
|
||||
final void Function() onReady;
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -197,6 +197,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.8.1"
|
||||
google_fonts:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: google_fonts
|
||||
sha256: ba03d03bcaa2f6cb7bd920e3b5027181db75ab524f8891c8bc3aa603885b8055
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.3"
|
||||
hive:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ dependencies:
|
|||
go_router: ^14.8.1
|
||||
http: ^1.6.0
|
||||
shadcn_flutter: ^0.0.51
|
||||
google_fonts: ^6.3.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue