web3terms/lib/main.dart

330 lines
11 KiB
Dart
Raw Normal View History

2025-02-18 21:15:53 +02:00
import 'package:flutter/material.dart';
2025-02-18 21:45:30 +02:00
import 'package:flutter/services.dart';
2025-02-18 21:15:53 +02:00
import 'package:flutter_map/flutter_map.dart';
2025-02-18 21:45:30 +02:00
import 'package:go_router/go_router.dart';
2025-02-19 10:38:55 +02:00
import 'package:google_fonts/google_fonts.dart';
2025-02-18 21:15:53 +02:00
import 'package:latlong2/latlong.dart';
import 'package:geolocator/geolocator.dart';
import 'package:map3terms/src/map3terms_scrambler.dart';
void main() {
2025-02-18 21:45:30 +02:00
runApp(Map3TermsApp());
2025-02-18 21:15:53 +02:00
}
2025-02-18 21:45:30 +02:00
class Map3TermsApp extends StatelessWidget {
2025-02-18 21:15:53 +02:00
@override
Widget build(BuildContext context) {
2025-02-18 21:45:30 +02:00
return MaterialApp.router(
title: 'Map3Terms Map',
2025-02-18 21:15:53 +02:00
theme: ThemeData.dark().copyWith(
2025-02-19 10:38:55 +02:00
primaryColor: Color(0xff5471a8),
scaffoldBackgroundColor: Color(0xff2d3138),
textTheme: GoogleFonts.poppinsTextTheme(),
2025-02-18 21:15:53 +02:00
),
2025-02-18 21:45:30 +02:00
routerConfig: _router,
2025-02-18 21:15:53 +02:00
);
}
}
2025-02-18 21:45:30 +02:00
class Map3TermsHome extends StatefulWidget {
final String? initialWords;
Map3TermsHome({this.initialWords});
2025-02-18 21:15:53 +02:00
@override
2025-02-18 21:45:30 +02:00
_Map3TermsHomeState createState() => _Map3TermsHomeState();
2025-02-18 21:15:53 +02:00
}
2025-02-18 21:45:30 +02:00
class _Map3TermsHomeState extends State<Map3TermsHome> {
2025-02-18 21:15:53 +02:00
final TextEditingController _inputController = TextEditingController();
final MapController _mapController = MapController();
LatLng _center = LatLng(51.50844113, -0.116708278); // Default: London
@override
void initState() {
super.initState();
2025-02-18 21:45:30 +02:00
WidgetsBinding.instance.addPostFrameCallback((_) async {
if (widget.initialWords != null && widget.initialWords!.isNotEmpty) {
await _initializeFromURL(widget.initialWords!);
} else {
await _updateWordsFromCenter();
}
});
}
Future<void> _initializeFromURL(String terms) async {
terms = terms.replaceAll("-", " "); // Convert URL-friendly format
try {
final coords = await wordsToCoord(terms);
setState(() {
_center = LatLng(coords[0], coords[1]);
_mapController.move(_center, 19.0);
_inputController.text = terms.replaceAll(".", " "); // Show spaces instead of dots
});
} catch (e) {
print("Error processing terms from URL: $e");
}
2025-02-18 21:15:53 +02:00
}
2025-02-18 21:45:30 +02:00
/// **Handles input change: Detects if input is coordinates or terms**
2025-02-18 21:15:53 +02:00
Future<void> _handleInput() async {
final String input = _inputController.text.trim();
if (_isCoordinate(input)) {
await _updateMapFromCoordinates(input);
} else {
await _updateMapFromWords();
}
}
/// **Checks if input is a valid coordinate format (-90 to 90, -180 to 180)**
bool _isCoordinate(String input) {
final RegExp coordRegex = RegExp(
r"^(-?[0-9]{1,2}(?:\.[0-9]+)?),\s*(-?[0-9]{1,3}(?:\.[0-9]+)?)$",
);
return coordRegex.hasMatch(input);
}
/// **Centers the map from coordinates entered in the text field**
Future<void> _updateMapFromCoordinates(String input) async {
try {
final parts = input.split(',');
final double lat = double.parse(parts[0].trim());
final double lon = double.parse(parts[1].trim());
if (lat < -90 || lat > 90 || lon < -180 || lon > 180) {
throw FormatException("Coordinates out of range.");
}
setState(() {
_center = LatLng(lat, lon);
_mapController.move(_center, _mapController.camera.zoom);
});
await _updateWordsFromCenter();
} catch (e) {
_showError("Invalid coordinate format. Use: lat, lon");
}
}
2025-02-18 21:45:30 +02:00
/// **Centers the map when terms are entered**
2025-02-18 21:15:53 +02:00
Future<void> _updateMapFromWords() async {
2025-02-18 21:45:30 +02:00
final terms = _inputController.text.trim().replaceAll(" ", ".");
if (terms.isEmpty) return;
2025-02-18 21:15:53 +02:00
try {
2025-02-18 21:45:30 +02:00
final coords = await wordsToCoord(terms);
2025-02-18 21:15:53 +02:00
setState(() {
_center = LatLng(coords[0], coords[1]);
_mapController.move(_center, _mapController.camera.zoom);
});
} catch (e) {
2025-02-18 21:45:30 +02:00
_showError("Invalid terms format.");
2025-02-18 21:15:53 +02:00
}
}
Future<void> _updateWordsFromCenter() async {
try {
2025-02-18 21:45:30 +02:00
final terms = await coordToWords([_center.latitude, _center.longitude]);
2025-02-18 21:15:53 +02:00
setState(() {
2025-02-18 21:45:30 +02:00
_inputController.text = terms.replaceAll(".", " ");
2025-02-18 21:15:53 +02:00
});
2025-02-18 21:45:30 +02:00
// 🌍 Update the browser URL dynamically
final String urlWords = terms.replaceAll(".", "-");
GoRouter.of(context).go("/?terms=$urlWords");
2025-02-18 21:15:53 +02:00
} catch (e) {
2025-02-18 21:45:30 +02:00
print("Error converting coordinates to terms: $e");
2025-02-18 21:15:53 +02:00
}
}
/// **Centers map on user location**
Future<void> _centerOnUserLocation() async {
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
_showError("Location permission denied.");
return;
}
}
final Position position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high);
setState(() {
_center = LatLng(position.latitude, position.longitude);
_mapController.move(_center, _mapController.camera.zoom);
});
_updateWordsFromCenter();
}
/// **Shows error messages in a snackbar**
void _showError(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message), backgroundColor: Colors.red),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
Column(
children: [
Padding(
padding: const EdgeInsets.all(10.0),
child: TextField(
controller: _inputController,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white),
decoration: InputDecoration(
2025-02-19 10:38:55 +02:00
labelText: 'Enter 3 Terms or Coordinates',
2025-02-18 21:15:53 +02:00
labelStyle: TextStyle(
color: Colors.grey,
fontSize: 16,
fontWeight: FontWeight.w800
),
filled: true,
2025-02-19 10:38:55 +02:00
fillColor: Color(0xff3d424e),
2025-02-18 21:15:53 +02:00
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: BorderSide.none,
),
suffixIcon: IconButton(
icon: Icon(Icons.search, color: Colors.white),
onPressed: _handleInput,
),
),
onSubmitted: (_) => _handleInput(),
),
),
Expanded(
child: Stack(
children: [
FlutterMap(
mapController: _mapController,
options: MapOptions(
initialCenter: _center,
initialZoom: 16.0,
onPositionChanged: (position, hasGesture) {
if (hasGesture) {
setState(() {
_center = position.center!;
});
_updateWordsFromCenter();
}
},
),
children: [
TileLayer(
urlTemplate: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
subdomains: ['a', 'b', 'c'],
),
PolygonLayer(
polygons: [
Polygon(
points: _boundingBox(_center),
2025-02-19 10:38:55 +02:00
color: Color(0xfffcba65).withValues(alpha: 0.1),
2025-02-18 21:15:53 +02:00
borderStrokeWidth: 2,
2025-02-19 10:38:55 +02:00
borderColor: Color(0xfffcba65),
2025-02-18 21:15:53 +02:00
),
],
),
],
),
Positioned.fill(
child: IgnorePointer(
child: CustomPaint(
painter: CrossOverlayPainter(),
),
),
),
]
),
),
]
),
Positioned(
bottom: 20,
right: 20,
child: FloatingActionButton(
onPressed: _centerOnUserLocation,
2025-02-19 10:38:55 +02:00
child: Icon(Icons.my_location, color: Colors.white),
backgroundColor: Color(0xff5471a8),
2025-02-18 21:15:53 +02:00
),
),
2025-02-18 21:45:30 +02:00
Positioned(
bottom: 80,
right: 20,
child: FloatingActionButton(
onPressed: () {
final String terms = _inputController.text.replaceAll(" ", "-"); // Convert to URL format
2025-02-19 11:07:34 +02:00
final String shareableUrl = "https://randogoth.github.io/web3terms/#/?terms=$terms";
2025-02-18 21:45:30 +02:00
Clipboard.setData(ClipboardData(text: shareableUrl));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Copied to clipboard: $shareableUrl")),
);
},
2025-02-19 10:38:55 +02:00
child: Icon(Icons.share, color: Colors.white),
backgroundColor: Color(0xff5471a8),
2025-02-18 21:45:30 +02:00
),
),
2025-02-18 21:15:53 +02:00
],
),
);
}
/// **Computes the bounding box based on word grid size (~6m)**
List<LatLng> _boundingBox(LatLng center) {
const double wordGridSize = 0.00006; // ~6m per word
return [
LatLng(center.latitude + wordGridSize, center.longitude - wordGridSize),
LatLng(center.latitude + wordGridSize, center.longitude + wordGridSize),
LatLng(center.latitude - wordGridSize, center.longitude + wordGridSize),
LatLng(center.latitude - wordGridSize, center.longitude - wordGridSize),
];
}
}
class CrossOverlayPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
2025-02-19 10:38:55 +02:00
..color = Color(0xff70a5d8).withValues(alpha: 0.5) // Adjust color & opacity
2025-02-18 21:15:53 +02:00
..strokeWidth = 1;
// Draw horizontal line across the entire screen
canvas.drawLine(
Offset(0, size.height / 2) , // Left edge
Offset(size.width, size.height / 2), // Right edge
paint,
);
// Draw vertical line across the entire screen
canvas.drawLine(
Offset(size.width / 2, 0), // Top edge
Offset(size.width / 2, size.height), // Bottom edge
paint,
);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
2025-02-18 21:45:30 +02:00
final GoRouter _router = GoRouter(
debugLogDiagnostics: true, // Helps debug routing issues
routes: [
GoRoute(
path: '/',
builder: (context, state) {
final String? terms = state.uri.queryParameters['terms']?.replaceAll("-", ".");
return Map3TermsHome(initialWords: terms);
},
),
],
);