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:go_router/go_router.dart';
|
||||||
import 'package:latlong2/latlong.dart' show LatLng;
|
import 'package:latlong2/latlong.dart' show LatLng;
|
||||||
import 'package:rra_app/pages/map/routing/osrm_routing_service.dart';
|
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_cache.dart';
|
||||||
import 'package:rra_app/pages/map/vector/tile_pyramid_painter.dart';
|
import 'package:rra_app/pages/map/vector/tile_pyramid_painter.dart';
|
||||||
import 'package:rra_app/pages/map/widgets/toolbar.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 _isAddPointArmed = false;
|
||||||
bool _isResolvingRoute = false;
|
bool _isResolvingRoute = false;
|
||||||
|
bool _isInspectingFeature = false;
|
||||||
String? _routeError;
|
String? _routeError;
|
||||||
int _routeRequestId = 0;
|
int _routeRequestId = 0;
|
||||||
int? _draggingInsertPointIndex;
|
int? _draggingInsertPointIndex;
|
||||||
Timer? _interactionIdleTimer;
|
Timer? _interactionIdleTimer;
|
||||||
|
MapDebugHit? _debugFeatureHit;
|
||||||
|
|
||||||
late final TilePyramidPainter _pyramidPainter;
|
late final TilePyramidPainter _pyramidPainter;
|
||||||
|
|
||||||
|
|
@ -103,12 +106,25 @@ class _MapPageState extends State<MapPage> with TickerProviderStateMixin {
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_zoomTicker.dispose();
|
_zoomTicker.dispose();
|
||||||
_pyramidPainter.dispose();
|
_pyramidPainter.dispose();
|
||||||
|
MapboxVectorTileCache.clear();
|
||||||
TilePyramidCache.clear();
|
TilePyramidCache.clear();
|
||||||
SchedulerBinding.instance.removeTimingsCallback(_onFrameTimings);
|
SchedulerBinding.instance.removeTimingsCallback(_onFrameTimings);
|
||||||
_interactionIdleTimer?.cancel();
|
_interactionIdleTimer?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void reassemble() {
|
||||||
|
super.reassemble();
|
||||||
|
MapboxVectorTileCache.clear();
|
||||||
|
TilePyramidCache.clear();
|
||||||
|
_pyramidPainter.markDirty();
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_fallbackTileZoom = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void _onFrameTimings(List<FrameTiming> timings) {
|
void _onFrameTimings(List<FrameTiming> timings) {
|
||||||
for (final timing in timings) {
|
for (final timing in timings) {
|
||||||
_frameMsWindow.add(timing.totalSpan.inMicroseconds / 1000.0);
|
_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}) {
|
int _idealTileZoom(double zoomValue, {required double devicePixelRatio}) {
|
||||||
final ratio = devicePixelRatio.clamp(1.0, 4.0).toDouble();
|
final ratio = devicePixelRatio.clamp(1.0, 4.0).toDouble();
|
||||||
final adjusted = zoomValue + (math.log(ratio / 2.0) / math.ln2);
|
// Bias toward higher-detail tiles on high-DPI screens so the visible
|
||||||
return adjusted.floor().clamp(_minTileZoom, _maxTileZoom);
|
// 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) {
|
void _setZoomAroundFocal(double requestedZoom, Offset focal) {
|
||||||
|
|
@ -417,6 +435,20 @@ class _MapPageState extends State<MapPage> with TickerProviderStateMixin {
|
||||||
_resolveRoute();
|
_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() {
|
List<Widget> _buildPointWidgets() {
|
||||||
final widgets = <Widget>[];
|
final widgets = <Widget>[];
|
||||||
for (var i = 0; i < _points.length; i++) {
|
for (var i = 0; i < _points.length; i++) {
|
||||||
|
|
@ -572,11 +604,12 @@ class _MapPageState extends State<MapPage> with TickerProviderStateMixin {
|
||||||
_queuePan(details.delta);
|
_queuePan(details.delta);
|
||||||
},
|
},
|
||||||
onTapUp: (details) {
|
onTapUp: (details) {
|
||||||
if (!_isAddPointArmed ||
|
if (_draggingInsertPointIndex != null) return;
|
||||||
_draggingInsertPointIndex != null) {
|
if (_isAddPointArmed) {
|
||||||
|
_addPoint(_screenToLatLng(details.localPosition));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_addPoint(_screenToLatLng(details.localPosition));
|
_inspectFeatureAt(details.localPosition);
|
||||||
},
|
},
|
||||||
child: ClipRect(
|
child: ClipRect(
|
||||||
child: Stack(
|
child: Stack(
|
||||||
|
|
@ -658,12 +691,23 @@ class _MapPageState extends State<MapPage> with TickerProviderStateMixin {
|
||||||
alignment: Alignment.topRight,
|
alignment: Alignment.topRight,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(10),
|
padding: const EdgeInsets.all(10),
|
||||||
child: IgnorePointer(
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
IgnorePointer(
|
||||||
child: _PerformanceHud(
|
child: _PerformanceHud(
|
||||||
fps: _displayFps,
|
fps: _displayFps,
|
||||||
frameMs: _displayFrameMs,
|
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 {
|
class _PerformanceHud extends StatelessWidget {
|
||||||
const _PerformanceHud({required this.fps, required this.frameMs});
|
const _PerformanceHud({required this.fps, required this.frameMs});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,4 +68,9 @@ class MapboxVectorTileCache {
|
||||||
_cache.remove(_cache.keys.first);
|
_cache.remove(_cache.keys.first);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void clear() {
|
||||||
|
_cache.clear();
|
||||||
|
_inFlight.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,18 +17,13 @@ class _TileCoord {
|
||||||
}
|
}
|
||||||
|
|
||||||
class CachedTile {
|
class CachedTile {
|
||||||
CachedTile(this.image, this.zoomBucket);
|
CachedTile(this.image);
|
||||||
|
|
||||||
final ui.Image image;
|
final ui.Image image;
|
||||||
final int zoomBucket;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Static tile pyramid image cache. Stores rasterised ui.Image per (z, x, y).
|
// Static tile pyramid image cache. Stores one rasterised ui.Image per
|
||||||
// Each entry also records the zoom bucket it was rendered at — callers can
|
// deterministic pyramid tile coordinate (z, x, y).
|
||||||
// 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.
|
|
||||||
class TilePyramidCache {
|
class TilePyramidCache {
|
||||||
TilePyramidCache._();
|
TilePyramidCache._();
|
||||||
|
|
||||||
|
|
@ -67,7 +62,6 @@ class TilePyramidCache {
|
||||||
int z,
|
int z,
|
||||||
int x,
|
int x,
|
||||||
int y,
|
int y,
|
||||||
int zoomBucket,
|
|
||||||
Future<ui.Image?> Function() render,
|
Future<ui.Image?> Function() render,
|
||||||
void Function() onReady,
|
void Function() onReady,
|
||||||
) {
|
) {
|
||||||
|
|
@ -77,14 +71,7 @@ class TilePyramidCache {
|
||||||
if (filter != null && !filter(z, x, y)) return;
|
if (filter != null && !filter(z, x, y)) return;
|
||||||
_inFlight.add(coord);
|
_inFlight.add(coord);
|
||||||
|
|
||||||
_queue.add(
|
_queue.add(_Queued(coord: coord, render: render, onReady: onReady));
|
||||||
_Queued(
|
|
||||||
coord: coord,
|
|
||||||
zoomBucket: zoomBucket,
|
|
||||||
render: render,
|
|
||||||
onReady: onReady,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
_drain();
|
_drain();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,7 +105,7 @@ class TilePyramidCache {
|
||||||
existing.image.dispose();
|
existing.image.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
_cache[item.coord] = CachedTile(image, item.zoomBucket);
|
_cache[item.coord] = CachedTile(image);
|
||||||
_approxBytes += _estimateBytes(image);
|
_approxBytes += _estimateBytes(image);
|
||||||
_trim();
|
_trim();
|
||||||
|
|
||||||
|
|
@ -176,13 +163,11 @@ class TilePyramidCache {
|
||||||
class _Queued {
|
class _Queued {
|
||||||
const _Queued({
|
const _Queued({
|
||||||
required this.coord,
|
required this.coord,
|
||||||
required this.zoomBucket,
|
|
||||||
required this.render,
|
required this.render,
|
||||||
required this.onReady,
|
required this.onReady,
|
||||||
});
|
});
|
||||||
|
|
||||||
final _TileCoord coord;
|
final _TileCoord coord;
|
||||||
final int zoomBucket;
|
|
||||||
final Future<ui.Image?> Function() render;
|
final Future<ui.Image?> Function() render;
|
||||||
final void Function() onReady;
|
final void Function() onReady;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -197,6 +197,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "14.8.1"
|
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:
|
hive:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ dependencies:
|
||||||
go_router: ^14.8.1
|
go_router: ^14.8.1
|
||||||
http: ^1.6.0
|
http: ^1.6.0
|
||||||
shadcn_flutter: ^0.0.51
|
shadcn_flutter: ^0.0.51
|
||||||
|
google_fonts: ^6.3.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue