Monolithic reimplementation of node fetching from overpass/offline areas. Prevent submissions in areas without cache coverage. Also fixes offline node loading.

This commit is contained in:
stopflock
2026-01-30 21:34:55 -06:00
parent 4a36c52982
commit 79d2fe711d
13 changed files with 206 additions and 15 deletions
+54 -1
View File
@@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:latlong2/latlong.dart';
import 'package:flutter_map/flutter_map.dart';
import '../app_state.dart';
import '../dev_config.dart';
@@ -8,6 +10,7 @@ import '../models/node_profile.dart';
import '../models/operator_profile.dart';
import '../services/localization_service.dart';
import '../services/map_data_provider.dart';
import '../services/node_data_manager.dart';
import '../services/changelog_service.dart';
import 'refine_tags_sheet.dart';
import 'proximity_warning_dialog.dart';
@@ -31,6 +34,13 @@ class _AddNodeSheetState extends State<AddNodeSheet> {
void initState() {
super.initState();
_checkTutorialStatus();
// Listen to node data manager for cache updates
NodeDataManager().addListener(_onCacheUpdated);
}
void _onCacheUpdated() {
// Rebuild when cache updates (e.g., when new data loads)
if (mounted) setState(() {});
}
Future<void> _checkTutorialStatus() async {
@@ -76,6 +86,9 @@ class _AddNodeSheetState extends State<AddNodeSheet> {
@override
void dispose() {
// Remove listener
NodeDataManager().removeListener(_onCacheUpdated);
// Clear tutorial callback when widget is disposed
if (_showTutorial) {
try {
@@ -401,10 +414,34 @@ class _AddNodeSheetState extends State<AddNodeSheet> {
final session = widget.session;
final submittableProfiles = appState.enabledProfiles.where((p) => p.isSubmittable).toList();
// Check if we have good cache coverage around the node position
bool hasGoodCoverage = true;
if (session.target != null) {
// Create a small bounds around the target position to check coverage
const double bufferDegrees = 0.001; // ~100m buffer
final targetBounds = LatLngBounds(
LatLng(session.target!.latitude - bufferDegrees, session.target!.longitude - bufferDegrees),
LatLng(session.target!.latitude + bufferDegrees, session.target!.longitude + bufferDegrees),
);
hasGoodCoverage = MapDataProvider().hasGoodCoverageFor(targetBounds);
// If strict coverage check fails, fall back to checking if we have any nodes nearby
// This handles the timing issue where cache might not be marked as "covered" yet
if (!hasGoodCoverage) {
final nearbyNodes = MapDataProvider().findNodesWithinDistance(
session.target!,
200.0, // 200m radius - if we have nodes nearby, we likely have good data
);
hasGoodCoverage = nearbyNodes.isNotEmpty;
}
}
final allowSubmit = appState.isLoggedIn &&
submittableProfiles.isNotEmpty &&
session.profile != null &&
session.profile!.isSubmittable;
session.profile!.isSubmittable &&
hasGoodCoverage;
void _navigateToLogin() {
Navigator.pushNamed(context, '/settings/osm-account');
@@ -517,6 +554,22 @@ class _AddNodeSheetState extends State<AddNodeSheet> {
),
],
),
)
else if (!hasGoodCoverage)
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
child: Row(
children: [
const Icon(Icons.cloud_download, color: Colors.blue, size: 20),
const SizedBox(width: 6),
Expanded(
child: Text(
locService.t('addNode.loadingAreaData'),
style: const TextStyle(color: Colors.blue, fontSize: 13),
),
),
],
),
),
const SizedBox(height: 8),
Padding(
+53 -1
View File
@@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:latlong2/latlong.dart';
import 'package:flutter_map/flutter_map.dart';
import '../app_state.dart';
import '../dev_config.dart';
@@ -8,6 +10,7 @@ import '../models/node_profile.dart';
import '../models/operator_profile.dart';
import '../services/localization_service.dart';
import '../services/map_data_provider.dart';
import '../services/node_data_manager.dart';
import '../services/changelog_service.dart';
import '../state/settings_state.dart';
import 'refine_tags_sheet.dart';
@@ -33,6 +36,13 @@ class _EditNodeSheetState extends State<EditNodeSheet> {
void initState() {
super.initState();
_checkTutorialStatus();
// Listen to node data manager for cache updates
NodeDataManager().addListener(_onCacheUpdated);
}
void _onCacheUpdated() {
// Rebuild when cache updates (e.g., when new data loads)
if (mounted) setState(() {});
}
Future<void> _checkTutorialStatus() async {
@@ -59,8 +69,12 @@ class _EditNodeSheetState extends State<EditNodeSheet> {
}
}
@override
@override
void dispose() {
// Remove listener
NodeDataManager().removeListener(_onCacheUpdated);
// Clear tutorial callback when widget is disposed
if (_showTutorial) {
try {
@@ -287,11 +301,33 @@ class _EditNodeSheetState extends State<EditNodeSheet> {
final session = widget.session;
final submittableProfiles = appState.enabledProfiles.where((p) => p.isSubmittable).toList();
final isSandboxMode = appState.uploadMode == UploadMode.sandbox;
// Check if we have good cache coverage around the node position
bool hasGoodCoverage = true;
final nodeCoord = session.originalNode.coord;
const double bufferDegrees = 0.001; // ~100m buffer
final targetBounds = LatLngBounds(
LatLng(nodeCoord.latitude - bufferDegrees, nodeCoord.longitude - bufferDegrees),
LatLng(nodeCoord.latitude + bufferDegrees, nodeCoord.longitude + bufferDegrees),
);
hasGoodCoverage = MapDataProvider().hasGoodCoverageFor(targetBounds);
// If strict coverage check fails, fall back to checking if we have any nodes nearby
// This handles the timing issue where cache might not be marked as "covered" yet
if (!hasGoodCoverage) {
final nearbyNodes = MapDataProvider().findNodesWithinDistance(
nodeCoord,
200.0, // 200m radius - if we have nodes nearby, we likely have good data
);
hasGoodCoverage = nearbyNodes.isNotEmpty;
}
final allowSubmit = kEnableNodeEdits &&
appState.isLoggedIn &&
submittableProfiles.isNotEmpty &&
session.profile != null &&
session.profile!.isSubmittable;
session.profile!.isSubmittable &&
hasGoodCoverage;
void _navigateToLogin() {
Navigator.pushNamed(context, '/settings/osm-account');
@@ -478,6 +514,22 @@ class _EditNodeSheetState extends State<EditNodeSheet> {
),
],
),
)
else if (!hasGoodCoverage)
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
child: Row(
children: [
const Icon(Icons.cloud_download, color: Colors.blue, size: 20),
const SizedBox(width: 6),
Expanded(
child: Text(
locService.t('editNode.loadingAreaData'),
style: const TextStyle(color: Colors.blue, fontSize: 13),
),
),
],
),
),
const SizedBox(height: 8),
Padding(
+2 -3
View File
@@ -19,12 +19,12 @@ class NodeProviderWithCache extends ChangeNotifier {
NodeProviderWithCache._internal();
final NodeDataManager _nodeDataManager = NodeDataManager();
final NodeSpatialCache _cache = NodeSpatialCache();
Timer? _debounceTimer;
/// Get cached nodes for the given bounds, filtered by enabled profiles
List<OsmNode> getCachedNodesForBounds(LatLngBounds bounds) {
final allNodes = _cache.getNodesFor(bounds);
// Use the same cache instance as NodeDataManager
final allNodes = NodeSpatialCache().getNodesFor(bounds);
final enabledProfiles = AppState.instance.enabledProfiles;
// If no profiles are enabled, show no nodes
@@ -68,7 +68,6 @@ class NodeProviderWithCache extends ChangeNotifier {
/// Clear the cache and repopulate with pending nodes from upload queue
void clearCache() {
_cache.clear();
_nodeDataManager.clearCache();
// Repopulate with pending nodes from upload queue if available
_repopulatePendingNodesAfterClear();