Link to OSM in node_details sheet. Add option to open node in other editors.

This commit is contained in:
stopflock
2025-11-16 16:45:54 -06:00
parent 3ea6d6b2ff
commit 05eedbb910
5 changed files with 247 additions and 11 deletions
-2
View File
@@ -107,10 +107,8 @@ cp lib/keys.dart.example lib/keys.dart
- Persistent cache for MY submissions: clean up when we see that node appear in overpass results or when older than 24h - Persistent cache for MY submissions: clean up when we see that node appear in overpass results or when older than 24h
- Dropdown on "refine tags" page to select acceptable options for camera:mount= - Dropdown on "refine tags" page to select acceptable options for camera:mount=
- Tutorial / info guide before submitting first node - Tutorial / info guide before submitting first node
- Link to OSM node in node_details_sheet
- Link to "my changes" on osm (username edit history) - Link to "my changes" on osm (username edit history)
- Option to "extract node from way" for nodes attached to a way to allow moving - Option to "extract node from way" for nodes attached to a way to allow moving
- Option to "open in other editor" for advanced edits: StreetComplete/EveryDoor/Vespucci/GO!! Map/OSM.org(iD)/Rapid/Level0/OSMand/OrganicMaps/CoMaps
### On Pause ### On Pause
- Suspected locations expansion to more regions - Suspected locations expansion to more regions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"1.3.3": { "1.3.3": {
"content": "• NEW: Added builtin surveillance device profiles for Rekor and Axis Communications ALPR cameras" "content": "• NEW: Added builtin surveillance device profiles for Rekor and Axis Communications ALPR cameras\n• NEW: Smart constraint system prevents moving nodes that are part of OSM ways/relations to protect data integrity\n• NEW: \"View on OSM\" button in node details opens the node page on OpenStreetMap.org\n• NEW: \"Advanced Edit\" options with links to iD Editor, RapiD, Vespucci, StreetComplete, Go Map!!, and EveryDoor\n• IMPROVED: Network status messages are now fully localized in all supported languages\n• IMPROVED: Map interaction controls for constrained nodes - zoom/rotate allowed, movement prevented\n• IMPROVED: Debounced snap-back system keeps constrained nodes centered smoothly\n• FIXED: Eliminated duplicate changelog service calls on app updates\n• FIXED: Network status now correctly says \"Node data slow\" instead of \"Camera data slow\""
}, },
"1.3.2": { "1.3.2": {
"content": "• HOTFIX: Temporarily disabled node editing to prevent OSM database issues while a bug is resolved\n• UX: Fixed Android navigation bar covering settings page content" "content": "• HOTFIX: Temporarily disabled node editing to prevent OSM database issues while a bug is resolved\n• UX: Fixed Android navigation bar covering settings page content"
@@ -0,0 +1,155 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import '../models/osm_node.dart';
import '../services/localization_service.dart';
class AdvancedEditOptionsSheet extends StatelessWidget {
final OsmNode node;
const AdvancedEditOptionsSheet({super.key, required this.node});
@override
Widget build(BuildContext context) {
final locService = LocalizationService.instance;
return SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Advanced Editing Options',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
'These editors offer more advanced features for complex edits.',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
const SizedBox(height: 16),
// Web Editors Section
Text(
'Web Editors',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
_buildEditorTile(
context: context,
icon: Icons.public,
title: 'iD Editor',
subtitle: 'Full-featured web editor - always works',
onTap: () => _launchEditor(context, 'https://www.openstreetmap.org/edit?editor=id&node=${node.id}'),
),
_buildEditorTile(
context: context,
icon: Icons.speed,
title: 'RapiD Editor',
subtitle: 'AI-assisted editing with Facebook data',
onTap: () => _launchEditor(context, 'https://rapideditor.org/edit#map=19/0/0&nodes=${node.id}'),
),
const SizedBox(height: 16),
// Mobile Editors Section
Text(
'Mobile Editors',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
if (Platform.isAndroid) ...[
_buildEditorTile(
context: context,
icon: Icons.android,
title: 'Vespucci',
subtitle: 'Advanced Android OSM editor',
onTap: () => _launchEditor(context, 'vespucci://edit?node=${node.id}'),
),
_buildEditorTile(
context: context,
icon: Icons.place,
title: 'StreetComplete',
subtitle: 'Survey-based mapping app',
onTap: () => _launchEditor(context, 'streetcomplete://quest?node=${node.id}'),
),
_buildEditorTile(
context: context,
icon: Icons.map,
title: 'EveryDoor',
subtitle: 'Fast POI editing',
onTap: () => _launchEditor(context, 'everydoor://edit?node=${node.id}'),
),
],
if (Platform.isIOS) ...[
_buildEditorTile(
context: context,
icon: Icons.phone_iphone,
title: 'Go Map!!',
subtitle: 'iOS OSM editor',
onTap: () => _launchEditor(context, 'gomaposm://edit?node=${node.id}'),
),
],
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(locService.t('actions.close')),
),
],
),
],
),
),
);
}
Widget _buildEditorTile({
required BuildContext context,
required IconData icon,
required String title,
required String subtitle,
required VoidCallback onTap,
}) {
return ListTile(
leading: Icon(icon, size: 24),
title: Text(title),
subtitle: Text(subtitle),
trailing: const Icon(Icons.launch, size: 18),
onTap: onTap,
contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 4),
);
}
void _launchEditor(BuildContext context, String url) async {
Navigator.pop(context); // Close the sheet first
try {
final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
} else {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Could not open $url - app may not be installed')),
);
}
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Could not open editor - app may not be installed')),
);
}
}
}
}
+35 -8
View File
@@ -8,6 +8,7 @@ import '../models/operator_profile.dart';
import '../services/localization_service.dart'; import '../services/localization_service.dart';
import '../state/settings_state.dart'; import '../state/settings_state.dart';
import 'refine_tags_sheet.dart'; import 'refine_tags_sheet.dart';
import 'advanced_edit_options_sheet.dart';
class EditNodeSheet extends StatelessWidget { class EditNodeSheet extends StatelessWidget {
const EditNodeSheet({super.key, required this.session}); const EditNodeSheet({super.key, required this.session});
@@ -214,15 +215,33 @@ class EditNodeSheet extends StatelessWidget {
if (session.originalNode.isConstrained) if (session.originalNode.isConstrained)
Padding( Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
child: Row( child: Column(
children: [ children: [
const Icon(Icons.info_outline, size: 20), Row(
const SizedBox(width: 8), children: [
Expanded( const Icon(Icons.info_outline, size: 20),
child: Text( const SizedBox(width: 8),
locService.t('editNode.cannotMoveConstrainedNode'), Expanded(
style: Theme.of(context).textTheme.bodyMedium, child: Text(
), locService.t('editNode.cannotMoveConstrainedNode'),
style: Theme.of(context).textTheme.bodyMedium,
),
),
],
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
OutlinedButton.icon(
onPressed: () => _openAdvancedEdit(context),
icon: const Icon(Icons.open_in_new, size: 16),
label: const Text('Use Advanced Editor'),
style: OutlinedButton.styleFrom(
minimumSize: const Size(0, 32),
),
),
],
), ),
], ],
), ),
@@ -349,4 +368,12 @@ class EditNodeSheet extends StatelessWidget {
}, },
); );
} }
void _openAdvancedEdit(BuildContext context) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => AdvancedEditOptionsSheet(node: session.originalNode),
);
}
} }
+56
View File
@@ -1,8 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import '../models/osm_node.dart'; import '../models/osm_node.dart';
import '../app_state.dart'; import '../app_state.dart';
import '../services/localization_service.dart'; import '../services/localization_service.dart';
import 'advanced_edit_options_sheet.dart';
class NodeTagSheet extends StatelessWidget { class NodeTagSheet extends StatelessWidget {
final OsmNode node; final OsmNode node;
@@ -67,6 +69,36 @@ class NodeTagSheet extends StatelessWidget {
} }
} }
void _viewOnOSM() async {
final url = 'https://www.openstreetmap.org/node/${node.id}';
try {
final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
} else {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Could not open OSM website')),
);
}
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Could not open OSM website')),
);
}
}
}
void _openAdvancedEdit() {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => AdvancedEditOptionsSheet(node: node),
);
}
return SafeArea( return SafeArea(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20), padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20),
@@ -108,6 +140,30 @@ class NodeTagSheet extends StatelessWidget {
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// First row: View and Advanced buttons
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton.icon(
onPressed: () => _viewOnOSM(),
icon: const Icon(Icons.open_in_new, size: 16),
label: const Text('View on OSM'),
),
const SizedBox(width: 8),
if (isEditable) ...[
OutlinedButton.icon(
onPressed: _openAdvancedEdit,
icon: const Icon(Icons.open_in_new, size: 18),
label: const Text('Advanced'),
style: OutlinedButton.styleFrom(
minimumSize: const Size(0, 36),
),
),
],
],
),
const SizedBox(height: 8),
// Second row: Edit, Delete, and Close buttons
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [