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

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
- Dropdown on "refine tags" page to select acceptable options for camera:mount=
- Tutorial / info guide before submitting first node
- Link to OSM node in node_details_sheet
- 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 "open in other editor" for advanced edits: StreetComplete/EveryDoor/Vespucci/GO!! Map/OSM.org(iD)/Rapid/Level0/OSMand/OrganicMaps/CoMaps
### On Pause
- Suspected locations expansion to more regions

View File

@@ -1,6 +1,6 @@
{
"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": {
"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"

View File

@@ -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')),
);
}
}
}
}

View File

@@ -8,6 +8,7 @@ import '../models/operator_profile.dart';
import '../services/localization_service.dart';
import '../state/settings_state.dart';
import 'refine_tags_sheet.dart';
import 'advanced_edit_options_sheet.dart';
class EditNodeSheet extends StatelessWidget {
const EditNodeSheet({super.key, required this.session});
@@ -214,15 +215,33 @@ class EditNodeSheet extends StatelessWidget {
if (session.originalNode.isConstrained)
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
child: Row(
child: Column(
children: [
const Icon(Icons.info_outline, size: 20),
const SizedBox(width: 8),
Expanded(
child: Text(
locService.t('editNode.cannotMoveConstrainedNode'),
style: Theme.of(context).textTheme.bodyMedium,
),
Row(
children: [
const Icon(Icons.info_outline, size: 20),
const SizedBox(width: 8),
Expanded(
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),
);
}
}

View File

@@ -1,8 +1,10 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import '../models/osm_node.dart';
import '../app_state.dart';
import '../services/localization_service.dart';
import 'advanced_edit_options_sheet.dart';
class NodeTagSheet extends StatelessWidget {
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(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20),
@@ -108,6 +140,30 @@ class NodeTagSheet extends StatelessWidget {
),
),
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(
mainAxisAlignment: MainAxisAlignment.end,
children: [